Merge pull request #2010 from code-yeongyu/fix/remove-quotio-provider
fix(model-requirements): remove custom quotio provider, restore standard providers
This commit is contained in:
commit
032d7fd139
@ -281,7 +281,7 @@ describe("generateOmoConfig - model fallback system", () => {
|
|||||||
expect((result.agents as Record<string, { model: string }>).sisyphus).toBeUndefined()
|
expect((result.agents as Record<string, { model: string }>).sisyphus).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses opencode/minimax-m2.5-free for librarian regardless of Z.ai", () => {
|
test("uses ZAI model for librarian when Z.ai is available", () => {
|
||||||
// #given user has Z.ai and Claude max20
|
// #given user has Z.ai and Claude max20
|
||||||
const config: InstallConfig = {
|
const config: InstallConfig = {
|
||||||
hasClaude: true,
|
hasClaude: true,
|
||||||
@ -297,8 +297,8 @@ describe("generateOmoConfig - model fallback system", () => {
|
|||||||
// #when generating config
|
// #when generating config
|
||||||
const result = generateOmoConfig(config)
|
const result = generateOmoConfig(config)
|
||||||
|
|
||||||
// #then librarian should use opencode/minimax-m2.5-free
|
// #then librarian should use ZAI model
|
||||||
expect((result.agents as Record<string, { model: string }>).librarian.model).toBe("opencode/minimax-m2.5-free")
|
expect((result.agents as Record<string, { model: string }>).librarian.model).toBe("zai-coding-plan/glm-4.7")
|
||||||
// #then Sisyphus uses Claude (OR logic)
|
// #then Sisyphus uses Claude (OR logic)
|
||||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-6")
|
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-6")
|
||||||
})
|
})
|
||||||
|
|||||||
@ -15,7 +15,7 @@ describe("model-resolution check", () => {
|
|||||||
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
|
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
|
||||||
expect(sisyphus).toBeDefined()
|
expect(sisyphus).toBeDefined()
|
||||||
expect(sisyphus!.requirement.fallbackChain[0]?.model).toBe("claude-opus-4-6")
|
expect(sisyphus!.requirement.fallbackChain[0]?.model).toBe("claude-opus-4-6")
|
||||||
expect(sisyphus!.requirement.fallbackChain[0]?.providers).toContain("quotio")
|
expect(sisyphus!.requirement.fallbackChain[0]?.providers).toContain("anthropic")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns category requirements with provider chains", async () => {
|
it("returns category requirements with provider chains", async () => {
|
||||||
@ -26,8 +26,8 @@ describe("model-resolution check", () => {
|
|||||||
// then: Should have category entries
|
// then: Should have category entries
|
||||||
const visual = info.categories.find((c) => c.name === "visual-engineering")
|
const visual = info.categories.find((c) => c.name === "visual-engineering")
|
||||||
expect(visual).toBeDefined()
|
expect(visual).toBeDefined()
|
||||||
expect(visual!.requirement.fallbackChain[0]?.model).toBe("claude-opus-4-6-thinking")
|
expect(visual!.requirement.fallbackChain[0]?.model).toBe("gemini-3-pro")
|
||||||
expect(visual!.requirement.fallbackChain[0]?.providers).toContain("quotio")
|
expect(visual!.requirement.fallbackChain[0]?.providers).toContain("google")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ describe("model-resolution check", () => {
|
|||||||
expect(sisyphus).toBeDefined()
|
expect(sisyphus).toBeDefined()
|
||||||
expect(sisyphus!.userOverride).toBeUndefined()
|
expect(sisyphus!.userOverride).toBeUndefined()
|
||||||
expect(sisyphus!.effectiveResolution).toContain("Provider fallback:")
|
expect(sisyphus!.effectiveResolution).toContain("Provider fallback:")
|
||||||
expect(sisyphus!.effectiveResolution).toContain("quotio")
|
expect(sisyphus!.effectiveResolution).toContain("anthropic")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("captures user variant for agent when configured", async () => {
|
it("captures user variant for agent when configured", async () => {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { ModelRequirement } from "../shared/model-requirements"
|
|||||||
|
|
||||||
// NOTE: These requirements are used by the CLI config generator (`generateModelConfig`).
|
// NOTE: These requirements are used by the CLI config generator (`generateModelConfig`).
|
||||||
// They intentionally use "install-time" provider IDs (anthropic/openai/google/opencode/etc),
|
// They intentionally use "install-time" provider IDs (anthropic/openai/google/opencode/etc),
|
||||||
// not runtime providers like `quotio`/`nvidia`.
|
// not runtime-only providers like `nvidia`.
|
||||||
|
|
||||||
export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||||
sisyphus: {
|
sisyphus: {
|
||||||
@ -150,4 +150,3 @@ export const CLI_CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> =
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -480,7 +480,7 @@ describe("generateModelConfig", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("librarian agent special cases", () => {
|
describe("librarian agent special cases", () => {
|
||||||
test("librarian uses ZAI when ZAI is available regardless of other providers", () => {
|
test("librarian uses ZAI model when ZAI is available regardless of other providers", () => {
|
||||||
// #given ZAI and Claude are available
|
// #given ZAI and Claude are available
|
||||||
const config = createConfig({
|
const config = createConfig({
|
||||||
hasClaude: true,
|
hasClaude: true,
|
||||||
@ -491,18 +491,18 @@ describe("generateModelConfig", () => {
|
|||||||
const result = generateModelConfig(config)
|
const result = generateModelConfig(config)
|
||||||
|
|
||||||
// #then librarian should use ZAI_MODEL
|
// #then librarian should use ZAI_MODEL
|
||||||
expect(result.agents?.librarian?.model).toBe("opencode/minimax-m2.5-free")
|
expect(result.agents?.librarian?.model).toBe("zai-coding-plan/glm-4.7")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("librarian always uses minimax-m2.5-free regardless of provider availability", () => {
|
test("librarian falls back to generic chain result when no librarian provider matches", () => {
|
||||||
// #given only Claude is available (no ZAI)
|
// #given only Claude is available (no ZAI)
|
||||||
const config = createConfig({ hasClaude: true })
|
const config = createConfig({ hasClaude: true })
|
||||||
|
|
||||||
// #when generateModelConfig is called
|
// #when generateModelConfig is called
|
||||||
const result = generateModelConfig(config)
|
const result = generateModelConfig(config)
|
||||||
|
|
||||||
// #then librarian should use opencode/minimax-m2.5-free (always first in chain)
|
// #then librarian should use generic chain result when chain providers are unavailable
|
||||||
expect(result.agents?.librarian?.model).toBe("opencode/minimax-m2.5-free")
|
expect(result.agents?.librarian?.model).toBe("anthropic/claude-sonnet-4-5")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -2921,8 +2921,8 @@ describe("BackgroundManager.handleEvent - session.deleted cascade", () => {
|
|||||||
|
|
||||||
describe("BackgroundManager.handleEvent - session.error", () => {
|
describe("BackgroundManager.handleEvent - session.error", () => {
|
||||||
const defaultRetryFallbackChain = [
|
const defaultRetryFallbackChain = [
|
||||||
{ providers: ["quotio"], model: "claude-opus-4-6", variant: "max" },
|
{ providers: ["anthropic"], model: "claude-opus-4-6", variant: "max" },
|
||||||
{ providers: ["quotio"], model: "gpt-5.3-codex", variant: "high" },
|
{ providers: ["anthropic"], model: "gpt-5.3-codex", variant: "high" },
|
||||||
]
|
]
|
||||||
|
|
||||||
const stubProcessKey = (manager: BackgroundManager) => {
|
const stubProcessKey = (manager: BackgroundManager) => {
|
||||||
@ -2945,7 +2945,7 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
|||||||
agent: "sisyphus",
|
agent: "sisyphus",
|
||||||
status: "running",
|
status: "running",
|
||||||
concurrencyKey: input.concurrencyKey,
|
concurrencyKey: input.concurrencyKey,
|
||||||
model: { providerID: "quotio", modelID: "claude-opus-4-6-thinking" },
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6-thinking" },
|
||||||
fallbackChain: input.fallbackChain ?? defaultRetryFallbackChain,
|
fallbackChain: input.fallbackChain ?? defaultRetryFallbackChain,
|
||||||
attemptCount: 0,
|
attemptCount: 0,
|
||||||
})
|
})
|
||||||
@ -3084,7 +3084,7 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
|||||||
//#given
|
//#given
|
||||||
const manager = createBackgroundManager()
|
const manager = createBackgroundManager()
|
||||||
const concurrencyManager = getConcurrencyManager(manager)
|
const concurrencyManager = getConcurrencyManager(manager)
|
||||||
const concurrencyKey = "quotio/claude-opus-4-6-thinking"
|
const concurrencyKey = "anthropic/claude-opus-4-6-thinking"
|
||||||
await concurrencyManager.acquire(concurrencyKey)
|
await concurrencyManager.acquire(concurrencyKey)
|
||||||
|
|
||||||
stubProcessKey(manager)
|
stubProcessKey(manager)
|
||||||
@ -3096,8 +3096,8 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
|||||||
description: "task that should retry",
|
description: "task that should retry",
|
||||||
concurrencyKey,
|
concurrencyKey,
|
||||||
fallbackChain: [
|
fallbackChain: [
|
||||||
{ providers: ["quotio"], model: "claude-opus-4-6", variant: "max" },
|
{ providers: ["anthropic"], model: "claude-opus-4-6", variant: "max" },
|
||||||
{ providers: ["quotio"], model: "claude-opus-4-5" },
|
{ providers: ["anthropic"], model: "claude-opus-4-5" },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -3120,7 +3120,7 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
|||||||
expect(task.status).toBe("pending")
|
expect(task.status).toBe("pending")
|
||||||
expect(task.attemptCount).toBe(1)
|
expect(task.attemptCount).toBe(1)
|
||||||
expect(task.model).toEqual({
|
expect(task.model).toEqual({
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
modelID: "claude-opus-4-6",
|
modelID: "claude-opus-4-6",
|
||||||
variant: "max",
|
variant: "max",
|
||||||
})
|
})
|
||||||
@ -3158,7 +3158,7 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
|||||||
expect(task.status).toBe("pending")
|
expect(task.status).toBe("pending")
|
||||||
expect(task.attemptCount).toBe(1)
|
expect(task.attemptCount).toBe(1)
|
||||||
expect(task.model).toEqual({
|
expect(task.model).toEqual({
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
modelID: "claude-opus-4-6",
|
modelID: "claude-opus-4-6",
|
||||||
variant: "max",
|
variant: "max",
|
||||||
})
|
})
|
||||||
@ -3201,7 +3201,7 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
|||||||
expect(task.status).toBe("pending")
|
expect(task.status).toBe("pending")
|
||||||
expect(task.attemptCount).toBe(1)
|
expect(task.attemptCount).toBe(1)
|
||||||
expect(task.model).toEqual({
|
expect(task.model).toEqual({
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
modelID: "claude-opus-4-6",
|
modelID: "claude-opus-4-6",
|
||||||
variant: "max",
|
variant: "max",
|
||||||
})
|
})
|
||||||
|
|||||||
@ -224,7 +224,7 @@ describe("TaskToastManager", () => {
|
|||||||
description: "Task with runtime fallback model",
|
description: "Task with runtime fallback model",
|
||||||
agent: "explore",
|
agent: "explore",
|
||||||
isBackground: false,
|
isBackground: false,
|
||||||
modelInfo: { model: "quotio/oswe-vscode-prime", type: "runtime-fallback" as const },
|
modelInfo: { model: "anthropic/oswe-vscode-prime", type: "runtime-fallback" as const },
|
||||||
}
|
}
|
||||||
|
|
||||||
// when - addTask is called
|
// when - addTask is called
|
||||||
@ -234,7 +234,7 @@ describe("TaskToastManager", () => {
|
|||||||
expect(mockClient.tui.showToast).toHaveBeenCalled()
|
expect(mockClient.tui.showToast).toHaveBeenCalled()
|
||||||
const call = mockClient.tui.showToast.mock.calls[0][0]
|
const call = mockClient.tui.showToast.mock.calls[0][0]
|
||||||
expect(call.body.message).toContain("[FALLBACK]")
|
expect(call.body.message).toContain("[FALLBACK]")
|
||||||
expect(call.body.message).toContain("quotio/oswe-vscode-prime")
|
expect(call.body.message).toContain("anthropic/oswe-vscode-prime")
|
||||||
expect(call.body.message).toContain("(runtime fallback)")
|
expect(call.body.message).toContain("(runtime fallback)")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ describe("beast-mode-system hook", () => {
|
|||||||
test("does not inject for other models", async () => {
|
test("does not inject for other models", async () => {
|
||||||
//#given
|
//#given
|
||||||
const sessionID = "ses_no_beast"
|
const sessionID = "ses_no_beast"
|
||||||
setSessionModel(sessionID, { providerID: "quotio", modelID: "gpt-5.3-codex" })
|
setSessionModel(sessionID, { providerID: "anthropic", modelID: "gpt-5.3-codex" })
|
||||||
const hook = createBeastModeSystemHook()
|
const hook = createBeastModeSystemHook()
|
||||||
const output = { system: [] as string[] }
|
const output = { system: [] as string[] }
|
||||||
|
|
||||||
|
|||||||
@ -23,14 +23,14 @@ describe("model fallback hook", () => {
|
|||||||
const set = setPendingModelFallback(
|
const set = setPendingModelFallback(
|
||||||
"ses_model_fallback_main",
|
"ses_model_fallback_main",
|
||||||
"Sisyphus (Ultraworker)",
|
"Sisyphus (Ultraworker)",
|
||||||
"quotio",
|
"anthropic",
|
||||||
"claude-opus-4-6-thinking",
|
"claude-opus-4-6-thinking",
|
||||||
)
|
)
|
||||||
expect(set).toBe(true)
|
expect(set).toBe(true)
|
||||||
|
|
||||||
const output = {
|
const output = {
|
||||||
message: {
|
message: {
|
||||||
model: { providerID: "quotio", modelID: "claude-opus-4-6-thinking" },
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6-thinking" },
|
||||||
variant: "max",
|
variant: "max",
|
||||||
},
|
},
|
||||||
parts: [{ type: "text", text: "continue" }],
|
parts: [{ type: "text", text: "continue" }],
|
||||||
@ -44,7 +44,7 @@ describe("model fallback hook", () => {
|
|||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(output.message["model"]).toEqual({
|
expect(output.message["model"]).toEqual({
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
modelID: "claude-opus-4-6",
|
modelID: "claude-opus-4-6",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -60,12 +60,12 @@ describe("model fallback hook", () => {
|
|||||||
const sessionID = "ses_model_fallback_main"
|
const sessionID = "ses_model_fallback_main"
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
setPendingModelFallback(sessionID, "Sisyphus (Ultraworker)", "quotio", "claude-opus-4-6-thinking"),
|
setPendingModelFallback(sessionID, "Sisyphus (Ultraworker)", "anthropic", "claude-opus-4-6-thinking"),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
|
|
||||||
const firstOutput = {
|
const firstOutput = {
|
||||||
message: {
|
message: {
|
||||||
model: { providerID: "quotio", modelID: "claude-opus-4-6-thinking" },
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6-thinking" },
|
||||||
variant: "max",
|
variant: "max",
|
||||||
},
|
},
|
||||||
parts: [{ type: "text", text: "continue" }],
|
parts: [{ type: "text", text: "continue" }],
|
||||||
@ -76,18 +76,18 @@ describe("model fallback hook", () => {
|
|||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(firstOutput.message["model"]).toEqual({
|
expect(firstOutput.message["model"]).toEqual({
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
modelID: "claude-opus-4-6",
|
modelID: "claude-opus-4-6",
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when - second error re-arms fallback and should advance to next entry
|
//#when - second error re-arms fallback and should advance to next entry
|
||||||
expect(
|
expect(
|
||||||
setPendingModelFallback(sessionID, "Sisyphus (Ultraworker)", "quotio", "claude-opus-4-6"),
|
setPendingModelFallback(sessionID, "Sisyphus (Ultraworker)", "anthropic", "claude-opus-4-6"),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
|
|
||||||
const secondOutput = {
|
const secondOutput = {
|
||||||
message: {
|
message: {
|
||||||
model: { providerID: "quotio", modelID: "claude-opus-4-6" },
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6" },
|
||||||
},
|
},
|
||||||
parts: [{ type: "text", text: "continue" }],
|
parts: [{ type: "text", text: "continue" }],
|
||||||
}
|
}
|
||||||
@ -95,10 +95,10 @@ describe("model fallback hook", () => {
|
|||||||
|
|
||||||
//#then - chain should progress to entry[1], not repeat entry[0]
|
//#then - chain should progress to entry[1], not repeat entry[0]
|
||||||
expect(secondOutput.message["model"]).toEqual({
|
expect(secondOutput.message["model"]).toEqual({
|
||||||
providerID: "quotio",
|
providerID: "opencode",
|
||||||
modelID: "gpt-5.3-codex",
|
modelID: "kimi-k2.5-free",
|
||||||
})
|
})
|
||||||
expect(secondOutput.message["variant"]).toBe("high")
|
expect(secondOutput.message["variant"]).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("shows toast when fallback is applied", async () => {
|
test("shows toast when fallback is applied", async () => {
|
||||||
@ -118,14 +118,14 @@ describe("model fallback hook", () => {
|
|||||||
const set = setPendingModelFallback(
|
const set = setPendingModelFallback(
|
||||||
"ses_model_fallback_toast",
|
"ses_model_fallback_toast",
|
||||||
"Sisyphus (Ultraworker)",
|
"Sisyphus (Ultraworker)",
|
||||||
"quotio",
|
"anthropic",
|
||||||
"claude-opus-4-6-thinking",
|
"claude-opus-4-6-thinking",
|
||||||
)
|
)
|
||||||
expect(set).toBe(true)
|
expect(set).toBe(true)
|
||||||
|
|
||||||
const output = {
|
const output = {
|
||||||
message: {
|
message: {
|
||||||
model: { providerID: "quotio", modelID: "claude-opus-4-6-thinking" },
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6-thinking" },
|
||||||
variant: "max",
|
variant: "max",
|
||||||
},
|
},
|
||||||
parts: [{ type: "text", text: "continue" }],
|
parts: [{ type: "text", text: "continue" }],
|
||||||
|
|||||||
@ -75,7 +75,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
},
|
},
|
||||||
parentID: "msg_user_1",
|
parentID: "msg_user_1",
|
||||||
modelID: "claude-opus-4-6-thinking",
|
modelID: "claude-opus-4-6-thinking",
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
mode: "Sisyphus (Ultraworker)",
|
mode: "Sisyphus (Ultraworker)",
|
||||||
agent: "Sisyphus (Ultraworker)",
|
agent: "Sisyphus (Ultraworker)",
|
||||||
path: { cwd: "/tmp", root: "/tmp" },
|
path: { cwd: "/tmp", root: "/tmp" },
|
||||||
@ -166,7 +166,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
time: { created: 1 },
|
time: { created: 1 },
|
||||||
content: [],
|
content: [],
|
||||||
modelID: "claude-opus-4-6-thinking",
|
modelID: "claude-opus-4-6-thinking",
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
agent: "Sisyphus (Ultraworker)",
|
agent: "Sisyphus (Ultraworker)",
|
||||||
path: { cwd: "/tmp", root: "/tmp" },
|
path: { cwd: "/tmp", root: "/tmp" },
|
||||||
},
|
},
|
||||||
@ -196,7 +196,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
{
|
{
|
||||||
sessionID,
|
sessionID,
|
||||||
agent: "sisyphus",
|
agent: "sisyphus",
|
||||||
model: { providerID: "quotio", modelID: "claude-opus-4-6-thinking" },
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6-thinking" },
|
||||||
},
|
},
|
||||||
output,
|
output,
|
||||||
)
|
)
|
||||||
@ -205,7 +205,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
expect(abortCalls).toEqual([sessionID])
|
expect(abortCalls).toEqual([sessionID])
|
||||||
expect(promptCalls).toEqual([sessionID])
|
expect(promptCalls).toEqual([sessionID])
|
||||||
expect(output.message["model"]).toEqual({
|
expect(output.message["model"]).toEqual({
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
modelID: "claude-opus-4-6",
|
modelID: "claude-opus-4-6",
|
||||||
})
|
})
|
||||||
expect(output.message["variant"]).toBe("max")
|
expect(output.message["variant"]).toBe("max")
|
||||||
@ -290,7 +290,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
type: "session.error",
|
type: "session.error",
|
||||||
properties: {
|
properties: {
|
||||||
sessionID,
|
sessionID,
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
modelID: "claude-opus-4-6-thinking",
|
modelID: "claude-opus-4-6-thinking",
|
||||||
error: {
|
error: {
|
||||||
name: "UnknownError",
|
name: "UnknownError",
|
||||||
@ -310,7 +310,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
{
|
{
|
||||||
sessionID,
|
sessionID,
|
||||||
agent: "sisyphus",
|
agent: "sisyphus",
|
||||||
model: { providerID: "quotio", modelID: "claude-opus-4-6-thinking" },
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6-thinking" },
|
||||||
},
|
},
|
||||||
output,
|
output,
|
||||||
)
|
)
|
||||||
@ -322,7 +322,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
|
|
||||||
//#then - first fallback entry applied (prefers current provider when available)
|
//#then - first fallback entry applied (prefers current provider when available)
|
||||||
expect(first.message["model"]).toEqual({
|
expect(first.message["model"]).toEqual({
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
modelID: "claude-opus-4-6",
|
modelID: "claude-opus-4-6",
|
||||||
})
|
})
|
||||||
expect(first.message["variant"]).toBe("max")
|
expect(first.message["variant"]).toBe("max")
|
||||||
@ -332,7 +332,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
|
|
||||||
//#then - second fallback entry applied (chain advanced)
|
//#then - second fallback entry applied (chain advanced)
|
||||||
expect(second.message["model"]).toEqual({
|
expect(second.message["model"]).toEqual({
|
||||||
providerID: "quotio",
|
providerID: "anthropic",
|
||||||
modelID: "gpt-5.3-codex",
|
modelID: "gpt-5.3-codex",
|
||||||
})
|
})
|
||||||
expect(second.message["variant"]).toBe("high")
|
expect(second.message["variant"]).toBe("high")
|
||||||
|
|||||||
@ -40,14 +40,14 @@ describe("model-error-classifier", () => {
|
|||||||
//#given
|
//#given
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(TEST_CACHE_DIR, "connected-providers.json"),
|
join(TEST_CACHE_DIR, "connected-providers.json"),
|
||||||
JSON.stringify({ connected: ["quotio", "nvidia"], updatedAt: new Date().toISOString() }, null, 2),
|
JSON.stringify({ connected: ["anthropic", "nvidia"], updatedAt: new Date().toISOString() }, null, 2),
|
||||||
)
|
)
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const provider = selectFallbackProvider(["quotio", "nvidia"], "nvidia")
|
const provider = selectFallbackProvider(["anthropic", "nvidia"], "nvidia")
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(provider).toBe("quotio")
|
expect(provider).toBe("anthropic")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("selectFallbackProvider falls back to next connected provider when first is disconnected", () => {
|
test("selectFallbackProvider falls back to next connected provider when first is disconnected", () => {
|
||||||
@ -58,7 +58,7 @@ describe("model-error-classifier", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const provider = selectFallbackProvider(["quotio", "nvidia"])
|
const provider = selectFallbackProvider(["anthropic", "nvidia"])
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(provider).toBe("nvidia")
|
expect(provider).toBe("nvidia")
|
||||||
@ -68,9 +68,9 @@ describe("model-error-classifier", () => {
|
|||||||
//#given - no cache file
|
//#given - no cache file
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const provider = selectFallbackProvider(["quotio", "nvidia"], "nvidia")
|
const provider = selectFallbackProvider(["anthropic", "nvidia"], "nvidia")
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(provider).toBe("quotio")
|
expect(provider).toBe("anthropic")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -131,5 +131,5 @@ export function selectFallbackProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return providers[0] || preferredProviderID || "quotio"
|
return providers[0] || preferredProviderID || "opencode"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,158 +6,494 @@ import {
|
|||||||
type ModelRequirement,
|
type ModelRequirement,
|
||||||
} from "./model-requirements"
|
} from "./model-requirements"
|
||||||
|
|
||||||
function flattenChains(): FallbackEntry[] {
|
|
||||||
return [
|
|
||||||
...Object.values(AGENT_MODEL_REQUIREMENTS).flatMap((r) => r.fallbackChain),
|
|
||||||
...Object.values(CATEGORY_MODEL_REQUIREMENTS).flatMap((r) => r.fallbackChain),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertNoExcludedModels(entry: FallbackEntry): void {
|
|
||||||
// User exclusions.
|
|
||||||
expect(entry.model).not.toBe("grok-code-fast-1")
|
|
||||||
if (entry.providers.includes("quotio")) {
|
|
||||||
expect(entry.model).not.toBe("tstars2.0")
|
|
||||||
expect(entry.model).not.toMatch(/^kiro-/i)
|
|
||||||
expect(entry.model).not.toMatch(/^tab_/i)
|
|
||||||
}
|
|
||||||
// Remove codex-mini models per request.
|
|
||||||
expect(entry.model).not.toMatch(/codex-mini/i)
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertNoOpencodeProvider(entry: FallbackEntry): void {
|
|
||||||
expect(entry.providers).not.toContain("opencode")
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertNoProviderPrefixForNonNamespacedProviders(entry: FallbackEntry): void {
|
|
||||||
// For these providers, model IDs should not be written as "provider/model".
|
|
||||||
const nonNamespaced = ["quotio", "openai", "github-copilot", "minimax", "minimax-coding-plan"]
|
|
||||||
for (const provider of entry.providers) {
|
|
||||||
if (!nonNamespaced.includes(provider)) continue
|
|
||||||
expect(entry.model.startsWith(`${provider}/`)).toBe(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("AGENT_MODEL_REQUIREMENTS", () => {
|
describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||||
test("defines all 10 builtin agents", () => {
|
test("oracle has valid fallbackChain with gpt-5.2 as primary", () => {
|
||||||
expect(Object.keys(AGENT_MODEL_REQUIREMENTS).sort()).toEqual([
|
// given - oracle agent requirement
|
||||||
"atlas",
|
const oracle = AGENT_MODEL_REQUIREMENTS["oracle"]
|
||||||
"explore",
|
|
||||||
|
// when - accessing oracle requirement
|
||||||
|
// then - fallbackChain exists with gpt-5.2 as first entry
|
||||||
|
expect(oracle).toBeDefined()
|
||||||
|
expect(oracle.fallbackChain).toBeArray()
|
||||||
|
expect(oracle.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const primary = oracle.fallbackChain[0]
|
||||||
|
expect(primary.providers).toContain("openai")
|
||||||
|
expect(primary.model).toBe("gpt-5.2")
|
||||||
|
expect(primary.variant).toBe("high")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sisyphus has claude-opus-4-6 as primary and requiresAnyModel", () => {
|
||||||
|
// #given - sisyphus agent requirement
|
||||||
|
const sisyphus = AGENT_MODEL_REQUIREMENTS["sisyphus"]
|
||||||
|
|
||||||
|
// #when - accessing Sisyphus requirement
|
||||||
|
// #then - fallbackChain has claude-opus-4-6 first, big-pickle last
|
||||||
|
expect(sisyphus).toBeDefined()
|
||||||
|
expect(sisyphus.fallbackChain).toBeArray()
|
||||||
|
expect(sisyphus.fallbackChain).toHaveLength(4)
|
||||||
|
expect(sisyphus.requiresAnyModel).toBe(true)
|
||||||
|
|
||||||
|
const primary = sisyphus.fallbackChain[0]
|
||||||
|
expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode"])
|
||||||
|
expect(primary.model).toBe("claude-opus-4-6")
|
||||||
|
expect(primary.variant).toBe("max")
|
||||||
|
|
||||||
|
const last = sisyphus.fallbackChain[3]
|
||||||
|
expect(last.providers[0]).toBe("opencode")
|
||||||
|
expect(last.model).toBe("big-pickle")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("librarian has valid fallbackChain with gemini-3-flash as primary", () => {
|
||||||
|
// given - librarian agent requirement
|
||||||
|
const librarian = AGENT_MODEL_REQUIREMENTS["librarian"]
|
||||||
|
|
||||||
|
// when - accessing librarian requirement
|
||||||
|
// then - fallbackChain exists with gemini-3-flash as first entry
|
||||||
|
expect(librarian).toBeDefined()
|
||||||
|
expect(librarian.fallbackChain).toBeArray()
|
||||||
|
expect(librarian.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const primary = librarian.fallbackChain[0]
|
||||||
|
expect(primary.providers[0]).toBe("google")
|
||||||
|
expect(primary.model).toBe("gemini-3-flash")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("explore has valid fallbackChain with grok-code-fast-1 as primary", () => {
|
||||||
|
// given - explore agent requirement
|
||||||
|
const explore = AGENT_MODEL_REQUIREMENTS["explore"]
|
||||||
|
|
||||||
|
// when - accessing explore requirement
|
||||||
|
// then - fallbackChain: grok → minimax-free → haiku → nano
|
||||||
|
expect(explore).toBeDefined()
|
||||||
|
expect(explore.fallbackChain).toBeArray()
|
||||||
|
expect(explore.fallbackChain).toHaveLength(4)
|
||||||
|
|
||||||
|
const primary = explore.fallbackChain[0]
|
||||||
|
expect(primary.providers).toContain("github-copilot")
|
||||||
|
expect(primary.model).toBe("grok-code-fast-1")
|
||||||
|
|
||||||
|
const secondary = explore.fallbackChain[1]
|
||||||
|
expect(secondary.providers).toContain("opencode")
|
||||||
|
expect(secondary.model).toBe("minimax-m2.5-free")
|
||||||
|
|
||||||
|
const tertiary = explore.fallbackChain[2]
|
||||||
|
expect(tertiary.providers).toContain("anthropic")
|
||||||
|
expect(tertiary.model).toBe("claude-haiku-4-5")
|
||||||
|
|
||||||
|
const quaternary = explore.fallbackChain[3]
|
||||||
|
expect(quaternary.providers).toContain("opencode")
|
||||||
|
expect(quaternary.model).toBe("gpt-5-nano")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("multimodal-looker has valid fallbackChain with kimi-k2.5-free as primary", () => {
|
||||||
|
// given - multimodal-looker agent requirement
|
||||||
|
const multimodalLooker = AGENT_MODEL_REQUIREMENTS["multimodal-looker"]
|
||||||
|
|
||||||
|
// when - accessing multimodal-looker requirement
|
||||||
|
// then - fallbackChain exists with kimi-k2.5-free first, gpt-5-nano last
|
||||||
|
expect(multimodalLooker).toBeDefined()
|
||||||
|
expect(multimodalLooker.fallbackChain).toBeArray()
|
||||||
|
expect(multimodalLooker.fallbackChain).toHaveLength(5)
|
||||||
|
|
||||||
|
const primary = multimodalLooker.fallbackChain[0]
|
||||||
|
expect(primary.providers[0]).toBe("opencode")
|
||||||
|
expect(primary.model).toBe("kimi-k2.5-free")
|
||||||
|
|
||||||
|
const last = multimodalLooker.fallbackChain[4]
|
||||||
|
expect(last.providers).toEqual(["openai", "github-copilot", "opencode"])
|
||||||
|
expect(last.model).toBe("gpt-5-nano")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("prometheus has claude-opus-4-6 as primary", () => {
|
||||||
|
// #given - prometheus agent requirement
|
||||||
|
const prometheus = AGENT_MODEL_REQUIREMENTS["prometheus"]
|
||||||
|
|
||||||
|
// #when - accessing Prometheus requirement
|
||||||
|
// #then - claude-opus-4-6 is first
|
||||||
|
expect(prometheus).toBeDefined()
|
||||||
|
expect(prometheus.fallbackChain).toBeArray()
|
||||||
|
expect(prometheus.fallbackChain.length).toBeGreaterThan(1)
|
||||||
|
|
||||||
|
const primary = prometheus.fallbackChain[0]
|
||||||
|
expect(primary.model).toBe("claude-opus-4-6")
|
||||||
|
expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode"])
|
||||||
|
expect(primary.variant).toBe("max")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("metis has claude-opus-4-6 as primary", () => {
|
||||||
|
// #given - metis agent requirement
|
||||||
|
const metis = AGENT_MODEL_REQUIREMENTS["metis"]
|
||||||
|
|
||||||
|
// #when - accessing Metis requirement
|
||||||
|
// #then - claude-opus-4-6 is first
|
||||||
|
expect(metis).toBeDefined()
|
||||||
|
expect(metis.fallbackChain).toBeArray()
|
||||||
|
expect(metis.fallbackChain.length).toBeGreaterThan(1)
|
||||||
|
|
||||||
|
const primary = metis.fallbackChain[0]
|
||||||
|
expect(primary.model).toBe("claude-opus-4-6")
|
||||||
|
expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode"])
|
||||||
|
expect(primary.variant).toBe("max")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("momus has valid fallbackChain with gpt-5.2 as primary", () => {
|
||||||
|
// given - momus agent requirement
|
||||||
|
const momus = AGENT_MODEL_REQUIREMENTS["momus"]
|
||||||
|
|
||||||
|
// when - accessing Momus requirement
|
||||||
|
// then - fallbackChain exists with gpt-5.2 as first entry, variant medium
|
||||||
|
expect(momus).toBeDefined()
|
||||||
|
expect(momus.fallbackChain).toBeArray()
|
||||||
|
expect(momus.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const primary = momus.fallbackChain[0]
|
||||||
|
expect(primary.model).toBe("gpt-5.2")
|
||||||
|
expect(primary.variant).toBe("medium")
|
||||||
|
expect(primary.providers[0]).toBe("openai")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("atlas has valid fallbackChain with kimi-k2.5-free as primary", () => {
|
||||||
|
// given - atlas agent requirement
|
||||||
|
const atlas = AGENT_MODEL_REQUIREMENTS["atlas"]
|
||||||
|
|
||||||
|
// when - accessing Atlas requirement
|
||||||
|
// then - fallbackChain exists with kimi-k2.5-free as first entry
|
||||||
|
expect(atlas).toBeDefined()
|
||||||
|
expect(atlas.fallbackChain).toBeArray()
|
||||||
|
expect(atlas.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const primary = atlas.fallbackChain[0]
|
||||||
|
expect(primary.model).toBe("kimi-k2.5-free")
|
||||||
|
expect(primary.providers[0]).toBe("opencode")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("hephaestus requires openai/github-copilot/opencode provider", () => {
|
||||||
|
// #given - hephaestus agent requirement
|
||||||
|
const hephaestus = AGENT_MODEL_REQUIREMENTS["hephaestus"]
|
||||||
|
|
||||||
|
// #when - accessing hephaestus requirement
|
||||||
|
// #then - requiresProvider is set to openai, github-copilot, opencode (not requiresModel)
|
||||||
|
expect(hephaestus).toBeDefined()
|
||||||
|
expect(hephaestus.requiresProvider).toEqual(["openai", "github-copilot", "opencode"])
|
||||||
|
expect(hephaestus.requiresModel).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("all 10 builtin agents have valid fallbackChain arrays", () => {
|
||||||
|
// #given - list of 10 agent names
|
||||||
|
const expectedAgents = [
|
||||||
|
"sisyphus",
|
||||||
"hephaestus",
|
"hephaestus",
|
||||||
|
"oracle",
|
||||||
"librarian",
|
"librarian",
|
||||||
|
"explore",
|
||||||
|
"multimodal-looker",
|
||||||
|
"prometheus",
|
||||||
"metis",
|
"metis",
|
||||||
"momus",
|
"momus",
|
||||||
"multimodal-looker",
|
"atlas",
|
||||||
"oracle",
|
]
|
||||||
"prometheus",
|
|
||||||
"sisyphus",
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("sisyphus: 2nd fallback is quotio gpt-5.3-codex (high)", () => {
|
// when - checking AGENT_MODEL_REQUIREMENTS
|
||||||
const sisyphus = AGENT_MODEL_REQUIREMENTS["sisyphus"]
|
const definedAgents = Object.keys(AGENT_MODEL_REQUIREMENTS)
|
||||||
expect(sisyphus.requiresAnyModel).toBe(true)
|
|
||||||
expect(sisyphus.fallbackChain.length).toBeGreaterThan(2)
|
|
||||||
|
|
||||||
expect(sisyphus.fallbackChain[0]).toEqual({
|
// #then - all agents present with valid fallbackChain
|
||||||
providers: ["quotio"],
|
expect(definedAgents).toHaveLength(10)
|
||||||
model: "claude-opus-4-6",
|
for (const agent of expectedAgents) {
|
||||||
variant: "max",
|
const requirement = AGENT_MODEL_REQUIREMENTS[agent]
|
||||||
})
|
expect(requirement).toBeDefined()
|
||||||
|
expect(requirement.fallbackChain).toBeArray()
|
||||||
|
expect(requirement.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
expect(sisyphus.fallbackChain[1]).toEqual({
|
for (const entry of requirement.fallbackChain) {
|
||||||
providers: ["quotio"],
|
expect(entry.providers).toBeArray()
|
||||||
model: "gpt-5.3-codex",
|
expect(entry.providers.length).toBeGreaterThan(0)
|
||||||
variant: "high",
|
expect(typeof entry.model).toBe("string")
|
||||||
})
|
expect(entry.model.length).toBeGreaterThan(0)
|
||||||
})
|
}
|
||||||
|
}
|
||||||
test("explore: uses speed chain, includes rome, and gpt-5-mini is copilot-first", () => {
|
|
||||||
const explore = AGENT_MODEL_REQUIREMENTS["explore"]
|
|
||||||
expect(explore.fallbackChain.length).toBeGreaterThan(4)
|
|
||||||
expect(explore.fallbackChain[0].model).toBe("claude-haiku-4-5")
|
|
||||||
expect(explore.fallbackChain.some((e) => e.model === "iflow-rome-30ba3b")).toBe(true)
|
|
||||||
|
|
||||||
const gptMini = explore.fallbackChain.find((e) => e.model === "gpt-5-mini")
|
|
||||||
expect(gptMini).toBeDefined()
|
|
||||||
expect(gptMini!.providers[0]).toBe("github-copilot")
|
|
||||||
expect(gptMini!.variant).toBe("high")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("multimodal-looker: prefers gemini image model first", () => {
|
|
||||||
const multimodal = AGENT_MODEL_REQUIREMENTS["multimodal-looker"]
|
|
||||||
expect(multimodal.fallbackChain[0]).toEqual({
|
|
||||||
providers: ["quotio"],
|
|
||||||
model: "gemini-3-pro-image",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("includes NVIDIA NIM additions in at least one agent chain", () => {
|
|
||||||
const all = Object.values(AGENT_MODEL_REQUIREMENTS).flatMap((r) => r.fallbackChain)
|
|
||||||
expect(all.some((e) => e.providers.includes("nvidia") && e.model === "qwen/qwen3.5-397b-a17b")).toBe(true)
|
|
||||||
expect(all.some((e) => e.providers.includes("nvidia") && e.model === "stepfun-ai/step-3.5-flash")).toBe(true)
|
|
||||||
expect(all.some((e) => e.providers.includes("nvidia") && e.model === "bytedance/seed-oss-36b-instruct")).toBe(true)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("CATEGORY_MODEL_REQUIREMENTS", () => {
|
describe("CATEGORY_MODEL_REQUIREMENTS", () => {
|
||||||
test("defines all 8 categories", () => {
|
test("ultrabrain has valid fallbackChain with gpt-5.3-codex as primary", () => {
|
||||||
expect(Object.keys(CATEGORY_MODEL_REQUIREMENTS).sort()).toEqual([
|
// given - ultrabrain category requirement
|
||||||
"artistry",
|
|
||||||
"deep",
|
|
||||||
"quick",
|
|
||||||
"ultrabrain",
|
|
||||||
"unspecified-high",
|
|
||||||
"unspecified-low",
|
|
||||||
"visual-engineering",
|
|
||||||
"writing",
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("deep requires gpt-5.3-codex", () => {
|
|
||||||
expect(CATEGORY_MODEL_REQUIREMENTS["deep"].requiresModel).toBe("gpt-5.3-codex")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("quick uses the speed chain (haiku primary)", () => {
|
|
||||||
expect(CATEGORY_MODEL_REQUIREMENTS["quick"].fallbackChain[0].model).toBe("claude-haiku-4-5")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("ultrabrain starts with gpt-5.3-codex (high)", () => {
|
|
||||||
const ultrabrain = CATEGORY_MODEL_REQUIREMENTS["ultrabrain"]
|
const ultrabrain = CATEGORY_MODEL_REQUIREMENTS["ultrabrain"]
|
||||||
expect(ultrabrain.fallbackChain[0]).toEqual({
|
|
||||||
providers: ["quotio"],
|
// when - accessing ultrabrain requirement
|
||||||
model: "gpt-5.3-codex",
|
// then - fallbackChain exists with gpt-5.3-codex as first entry
|
||||||
|
expect(ultrabrain).toBeDefined()
|
||||||
|
expect(ultrabrain.fallbackChain).toBeArray()
|
||||||
|
expect(ultrabrain.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const primary = ultrabrain.fallbackChain[0]
|
||||||
|
expect(primary.variant).toBe("xhigh")
|
||||||
|
expect(primary.model).toBe("gpt-5.3-codex")
|
||||||
|
expect(primary.providers[0]).toBe("openai")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("deep has valid fallbackChain with gpt-5.3-codex as primary", () => {
|
||||||
|
// given - deep category requirement
|
||||||
|
const deep = CATEGORY_MODEL_REQUIREMENTS["deep"]
|
||||||
|
|
||||||
|
// when - accessing deep requirement
|
||||||
|
// then - fallbackChain exists with gpt-5.3-codex as first entry, medium variant
|
||||||
|
expect(deep).toBeDefined()
|
||||||
|
expect(deep.fallbackChain).toBeArray()
|
||||||
|
expect(deep.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const primary = deep.fallbackChain[0]
|
||||||
|
expect(primary.variant).toBe("medium")
|
||||||
|
expect(primary.model).toBe("gpt-5.3-codex")
|
||||||
|
expect(primary.providers[0]).toBe("openai")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("visual-engineering has valid fallbackChain with gemini-3-pro high as primary", () => {
|
||||||
|
// given - visual-engineering category requirement
|
||||||
|
const visualEngineering = CATEGORY_MODEL_REQUIREMENTS["visual-engineering"]
|
||||||
|
|
||||||
|
// when - accessing visual-engineering requirement
|
||||||
|
// then - fallbackChain: gemini-3-pro(high) → glm-5 → opus-4-6(max)
|
||||||
|
expect(visualEngineering).toBeDefined()
|
||||||
|
expect(visualEngineering.fallbackChain).toBeArray()
|
||||||
|
expect(visualEngineering.fallbackChain).toHaveLength(3)
|
||||||
|
|
||||||
|
const primary = visualEngineering.fallbackChain[0]
|
||||||
|
expect(primary.providers[0]).toBe("google")
|
||||||
|
expect(primary.model).toBe("gemini-3-pro")
|
||||||
|
expect(primary.variant).toBe("high")
|
||||||
|
|
||||||
|
const second = visualEngineering.fallbackChain[1]
|
||||||
|
expect(second.providers[0]).toBe("zai-coding-plan")
|
||||||
|
expect(second.model).toBe("glm-5")
|
||||||
|
|
||||||
|
const third = visualEngineering.fallbackChain[2]
|
||||||
|
expect(third.model).toBe("claude-opus-4-6")
|
||||||
|
expect(third.variant).toBe("max")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test("quick has valid fallbackChain with claude-haiku-4-5 as primary", () => {
|
||||||
|
// given - quick category requirement
|
||||||
|
const quick = CATEGORY_MODEL_REQUIREMENTS["quick"]
|
||||||
|
|
||||||
|
// when - accessing quick requirement
|
||||||
|
// then - fallbackChain exists with claude-haiku-4-5 as first entry
|
||||||
|
expect(quick).toBeDefined()
|
||||||
|
expect(quick.fallbackChain).toBeArray()
|
||||||
|
expect(quick.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const primary = quick.fallbackChain[0]
|
||||||
|
expect(primary.model).toBe("claude-haiku-4-5")
|
||||||
|
expect(primary.providers[0]).toBe("anthropic")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("unspecified-low has valid fallbackChain with claude-sonnet-4-6 as primary", () => {
|
||||||
|
// given - unspecified-low category requirement
|
||||||
|
const unspecifiedLow = CATEGORY_MODEL_REQUIREMENTS["unspecified-low"]
|
||||||
|
|
||||||
|
// when - accessing unspecified-low requirement
|
||||||
|
// then - fallbackChain exists with claude-sonnet-4-6 as first entry
|
||||||
|
expect(unspecifiedLow).toBeDefined()
|
||||||
|
expect(unspecifiedLow.fallbackChain).toBeArray()
|
||||||
|
expect(unspecifiedLow.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const primary = unspecifiedLow.fallbackChain[0]
|
||||||
|
expect(primary.model).toBe("claude-sonnet-4-6")
|
||||||
|
expect(primary.providers[0]).toBe("anthropic")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("unspecified-high has claude-opus-4-6 as primary", () => {
|
||||||
|
// #given - unspecified-high category requirement
|
||||||
|
const unspecifiedHigh = CATEGORY_MODEL_REQUIREMENTS["unspecified-high"]
|
||||||
|
|
||||||
|
// #when - accessing unspecified-high requirement
|
||||||
|
// #then - claude-opus-4-6 is first
|
||||||
|
expect(unspecifiedHigh).toBeDefined()
|
||||||
|
expect(unspecifiedHigh.fallbackChain).toBeArray()
|
||||||
|
expect(unspecifiedHigh.fallbackChain.length).toBeGreaterThan(1)
|
||||||
|
|
||||||
|
const primary = unspecifiedHigh.fallbackChain[0]
|
||||||
|
expect(primary.model).toBe("claude-opus-4-6")
|
||||||
|
expect(primary.variant).toBe("max")
|
||||||
|
expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("artistry has valid fallbackChain with gemini-3-pro as primary", () => {
|
||||||
|
// given - artistry category requirement
|
||||||
|
const artistry = CATEGORY_MODEL_REQUIREMENTS["artistry"]
|
||||||
|
|
||||||
|
// when - accessing artistry requirement
|
||||||
|
// then - fallbackChain exists with gemini-3-pro as first entry
|
||||||
|
expect(artistry).toBeDefined()
|
||||||
|
expect(artistry.fallbackChain).toBeArray()
|
||||||
|
expect(artistry.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const primary = artistry.fallbackChain[0]
|
||||||
|
expect(primary.model).toBe("gemini-3-pro")
|
||||||
|
expect(primary.variant).toBe("high")
|
||||||
|
expect(primary.providers[0]).toBe("google")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("writing has valid fallbackChain with gemini-3-flash as primary", () => {
|
||||||
|
// given - writing category requirement
|
||||||
|
const writing = CATEGORY_MODEL_REQUIREMENTS["writing"]
|
||||||
|
|
||||||
|
// when - accessing writing requirement
|
||||||
|
// then - fallbackChain: gemini-3-flash → claude-sonnet-4-6
|
||||||
|
expect(writing).toBeDefined()
|
||||||
|
expect(writing.fallbackChain).toBeArray()
|
||||||
|
expect(writing.fallbackChain).toHaveLength(2)
|
||||||
|
|
||||||
|
const primary = writing.fallbackChain[0]
|
||||||
|
expect(primary.model).toBe("gemini-3-flash")
|
||||||
|
expect(primary.providers[0]).toBe("google")
|
||||||
|
|
||||||
|
const second = writing.fallbackChain[1]
|
||||||
|
expect(second.model).toBe("claude-sonnet-4-6")
|
||||||
|
expect(second.providers[0]).toBe("anthropic")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("all 8 categories have valid fallbackChain arrays", () => {
|
||||||
|
// given - list of 8 category names
|
||||||
|
const expectedCategories = [
|
||||||
|
"visual-engineering",
|
||||||
|
"ultrabrain",
|
||||||
|
"deep",
|
||||||
|
"artistry",
|
||||||
|
"quick",
|
||||||
|
"unspecified-low",
|
||||||
|
"unspecified-high",
|
||||||
|
"writing",
|
||||||
|
]
|
||||||
|
|
||||||
|
// when - checking CATEGORY_MODEL_REQUIREMENTS
|
||||||
|
const definedCategories = Object.keys(CATEGORY_MODEL_REQUIREMENTS)
|
||||||
|
|
||||||
|
// then - all categories present with valid fallbackChain
|
||||||
|
expect(definedCategories).toHaveLength(8)
|
||||||
|
for (const category of expectedCategories) {
|
||||||
|
const requirement = CATEGORY_MODEL_REQUIREMENTS[category]
|
||||||
|
expect(requirement).toBeDefined()
|
||||||
|
expect(requirement.fallbackChain).toBeArray()
|
||||||
|
expect(requirement.fallbackChain.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
for (const entry of requirement.fallbackChain) {
|
||||||
|
expect(entry.providers).toBeArray()
|
||||||
|
expect(entry.providers.length).toBeGreaterThan(0)
|
||||||
|
expect(typeof entry.model).toBe("string")
|
||||||
|
expect(entry.model.length).toBeGreaterThan(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("FallbackEntry type", () => {
|
||||||
|
test("FallbackEntry structure is correct", () => {
|
||||||
|
// given - a valid FallbackEntry object
|
||||||
|
const entry: FallbackEntry = {
|
||||||
|
providers: ["anthropic", "github-copilot", "opencode"],
|
||||||
|
model: "claude-opus-4-6",
|
||||||
variant: "high",
|
variant: "high",
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("ModelRequirements invariants", () => {
|
|
||||||
test("all entries have non-empty providers and a non-empty model", () => {
|
|
||||||
for (const entry of flattenChains()) {
|
|
||||||
expect(entry.providers.length).toBeGreaterThan(0)
|
|
||||||
expect(typeof entry.model).toBe("string")
|
|
||||||
expect(entry.model.length).toBeGreaterThan(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when - accessing properties
|
||||||
|
// then - all properties are accessible
|
||||||
|
expect(entry.providers).toEqual(["anthropic", "github-copilot", "opencode"])
|
||||||
|
expect(entry.model).toBe("claude-opus-4-6")
|
||||||
|
expect(entry.variant).toBe("high")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("no entry uses opencode provider and no excluded models are present", () => {
|
test("FallbackEntry variant is optional", () => {
|
||||||
for (const entry of flattenChains()) {
|
// given - a FallbackEntry without variant
|
||||||
assertNoOpencodeProvider(entry)
|
const entry: FallbackEntry = {
|
||||||
assertNoExcludedModels(entry)
|
providers: ["opencode", "anthropic"],
|
||||||
assertNoProviderPrefixForNonNamespacedProviders(entry)
|
model: "big-pickle",
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("Type sanity", () => {
|
// when - accessing variant
|
||||||
test("FallbackEntry.variant is optional", () => {
|
// then - variant is undefined
|
||||||
const entry: FallbackEntry = { providers: ["quotio"], model: "claude-haiku-4-5" }
|
|
||||||
expect(entry.variant).toBeUndefined()
|
expect(entry.variant).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("ModelRequirement.variant is optional", () => {
|
describe("ModelRequirement type", () => {
|
||||||
const req: ModelRequirement = { fallbackChain: [{ providers: ["quotio"], model: "claude-haiku-4-5" }] }
|
test("ModelRequirement structure with fallbackChain is correct", () => {
|
||||||
expect(req.variant).toBeUndefined()
|
// given - a valid ModelRequirement object
|
||||||
|
const requirement: ModelRequirement = {
|
||||||
|
fallbackChain: [
|
||||||
|
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-6", variant: "max" },
|
||||||
|
{ providers: ["openai", "github-copilot"], model: "gpt-5.2", variant: "high" },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when - accessing properties
|
||||||
|
// then - fallbackChain is accessible with correct structure
|
||||||
|
expect(requirement.fallbackChain).toBeArray()
|
||||||
|
expect(requirement.fallbackChain).toHaveLength(2)
|
||||||
|
expect(requirement.fallbackChain[0].model).toBe("claude-opus-4-6")
|
||||||
|
expect(requirement.fallbackChain[1].model).toBe("gpt-5.2")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ModelRequirement variant is optional", () => {
|
||||||
|
// given - a ModelRequirement without top-level variant
|
||||||
|
const requirement: ModelRequirement = {
|
||||||
|
fallbackChain: [{ providers: ["opencode"], model: "big-pickle" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when - accessing variant
|
||||||
|
// then - variant is undefined
|
||||||
|
expect(requirement.variant).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("no model in fallbackChain has provider prefix", () => {
|
||||||
|
// given - all agent and category requirements
|
||||||
|
const allRequirements = [
|
||||||
|
...Object.values(AGENT_MODEL_REQUIREMENTS),
|
||||||
|
...Object.values(CATEGORY_MODEL_REQUIREMENTS),
|
||||||
|
]
|
||||||
|
|
||||||
|
// when - checking each model in fallbackChain
|
||||||
|
// then - none contain "/" (provider prefix)
|
||||||
|
for (const req of allRequirements) {
|
||||||
|
for (const entry of req.fallbackChain) {
|
||||||
|
expect(entry.model).not.toContain("/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("all fallbackChain entries have non-empty providers array", () => {
|
||||||
|
// given - all agent and category requirements
|
||||||
|
const allRequirements = [
|
||||||
|
...Object.values(AGENT_MODEL_REQUIREMENTS),
|
||||||
|
...Object.values(CATEGORY_MODEL_REQUIREMENTS),
|
||||||
|
]
|
||||||
|
|
||||||
|
// when - checking each entry in fallbackChain
|
||||||
|
// then - all have non-empty providers array
|
||||||
|
for (const req of allRequirements) {
|
||||||
|
for (const entry of req.fallbackChain) {
|
||||||
|
expect(entry.providers).toBeArray()
|
||||||
|
expect(entry.providers.length).toBeGreaterThan(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("requiresModel field in categories", () => {
|
||||||
|
test("deep category has requiresModel set to gpt-5.3-codex", () => {
|
||||||
|
// given
|
||||||
|
const deep = CATEGORY_MODEL_REQUIREMENTS["deep"]
|
||||||
|
|
||||||
|
// when / #then
|
||||||
|
expect(deep.requiresModel).toBe("gpt-5.3-codex")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("artistry category has requiresModel set to gemini-3-pro", () => {
|
||||||
|
// given
|
||||||
|
const artistry = CATEGORY_MODEL_REQUIREMENTS["artistry"]
|
||||||
|
|
||||||
|
// when / #then
|
||||||
|
expect(artistry.requiresModel).toBe("gemini-3-pro")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -12,204 +12,141 @@ export type ModelRequirement = {
|
|||||||
requiresProvider?: string[] // If set, only activates when any of these providers is connected
|
requiresProvider?: string[] // If set, only activates when any of these providers is connected
|
||||||
}
|
}
|
||||||
|
|
||||||
function fb(providers: string[] | string, model: string, variant?: string): FallbackEntry {
|
|
||||||
return {
|
|
||||||
providers: Array.isArray(providers) ? providers : [providers],
|
|
||||||
model,
|
|
||||||
...(variant !== undefined ? { variant } : {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dedupeChain(chain: FallbackEntry[]): FallbackEntry[] {
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const result: FallbackEntry[] = []
|
|
||||||
for (const entry of chain) {
|
|
||||||
const key = `${entry.model}:${entry.variant ?? ""}`
|
|
||||||
if (seen.has(key)) continue
|
|
||||||
seen.add(key)
|
|
||||||
result.push(entry)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider preference rules:
|
|
||||||
// - Never use the paid `opencode` provider as an automatic fallback.
|
|
||||||
// - Prefer `quotio` when the same model exists across multiple providers.
|
|
||||||
// - Prefer `github-copilot` first for `gpt-5-mini` (unlimited), fall back to `quotio`.
|
|
||||||
// Note: user requested "Quotio-first" and to avoid the OpenCode provider; we keep runtime fallbacks on
|
|
||||||
// `quotio` + `nvidia` (+ `github-copilot` for unlimited GPT mini) unless explicitly requested otherwise.
|
|
||||||
const P_GPT: string[] = ["quotio"]
|
|
||||||
const P_GPT_MINI: string[] = ["github-copilot", "quotio"]
|
|
||||||
|
|
||||||
// Benchmark-driven ordering (user-provided table + NVIDIA NIM docs), tuned per-agent for quality vs speed.
|
|
||||||
|
|
||||||
const SPEED_CHAIN: FallbackEntry[] = [
|
|
||||||
fb("quotio", "claude-haiku-4-5"), fb("quotio", "oswe-vscode-prime"),
|
|
||||||
fb(P_GPT_MINI, "gpt-5-mini", "high"), fb(P_GPT_MINI, "gpt-4.1"),
|
|
||||||
fb("nvidia", "nvidia/nemotron-3-nano-30b-a3b"), fb("quotio", "iflow-rome-30ba3b"),
|
|
||||||
fb("minimax-coding-plan", "MiniMax-M2.5"), fb("nvidia", "bytedance/seed-oss-36b-instruct"),
|
|
||||||
fb("quotio", "claude-sonnet-4-5"),
|
|
||||||
]
|
|
||||||
|
|
||||||
const QUALITY_CODING_CHAIN: FallbackEntry[] = [
|
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
|
||||||
fb("nvidia", "stepfun-ai/step-3.5-flash"),
|
|
||||||
fb("nvidia", "qwen/qwen3.5-397b-a17b"),
|
|
||||||
fb("quotio", "glm-5"),
|
|
||||||
fb("nvidia", "z-ai/glm5"),
|
|
||||||
fb("quotio", "deepseek-v3.2-reasoner"),
|
|
||||||
fb("quotio", "deepseek-r1"),
|
|
||||||
fb("nvidia", "deepseek-ai/deepseek-r1"),
|
|
||||||
fb("quotio", "qwen3-235b-a22b-thinking-2507"),
|
|
||||||
fb("nvidia", "qwen/qwen3-next-80b-a3b-thinking"),
|
|
||||||
fb("nvidia", "qwen/qwen3-coder-480b-a35b-instruct"),
|
|
||||||
fb("nvidia", "bytedance/seed-oss-36b-instruct"),
|
|
||||||
fb("quotio", "kimi-k2-thinking"),
|
|
||||||
fb("quotio", "kimi-k2.5"),
|
|
||||||
fb("nvidia", "moonshotai/kimi-k2.5"),
|
|
||||||
fb("minimax-coding-plan", "MiniMax-M2.5"),
|
|
||||||
fb("minimax-coding-plan", "MiniMax-M2.5-highspeed"),
|
|
||||||
fb("minimax", "MiniMax-M2.5"),
|
|
||||||
fb("quotio", "minimax-m2.5"),
|
|
||||||
fb("quotio", "claude-sonnet-4-5-thinking"),
|
|
||||||
]
|
|
||||||
|
|
||||||
export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||||
sisyphus: {
|
sisyphus: {
|
||||||
fallbackChain: [
|
fallbackChain: [
|
||||||
// 1st fallback: switch away from Opus Thinking to the non-thinking model (often more available).
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||||
fb("quotio", "claude-opus-4-6", "max"),
|
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||||
// 2nd fallback: user-requested.
|
{ providers: ["zai-coding-plan", "opencode"], model: "glm-5" },
|
||||||
fb("quotio", "gpt-5.3-codex", "high"),
|
{ providers: ["opencode"], model: "big-pickle" },
|
||||||
...QUALITY_CODING_CHAIN,
|
|
||||||
...SPEED_CHAIN,
|
|
||||||
],
|
],
|
||||||
requiresAnyModel: true,
|
requiresAnyModel: true,
|
||||||
},
|
},
|
||||||
hephaestus: {
|
hephaestus: {
|
||||||
fallbackChain: [
|
fallbackChain: [
|
||||||
fb("quotio", "gpt-5.3-codex", "high"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.3-codex", variant: "medium" },
|
||||||
...QUALITY_CODING_CHAIN,
|
|
||||||
],
|
],
|
||||||
requiresAnyModel: true,
|
requiresProvider: ["openai", "github-copilot", "opencode"],
|
||||||
},
|
},
|
||||||
oracle: {
|
oracle: {
|
||||||
fallbackChain: dedupeChain([
|
|
||||||
fb("quotio", "gpt-5.3-codex", "high"),
|
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
|
||||||
fb("quotio", "claude-sonnet-4-5-thinking"),
|
|
||||||
...QUALITY_CODING_CHAIN,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
librarian: {
|
|
||||||
fallbackChain: [
|
fallbackChain: [
|
||||||
fb("quotio", "claude-sonnet-4-5"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||||
...SPEED_CHAIN,
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" },
|
||||||
...QUALITY_CODING_CHAIN,
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
librarian: {
|
||||||
|
fallbackChain: [
|
||||||
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||||
|
{ providers: ["opencode"], model: "minimax-m2.5-free" },
|
||||||
|
{ providers: ["opencode"], model: "big-pickle" },
|
||||||
|
],
|
||||||
|
},
|
||||||
explore: {
|
explore: {
|
||||||
fallbackChain: SPEED_CHAIN,
|
fallbackChain: [
|
||||||
|
{ providers: ["github-copilot"], model: "grok-code-fast-1" },
|
||||||
|
{ providers: ["opencode"], model: "minimax-m2.5-free" },
|
||||||
|
{ providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" },
|
||||||
|
{ providers: ["opencode"], model: "gpt-5-nano" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"multimodal-looker": {
|
"multimodal-looker": {
|
||||||
fallbackChain: [
|
fallbackChain: [
|
||||||
fb("quotio", "gemini-3-pro-image"),
|
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||||
fb("quotio", "gemini-3-pro-high"),
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||||
fb("quotio", "gemini-3-flash"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||||
fb("quotio", "kimi-k2.5"),
|
{ providers: ["zai-coding-plan"], model: "glm-4.6v" },
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5-nano" },
|
||||||
fb("quotio", "claude-sonnet-4-5-thinking"),
|
|
||||||
fb("quotio", "claude-haiku-4-5"),
|
|
||||||
fb("quotio", "gpt-5-nano"),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
prometheus: {
|
prometheus: {
|
||||||
fallbackChain: dedupeChain([
|
fallbackChain: [
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||||
fb("quotio", "gpt-5.3-codex", "high"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||||
fb("quotio", "claude-sonnet-4-5-thinking"),
|
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||||
...QUALITY_CODING_CHAIN,
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||||
]),
|
],
|
||||||
},
|
},
|
||||||
metis: {
|
metis: {
|
||||||
fallbackChain: dedupeChain([
|
fallbackChain: [
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||||
fb("quotio", "gpt-5.3-codex", "high"),
|
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||||
fb("quotio", "claude-sonnet-4-5-thinking"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||||
...QUALITY_CODING_CHAIN,
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" },
|
||||||
]),
|
],
|
||||||
},
|
},
|
||||||
momus: {
|
momus: {
|
||||||
fallbackChain: dedupeChain([
|
fallbackChain: [
|
||||||
fb("quotio", "gpt-5.3-codex", "high"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "medium" },
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||||
...QUALITY_CODING_CHAIN,
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" },
|
||||||
]),
|
],
|
||||||
},
|
},
|
||||||
atlas: {
|
atlas: {
|
||||||
fallbackChain: dedupeChain([
|
fallbackChain: [
|
||||||
fb("quotio", "claude-sonnet-4-5-thinking"),
|
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" },
|
||||||
fb("quotio", "gpt-5.3-codex", "medium"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||||
...QUALITY_CODING_CHAIN,
|
],
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||||
"visual-engineering": {
|
"visual-engineering": {
|
||||||
fallbackChain: [
|
fallbackChain: [
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" },
|
||||||
fb("quotio", "gemini-3-pro-image"),
|
{ providers: ["zai-coding-plan", "opencode"], model: "glm-5" },
|
||||||
fb("quotio", "kimi-k2-thinking"),
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||||
fb("quotio", "kimi-k2.5"),
|
|
||||||
fb("quotio", "claude-sonnet-4-5-thinking"),
|
|
||||||
fb("quotio", "gpt-5.3-codex", "medium"),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ultrabrain: {
|
ultrabrain: {
|
||||||
fallbackChain: dedupeChain([
|
fallbackChain: [
|
||||||
fb("quotio", "gpt-5.3-codex", "high"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.3-codex", variant: "xhigh" },
|
||||||
...QUALITY_CODING_CHAIN,
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" },
|
||||||
]),
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
deep: {
|
deep: {
|
||||||
fallbackChain: [
|
fallbackChain: [
|
||||||
fb("quotio", "gpt-5.3-codex", "medium"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.3-codex", variant: "medium" },
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||||
fb("quotio", "claude-sonnet-4-5-thinking"),
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" },
|
||||||
...QUALITY_CODING_CHAIN,
|
|
||||||
],
|
],
|
||||||
requiresModel: "gpt-5.3-codex",
|
requiresModel: "gpt-5.3-codex",
|
||||||
},
|
},
|
||||||
artistry: {
|
artistry: {
|
||||||
fallbackChain: [
|
fallbackChain: [
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" },
|
||||||
fb("quotio", "claude-sonnet-4-5-thinking"),
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||||
fb("quotio", "claude-sonnet-4-5"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||||
],
|
],
|
||||||
requiresModel: "claude-opus-4-6",
|
requiresModel: "gemini-3-pro",
|
||||||
},
|
},
|
||||||
quick: {
|
quick: {
|
||||||
fallbackChain: SPEED_CHAIN,
|
fallbackChain: [
|
||||||
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" },
|
||||||
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||||
|
{ providers: ["opencode"], model: "gpt-5-nano" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"unspecified-low": {
|
"unspecified-low": {
|
||||||
fallbackChain: SPEED_CHAIN,
|
fallbackChain: [
|
||||||
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" },
|
||||||
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.3-codex", variant: "medium" },
|
||||||
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
fallbackChain: dedupeChain([
|
fallbackChain: [
|
||||||
fb("quotio", "claude-opus-4-6-thinking"),
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||||
fb("quotio", "gpt-5.3-codex", "high"),
|
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||||
...QUALITY_CODING_CHAIN,
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||||
]),
|
],
|
||||||
},
|
},
|
||||||
writing: {
|
writing: {
|
||||||
fallbackChain: [
|
fallbackChain: [
|
||||||
fb("quotio", "claude-sonnet-4-5"),
|
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||||
fb("quotio", "glm-5"),
|
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" },
|
||||||
fb("quotio", "kimi-k2.5"),
|
|
||||||
fb("quotio", "claude-haiku-4-5"),
|
|
||||||
fb("quotio", "gemini-3-flash"),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -550,21 +550,21 @@ describe("resolveModelWithFallback", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("falls through to system default when no provider in fallback is connected", () => {
|
test("falls through to system default when no provider in fallback is connected", () => {
|
||||||
// given - user only has quotio connected, but fallback chain has anthropic/opencode
|
// given - user only has anthropic connected, but fallback chain has openai/opencode
|
||||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["quotio"])
|
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["anthropic"])
|
||||||
const input: ExtendedModelResolutionInput = {
|
const input: ExtendedModelResolutionInput = {
|
||||||
fallbackChain: [
|
fallbackChain: [
|
||||||
{ providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" },
|
{ providers: ["openai", "opencode"], model: "claude-haiku-4-5" },
|
||||||
],
|
],
|
||||||
availableModels: new Set(),
|
availableModels: new Set(),
|
||||||
systemDefaultModel: "quotio/claude-opus-4-6-20251101",
|
systemDefaultModel: "anthropic/claude-opus-4-6-20251101",
|
||||||
}
|
}
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = resolveModelWithFallback(input)
|
const result = resolveModelWithFallback(input)
|
||||||
|
|
||||||
// then - no provider in fallback is connected, fall through to system default
|
// then - no provider in fallback is connected, fall through to system default
|
||||||
expect(result!.model).toBe("quotio/claude-opus-4-6-20251101")
|
expect(result!.model).toBe("anthropic/claude-opus-4-6-20251101")
|
||||||
expect(result!.source).toBe("system-default")
|
expect(result!.source).toBe("system-default")
|
||||||
cacheSpy.mockRestore()
|
cacheSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -19,7 +19,7 @@ describe("session-model-state", () => {
|
|||||||
test("clears a session model", () => {
|
test("clears a session model", () => {
|
||||||
//#given
|
//#given
|
||||||
const sessionID = "ses_clear"
|
const sessionID = "ses_clear"
|
||||||
setSessionModel(sessionID, { providerID: "quotio", modelID: "gpt-5.3-codex" })
|
setSessionModel(sessionID, { providerID: "anthropic", modelID: "gpt-5.3-codex" })
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
clearSessionModel(sessionID)
|
clearSessionModel(sessionID)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user