fix(cli): preserve installer fallback chains in generated config (#3144)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-06 13:17:38 +09:00
parent abda3c52f0
commit 57b152994f
3 changed files with 87 additions and 4 deletions
@@ -98,6 +98,41 @@ describe("generateOmoConfig - model fallback system", () => {
expect((result.agents as Record<string, { model: string }>)['multimodal-looker'].model).toBe("openai/gpt-5.4")
})
test("adds fallback_models when multiple providers are available", () => {
//#given
const config: InstallConfig = {
hasClaude: true,
isMax20: false,
hasOpenAI: true,
hasGemini: false,
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
hasOpencodeGo: false,
}
//#when
const result = generateOmoConfig(config)
const agents = result.agents as Record<string, {
model: string
variant?: string
fallback_models?: Array<{ model: string; variant?: string }>
}>
const categories = result.categories as Record<string, {
model: string
variant?: string
fallback_models?: Array<{ model: string; variant?: string }>
}>
//#then
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-6")
expect(agents.sisyphus.fallback_models?.map((entry) => entry.model)).toContain("openai/gpt-5.4")
expect(agents.sisyphus.fallback_models?.find((entry) => entry.model === "openai/gpt-5.4")?.variant).toBe("medium")
expect(categories.deep.model).toBe("openai/gpt-5.4")
expect(categories.deep.fallback_models?.map((entry) => entry.model)).toContain("anthropic/claude-opus-4-6")
})
test("uses haiku for explore when Claude max20", () => {
//#given
const config: InstallConfig = {
+4
View File
@@ -1,3 +1,5 @@
import type { FallbackModelObject } from "../config/schema/fallback-models"
export interface ProviderAvailability {
native: {
claude: boolean
@@ -15,11 +17,13 @@ kimiForCoding: boolean
export interface AgentConfig {
model: string
variant?: string
fallback_models?: FallbackModelObject[]
}
export interface CategoryConfig {
model: string
variant?: string
fallback_models?: FallbackModelObject[]
}
export interface GeneratedOmoConfig {
+48 -4
View File
@@ -2,11 +2,13 @@ import {
CLI_AGENT_MODEL_REQUIREMENTS,
CLI_CATEGORY_MODEL_REQUIREMENTS,
} from "./model-fallback-requirements"
import type { FallbackModelObject } from "../config/schema/fallback-models"
import type { FallbackEntry } from "../shared/model-requirements"
import type { InstallConfig } from "./types"
import type { AgentConfig, CategoryConfig, GeneratedOmoConfig } from "./model-fallback-types"
import { applyOpenAiOnlyModelCatalog, isOpenAiOnlyAvailability } from "./openai-only-model-catalog"
import { toProviderAvailability } from "./provider-availability"
import { isProviderAvailable, toProviderAvailability } from "./provider-availability"
import {
getSisyphusFallbackChain,
isAnyFallbackEntryAvailable,
@@ -14,6 +16,7 @@ import {
isRequiredProviderAvailable,
resolveModelFromChain,
} from "./fallback-chain-resolution"
import { transformModelForProvider } from "./provider-model-id-transform"
export type { GeneratedOmoConfig } from "./model-fallback-types"
@@ -22,6 +25,44 @@ const ZAI_MODEL = "zai-coding-plan/glm-4.7"
const ULTIMATE_FALLBACK = "opencode/gpt-5-nano"
const SCHEMA_URL = "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json"
function toFallbackModelObject(entry: FallbackEntry, provider: string): FallbackModelObject {
return {
model: `${provider}/${transformModelForProvider(provider, entry.model)}`,
...(entry.variant ? { variant: entry.variant } : {}),
...(entry.reasoningEffort ? { reasoningEffort: entry.reasoningEffort as FallbackModelObject["reasoningEffort"] } : {}),
...(entry.temperature !== undefined ? { temperature: entry.temperature } : {}),
...(entry.top_p !== undefined ? { top_p: entry.top_p } : {}),
...(entry.maxTokens !== undefined ? { maxTokens: entry.maxTokens } : {}),
...(entry.thinking ? { thinking: entry.thinking } : {}),
}
}
function attachFallbackModels<T extends AgentConfig | CategoryConfig>(
config: T,
fallbackChain: FallbackEntry[],
availability: ReturnType<typeof toProviderAvailability>,
): T {
const expandedFallbacks = fallbackChain.flatMap((entry) =>
entry.providers
.filter((provider) => isProviderAvailable(provider, availability))
.map((provider) => toFallbackModelObject(entry, provider))
)
const primaryIndex = expandedFallbacks.findIndex((entry) => entry.model === config.model)
if (primaryIndex === -1) {
return config
}
const fallbackModels = expandedFallbacks.slice(primaryIndex + 1)
if (fallbackModels.length === 0) {
return config
}
return {
...config,
fallback_models: fallbackModels,
}
}
export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
@@ -85,7 +126,8 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
const resolved = resolveModelFromChain(fallbackChain, avail)
if (resolved) {
const variant = resolved.variant ?? req.variant
agents[role] = variant ? { model: resolved.model, variant } : { model: resolved.model }
const agentConfig = variant ? { model: resolved.model, variant } : { model: resolved.model }
agents[role] = attachFallbackModels(agentConfig, fallbackChain, avail)
}
continue
}
@@ -100,7 +142,8 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
const resolved = resolveModelFromChain(req.fallbackChain, avail)
if (resolved) {
const variant = resolved.variant ?? req.variant
agents[role] = variant ? { model: resolved.model, variant } : { model: resolved.model }
const agentConfig = variant ? { model: resolved.model, variant } : { model: resolved.model }
agents[role] = attachFallbackModels(agentConfig, req.fallbackChain, avail)
} else {
agents[role] = { model: ULTIMATE_FALLBACK }
}
@@ -123,7 +166,8 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
const resolved = resolveModelFromChain(fallbackChain, avail)
if (resolved) {
const variant = resolved.variant ?? req.variant
categories[cat] = variant ? { model: resolved.model, variant } : { model: resolved.model }
const categoryConfig = variant ? { model: resolved.model, variant } : { model: resolved.model }
categories[cat] = attachFallbackModels(categoryConfig, fallbackChain, avail)
} else {
categories[cat] = { model: ULTIMATE_FALLBACK }
}