feat(cli): add model fallback system with provider priority

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)
This commit is contained in:
justsisyphus 2026-01-21 00:05:51 +09:00
parent 6865995227
commit c00f210922
7 changed files with 368 additions and 55 deletions

View File

@ -33,7 +33,7 @@ follow these steps:
### Step 0: Ask user about subscriptions ### 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?** 1. **Do you have a Claude Pro/Max Subscription?**
- If **yes**: **Are you on max20 (20x mode)?** - 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` - **not max20**`--claude=yes`
- If **no**`--claude=no` - If **no**`--claude=no`
2. **Do you have a ChatGPT Subscription?** 2. **Will you integrate Gemini models?**
- If **yes**`--chatgpt=yes`
- If **no**`--chatgpt=no`
3. **Will you integrate Gemini models?**
- If **yes**`--gemini=yes` - If **yes**`--gemini=yes`
- If **no**`--gemini=no` - 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 **yes**`--copilot=yes`
- If **no**`--copilot=no` - 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. 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) ### 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: Based on user's answers, run the CLI installer with appropriate flags:
```bash ```bash
bunx oh-my-opencode install --no-tui --claude=<yes|no|max20> --chatgpt=<yes|no> --gemini=<yes|no> --copilot=<yes|no> bunx oh-my-opencode install --no-tui --claude=<yes|no|max20> --gemini=<yes|no> --copilot=<yes|no> [--opencode-zen=<yes|no>] [--zai-coding-plan=<yes|no>]
``` ```
**Examples:** **Examples:**
- User has all subscriptions with max20: `bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes --copilot=no` - User has all native subscriptions: `bunx oh-my-opencode install --no-tui --claude=max20 --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 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 --chatgpt=no --gemini=no --copilot=yes` - User has only GitHub Copilot: `bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes`
- User has no subscriptions: `bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no` - 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: The CLI will:
- Register the plugin in `opencode.json` - Register the plugin in `opencode.json`
@ -158,23 +166,49 @@ opencode auth login
#### GitHub Copilot (Fallback Provider) #### 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 ##### 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 | | Agent | Model |
| ------------- | -------------------------------- | | ------------- | -------------------------------- |
| **Sisyphus** | `github-copilot/claude-opus-4.5` | | **Sisyphus** | `github-copilot/claude-opus-4.5` |
| **Oracle** | `github-copilot/gpt-5.2` | | **Oracle** | `github-copilot/gpt-5.2` |
| **Explore** | `grok code` (default) | | **Explore** | `github-copilot/grok-code-fast-1`|
| **Librarian** | `glm 4.7 free` (default) | | **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. 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 ##### Setup
Run the installer and select "Yes" for GitHub Copilot: Run the installer and select "Yes" for GitHub Copilot:

View File

@ -200,57 +200,81 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
}) })
}) })
describe("generateOmoConfig - v3 beta: no hardcoded models", () => { describe("generateOmoConfig - model fallback system", () => {
test("generates minimal config with only $schema", () => { test("generates native models when Claude available", () => {
// #given any install config // #given user has Claude subscription
const config: InstallConfig = { const config: InstallConfig = {
hasClaude: true, hasClaude: true,
isMax20: false, isMax20: false,
hasGemini: false, hasGemini: false,
hasCopilot: false, hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
} }
// #when generating config // #when generating config
const result = generateOmoConfig(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.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
expect(result.agents).toBeUndefined() expect(result.agents).toBeDefined()
expect(result.categories).toBeUndefined() expect((result.agents as Record<string, { model: string }>).Sisyphus.model).toBe("anthropic/claude-opus-4-5")
}) })
test("does not include model fields regardless of provider config", () => { test("uses github-copilot fallback when only copilot available", () => {
// #given user has multiple providers // #given user has only copilot
const config: InstallConfig = { const config: InstallConfig = {
hasClaude: true, hasClaude: false,
isMax20: true, isMax20: false,
hasGemini: true, hasGemini: false,
hasCopilot: true, hasCopilot: true,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
} }
// #when generating config // #when generating config
const result = generateOmoConfig(config) const result = generateOmoConfig(config)
// #then should not have agents or categories with model fields // #then should use github-copilot models
expect(result.agents).toBeUndefined() expect((result.agents as Record<string, { model: string }>).Sisyphus.model).toBe("github-copilot/claude-opus-4.5")
expect(result.categories).toBeUndefined()
}) })
test("does not include model fields when no providers configured", () => { test("uses ultimate fallback when no providers configured", () => {
// #given user has no providers // #given user has no providers
const config: InstallConfig = { const config: InstallConfig = {
hasClaude: false, hasClaude: false,
isMax20: false, isMax20: false,
hasGemini: false, hasGemini: false,
hasCopilot: false, hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
} }
// #when generating config // #when generating config
const result = generateOmoConfig(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.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
expect(result.agents).toBeUndefined() expect((result.agents as Record<string, { model: string }>).Sisyphus.model).toBe("opencode/glm-4.7-free")
expect(result.categories).toBeUndefined() })
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<string, { model: string }>).librarian.model).toBe("zai-coding-plan/glm-4.7")
// #then other agents should use native
expect((result.agents as Record<string, { model: string }>).Sisyphus.model).toBe("anthropic/claude-opus-4-5")
}) })
}) })

View File

@ -6,6 +6,7 @@ import {
type OpenCodeConfigPaths, type OpenCodeConfigPaths,
} from "../shared" } from "../shared"
import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types" import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
import { generateModelConfig } from "./model-fallback"
const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const
@ -306,14 +307,8 @@ function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial
return result return result
} }
export function generateOmoConfig(_installConfig: InstallConfig): Record<string, unknown> { export function generateOmoConfig(installConfig: InstallConfig): Record<string, unknown> {
// v3 beta: No hardcoded model strings - users rely on their OpenCode configured model return generateModelConfig(installConfig)
// Users who want specific models configure them explicitly after install
const config: Record<string, unknown> = {
$schema: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
}
return config
} }
export function writeOmoConfig(installConfig: InstallConfig): ConfigMergeResult { export function writeOmoConfig(installConfig: InstallConfig): ConfigMergeResult {
@ -581,14 +576,14 @@ export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
} }
export function detectCurrentConfig(): DetectedConfig { 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 = { const result: DetectedConfig = {
isInstalled: false, isInstalled: false,
hasClaude: true, hasClaude: true,
isMax20: true, isMax20: true,
hasGemini: false, hasGemini: false,
hasCopilot: false, hasCopilot: false,
hasOpencodeZen: true,
hasZaiCodingPlan: false,
} }
const { format, path } = detectConfigFormat() const { format, path } = detectConfigFormat()

View File

@ -26,16 +26,21 @@ program
.option("--claude <value>", "Claude subscription: no, yes, max20") .option("--claude <value>", "Claude subscription: no, yes, max20")
.option("--gemini <value>", "Gemini integration: no, yes") .option("--gemini <value>", "Gemini integration: no, yes")
.option("--copilot <value>", "GitHub Copilot subscription: no, yes") .option("--copilot <value>", "GitHub Copilot subscription: no, yes")
.option("--opencode-zen <value>", "OpenCode Zen access: no, yes (default: no)")
.option("--zai-coding-plan <value>", "Z.ai Coding Plan subscription: no, yes (default: no)")
.option("--skip-auth", "Skip authentication setup hints") .option("--skip-auth", "Skip authentication setup hints")
.addHelpText("after", ` .addHelpText("after", `
Examples: Examples:
$ bunx oh-my-opencode install $ 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=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: Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai):
Claude Required for Sisyphus (main orchestrator) and Librarian agents Claude Native anthropic/ models (Opus, Sonnet, Haiku)
Gemini Powers frontend, documentation, and multimodal agents 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) => { .action(async (options) => {
const args: InstallArgs = { const args: InstallArgs = {
@ -43,6 +48,8 @@ Model Providers:
claude: options.claude, claude: options.claude,
gemini: options.gemini, gemini: options.gemini,
copilot: options.copilot, copilot: options.copilot,
opencodeZen: options.opencodeZen,
zaiCodingPlan: options.zaiCodingPlan,
skipAuth: options.skipAuth ?? false, skipAuth: options.skipAuth ?? false,
} }
const exitCode = await install(args) const exitCode = await install(args)

View File

@ -40,17 +40,18 @@ function formatConfigSummary(config: InstallConfig): string {
const claudeDetail = config.hasClaude ? (config.isMax20 ? "max20" : "standard") : undefined const claudeDetail = config.hasClaude ? (config.isMax20 ? "max20" : "standard") : undefined
lines.push(formatProvider("Claude", config.hasClaude, claudeDetail)) lines.push(formatProvider("Claude", config.hasClaude, claudeDetail))
lines.push(formatProvider("Gemini", config.hasGemini)) 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("")
lines.push(color.dim("─".repeat(40))) lines.push(color.dim("─".repeat(40)))
lines.push("") lines.push("")
// v3 beta: No hardcoded models - agents use OpenCode's configured default model lines.push(color.bold(color.white("Model Assignment")))
lines.push(color.bold(color.white("Agent Models")))
lines.push("") lines.push("")
lines.push(` ${SYMBOLS.info} Agents will use your OpenCode default model`) lines.push(` ${SYMBOLS.info} Models auto-configured based on provider priority`)
lines.push(` ${SYMBOLS.bullet} Configure specific models in ${color.cyan("oh-my-opencode.json")} if needed`) lines.push(` ${SYMBOLS.bullet} Priority: Native > Copilot > OpenCode Zen > Z.ai`)
return lines.join("\n") 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)`) 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 } return { valid: errors.length === 0, errors }
} }
@ -135,10 +144,12 @@ function argsToConfig(args: InstallArgs): InstallConfig {
isMax20: args.claude === "max20", isMax20: args.claude === "max20",
hasGemini: args.gemini === "yes", hasGemini: args.gemini === "yes",
hasCopilot: args.copilot === "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" let claude: ClaudeSubscription = "no"
if (detected.hasClaude) { if (detected.hasClaude) {
claude = detected.isMax20 ? "max20" : "yes" claude = detected.isMax20 ? "max20" : "yes"
@ -148,6 +159,8 @@ function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubs
claude, claude,
gemini: detected.hasGemini ? "yes" : "no", gemini: detected.hasGemini ? "yes" : "no",
copilot: detected.hasCopilot ? "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<InstallConfig | nul
return null return null
} }
const opencodeZen = await p.select({
message: "Do you have access to OpenCode Zen (opencode/ models)?",
options: [
{ value: "no" as const, label: "No", hint: "Will use other configured providers" },
{ value: "yes" as const, label: "Yes", hint: "opencode/claude-opus-4-5, opencode/gpt-5.2, etc." },
],
initialValue: initial.opencodeZen,
})
if (p.isCancel(opencodeZen)) {
p.cancel("Installation cancelled.")
return null
}
const zaiCodingPlan = await p.select({
message: "Do you have a Z.ai Coding Plan subscription?",
options: [
{ value: "no" as const, label: "No", hint: "Will use other configured providers" },
{ value: "yes" as const, label: "Yes", hint: "zai-coding-plan/glm-4.7 for Librarian" },
],
initialValue: initial.zaiCodingPlan,
})
if (p.isCancel(zaiCodingPlan)) {
p.cancel("Installation cancelled.")
return null
}
return { return {
hasClaude: claude !== "no", hasClaude: claude !== "no",
isMax20: claude === "max20", isMax20: claude === "max20",
hasGemini: gemini === "yes", hasGemini: gemini === "yes",
hasCopilot: copilot === "yes", hasCopilot: copilot === "yes",
hasOpencodeZen: opencodeZen === "yes",
hasZaiCodingPlan: zaiCodingPlan === "yes",
} }
} }

