43023b1eb4
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
281 lines
11 KiB
TypeScript
281 lines
11 KiB
TypeScript
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
|
|
import type { DelegateTaskArgs } from "./types"
|
|
import type { ExecutorContext } from "./executor-types"
|
|
import type { FallbackEntry } from "../../shared/model-requirements"
|
|
import { mergeCategories } from "../../shared/merge-categories"
|
|
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
|
import { resolveCategoryConfig } from "./categories"
|
|
import { parseModelString } from "./model-string-parser"
|
|
import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
|
import { normalizeFallbackModels, flattenToFallbackModelStrings } from "../../shared/model-resolver"
|
|
import { buildFallbackChainFromModels, findMostSpecificFallbackEntry } from "../../shared/fallback-chain-from-models"
|
|
import { getAvailableModelsForDelegateTask } from "./available-models"
|
|
import { resolveModelForDelegateTask } from "./model-selection"
|
|
|
|
import type { CategoryConfig } from "../../config/schema"
|
|
import type { DelegatedModelConfig } from "./types"
|
|
|
|
function applyCategoryParams(base: DelegatedModelConfig, config: CategoryConfig): DelegatedModelConfig {
|
|
const result = { ...base }
|
|
if (config.temperature !== undefined) result.temperature = config.temperature
|
|
if (config.top_p !== undefined) result.top_p = config.top_p
|
|
if (config.maxTokens !== undefined) result.maxTokens = config.maxTokens
|
|
if (config.reasoningEffort !== undefined) result.reasoningEffort = config.reasoningEffort
|
|
if (config.thinking !== undefined) result.thinking = config.thinking
|
|
return result
|
|
}
|
|
|
|
export interface CategoryResolutionResult {
|
|
agentToUse: string
|
|
categoryModel: DelegatedModelConfig | undefined
|
|
categoryPromptAppend: string | undefined
|
|
maxPromptTokens?: number
|
|
modelInfo: ModelFallbackInfo | undefined
|
|
actualModel: string | undefined
|
|
isUnstableAgent: boolean
|
|
fallbackChain?: FallbackEntry[] // For runtime retry on model errors
|
|
error?: string
|
|
}
|
|
|
|
export async function resolveCategoryExecution(
|
|
args: DelegateTaskArgs,
|
|
executorCtx: ExecutorContext,
|
|
inheritedModel: string | undefined,
|
|
systemDefaultModel: string | undefined
|
|
): Promise<CategoryResolutionResult> {
|
|
const { client, userCategories, sisyphusJuniorModel } = executorCtx
|
|
|
|
const categoryName = args.category!
|
|
const enabledCategories = mergeCategories(userCategories)
|
|
const categoryExists = enabledCategories[categoryName] !== undefined
|
|
|
|
if (!categoryExists) {
|
|
const allCategoryNames = Object.keys(enabledCategories).join(", ")
|
|
return {
|
|
agentToUse: "",
|
|
categoryModel: undefined,
|
|
categoryPromptAppend: undefined,
|
|
maxPromptTokens: undefined,
|
|
modelInfo: undefined,
|
|
actualModel: undefined,
|
|
isUnstableAgent: false,
|
|
error: `Unknown category: "${categoryName}". Available: ${allCategoryNames}`,
|
|
}
|
|
}
|
|
|
|
const availableModels = await getAvailableModelsForDelegateTask(client)
|
|
|
|
const resolved = resolveCategoryConfig(categoryName, {
|
|
userCategories,
|
|
inheritedModel,
|
|
systemDefaultModel,
|
|
availableModels,
|
|
})
|
|
|
|
if (!resolved) {
|
|
const requirement = CATEGORY_MODEL_REQUIREMENTS[categoryName]
|
|
const allCategoryNames = Object.keys(enabledCategories).join(", ")
|
|
|
|
if (categoryExists && requirement?.requiresModel) {
|
|
return {
|
|
agentToUse: "",
|
|
categoryModel: undefined,
|
|
categoryPromptAppend: undefined,
|
|
maxPromptTokens: undefined,
|
|
modelInfo: undefined,
|
|
actualModel: undefined,
|
|
isUnstableAgent: false,
|
|
error: `Category "${categoryName}" requires model "${requirement.requiresModel}" which is not available.
|
|
|
|
To use this category:
|
|
1. Connect a provider with this model: ${requirement.requiresModel}
|
|
2. Or configure an alternative model in your oh-my-opencode.json for this category
|
|
|
|
Available categories: ${allCategoryNames}`,
|
|
}
|
|
}
|
|
|
|
return {
|
|
agentToUse: "",
|
|
categoryModel: undefined,
|
|
categoryPromptAppend: undefined,
|
|
maxPromptTokens: undefined,
|
|
modelInfo: undefined,
|
|
actualModel: undefined,
|
|
isUnstableAgent: false,
|
|
error: `Unknown category: "${categoryName}". Available: ${allCategoryNames}`,
|
|
}
|
|
}
|
|
|
|
const requirement = CATEGORY_MODEL_REQUIREMENTS[args.category!]
|
|
const normalizedConfiguredFallbackModels = normalizeFallbackModels(resolved.config.fallback_models)
|
|
let actualModel: string | undefined
|
|
let modelInfo: ModelFallbackInfo | undefined
|
|
let categoryModel: DelegatedModelConfig | undefined
|
|
let isModelResolutionSkipped = false
|
|
let fallbackEntry: FallbackEntry | undefined
|
|
let matchedFallback = false
|
|
|
|
const overrideModel = sisyphusJuniorModel
|
|
const explicitCategoryModel = userCategories?.[args.category!]?.model
|
|
|
|
if (!requirement) {
|
|
// Precedence: explicit category model > sisyphus-junior default > category resolved model
|
|
// This keeps `sisyphus-junior.model` useful as a global default while allowing
|
|
// per-category overrides via `categories[category].model`.
|
|
actualModel = explicitCategoryModel ?? overrideModel ?? resolved.model
|
|
if (actualModel) {
|
|
modelInfo = explicitCategoryModel || overrideModel
|
|
? { model: actualModel, type: "user-defined", source: "override" }
|
|
: { model: actualModel, type: "system-default", source: "system-default" }
|
|
const parsedModel = parseModelString(actualModel)
|
|
const variantToUse = userCategories?.[args.category!]?.variant ?? resolved.config.variant
|
|
categoryModel = parsedModel
|
|
? applyCategoryParams({ ...parsedModel, variant: variantToUse ?? parsedModel.variant }, resolved.config)
|
|
: undefined
|
|
}
|
|
} else {
|
|
const resolution = resolveModelForDelegateTask({
|
|
userModel: explicitCategoryModel ?? overrideModel,
|
|
userFallbackModels: flattenToFallbackModelStrings(normalizedConfiguredFallbackModels),
|
|
categoryDefaultModel: resolved.model,
|
|
isUserConfiguredCategoryModel: resolved.isUserConfiguredModel,
|
|
fallbackChain: requirement.fallbackChain,
|
|
availableModels,
|
|
systemDefaultModel,
|
|
})
|
|
|
|
if (resolution && "skipped" in resolution) {
|
|
isModelResolutionSkipped = true
|
|
const userModelOverride = explicitCategoryModel ?? overrideModel
|
|
if (userModelOverride) {
|
|
actualModel = userModelOverride
|
|
const parsedModel = parseModelString(actualModel)
|
|
const variantToUse = userCategories?.[args.category!]?.variant ?? resolved.config.variant
|
|
categoryModel = parsedModel
|
|
? applyCategoryParams({ ...parsedModel, variant: variantToUse ?? parsedModel.variant }, resolved.config)
|
|
: undefined
|
|
modelInfo = { model: actualModel, type: "user-defined", source: "override" }
|
|
}
|
|
} else if (resolution) {
|
|
const {
|
|
model: resolvedModel,
|
|
variant: resolvedVariant,
|
|
fallbackEntry: resolvedFallbackEntry,
|
|
matchedFallback: resolvedMatchedFallback,
|
|
} = resolution
|
|
fallbackEntry = resolvedFallbackEntry
|
|
matchedFallback = resolvedMatchedFallback === true
|
|
actualModel = resolvedModel
|
|
|
|
if (!parseModelString(actualModel)) {
|
|
return {
|
|
agentToUse: "",
|
|
categoryModel: undefined,
|
|
categoryPromptAppend: undefined,
|
|
maxPromptTokens: undefined,
|
|
modelInfo: undefined,
|
|
actualModel: undefined,
|
|
isUnstableAgent: false,
|
|
error: `Invalid model format "${actualModel}". Expected "provider/model" format (e.g., "anthropic/claude-sonnet-4-6").`,
|
|
}
|
|
}
|
|
|
|
const type: "user-defined" | "inherited" | "category-default" | "system-default" =
|
|
(explicitCategoryModel || overrideModel)
|
|
? "user-defined"
|
|
: (systemDefaultModel && actualModel === systemDefaultModel)
|
|
? "system-default"
|
|
: "category-default"
|
|
|
|
const source: "override" | "category-default" | "system-default" =
|
|
type === "user-defined"
|
|
? "override"
|
|
: type === "system-default"
|
|
? "system-default"
|
|
: "category-default"
|
|
|
|
modelInfo = { model: actualModel, type, source }
|
|
|
|
const parsedModel = parseModelString(actualModel)
|
|
const variantToUse = userCategories?.[args.category!]?.variant ?? resolvedVariant ?? resolved.config.variant
|
|
categoryModel = parsedModel
|
|
? applyCategoryParams({ ...parsedModel, variant: variantToUse ?? parsedModel.variant }, resolved.config)
|
|
: undefined
|
|
}
|
|
}
|
|
|
|
if (!categoryModel && actualModel) {
|
|
const parsedModel = parseModelString(actualModel)
|
|
categoryModel = parsedModel ?? undefined
|
|
}
|
|
const categoryPromptAppend = resolved.promptAppend || undefined
|
|
|
|
if (!categoryModel && !actualModel && !isModelResolutionSkipped) {
|
|
const categoryNames = Object.keys(enabledCategories)
|
|
return {
|
|
agentToUse: "",
|
|
categoryModel: undefined,
|
|
categoryPromptAppend: undefined,
|
|
maxPromptTokens: undefined,
|
|
modelInfo: undefined,
|
|
actualModel: undefined,
|
|
isUnstableAgent: false,
|
|
error: `Model not configured for category "${args.category}".
|
|
|
|
Configure in one of:
|
|
1. OpenCode: Set "model" in opencode.json
|
|
2. Oh-My-OpenCode: Set category model in oh-my-opencode.json
|
|
3. Provider: Connect a provider with available models
|
|
|
|
Current category: ${args.category}
|
|
Available categories: ${categoryNames.join(", ")}`,
|
|
}
|
|
}
|
|
|
|
const resolvedModel = actualModel?.toLowerCase()
|
|
const isUnstableAgent = resolved.config.is_unstable_agent ?? (resolvedModel ? resolvedModel.includes("gemini") || resolvedModel.includes("minimax") || resolvedModel.includes("kimi") : false)
|
|
|
|
const defaultProviderID = categoryModel?.providerID
|
|
?? parseModelString(actualModel ?? "")?.providerID
|
|
?? "opencode"
|
|
const configuredFallbackChain = buildFallbackChainFromModels(
|
|
normalizedConfiguredFallbackModels,
|
|
defaultProviderID,
|
|
)
|
|
|
|
// Only promote fallback-only settings when resolution actually selected a fallback model.
|
|
const effectiveEntry = matchedFallback && categoryModel
|
|
? (
|
|
fallbackEntry
|
|
?? (configuredFallbackChain
|
|
? findMostSpecificFallbackEntry(categoryModel.providerID, categoryModel.modelID, configuredFallbackChain)
|
|
: undefined)
|
|
)
|
|
: undefined
|
|
|
|
if (categoryModel && effectiveEntry) {
|
|
categoryModel = {
|
|
...categoryModel,
|
|
variant: userCategories?.[args.category!]?.variant ?? effectiveEntry.variant ?? categoryModel.variant,
|
|
reasoningEffort: effectiveEntry.reasoningEffort ?? categoryModel.reasoningEffort,
|
|
temperature: effectiveEntry.temperature ?? categoryModel.temperature,
|
|
top_p: effectiveEntry.top_p ?? categoryModel.top_p,
|
|
maxTokens: effectiveEntry.maxTokens ?? categoryModel.maxTokens,
|
|
thinking: effectiveEntry.thinking ?? categoryModel.thinking,
|
|
}
|
|
}
|
|
|
|
return {
|
|
agentToUse: SISYPHUS_JUNIOR_AGENT,
|
|
categoryModel,
|
|
categoryPromptAppend,
|
|
maxPromptTokens: resolved.config.max_prompt_tokens,
|
|
modelInfo,
|
|
actualModel,
|
|
isUnstableAgent,
|
|
// Don't use hardcoded fallback chain when resolution was skipped (cold cache)
|
|
fallbackChain: configuredFallbackChain ?? (isModelResolutionSkipped ? undefined : requirement?.fallbackChain),
|
|
}
|
|
}
|