feat(config): support object-style fallback_models with per-model settings

Add support for object-style entries in fallback_models arrays, enabling
per-model configuration of variant, reasoningEffort, temperature, top_p,
maxTokens, and thinking settings.

- Zod schema for FallbackModelObject with full validation
- normalizeFallbackModels() and flattenToFallbackModelStrings() utilities
- Provider-agnostic model resolution pipeline with fallback chain
- Session prompt params state management
- Fallback chain construction with prefix-match lookup
- Integration across delegate-task, background-agent, and plugin layers
This commit is contained in:
Ravi Tharuma
2026-03-18 14:21:27 +01:00
parent 7761e48dca
commit a77a16c494
34 changed files with 1686 additions and 126 deletions
+30 -6
View File
@@ -7,14 +7,16 @@ 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 { 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 { DelegatedModelConfig } from "./types"
export interface CategoryResolutionResult {
agentToUse: string
categoryModel: { providerID: string; modelID: string; variant?: string } | undefined
categoryModel: DelegatedModelConfig | undefined
categoryPromptAppend: string | undefined
maxPromptTokens?: number
modelInfo: ModelFallbackInfo | undefined
@@ -84,8 +86,9 @@ Available categories: ${allCategoryNames}`,
const normalizedConfiguredFallbackModels = normalizeFallbackModels(resolved.config.fallback_models)
let actualModel: string | undefined
let modelInfo: ModelFallbackInfo | undefined
let categoryModel: { providerID: string; modelID: string; variant?: string } | undefined
let categoryModel: DelegatedModelConfig | undefined
let isModelResolutionSkipped = false
let fallbackEntry: FallbackEntry | undefined
const overrideModel = sisyphusJuniorModel
const explicitCategoryModel = userCategories?.[args.category!]?.model
@@ -108,7 +111,7 @@ Available categories: ${allCategoryNames}`,
} else {
const resolution = resolveModelForDelegateTask({
userModel: explicitCategoryModel ?? overrideModel,
userFallbackModels: normalizedConfiguredFallbackModels,
userFallbackModels: flattenToFallbackModelStrings(normalizedConfiguredFallbackModels),
categoryDefaultModel: resolved.model,
isUserConfiguredCategoryModel: resolved.isUserConfiguredModel,
fallbackChain: requirement.fallbackChain,
@@ -119,7 +122,8 @@ Available categories: ${allCategoryNames}`,
if (resolution && "skipped" in resolution) {
isModelResolutionSkipped = true
} else if (resolution) {
const { model: resolvedModel, variant: resolvedVariant } = resolution
const { model: resolvedModel, variant: resolvedVariant, fallbackEntry: resolvedFallbackEntry } = resolution
fallbackEntry = resolvedFallbackEntry
actualModel = resolvedModel
if (!parseModelString(actualModel)) {
@@ -198,6 +202,26 @@ Available categories: ${categoryNames.join(", ")}`,
defaultProviderID,
)
// Apply per-model settings from the source that provided the match:
// 1. fallbackEntry from resolver (built-in chain match) — exact, no lookup needed
// 2. configuredFallbackChain (user's fallback_models) — prefix match against user config
const effectiveEntry = fallbackEntry
?? (categoryModel && configuredFallbackChain
? findMostSpecificFallbackEntry(categoryModel.providerID, categoryModel.modelID, configuredFallbackChain)
: undefined)
if (categoryModel && effectiveEntry) {
categoryModel = {
...categoryModel,
variant: userCategories?.[args.category!]?.variant ?? effectiveEntry.variant ?? categoryModel.variant,
reasoningEffort: effectiveEntry.reasoningEffort,
temperature: effectiveEntry.temperature,
top_p: effectiveEntry.top_p,
maxTokens: effectiveEntry.maxTokens,
thinking: effectiveEntry.thinking,
}
}
return {
agentToUse: SISYPHUS_JUNIOR_AGENT,
categoryModel,