204
src/cli/model-fallback.ts Normal file
View File

@ -0,0 +1,204 @@
import type { InstallConfig } from "./types"
type ProviderTier = "native" | "github-copilot" | "opencode" | "zai-coding-plan"
type ModelCapability =
| "opus-level"
| "sonnet-level"
| "haiku-level"
| "reasoning"
| "codex"
| "visual"
| "fast"
| "glm"
interface ProviderAvailability {
native: {
claude: boolean
openai: boolean
gemini: boolean
}
copilot: boolean
opencode: boolean
zai: boolean
}
export interface GeneratedOmoConfig {
$schema: string
agents?: Record<string, { model: string }>
categories?: Record<string, { model: string }>
[key: string]: unknown
}
const MODEL_CATALOG: Record<ProviderTier, Partial<Record<ModelCapability, string>>> = {
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<string, ModelCapability> = {
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<string, ModelCapability> = {
"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<string, { model: string }> = {}
const categories: Record<string, { model: string }> = {}
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,
}
}

View File

@ -6,6 +6,8 @@ export interface InstallArgs {
claude?: ClaudeSubscription claude?: ClaudeSubscription
gemini?: BooleanArg gemini?: BooleanArg
copilot?: BooleanArg copilot?: BooleanArg
opencodeZen?: BooleanArg
zaiCodingPlan?: BooleanArg
skipAuth?: boolean skipAuth?: boolean
} }
@ -14,6 +16,8 @@ export interface InstallConfig {
isMax20: boolean isMax20: boolean
hasGemini: boolean hasGemini: boolean
hasCopilot: boolean hasCopilot: boolean
hasOpencodeZen: boolean
hasZaiCodingPlan: boolean
} }
export interface ConfigMergeResult { export interface ConfigMergeResult {
@ -28,4 +32,6 @@ export interface DetectedConfig {
isMax20: boolean isMax20: boolean
hasGemini: boolean hasGemini: boolean
hasCopilot: boolean hasCopilot: boolean
hasOpencodeZen: boolean
hasZaiCodingPlan: boolean
} }