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
+49 -2
View File
@@ -2,6 +2,7 @@ import type { BackgroundTask, LaunchInput, ResumeInput } from "./types"
import type { OpencodeClient, OnSubagentSessionCreated, QueueItem } from "./constants"
import { TMUX_CALLBACK_DELAY_MS } from "./constants"
import { log, getAgentToolRestrictions, promptWithModelSuggestionRetry, createInternalAgentTextPart } from "../../shared"
import { setSessionPromptParams } from "../../shared/session-prompt-params-state"
import { subagentSessions } from "../claude-code-session-state"
import { getTaskToastManager } from "../task-toast-manager"
import { isInsideTmux } from "../../shared/tmux"
@@ -128,10 +129,33 @@ export async function startTask(
})
const launchModel = input.model
? { providerID: input.model.providerID, modelID: input.model.modelID }
? {
providerID: input.model.providerID,
modelID: input.model.modelID,
}
: undefined
const launchVariant = input.model?.variant
if (input.model) {
const promptOptions: Record<string, unknown> = {
...(input.model.reasoningEffort ? { reasoningEffort: input.model.reasoningEffort } : {}),
...(input.model.thinking ? { thinking: input.model.thinking } : {}),
...(input.model.maxTokens !== undefined ? { maxTokens: input.model.maxTokens } : {}),
}
if (
input.model.temperature !== undefined ||
input.model.top_p !== undefined ||
Object.keys(promptOptions).length > 0
) {
setSessionPromptParams(sessionID, {
...(input.model.temperature !== undefined ? { temperature: input.model.temperature } : {}),
...(input.model.top_p !== undefined ? { topP: input.model.top_p } : {}),
...(Object.keys(promptOptions).length > 0 ? { options: promptOptions } : {}),
})
}
}
promptWithModelSuggestionRetry(client, {
path: { id: sessionID },
body: {
@@ -213,10 +237,33 @@ export async function resumeTask(
})
const resumeModel = task.model
? { providerID: task.model.providerID, modelID: task.model.modelID }
? {
providerID: task.model.providerID,
modelID: task.model.modelID,
}
: undefined
const resumeVariant = task.model?.variant
if (task.model) {
const promptOptions: Record<string, unknown> = {
...(task.model.reasoningEffort ? { reasoningEffort: task.model.reasoningEffort } : {}),
...(task.model.thinking ? { thinking: task.model.thinking } : {}),
...(task.model.maxTokens !== undefined ? { maxTokens: task.model.maxTokens } : {}),
}
if (
task.model.temperature !== undefined ||
task.model.top_p !== undefined ||
Object.keys(promptOptions).length > 0
) {
setSessionPromptParams(task.sessionID, {
...(task.model.temperature !== undefined ? { temperature: task.model.temperature } : {}),
...(task.model.top_p !== undefined ? { topP: task.model.top_p } : {}),
...(Object.keys(promptOptions).length > 0 ? { options: promptOptions } : {}),
})
}
}
client.session.promptAsync({
path: { id: task.sessionID },
body: {