11e9276498
When a category has no CATEGORY_MODEL_REQUIREMENTS entry (e.g. user-defined categories like solana-re), the !requirement branch set actualModel but never built categoryModel with variant from the user config. The bottom fallback then created categoryModel via parseModelString alone, silently dropping the variant. Mirror the requirement branch logic: read variant from userCategories and resolved.config, and build categoryModel with it. Fixes #2538
209 lines
7.8 KiB
TypeScript
209 lines
7.8 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 } from "../../shared/model-resolver"
|
|
import { buildFallbackChainFromModels } from "../../shared/fallback-chain-from-models"
|
|
import { getAvailableModelsForDelegateTask } from "./available-models"
|
|
import { resolveModelForDelegateTask } from "./model-selection"
|
|
|
|
export interface CategoryResolutionResult {
|
|
agentToUse: string
|
|
categoryModel: { providerID: string; modelID: string; variant?: string } | 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 availableModels = await getAvailableModelsForDelegateTask(client)
|
|
|
|
const categoryName = args.category!
|
|
const enabledCategories = mergeCategories(userCategories)
|
|
const categoryExists = enabledCategories[categoryName] !== undefined
|
|
|
|
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: { providerID: string; modelID: string; variant?: string } | undefined
|
|
|
|
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
|
|
? (variantToUse ? { ...parsedModel, variant: variantToUse } : parsedModel)
|
|
: undefined
|
|
}
|
|
} else {
|
|
const resolution = resolveModelForDelegateTask({
|
|
userModel: explicitCategoryModel ?? overrideModel,
|
|
userFallbackModels: normalizedConfiguredFallbackModels,
|
|
categoryDefaultModel: resolved.model,
|
|
fallbackChain: requirement.fallbackChain,
|
|
availableModels,
|
|
systemDefaultModel,
|
|
})
|
|
|
|
if (resolution) {
|
|
const { model: resolvedModel, variant: resolvedVariant } = resolution
|
|
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
|
|
? (variantToUse ? { ...parsedModel, variant: variantToUse } : parsedModel)
|
|
: undefined
|
|
}
|
|
}
|
|
|
|
if (!categoryModel && actualModel) {
|
|
const parsedModel = parseModelString(actualModel)
|
|
categoryModel = parsedModel ?? undefined
|
|
}
|
|
const categoryPromptAppend = resolved.promptAppend || undefined
|
|
|
|
if (!categoryModel && !actualModel) {
|
|
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 unstableModel = actualModel?.toLowerCase()
|
|
const categoryConfigModel = resolved.config.model?.toLowerCase()
|
|
const isUnstableAgent = resolved.config.is_unstable_agent === true || [unstableModel, categoryConfigModel].some(m => m ? m.includes("gemini") || m.includes("minimax") || m.includes("kimi") : false)
|
|
|
|
const defaultProviderID = categoryModel?.providerID
|
|
?? parseModelString(actualModel ?? "")?.providerID
|
|
?? "opencode"
|
|
const configuredFallbackChain = buildFallbackChainFromModels(
|
|
normalizedConfiguredFallbackModels,
|
|
defaultProviderID,
|
|
)
|
|
|
|
return {
|
|
agentToUse: SISYPHUS_JUNIOR_AGENT,
|
|
categoryModel,
|
|
categoryPromptAppend,
|
|
maxPromptTokens: resolved.config.max_prompt_tokens,
|
|
modelInfo,
|
|
actualModel,
|
|
isUnstableAgent,
|
|
fallbackChain: configuredFallbackChain ?? requirement?.fallbackChain,
|
|
}
|
|
}
|