From c00f210922da93893302103f3902634c320150bc Mon Sep 17 00:00:00 2001 From: justsisyphus Date: Wed, 21 Jan 2026 00:05:51 +0900 Subject: [PATCH] feat(cli): add model fallback system with provider priority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements intelligent model selection based on available providers. Priority order: Native > Copilot > OpenCode Zen > Z.ai > Ultimate Fallback. - Add model-fallback.ts with MODEL_CATALOG and capability-based resolution - Extend InstallConfig with hasOpencodeZen and hasZaiCodingPlan - Add TUI prompts and CLI args for new provider options - Z.ai gets Librarian priority (zai-coding-plan/glm-4.7) 🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- docs/guide/installation.md | 68 ++++++++--- src/cli/config-manager.test.ts | 60 +++++++--- src/cli/config-manager.ts | 15 +-- src/cli/index.ts | 15 ++- src/cli/install.ts | 55 ++++++++- src/cli/model-fallback.ts | 204 +++++++++++++++++++++++++++++++++ src/cli/types.ts | 6 + 7 files changed, 368 insertions(+), 55 deletions(-) create mode 100644 src/cli/model-fallback.ts diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 23e2a5f90..c6619e206 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -33,7 +33,7 @@ follow these steps: ### Step 0: Ask user about subscriptions -Ask the user these 3 questions to determine CLI options: +Ask the user these questions to determine CLI options: 1. **Do you have a Claude Pro/Max Subscription?** - If **yes**: **Are you on max20 (20x mode)?** @@ -41,18 +41,24 @@ Ask the user these 3 questions to determine CLI options: - **not max20** → `--claude=yes` - If **no** → `--claude=no` -2. **Do you have a ChatGPT Subscription?** - - If **yes** → `--chatgpt=yes` - - If **no** → `--chatgpt=no` - -3. **Will you integrate Gemini models?** +2. **Will you integrate Gemini models?** - If **yes** → `--gemini=yes` - If **no** → `--gemini=no` -4. **Do you have a GitHub Copilot Subscription?** +3. **Do you have a GitHub Copilot Subscription?** - If **yes** → `--copilot=yes` - If **no** → `--copilot=no` +4. **Do you have access to OpenCode Zen (opencode/ models)?** + - If **yes** → `--opencode-zen=yes` + - If **no** → `--opencode-zen=no` (default) + +5. **Do you have a Z.ai Coding Plan subscription?** + - If **yes** → `--zai-coding-plan=yes` + - If **no** → `--zai-coding-plan=no` (default) + +**Provider Priority**: Native (anthropic/, openai/, google/) > GitHub Copilot > OpenCode Zen > Z.ai Coding Plan + MUST STRONGLY WARNING, WHEN USER SAID THEY DON'T HAVE CLAUDE SUBSCRIPTION, SISYPHUS AGENT MIGHT NOT WORK IDEALY. ### Step 1: Install OpenCode (if not installed) @@ -74,14 +80,16 @@ Spawn a subagent to handle installation and report back - to save context. Based on user's answers, run the CLI installer with appropriate flags: ```bash -bunx oh-my-opencode install --no-tui --claude= --chatgpt= --gemini= --copilot= +bunx oh-my-opencode install --no-tui --claude= --gemini= --copilot= [--opencode-zen=] [--zai-coding-plan=] ``` **Examples:** -- User has all subscriptions with max20: `bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes --copilot=no` -- User has only Claude (no max20): `bunx oh-my-opencode install --no-tui --claude=yes --chatgpt=no --gemini=no --copilot=no` -- User has only GitHub Copilot: `bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no --copilot=yes` -- User has no subscriptions: `bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no` +- User has all native subscriptions: `bunx oh-my-opencode install --no-tui --claude=max20 --gemini=yes --copilot=no` +- User has only Claude: `bunx oh-my-opencode install --no-tui --claude=yes --gemini=no --copilot=no` +- User has only GitHub Copilot: `bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes` +- User has Z.ai for Librarian: `bunx oh-my-opencode install --no-tui --claude=yes --gemini=no --copilot=no --zai-coding-plan=yes` +- User has only OpenCode Zen: `bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=no --opencode-zen=yes` +- User has no subscriptions: `bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=no` The CLI will: - Register the plugin in `opencode.json` @@ -158,23 +166,49 @@ opencode auth login #### GitHub Copilot (Fallback Provider) -GitHub Copilot is supported as a **fallback provider** when native providers (Claude, ChatGPT, Gemini) are unavailable. The installer configures Copilot with lower priority than native providers. +GitHub Copilot is supported as a **fallback provider** when native providers are unavailable. -**Priority**: Native providers (Claude/ChatGPT/Gemini) > GitHub Copilot > Free models +**Priority**: Native (anthropic/, openai/, google/) > GitHub Copilot > OpenCode Zen > Z.ai Coding Plan ##### Model Mappings -When GitHub Copilot is enabled, oh-my-opencode uses these model assignments: +When GitHub Copilot is the best available provider, oh-my-opencode uses these model assignments: | Agent | Model | | ------------- | -------------------------------- | | **Sisyphus** | `github-copilot/claude-opus-4.5` | | **Oracle** | `github-copilot/gpt-5.2` | -| **Explore** | `grok code` (default) | -| **Librarian** | `glm 4.7 free` (default) | +| **Explore** | `github-copilot/grok-code-fast-1`| +| **Librarian** | `zai-coding-plan/glm-4.7` (if Z.ai available) or fallback | GitHub Copilot acts as a proxy provider, routing requests to underlying models based on your subscription. +#### Z.ai Coding Plan + +Z.ai Coding Plan provides access to GLM-4.7 models. When enabled, the **Librarian agent always uses `zai-coding-plan/glm-4.7`** regardless of other available providers. + +If Z.ai is the only provider available, all agents will use GLM models: + +| Agent | Model | +| ------------- | -------------------------------- | +| **Sisyphus** | `zai-coding-plan/glm-4.7` | +| **Oracle** | `zai-coding-plan/glm-4.7` | +| **Explore** | `zai-coding-plan/glm-4.7-flash` | +| **Librarian** | `zai-coding-plan/glm-4.7` | + +#### OpenCode Zen + +OpenCode Zen provides access to `opencode/` prefixed models including `opencode/claude-opus-4-5`, `opencode/gpt-5.2`, `opencode/grok-code`, and `opencode/glm-4.7-free`. + +When OpenCode Zen is the best available provider (no native or Copilot), these models are used: + +| Agent | Model | +| ------------- | -------------------------------- | +| **Sisyphus** | `opencode/claude-opus-4-5` | +| **Oracle** | `opencode/gpt-5.2` | +| **Explore** | `opencode/grok-code` | +| **Librarian** | `opencode/glm-4.7-free` | + ##### Setup Run the installer and select "Yes" for GitHub Copilot: diff --git a/src/cli/config-manager.test.ts b/src/cli/config-manager.test.ts index 4131d8f72..a20cb809a 100644 --- a/src/cli/config-manager.test.ts +++ b/src/cli/config-manager.test.ts @@ -200,57 +200,81 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => { }) }) -describe("generateOmoConfig - v3 beta: no hardcoded models", () => { - test("generates minimal config with only $schema", () => { - // #given any install config +describe("generateOmoConfig - model fallback system", () => { + test("generates native models when Claude available", () => { + // #given user has Claude subscription const config: InstallConfig = { hasClaude: true, isMax20: false, hasGemini: false, hasCopilot: false, + hasOpencodeZen: false, + hasZaiCodingPlan: false, } // #when generating config const result = generateOmoConfig(config) - // #then should only contain $schema, no agents or categories + // #then should use native anthropic models expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json") - expect(result.agents).toBeUndefined() - expect(result.categories).toBeUndefined() + expect(result.agents).toBeDefined() + expect((result.agents as Record).Sisyphus.model).toBe("anthropic/claude-opus-4-5") }) - test("does not include model fields regardless of provider config", () => { - // #given user has multiple providers + test("uses github-copilot fallback when only copilot available", () => { + // #given user has only copilot const config: InstallConfig = { - hasClaude: true, - isMax20: true, - hasGemini: true, + hasClaude: false, + isMax20: false, + hasGemini: false, hasCopilot: true, + hasOpencodeZen: false, + hasZaiCodingPlan: false, } // #when generating config const result = generateOmoConfig(config) - // #then should not have agents or categories with model fields - expect(result.agents).toBeUndefined() - expect(result.categories).toBeUndefined() + // #then should use github-copilot models + expect((result.agents as Record).Sisyphus.model).toBe("github-copilot/claude-opus-4.5") }) - test("does not include model fields when no providers configured", () => { + test("uses ultimate fallback when no providers configured", () => { // #given user has no providers const config: InstallConfig = { hasClaude: false, isMax20: false, hasGemini: false, hasCopilot: false, + hasOpencodeZen: false, + hasZaiCodingPlan: false, } // #when generating config const result = generateOmoConfig(config) - // #then should still only contain $schema + // #then should use ultimate fallback for all agents expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json") - expect(result.agents).toBeUndefined() - expect(result.categories).toBeUndefined() + expect((result.agents as Record).Sisyphus.model).toBe("opencode/glm-4.7-free") + }) + + test("uses zai-coding-plan/glm-4.7 for librarian when Z.ai available", () => { + // #given user has Z.ai and Claude + const config: InstallConfig = { + hasClaude: true, + isMax20: false, + hasGemini: false, + hasCopilot: false, + hasOpencodeZen: false, + hasZaiCodingPlan: true, + } + + // #when generating config + const result = generateOmoConfig(config) + + // #then librarian should use zai-coding-plan/glm-4.7 + expect((result.agents as Record).librarian.model).toBe("zai-coding-plan/glm-4.7") + // #then other agents should use native + expect((result.agents as Record).Sisyphus.model).toBe("anthropic/claude-opus-4-5") }) }) diff --git a/src/cli/config-manager.ts b/src/cli/config-manager.ts index f3aadcf7c..8a9c2e5c0 100644 --- a/src/cli/config-manager.ts +++ b/src/cli/config-manager.ts @@ -6,6 +6,7 @@ import { type OpenCodeConfigPaths, } from "../shared" import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types" +import { generateModelConfig } from "./model-fallback" const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const @@ -306,14 +307,8 @@ function deepMerge>(target: T, source: Partial return result } -export function generateOmoConfig(_installConfig: InstallConfig): Record { - // v3 beta: No hardcoded model strings - users rely on their OpenCode configured model - // Users who want specific models configure them explicitly after install - const config: Record = { - $schema: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", - } - - return config +export function generateOmoConfig(installConfig: InstallConfig): Record { + return generateModelConfig(installConfig) } export function writeOmoConfig(installConfig: InstallConfig): ConfigMergeResult { @@ -581,14 +576,14 @@ export function addProviderConfig(config: InstallConfig): ConfigMergeResult { } export function detectCurrentConfig(): DetectedConfig { - // v3 beta: Since we no longer generate hardcoded model strings, - // detection only checks for plugin installation and Gemini auth plugin const result: DetectedConfig = { isInstalled: false, hasClaude: true, isMax20: true, hasGemini: false, hasCopilot: false, + hasOpencodeZen: true, + hasZaiCodingPlan: false, } const { format, path } = detectConfigFormat() diff --git a/src/cli/index.ts b/src/cli/index.ts index dbfcf88fc..c554875a9 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -26,16 +26,21 @@ program .option("--claude ", "Claude subscription: no, yes, max20") .option("--gemini ", "Gemini integration: no, yes") .option("--copilot ", "GitHub Copilot subscription: no, yes") + .option("--opencode-zen ", "OpenCode Zen access: no, yes (default: no)") + .option("--zai-coding-plan ", "Z.ai Coding Plan subscription: no, yes (default: no)") .option("--skip-auth", "Skip authentication setup hints") .addHelpText("after", ` Examples: $ bunx oh-my-opencode install $ bunx oh-my-opencode install --no-tui --claude=max20 --gemini=yes --copilot=no - $ bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes + $ bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes --opencode-zen=yes -Model Providers: - Claude Required for Sisyphus (main orchestrator) and Librarian agents - Gemini Powers frontend, documentation, and multimodal agents +Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai): + Claude Native anthropic/ models (Opus, Sonnet, Haiku) + Gemini Native google/ models (Gemini 3 Pro, Flash) + Copilot github-copilot/ models (fallback) + OpenCode Zen opencode/ models (opencode/claude-opus-4-5, etc.) + Z.ai zai-coding-plan/glm-4.7 (Librarian priority) `) .action(async (options) => { const args: InstallArgs = { @@ -43,6 +48,8 @@ Model Providers: claude: options.claude, gemini: options.gemini, copilot: options.copilot, + opencodeZen: options.opencodeZen, + zaiCodingPlan: options.zaiCodingPlan, skipAuth: options.skipAuth ?? false, } const exitCode = await install(args) diff --git a/src/cli/install.ts b/src/cli/install.ts index 6b0238c09..8087ff69a 100644 --- a/src/cli/install.ts +++ b/src/cli/install.ts @@ -40,17 +40,18 @@ function formatConfigSummary(config: InstallConfig): string { const claudeDetail = config.hasClaude ? (config.isMax20 ? "max20" : "standard") : undefined lines.push(formatProvider("Claude", config.hasClaude, claudeDetail)) lines.push(formatProvider("Gemini", config.hasGemini)) - lines.push(formatProvider("GitHub Copilot", config.hasCopilot, "fallback provider")) + lines.push(formatProvider("GitHub Copilot", config.hasCopilot, "fallback")) + lines.push(formatProvider("OpenCode Zen", config.hasOpencodeZen, "opencode/ models")) + lines.push(formatProvider("Z.ai Coding Plan", config.hasZaiCodingPlan, "Librarian: glm-4.7")) lines.push("") lines.push(color.dim("─".repeat(40))) lines.push("") - // v3 beta: No hardcoded models - agents use OpenCode's configured default model - lines.push(color.bold(color.white("Agent Models"))) + lines.push(color.bold(color.white("Model Assignment"))) lines.push("") - lines.push(` ${SYMBOLS.info} Agents will use your OpenCode default model`) - lines.push(` ${SYMBOLS.bullet} Configure specific models in ${color.cyan("oh-my-opencode.json")} if needed`) + lines.push(` ${SYMBOLS.info} Models auto-configured based on provider priority`) + lines.push(` ${SYMBOLS.bullet} Priority: Native > Copilot > OpenCode Zen > Z.ai`) return lines.join("\n") } @@ -126,6 +127,14 @@ function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string errors.push(`Invalid --copilot value: ${args.copilot} (expected: no, yes)`) } + if (args.opencodeZen !== undefined && !["no", "yes"].includes(args.opencodeZen)) { + errors.push(`Invalid --opencode-zen value: ${args.opencodeZen} (expected: no, yes)`) + } + + if (args.zaiCodingPlan !== undefined && !["no", "yes"].includes(args.zaiCodingPlan)) { + errors.push(`Invalid --zai-coding-plan value: ${args.zaiCodingPlan} (expected: no, yes)`) + } + return { valid: errors.length === 0, errors } } @@ -135,10 +144,12 @@ function argsToConfig(args: InstallArgs): InstallConfig { isMax20: args.claude === "max20", hasGemini: args.gemini === "yes", hasCopilot: args.copilot === "yes", + hasOpencodeZen: args.opencodeZen === "yes", + hasZaiCodingPlan: args.zaiCodingPlan === "yes", } } -function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubscription; gemini: BooleanArg; copilot: BooleanArg } { +function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubscription; gemini: BooleanArg; copilot: BooleanArg; opencodeZen: BooleanArg; zaiCodingPlan: BooleanArg } { let claude: ClaudeSubscription = "no" if (detected.hasClaude) { claude = detected.isMax20 ? "max20" : "yes" @@ -148,6 +159,8 @@ function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubs claude, gemini: detected.hasGemini ? "yes" : "no", copilot: detected.hasCopilot ? "yes" : "no", + opencodeZen: detected.hasOpencodeZen ? "yes" : "no", + zaiCodingPlan: detected.hasZaiCodingPlan ? "yes" : "no", } } @@ -197,11 +210,41 @@ async function runTuiMode(detected: DetectedConfig): Promise + categories?: Record + [key: string]: unknown +} + +const MODEL_CATALOG: Record>> = { + native: { + "opus-level": "anthropic/claude-opus-4-5", + "sonnet-level": "anthropic/claude-sonnet-4-5", + "haiku-level": "anthropic/claude-haiku-4-5", + reasoning: "openai/gpt-5.2", + codex: "openai/gpt-5.2-codex", + visual: "google/gemini-3-pro-preview", + fast: "google/gemini-3-flash-preview", + }, + "github-copilot": { + "opus-level": "github-copilot/claude-opus-4.5", + "sonnet-level": "github-copilot/claude-sonnet-4.5", + "haiku-level": "github-copilot/claude-haiku-4.5", + reasoning: "github-copilot/gpt-5.2", + codex: "github-copilot/gpt-5.2-codex", + visual: "github-copilot/gemini-3-pro-preview", + fast: "github-copilot/grok-code-fast-1", + }, + opencode: { + "opus-level": "opencode/claude-opus-4-5", + "sonnet-level": "opencode/claude-sonnet-4-5", + "haiku-level": "opencode/claude-haiku-4-5", + reasoning: "opencode/gpt-5.2", + codex: "opencode/gpt-5.2-codex", + visual: "opencode/gemini-3-pro", + fast: "opencode/grok-code", + glm: "opencode/glm-4.7-free", + }, + "zai-coding-plan": { + "opus-level": "zai-coding-plan/glm-4.7", + "sonnet-level": "zai-coding-plan/glm-4.7", + "haiku-level": "zai-coding-plan/glm-4.7-flash", + reasoning: "zai-coding-plan/glm-4.7", + codex: "zai-coding-plan/glm-4.7", + visual: "zai-coding-plan/glm-4.7", + fast: "zai-coding-plan/glm-4.7-flash", + glm: "zai-coding-plan/glm-4.7", + }, +} + +const AGENT_REQUIREMENTS: Record = { + Sisyphus: "opus-level", + oracle: "reasoning", + librarian: "glm", + explore: "fast", + "multimodal-looker": "visual", + "Prometheus (Planner)": "opus-level", + "Metis (Plan Consultant)": "sonnet-level", + "Momus (Plan Reviewer)": "sonnet-level", + Atlas: "opus-level", +} + +const CATEGORY_REQUIREMENTS: Record = { + "visual-engineering": "visual", + ultrabrain: "codex", + artistry: "visual", + quick: "haiku-level", + "unspecified-low": "sonnet-level", + "unspecified-high": "opus-level", + writing: "fast", +} + +const ULTIMATE_FALLBACK = "opencode/glm-4.7-free" +const SCHEMA_URL = "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json" + +function toProviderAvailability(config: InstallConfig): ProviderAvailability { + return { + native: { + claude: config.hasClaude, + openai: config.hasClaude, + gemini: config.hasGemini, + }, + copilot: config.hasCopilot, + opencode: config.hasOpencodeZen, + zai: config.hasZaiCodingPlan, + } +} + +function getProviderPriority(avail: ProviderAvailability): ProviderTier[] { + const tiers: ProviderTier[] = [] + + if (avail.native.claude || avail.native.openai || avail.native.gemini) { + tiers.push("native") + } + if (avail.copilot) tiers.push("github-copilot") + if (avail.opencode) tiers.push("opencode") + if (avail.zai) tiers.push("zai-coding-plan") + + return tiers +} + +function hasCapability( + tier: ProviderTier, + capability: ModelCapability, + avail: ProviderAvailability +): boolean { + if (tier === "native") { + switch (capability) { + case "opus-level": + case "sonnet-level": + case "haiku-level": + return avail.native.claude + case "reasoning": + case "codex": + return avail.native.openai || avail.native.claude + case "visual": + case "fast": + return avail.native.gemini + case "glm": + return false + } + } + return true +} + +function resolveModel(capability: ModelCapability, avail: ProviderAvailability): string { + const tiers = getProviderPriority(avail) + + for (const tier of tiers) { + if (hasCapability(tier, capability, avail)) { + const model = MODEL_CATALOG[tier][capability] + if (model) return model + } + } + + return ULTIMATE_FALLBACK +} + +export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig { + const avail = toProviderAvailability(config) + const hasAnyProvider = + avail.native.claude || + avail.native.openai || + avail.native.gemini || + avail.copilot || + avail.opencode || + avail.zai + + if (!hasAnyProvider) { + return { + $schema: SCHEMA_URL, + agents: Object.fromEntries( + Object.keys(AGENT_REQUIREMENTS).map((role) => [role, { model: ULTIMATE_FALLBACK }]) + ), + categories: Object.fromEntries( + Object.keys(CATEGORY_REQUIREMENTS).map((cat) => [cat, { model: ULTIMATE_FALLBACK }]) + ), + } + } + + const agents: Record = {} + const categories: Record = {} + + for (const [role, capability] of Object.entries(AGENT_REQUIREMENTS)) { + if (role === "librarian" && avail.zai) { + agents[role] = { model: "zai-coding-plan/glm-4.7" } + } else { + agents[role] = { model: resolveModel(capability, avail) } + } + } + + for (const [cat, capability] of Object.entries(CATEGORY_REQUIREMENTS)) { + categories[cat] = { model: resolveModel(capability, avail) } + } + + return { + $schema: SCHEMA_URL, + agents, + categories, + } +} diff --git a/src/cli/types.ts b/src/cli/types.ts index 72b2dc750..521b2f48d 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -6,6 +6,8 @@ export interface InstallArgs { claude?: ClaudeSubscription gemini?: BooleanArg copilot?: BooleanArg + opencodeZen?: BooleanArg + zaiCodingPlan?: BooleanArg skipAuth?: boolean } @@ -14,6 +16,8 @@ export interface InstallConfig { isMax20: boolean hasGemini: boolean hasCopilot: boolean + hasOpencodeZen: boolean + hasZaiCodingPlan: boolean } export interface ConfigMergeResult { @@ -28,4 +32,6 @@ export interface DetectedConfig { isMax20: boolean hasGemini: boolean hasCopilot: boolean + hasOpencodeZen: boolean + hasZaiCodingPlan: boolean }