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:
@@ -1,8 +1,17 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
|
||||
import { createChatParamsHandler } from "./chat-params"
|
||||
import {
|
||||
clearSessionPromptParams,
|
||||
getSessionPromptParams,
|
||||
setSessionPromptParams,
|
||||
} from "../shared/session-prompt-params-state"
|
||||
|
||||
describe("createChatParamsHandler", () => {
|
||||
afterEach(() => {
|
||||
clearSessionPromptParams("ses_chat_params")
|
||||
})
|
||||
|
||||
test("normalizes object-style agent payload and runs chat.params hooks", async () => {
|
||||
//#given
|
||||
let called = false
|
||||
@@ -35,7 +44,6 @@ describe("createChatParamsHandler", () => {
|
||||
//#then
|
||||
expect(called).toBe(true)
|
||||
})
|
||||
|
||||
test("passes the original mutable message object to chat.params hooks", async () => {
|
||||
//#given
|
||||
const handler = createChatParamsHandler({
|
||||
@@ -68,4 +76,61 @@ describe("createChatParamsHandler", () => {
|
||||
//#then
|
||||
expect(message.variant).toBe("high")
|
||||
})
|
||||
|
||||
test("applies stored prompt params for the session", async () => {
|
||||
//#given
|
||||
setSessionPromptParams("ses_chat_params", {
|
||||
temperature: 0.4,
|
||||
topP: 0.7,
|
||||
options: {
|
||||
reasoningEffort: "high",
|
||||
thinking: { type: "disabled" },
|
||||
maxTokens: 4096,
|
||||
},
|
||||
})
|
||||
|
||||
const handler = createChatParamsHandler({
|
||||
anthropicEffort: null,
|
||||
})
|
||||
|
||||
const input = {
|
||||
sessionID: "ses_chat_params",
|
||||
agent: { name: "oracle" },
|
||||
model: { providerID: "openai", modelID: "gpt-5.4" },
|
||||
provider: { id: "openai" },
|
||||
message: {},
|
||||
}
|
||||
|
||||
const output = {
|
||||
temperature: 0.1,
|
||||
topP: 1,
|
||||
topK: 1,
|
||||
options: { existing: true },
|
||||
}
|
||||
|
||||
//#when
|
||||
await handler(input, output)
|
||||
|
||||
//#then
|
||||
expect(output).toEqual({
|
||||
temperature: 0.4,
|
||||
topP: 0.7,
|
||||
topK: 1,
|
||||
options: {
|
||||
existing: true,
|
||||
reasoningEffort: "high",
|
||||
thinking: { type: "disabled" },
|
||||
maxTokens: 4096,
|
||||
},
|
||||
})
|
||||
expect(getSessionPromptParams("ses_chat_params")).toEqual({
|
||||
temperature: 0.4,
|
||||
topP: 0.7,
|
||||
options: {
|
||||
reasoningEffort: "high",
|
||||
thinking: { type: "disabled" },
|
||||
maxTokens: 4096,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getSessionPromptParams } from "../shared/session-prompt-params-state"
|
||||
|
||||
export type ChatParamsInput = {
|
||||
sessionID: string
|
||||
agent: { name?: string }
|
||||
@@ -82,6 +84,22 @@ export function createChatParamsHandler(args: {
|
||||
if (!normalizedInput) return
|
||||
if (!isChatParamsOutput(output)) return
|
||||
|
||||
const storedPromptParams = getSessionPromptParams(normalizedInput.sessionID)
|
||||
if (storedPromptParams) {
|
||||
if (storedPromptParams.temperature !== undefined) {
|
||||
output.temperature = storedPromptParams.temperature
|
||||
}
|
||||
if (storedPromptParams.topP !== undefined) {
|
||||
output.topP = storedPromptParams.topP
|
||||
}
|
||||
if (storedPromptParams.options) {
|
||||
output.options = {
|
||||
...output.options,
|
||||
...storedPromptParams.options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await args.anthropicEffort?.["chat.params"]?.(normalizedInput, output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test"
|
||||
|
||||
import { _resetForTesting, getSessionAgent, updateSessionAgent } from "../features/claude-code-session-state"
|
||||
import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/session-model-state"
|
||||
import { clearSessionPromptParams } from "../shared/session-prompt-params-state"
|
||||
import { createEventHandler } from "./event"
|
||||
|
||||
function createMinimalEventHandler() {
|
||||
@@ -53,6 +54,8 @@ describe("createEventHandler compaction agent filtering", () => {
|
||||
_resetForTesting()
|
||||
clearSessionModel("ses_compaction_poisoning")
|
||||
clearSessionModel("ses_compaction_model_poisoning")
|
||||
clearSessionPromptParams("ses_compaction_poisoning")
|
||||
clearSessionPromptParams("ses_compaction_model_poisoning")
|
||||
})
|
||||
|
||||
it("does not overwrite the stored session agent with compaction", async () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createEventHandler } from "./event"
|
||||
import { createChatMessageHandler } from "./chat-message"
|
||||
import { _resetForTesting, setMainSession } from "../features/claude-code-session-state"
|
||||
import { clearPendingModelFallback, createModelFallbackHook } from "../hooks/model-fallback/hook"
|
||||
import { getSessionPromptParams, setSessionPromptParams } from "../shared/session-prompt-params-state"
|
||||
|
||||
type EventInput = { event: { type: string; properties?: unknown } }
|
||||
|
||||
@@ -441,6 +442,45 @@ describe("createEventHandler - event forwarding", () => {
|
||||
expect(disconnectedSessions).toEqual([sessionID])
|
||||
expect(deletedSessions).toEqual([sessionID])
|
||||
})
|
||||
|
||||
it("clears stored prompt params on session.deleted", async () => {
|
||||
//#given
|
||||
const eventHandler = createEventHandler({
|
||||
ctx: {} as never,
|
||||
pluginConfig: {} as never,
|
||||
firstMessageVariantGate: {
|
||||
markSessionCreated: () => {},
|
||||
clear: () => {},
|
||||
},
|
||||
managers: {
|
||||
skillMcpManager: {
|
||||
disconnectSession: async () => {},
|
||||
},
|
||||
tmuxSessionManager: {
|
||||
onSessionCreated: async () => {},
|
||||
onSessionDeleted: async () => {},
|
||||
},
|
||||
} as never,
|
||||
hooks: {} as never,
|
||||
})
|
||||
const sessionID = "ses_prompt_params_deleted"
|
||||
setSessionPromptParams(sessionID, {
|
||||
temperature: 0.4,
|
||||
topP: 0.7,
|
||||
options: { reasoningEffort: "high" },
|
||||
})
|
||||
|
||||
//#when
|
||||
await eventHandler({
|
||||
event: {
|
||||
type: "session.deleted",
|
||||
properties: { info: { id: sessionID } },
|
||||
},
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(getSessionPromptParams(sessionID)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("createEventHandler - retry dedupe lifecycle", () => {
|
||||
|
||||
+6
-4
@@ -16,7 +16,7 @@ import {
|
||||
setSessionFallbackChain,
|
||||
setPendingModelFallback,
|
||||
} from "../hooks/model-fallback/hook";
|
||||
import { getFallbackModelsForSession } from "../hooks/runtime-fallback/fallback-models";
|
||||
import { getRawFallbackModels } from "../hooks/runtime-fallback/fallback-models";
|
||||
import { resetMessageCursor } from "../shared";
|
||||
import { getAgentConfigKey } from "../shared/agent-display-names";
|
||||
import { readConnectedProvidersCache } from "../shared/connected-providers-cache";
|
||||
@@ -25,6 +25,7 @@ import { shouldRetryError } from "../shared/model-error-classifier";
|
||||
import { buildFallbackChainFromModels } from "../shared/fallback-chain-from-models";
|
||||
import { extractRetryAttempt, normalizeRetryStatusMessage } from "../shared/retry-status-utils";
|
||||
import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/session-model-state";
|
||||
import { clearSessionPromptParams } from "../shared/session-prompt-params-state";
|
||||
import { deleteSessionTools } from "../shared/session-tools-store";
|
||||
import { lspManager } from "../tools";
|
||||
|
||||
@@ -110,10 +111,10 @@ function applyUserConfiguredFallbackChain(
|
||||
pluginConfig: OhMyOpenCodeConfig,
|
||||
): void {
|
||||
const agentKey = getAgentConfigKey(agentName);
|
||||
const configuredFallbackModels = getFallbackModelsForSession(sessionID, agentKey, pluginConfig);
|
||||
if (configuredFallbackModels.length === 0) return;
|
||||
const rawFallbackModels = getRawFallbackModels(sessionID, agentKey, pluginConfig);
|
||||
if (!rawFallbackModels || rawFallbackModels.length === 0) return;
|
||||
|
||||
const fallbackChain = buildFallbackChainFromModels(configuredFallbackModels, currentProviderID);
|
||||
const fallbackChain = buildFallbackChainFromModels(rawFallbackModels, currentProviderID);
|
||||
|
||||
if (fallbackChain && fallbackChain.length > 0) {
|
||||
setSessionFallbackChain(sessionID, fallbackChain);
|
||||
@@ -330,6 +331,7 @@ export function createEventHandler(args: {
|
||||
resetMessageCursor(sessionInfo.id);
|
||||
firstMessageVariantGate.clear(sessionInfo.id);
|
||||
clearSessionModel(sessionInfo.id);
|
||||
clearSessionPromptParams(sessionInfo.id);
|
||||
syncSubagentSessions.delete(sessionInfo.id);
|
||||
if (wasSyncSubagentSession) {
|
||||
subagentSessions.delete(sessionInfo.id);
|
||||
|
||||
Reference in New Issue
Block a user