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 { isProviderAvailable, toProviderAvailability } from "./provider-availability" import { getSisyphusFallbackChain, isAnyFallbackEntryAvailable, isRequiredModelAvailable, isRequiredProviderAvailable, resolveModelFromChain, } from "./fallback-chain-resolution" import { transformModelForProvider } from "./provider-model-id-transform" export type { GeneratedOmoConfig } from "./model-fallback-types" 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 collectAvailableFallbacks( fallbackChain: FallbackEntry[], availability: ReturnType, ): FallbackModelObject[] { const expandedFallbacks = fallbackChain.flatMap((entry) => entry.providers .filter((provider) => isProviderAvailable(provider, availability)) .map((provider) => toFallbackModelObject(entry, provider)) ) return expandedFallbacks.filter((entry, index, allEntries) => allEntries.findIndex((candidate) => candidate.model === entry.model && candidate.variant === entry.variant ) === index ) } function attachFallbackModels( config: T, fallbackChain: FallbackEntry[], availability: ReturnType, ): T { const uniqueFallbacks = collectAvailableFallbacks(fallbackChain, availability) const primaryIndex = uniqueFallbacks.findIndex((entry) => entry.model === config.model) if (primaryIndex === -1) { return config } const fallbackModels = uniqueFallbacks.slice(primaryIndex + 1) if (fallbackModels.length === 0) { return config } return { ...config, fallback_models: fallbackModels, } } function attachAllFallbackModels( config: T, fallbackChain: FallbackEntry[], availability: ReturnType, ): 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 { const avail = toProviderAvailability(config) const hasAnyProvider = avail.native.claude || avail.native.openai || avail.native.gemini || avail.opencodeZen || avail.copilot || avail.zai || avail.kimiForCoding || avail.opencodeGo || avail.vercelAiGateway if (!hasAnyProvider) { return { $schema: SCHEMA_URL, agents: Object.fromEntries( Object.entries(CLI_AGENT_MODEL_REQUIREMENTS) .filter(([role, req]) => !(role === "sisyphus" && req.requiresAnyModel)) .map(([role]) => [role, { model: ULTIMATE_FALLBACK }]) ), categories: Object.fromEntries( Object.keys(CLI_CATEGORY_MODEL_REQUIREMENTS).map((cat) => [cat, { model: ULTIMATE_FALLBACK }]) ), } } const agents: Record = {} const categories: Record = {} for (const [role, req] of Object.entries(CLI_AGENT_MODEL_REQUIREMENTS)) { if (role === "librarian") { let agentConfig: AgentConfig | undefined if (avail.native.openai) { agentConfig = { model: "openai/gpt-5.4-mini-fast" } } else if (avail.opencodeGo) { agentConfig = { model: "opencode-go/qwen3.5-plus" } } else if (avail.zai) { agentConfig = { model: ZAI_MODEL } } else if (avail.vercelAiGateway) { agentConfig = { model: "vercel/minimax/minimax-m2.7" } } if (agentConfig) { agents[role] = attachAllFallbackModels(agentConfig, req.fallbackChain, avail) } continue } if (role === "explore") { let agentConfig: AgentConfig if (avail.native.openai) { agentConfig = { model: "openai/gpt-5.4-mini-fast" } } else if (avail.native.claude) { agentConfig = { model: "anthropic/claude-haiku-4-5" } } else if (avail.opencodeZen) { agentConfig = { model: "opencode/claude-haiku-4-5" } } else if (avail.opencodeGo) { agentConfig = { model: "opencode-go/qwen3.5-plus" } } else if (avail.copilot) { agentConfig = { model: "github-copilot/gpt-5-mini" } } else if (avail.vercelAiGateway) { agentConfig = { model: "vercel/minimax/minimax-m2.7-highspeed" } } else { agentConfig = { model: "opencode/gpt-5-nano" } } agents[role] = attachAllFallbackModels(agentConfig, req.fallbackChain, avail) continue } if (role === "sisyphus") { const fallbackChain = getSisyphusFallbackChain() if (req.requiresAnyModel && !isAnyFallbackEntryAvailable(fallbackChain, avail)) { continue } const resolved = resolveModelFromChain(fallbackChain, avail) if (resolved) { const variant = resolved.variant ?? req.variant const agentConfig = variant ? { model: resolved.model, variant } : { model: resolved.model } agents[role] = attachFallbackModels(agentConfig, fallbackChain, avail) } continue } if (req.requiresModel && !isRequiredModelAvailable(req.requiresModel, req.fallbackChain, avail)) { continue } if (req.requiresProvider && !isRequiredProviderAvailable(req.requiresProvider, avail)) { continue } const resolved = resolveModelFromChain(req.fallbackChain, avail) if (resolved) { const variant = resolved.variant ?? req.variant const agentConfig = variant ? { model: resolved.model, variant } : { model: resolved.model } agents[role] = attachFallbackModels(agentConfig, req.fallbackChain, avail) } else { agents[role] = { model: ULTIMATE_FALLBACK } } } for (const [cat, req] of Object.entries(CLI_CATEGORY_MODEL_REQUIREMENTS)) { // Special case: unspecified-high downgrades to unspecified-low when not isMaxPlan const fallbackChain = cat === "unspecified-high" && !avail.isMaxPlan ? CLI_CATEGORY_MODEL_REQUIREMENTS["unspecified-low"].fallbackChain : req.fallbackChain if (req.requiresModel && !isRequiredModelAvailable(req.requiresModel, req.fallbackChain, avail)) { continue } if (req.requiresProvider && !isRequiredProviderAvailable(req.requiresProvider, avail)) { continue } const resolved = resolveModelFromChain(fallbackChain, avail) if (resolved) { const variant = resolved.variant ?? req.variant const categoryConfig = variant ? { model: resolved.model, variant } : { model: resolved.model } categories[cat] = attachFallbackModels(categoryConfig, fallbackChain, avail) } else { categories[cat] = { model: ULTIMATE_FALLBACK } } } const generatedConfig: GeneratedOmoConfig = { $schema: SCHEMA_URL, agents, categories, } return isOpenAiOnlyAvailability(avail) ? applyOpenAiOnlyModelCatalog(generatedConfig) : generatedConfig } export function shouldShowChatGPTOnlyWarning(config: InstallConfig): boolean { return !config.hasClaude && !config.hasGemini && config.hasOpenAI }