diff --git a/src/cli/config-manager/generate-omo-config.test.ts b/src/cli/config-manager/generate-omo-config.test.ts index 2a1a24e5b..ded569247 100644 --- a/src/cli/config-manager/generate-omo-config.test.ts +++ b/src/cli/config-manager/generate-omo-config.test.ts @@ -98,6 +98,41 @@ describe("generateOmoConfig - model fallback system", () => { expect((result.agents as Record)['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 + }> + const categories = result.categories as Record + }> + + //#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 = { diff --git a/src/cli/model-fallback-types.ts b/src/cli/model-fallback-types.ts index a00387010..b195f698a 100644 --- a/src/cli/model-fallback-types.ts +++ b/src/cli/model-fallback-types.ts @@ -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 { diff --git a/src/cli/model-fallback.ts b/src/cli/model-fallback.ts index 331c97142..b7f663acd 100644 --- a/src/cli/model-fallback.ts +++ b/src/cli/model-fallback.ts @@ -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( + config: T, + fallbackChain: FallbackEntry[], + availability: ReturnType, +): 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 } }