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,68 +1,96 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { describe, test, expect, mock, afterEach } from "bun:test"
|
||||
import { startTask } from "./spawner"
|
||||
import type { BackgroundTask } from "./types"
|
||||
import {
|
||||
clearSessionPromptParams,
|
||||
getSessionPromptParams,
|
||||
} from "../../shared/session-prompt-params-state"
|
||||
|
||||
import { createTask, startTask } from "./spawner"
|
||||
describe("background-agent spawner fallback model promotion", () => {
|
||||
afterEach(() => {
|
||||
clearSessionPromptParams("session-123")
|
||||
})
|
||||
|
||||
describe("background-agent spawner.startTask", () => {
|
||||
test("applies explicit child session permission rules when creating child session", async () => {
|
||||
test("passes promoted fallback model settings through supported prompt channels", async () => {
|
||||
//#given
|
||||
const createCalls: any[] = []
|
||||
const parentPermission = [
|
||||
{ permission: "question", action: "allow" as const, pattern: "*" },
|
||||
{ permission: "plan_enter", action: "deny" as const, pattern: "*" },
|
||||
]
|
||||
|
||||
let promptArgs: any
|
||||
const client = {
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/parent/dir", permission: parentPermission } }),
|
||||
create: async (args?: any) => {
|
||||
createCalls.push(args)
|
||||
return { data: { id: "ses_child" } }
|
||||
},
|
||||
promptAsync: async () => ({}),
|
||||
get: mock(async () => ({ data: { directory: "/tmp/test" } })),
|
||||
create: mock(async () => ({ data: { id: "session-123" } })),
|
||||
promptAsync: mock(async (input: any) => {
|
||||
promptArgs = input
|
||||
return { data: {} }
|
||||
}),
|
||||
},
|
||||
}
|
||||
} as any
|
||||
|
||||
const task = createTask({
|
||||
const concurrencyManager = {
|
||||
release: mock(() => {}),
|
||||
} as any
|
||||
|
||||
const onTaskError = mock(() => {})
|
||||
|
||||
const task: BackgroundTask = {
|
||||
id: "bg_test123",
|
||||
status: "pending",
|
||||
queuedAt: new Date(),
|
||||
description: "Test task",
|
||||
prompt: "Do work",
|
||||
agent: "explore",
|
||||
parentSessionID: "ses_parent",
|
||||
parentMessageID: "msg_parent",
|
||||
})
|
||||
|
||||
const item = {
|
||||
task,
|
||||
input: {
|
||||
description: task.description,
|
||||
prompt: task.prompt,
|
||||
agent: task.agent,
|
||||
parentSessionID: task.parentSessionID,
|
||||
parentMessageID: task.parentMessageID,
|
||||
parentModel: task.parentModel,
|
||||
parentAgent: task.parentAgent,
|
||||
model: task.model,
|
||||
sessionPermission: [
|
||||
{ permission: "question", action: "deny", pattern: "*" },
|
||||
],
|
||||
prompt: "Do the thing",
|
||||
agent: "oracle",
|
||||
parentSessionID: "parent-1",
|
||||
parentMessageID: "message-1",
|
||||
model: {
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.4",
|
||||
variant: "low",
|
||||
reasoningEffort: "high",
|
||||
temperature: 0.4,
|
||||
top_p: 0.7,
|
||||
maxTokens: 4096,
|
||||
thinking: { type: "disabled" },
|
||||
},
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
client,
|
||||
directory: "/fallback",
|
||||
concurrencyManager: { release: () => {} },
|
||||
tmuxEnabled: false,
|
||||
onTaskError: () => {},
|
||||
const input = {
|
||||
description: "Test task",
|
||||
prompt: "Do the thing",
|
||||
agent: "oracle",
|
||||
parentSessionID: "parent-1",
|
||||
parentMessageID: "message-1",
|
||||
model: task.model,
|
||||
}
|
||||
|
||||
//#when
|
||||
await startTask(item as any, ctx as any)
|
||||
await startTask(
|
||||
{ task, input },
|
||||
{
|
||||
client,
|
||||
directory: "/tmp/test",
|
||||
concurrencyManager,
|
||||
tmuxEnabled: false,
|
||||
onTaskError,
|
||||
},
|
||||
)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
//#then
|
||||
expect(createCalls).toHaveLength(1)
|
||||
expect(createCalls[0]?.body?.permission).toEqual([
|
||||
{ permission: "question", action: "deny", pattern: "*" },
|
||||
])
|
||||
expect(promptArgs.body.model).toEqual({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.4",
|
||||
})
|
||||
expect(promptArgs.body.variant).toBe("low")
|
||||
expect(promptArgs.body.options).toBeUndefined()
|
||||
expect(getSessionPromptParams("session-123")).toEqual({
|
||||
temperature: 0.4,
|
||||
topP: 0.7,
|
||||
options: {
|
||||
reasoningEffort: "high",
|
||||
thinking: { type: "disabled" },
|
||||
maxTokens: 4096,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("keeps agent when explicit model is configured", async () => {
|
||||
|
||||
Reference in New Issue
Block a user