fix(cli): include fallback_models when writing agent model config (#3144)
Installer now generates fallback_models array alongside explicit model overrides, preserving the fallback chain. 46 tests pass, tsc clean. Closes #3144
This commit is contained in:
@@ -915,6 +915,14 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"model": "opencode/claude-sonnet-4-6",
|
||||
},
|
||||
"explore": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "opencode/minimax-m2.7",
|
||||
},
|
||||
{
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
@@ -1132,6 +1140,14 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"model": "opencode/claude-sonnet-4-6",
|
||||
},
|
||||
"explore": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "opencode/minimax-m2.7",
|
||||
},
|
||||
{
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
@@ -1353,6 +1369,11 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"model": "github-copilot/claude-sonnet-4.6",
|
||||
},
|
||||
"explore": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "github-copilot/grok-code-fast-1",
|
||||
},
|
||||
],
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"hephaestus": {
|
||||
@@ -1534,6 +1555,11 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"model": "github-copilot/claude-sonnet-4.6",
|
||||
},
|
||||
"explore": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "github-copilot/grok-code-fast-1",
|
||||
},
|
||||
],
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"hephaestus": {
|
||||
@@ -1842,6 +1868,17 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"model": "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
"explore": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "opencode/minimax-m2.7",
|
||||
},
|
||||
{
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
{
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
@@ -2114,6 +2151,11 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"model": "github-copilot/claude-sonnet-4.6",
|
||||
},
|
||||
"explore": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "github-copilot/grok-code-fast-1",
|
||||
},
|
||||
],
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"hephaestus": {
|
||||
@@ -2353,6 +2395,11 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"librarian": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
],
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
@@ -2573,6 +2620,17 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"model": "github-copilot/claude-sonnet-4.6",
|
||||
},
|
||||
"explore": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "github-copilot/grok-code-fast-1",
|
||||
},
|
||||
{
|
||||
"model": "opencode/minimax-m2.7",
|
||||
},
|
||||
{
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
@@ -2586,6 +2644,17 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "opencode/minimax-m2.7-highspeed",
|
||||
},
|
||||
{
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
{
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
@@ -2949,6 +3018,20 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"model": "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
"explore": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "github-copilot/grok-code-fast-1",
|
||||
},
|
||||
{
|
||||
"model": "opencode/minimax-m2.7",
|
||||
},
|
||||
{
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
{
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
@@ -2966,6 +3049,20 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "opencode/minimax-m2.7-highspeed",
|
||||
},
|
||||
{
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
{
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
{
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
@@ -3472,6 +3569,20 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"model": "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
"explore": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "github-copilot/grok-code-fast-1",
|
||||
},
|
||||
{
|
||||
"model": "opencode/minimax-m2.7",
|
||||
},
|
||||
{
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
{
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
@@ -3489,6 +3600,20 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"fallback_models": [
|
||||
{
|
||||
"model": "opencode/minimax-m2.7-highspeed",
|
||||
},
|
||||
{
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
{
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
{
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
|
||||
@@ -549,6 +549,58 @@ describe("generateModelConfig", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("special-case agents include fallback_models", () => {
|
||||
test("explore includes fallback_models when Copilot and Claude are both available", () => {
|
||||
// #given both Copilot and Claude are available
|
||||
const config = createConfig({ hasCopilot: true, hasClaude: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should have fallback_models from the remaining chain entries
|
||||
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
|
||||
expect(result.agents?.explore?.fallback_models).toBeDefined()
|
||||
expect(result.agents?.explore?.fallback_models?.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test("explore omits fallback_models when only one provider matches chain entries", () => {
|
||||
// #given only Claude is available
|
||||
const config = createConfig({ hasClaude: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should not have fallback_models (only one chain entry matches)
|
||||
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
|
||||
expect(result.agents?.explore?.fallback_models).toBeUndefined()
|
||||
})
|
||||
|
||||
test("librarian includes fallback_models when opencode-go and Claude are both available", () => {
|
||||
// #given opencode-go and Claude are available
|
||||
const config = createConfig({ hasOpencodeGo: true, hasClaude: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then librarian should have fallback_models
|
||||
expect(result.agents?.librarian?.model).toBe("opencode-go/minimax-m2.7")
|
||||
expect(result.agents?.librarian?.fallback_models).toBeDefined()
|
||||
expect(result.agents?.librarian?.fallback_models?.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test("librarian omits fallback_models when only one provider matches", () => {
|
||||
// #given only opencode-go is available
|
||||
const config = createConfig({ hasOpencodeGo: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then librarian should not have fallback_models
|
||||
expect(result.agents?.librarian?.model).toBe("opencode-go/minimax-m2.7")
|
||||
expect(result.agents?.librarian?.fallback_models).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("schema URL", () => {
|
||||
test("always includes correct schema URL", () => {
|
||||
// #given any config
|
||||
|
||||
+41
-11
@@ -37,22 +37,29 @@ function toFallbackModelObject(entry: FallbackEntry, provider: string): Fallback
|
||||
}
|
||||
}
|
||||
|
||||
function attachFallbackModels<T extends AgentConfig | CategoryConfig>(
|
||||
config: T,
|
||||
function collectAvailableFallbacks(
|
||||
fallbackChain: FallbackEntry[],
|
||||
availability: ReturnType<typeof toProviderAvailability>,
|
||||
): T {
|
||||
): FallbackModelObject[] {
|
||||
const expandedFallbacks = fallbackChain.flatMap((entry) =>
|
||||
entry.providers
|
||||
.filter((provider) => isProviderAvailable(provider, availability))
|
||||
.map((provider) => toFallbackModelObject(entry, provider))
|
||||
)
|
||||
const uniqueFallbacks = expandedFallbacks.filter((entry, index, allEntries) =>
|
||||
return expandedFallbacks.filter((entry, index, allEntries) =>
|
||||
allEntries.findIndex((candidate) =>
|
||||
candidate.model === entry.model &&
|
||||
candidate.variant === entry.variant
|
||||
) === index
|
||||
)
|
||||
}
|
||||
|
||||
function attachFallbackModels<T extends AgentConfig | CategoryConfig>(
|
||||
config: T,
|
||||
fallbackChain: FallbackEntry[],
|
||||
availability: ReturnType<typeof toProviderAvailability>,
|
||||
): T {
|
||||
const uniqueFallbacks = collectAvailableFallbacks(fallbackChain, availability)
|
||||
const primaryIndex = uniqueFallbacks.findIndex((entry) => entry.model === config.model)
|
||||
if (primaryIndex === -1) {
|
||||
return config
|
||||
@@ -69,6 +76,23 @@ function attachFallbackModels<T extends AgentConfig | CategoryConfig>(
|
||||
}
|
||||
}
|
||||
|
||||
function attachAllFallbackModels<T extends AgentConfig | CategoryConfig>(
|
||||
config: T,
|
||||
fallbackChain: FallbackEntry[],
|
||||
availability: ReturnType<typeof toProviderAvailability>,
|
||||
): T {
|
||||
const uniqueFallbacks = collectAvailableFallbacks(fallbackChain, availability)
|
||||
const fallbackModels = uniqueFallbacks.filter((entry) => entry.model !== config.model)
|
||||
if (fallbackModels.length === 0) {
|
||||
return config
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
fallback_models: fallbackModels,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
@@ -101,26 +125,32 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
|
||||
for (const [role, req] of Object.entries(CLI_AGENT_MODEL_REQUIREMENTS)) {
|
||||
if (role === "librarian") {
|
||||
let agentConfig: AgentConfig | undefined
|
||||
if (avail.opencodeGo) {
|
||||
agents[role] = { model: "opencode-go/minimax-m2.7" }
|
||||
agentConfig = { model: "opencode-go/minimax-m2.7" }
|
||||
} else if (avail.zai) {
|
||||
agents[role] = { model: ZAI_MODEL }
|
||||
agentConfig = { model: ZAI_MODEL }
|
||||
}
|
||||
if (agentConfig) {
|
||||
agents[role] = attachAllFallbackModels(agentConfig, req.fallbackChain, avail)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (role === "explore") {
|
||||
let agentConfig: AgentConfig
|
||||
if (avail.native.claude) {
|
||||
agents[role] = { model: "anthropic/claude-haiku-4-5" }
|
||||
agentConfig = { model: "anthropic/claude-haiku-4-5" }
|
||||
} else if (avail.opencodeZen) {
|
||||
agents[role] = { model: "opencode/claude-haiku-4-5" }
|
||||
agentConfig = { model: "opencode/claude-haiku-4-5" }
|
||||
} else if (avail.opencodeGo) {
|
||||
agents[role] = { model: "opencode-go/minimax-m2.7" }
|
||||
agentConfig = { model: "opencode-go/minimax-m2.7" }
|
||||
} else if (avail.copilot) {
|
||||
agents[role] = { model: "github-copilot/gpt-5-mini" }
|
||||
agentConfig = { model: "github-copilot/gpt-5-mini" }
|
||||
} else {
|
||||
agents[role] = { model: "opencode/gpt-5-nano" }
|
||||
agentConfig = { model: "opencode/gpt-5-nano" }
|
||||
}
|
||||
agents[role] = attachAllFallbackModels(agentConfig, req.fallbackChain, avail)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user