feat(agents): add Hephaestus - autonomous deep worker agent (#1287)
* refactor(keyword-detector): split constants into domain-specific modules * feat(shared): add requiresAnyModel and isAnyFallbackModelAvailable * feat(config): add hephaestus to agent schemas * feat(agents): add Hephaestus autonomous deep worker * feat(cli): update model-fallback for hephaestus support * feat(plugin): add hephaestus to config handler with ordering * test(delegate-task): update tests for hephaestus agent * docs: update AGENTS.md files for hephaestus * docs: add hephaestus to READMEs * chore: regenerate config schema * fix(delegate-task): bypass requiresModel check when user provides explicit config * docs(hephaestus): add 4-part context structure for explore/librarian prompts * docs: fix review comments from cubic (non-breaking changes) - Move Hephaestus from Primary Agents to Subagents (uses own fallback chain) - Fix Hephaestus fallback chain documentation (claude-opus-4-5 → gemini-3-pro) - Add settings.local.json to claude-code-hooks config sources - Fix delegate_task parameters in ultrawork prompt (agent→subagent_type, background→run_in_background, add load_skills) - Update line counts in AGENTS.md (index.ts: 788, manager.ts: 1440) * docs: fix additional documentation inconsistencies from oracle review - Fix delegate_task parameters in Background Agents example (docs/features.md) - Fix Hephaestus fallback chain in root AGENTS.md to match model-requirements.ts * docs: clarify Hephaestus has no fallback (requires gpt-5.2-codex only) Hephaestus uses requiresModel constraint - it only activates when gpt-5.2-codex is available. The fallback chain in code is unreachable, so documentation should not mention fallbacks. * fix(hephaestus): remove unreachable fallback chain entries Hephaestus has requiresModel: gpt-5.2-codex which means the agent only activates when that specific model is available. The fallback entries (claude-opus-4-5, gemini-3-pro) were unreachable and misleading. --------- Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
This commit is contained in:
+8
-4
@@ -2,15 +2,17 @@
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
CLI entry: `bunx oh-my-opencode`. Interactive installer, doctor diagnostics. Commander.js + @clack/prompts.
|
||||
CLI entry: `bunx oh-my-opencode`. 4 commands with Commander.js + @clack/prompts TUI.
|
||||
|
||||
**Commands**: install (interactive setup), doctor (14 health checks), run (session launcher), get-local-version
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
cli/
|
||||
├── index.ts # Commander.js entry (4 commands)
|
||||
├── install.ts # Interactive TUI (520 lines)
|
||||
├── config-manager.ts # JSONC parsing (664 lines)
|
||||
├── install.ts # Interactive TUI (542 lines)
|
||||
├── config-manager.ts # JSONC parsing (667 lines)
|
||||
├── types.ts # InstallArgs, InstallConfig
|
||||
├── model-fallback.ts # Model fallback configuration
|
||||
├── doctor/
|
||||
@@ -19,7 +21,7 @@ cli/
|
||||
│ ├── formatter.ts # Colored output
|
||||
│ ├── constants.ts # Check IDs, symbols
|
||||
│ ├── types.ts # CheckResult, CheckDefinition (114 lines)
|
||||
│ └── checks/ # 14 checks, 21 files
|
||||
│ └── checks/ # 14 checks, 23 files
|
||||
│ ├── version.ts # OpenCode + plugin version
|
||||
│ ├── config.ts # JSONC validity, Zod
|
||||
│ ├── auth.ts # Anthropic, OpenAI, Google
|
||||
@@ -30,6 +32,8 @@ cli/
|
||||
│ └── gh.ts # GitHub CLI
|
||||
├── run/
|
||||
│ └── index.ts # Session launcher
|
||||
├── mcp-oauth/
|
||||
│ └── index.ts # MCP OAuth flow
|
||||
└── get-local-version/
|
||||
└── index.ts # Version detection
|
||||
```
|
||||
|
||||
@@ -10,6 +10,9 @@ exports[`generateModelConfig no providers available returns ULTIMATE_FALLBACK fo
|
||||
"explore": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@@ -28,9 +31,6 @@ exports[`generateModelConfig no providers available returns ULTIMATE_FALLBACK fo
|
||||
"prometheus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
@@ -94,18 +94,11 @@ exports[`generateModelConfig single native provider uses Claude models when only
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
"deep": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
@@ -168,14 +161,6 @@ exports[`generateModelConfig single native provider uses Claude models with isMa
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
"deep": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
@@ -211,6 +196,10 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"explore": {
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@@ -233,15 +222,8 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"model": "openai/gpt-5.2",
|
||||
"variant": "high",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"variant": "high",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
"deep": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
@@ -281,6 +263,10 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"explore": {
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@@ -303,15 +289,8 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"model": "openai/gpt-5.2",
|
||||
"variant": "high",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
"deep": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
@@ -372,19 +351,12 @@ exports[`generateModelConfig single native provider uses Gemini models when only
|
||||
"prometheus": {
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"deep": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
@@ -439,19 +411,12 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
|
||||
"prometheus": {
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"deep": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
@@ -485,6 +450,10 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
@@ -508,7 +477,8 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
@@ -553,6 +523,10 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
@@ -623,6 +597,10 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"explore": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@@ -646,7 +624,8 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
@@ -691,6 +670,10 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"explore": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@@ -761,6 +744,10 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"explore": {
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
@@ -784,7 +771,8 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
@@ -829,6 +817,10 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"explore": {
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
@@ -918,16 +910,10 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian whe
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"deep": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@@ -983,12 +969,6 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"deep": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@@ -1021,6 +1001,10 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@@ -1044,7 +1028,8 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
@@ -1089,6 +1074,10 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"explore": {
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
@@ -1112,7 +1101,8 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
@@ -1180,18 +1170,11 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
"deep": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
@@ -1249,7 +1232,8 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
@@ -1257,10 +1241,6 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"deep": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
@@ -1294,6 +1274,10 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"explore": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
@@ -1317,7 +1301,8 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
@@ -1362,6 +1347,10 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
@@ -1385,7 +1374,8 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
@@ -1430,6 +1420,10 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("getPluginNameWithVersion", () => {
|
||||
})
|
||||
|
||||
test("returns @latest when current version matches latest tag", async () => {
|
||||
// given npm dist-tags with latest=2.14.0
|
||||
// #given npm dist-tags with latest=2.14.0
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -19,15 +19,15 @@ describe("getPluginNameWithVersion", () => {
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// when current version is 2.14.0
|
||||
// #when current version is 2.14.0
|
||||
const result = await getPluginNameWithVersion("2.14.0")
|
||||
|
||||
// then should use @latest tag
|
||||
// #then should use @latest tag
|
||||
expect(result).toBe("oh-my-opencode@latest")
|
||||
})
|
||||
|
||||
test("returns @beta when current version matches beta tag", async () => {
|
||||
// given npm dist-tags with beta=3.0.0-beta.3
|
||||
// #given npm dist-tags with beta=3.0.0-beta.3
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -35,15 +35,15 @@ describe("getPluginNameWithVersion", () => {
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// when current version is 3.0.0-beta.3
|
||||
// #when current version is 3.0.0-beta.3
|
||||
const result = await getPluginNameWithVersion("3.0.0-beta.3")
|
||||
|
||||
// then should use @beta tag
|
||||
// #then should use @beta tag
|
||||
expect(result).toBe("oh-my-opencode@beta")
|
||||
})
|
||||
|
||||
test("returns @next when current version matches next tag", async () => {
|
||||
// given npm dist-tags with next=3.1.0-next.1
|
||||
// #given npm dist-tags with next=3.1.0-next.1
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -51,15 +51,15 @@ describe("getPluginNameWithVersion", () => {
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// when current version is 3.1.0-next.1
|
||||
// #when current version is 3.1.0-next.1
|
||||
const result = await getPluginNameWithVersion("3.1.0-next.1")
|
||||
|
||||
// then should use @next tag
|
||||
// #then should use @next tag
|
||||
expect(result).toBe("oh-my-opencode@next")
|
||||
})
|
||||
|
||||
test("returns pinned version when no tag matches", async () => {
|
||||
// given npm dist-tags with beta=3.0.0-beta.3
|
||||
// #given npm dist-tags with beta=3.0.0-beta.3
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -67,26 +67,26 @@ describe("getPluginNameWithVersion", () => {
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// when current version is old beta 3.0.0-beta.2
|
||||
// #when current version is old beta 3.0.0-beta.2
|
||||
const result = await getPluginNameWithVersion("3.0.0-beta.2")
|
||||
|
||||
// then should pin to specific version
|
||||
// #then should pin to specific version
|
||||
expect(result).toBe("oh-my-opencode@3.0.0-beta.2")
|
||||
})
|
||||
|
||||
test("returns pinned version when fetch fails", async () => {
|
||||
// given network failure
|
||||
// #given network failure
|
||||
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
|
||||
|
||||
// when current version is 3.0.0-beta.3
|
||||
// #when current version is 3.0.0-beta.3
|
||||
const result = await getPluginNameWithVersion("3.0.0-beta.3")
|
||||
|
||||
// then should fall back to pinned version
|
||||
// #then should fall back to pinned version
|
||||
expect(result).toBe("oh-my-opencode@3.0.0-beta.3")
|
||||
})
|
||||
|
||||
test("returns pinned version when npm returns non-ok response", async () => {
|
||||
// given npm returns 404
|
||||
// #given npm returns 404
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
@@ -94,15 +94,15 @@ describe("getPluginNameWithVersion", () => {
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// when current version is 2.14.0
|
||||
// #when current version is 2.14.0
|
||||
const result = await getPluginNameWithVersion("2.14.0")
|
||||
|
||||
// then should fall back to pinned version
|
||||
// #then should fall back to pinned version
|
||||
expect(result).toBe("oh-my-opencode@2.14.0")
|
||||
})
|
||||
|
||||
test("prioritizes latest over other tags when version matches multiple", async () => {
|
||||
// given version matches both latest and beta (during release promotion)
|
||||
// #given version matches both latest and beta (during release promotion)
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -110,10 +110,10 @@ describe("getPluginNameWithVersion", () => {
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// when current version matches both
|
||||
// #when current version matches both
|
||||
const result = await getPluginNameWithVersion("3.0.0")
|
||||
|
||||
// then should prioritize @latest
|
||||
// #then should prioritize @latest
|
||||
expect(result).toBe("oh-my-opencode@latest")
|
||||
})
|
||||
})
|
||||
@@ -126,7 +126,7 @@ describe("fetchNpmDistTags", () => {
|
||||
})
|
||||
|
||||
test("returns dist-tags on success", async () => {
|
||||
// given npm returns dist-tags
|
||||
// #given npm returns dist-tags
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -134,26 +134,26 @@ describe("fetchNpmDistTags", () => {
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// when fetching dist-tags
|
||||
// #when fetching dist-tags
|
||||
const result = await fetchNpmDistTags("oh-my-opencode")
|
||||
|
||||
// then should return the tags
|
||||
// #then should return the tags
|
||||
expect(result).toEqual({ latest: "2.14.0", beta: "3.0.0-beta.3" })
|
||||
})
|
||||
|
||||
test("returns null on network failure", async () => {
|
||||
// given network failure
|
||||
// #given network failure
|
||||
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
|
||||
|
||||
// when fetching dist-tags
|
||||
// #when fetching dist-tags
|
||||
const result = await fetchNpmDistTags("oh-my-opencode")
|
||||
|
||||
// then should return null
|
||||
// #then should return null
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
test("returns null on non-ok response", async () => {
|
||||
// given npm returns 404
|
||||
// #given npm returns 404
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
@@ -161,10 +161,10 @@ describe("fetchNpmDistTags", () => {
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// when fetching dist-tags
|
||||
// #when fetching dist-tags
|
||||
const result = await fetchNpmDistTags("oh-my-opencode")
|
||||
|
||||
// then should return null
|
||||
// #then should return null
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -202,19 +202,19 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
})
|
||||
|
||||
test("Gemini models have variant definitions", () => {
|
||||
// given the antigravity provider config
|
||||
// #given the antigravity provider config
|
||||
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
||||
|
||||
// when checking Gemini Pro variants
|
||||
// #when checking Gemini Pro variants
|
||||
const pro = models["antigravity-gemini-3-pro"]
|
||||
// then should have low and high variants
|
||||
// #then should have low and high variants
|
||||
expect(pro.variants).toBeTruthy()
|
||||
expect(pro.variants.low).toBeTruthy()
|
||||
expect(pro.variants.high).toBeTruthy()
|
||||
|
||||
// when checking Gemini Flash variants
|
||||
// #when checking Gemini Flash variants
|
||||
const flash = models["antigravity-gemini-3-flash"]
|
||||
// then should have minimal, low, medium, high variants
|
||||
// #then should have minimal, low, medium, high variants
|
||||
expect(flash.variants).toBeTruthy()
|
||||
expect(flash.variants.minimal).toBeTruthy()
|
||||
expect(flash.variants.low).toBeTruthy()
|
||||
@@ -223,14 +223,14 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
})
|
||||
|
||||
test("Claude thinking models have variant definitions", () => {
|
||||
// given the antigravity provider config
|
||||
// #given the antigravity provider config
|
||||
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
||||
|
||||
// when checking Claude thinking variants
|
||||
// #when checking Claude thinking variants
|
||||
const sonnetThinking = models["antigravity-claude-sonnet-4-5-thinking"]
|
||||
const opusThinking = models["antigravity-claude-opus-4-5-thinking"]
|
||||
|
||||
// then both should have low and max variants
|
||||
// #then both should have low and max variants
|
||||
for (const model of [sonnetThinking, opusThinking]) {
|
||||
expect(model.variants).toBeTruthy()
|
||||
expect(model.variants.low).toBeTruthy()
|
||||
@@ -241,7 +241,7 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
|
||||
describe("generateOmoConfig - model fallback system", () => {
|
||||
test("generates native sonnet models when Claude standard subscription", () => {
|
||||
// given user has Claude standard subscription (not max20)
|
||||
// #given user has Claude standard subscription (not max20)
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: false,
|
||||
@@ -253,17 +253,17 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// when generating config
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// then should use native anthropic sonnet (cost-efficient for standard plan)
|
||||
// #then Sisyphus uses Claude (OR logic - at least one provider available)
|
||||
expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
|
||||
expect(result.agents).toBeDefined()
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-sonnet-4-5")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("generates native opus models when Claude max20 subscription", () => {
|
||||
// given user has Claude max20 subscription
|
||||
// #given user has Claude max20 subscription
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: true,
|
||||
@@ -275,15 +275,15 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// when generating config
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// then should use native anthropic opus (max power for max20 plan)
|
||||
// #then Sisyphus uses Claude (OR logic - at least one provider available)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("uses github-copilot sonnet fallback when only copilot available", () => {
|
||||
// given user has only copilot (no max plan)
|
||||
// #given user has only copilot (no max plan)
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
@@ -295,15 +295,15 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// when generating config
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// then should use github-copilot sonnet models (copilot fallback)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("github-copilot/claude-sonnet-4.5")
|
||||
// #then Sisyphus uses Copilot (OR logic - copilot is in claude-opus-4-5 providers)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("github-copilot/claude-opus-4.5")
|
||||
})
|
||||
|
||||
test("uses ultimate fallback when no providers configured", () => {
|
||||
// given user has no providers
|
||||
// #given user has no providers
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
@@ -315,16 +315,16 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// when generating config
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// then should use ultimate fallback for all agents
|
||||
// #then Sisyphus is omitted (requires all fallback providers)
|
||||
expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("opencode/glm-4.7-free")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus).toBeUndefined()
|
||||
})
|
||||
|
||||
test("uses zai-coding-plan/glm-4.7 for librarian when Z.ai available", () => {
|
||||
// given user has Z.ai and Claude max20
|
||||
// #given user has Z.ai and Claude max20
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: true,
|
||||
@@ -336,17 +336,17 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// when generating config
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// then librarian should use zai-coding-plan/glm-4.7
|
||||
// #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 opus (max20 plan)
|
||||
// #then Sisyphus uses Claude (OR logic)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("uses native OpenAI models when only ChatGPT available", () => {
|
||||
// given user has only ChatGPT subscription
|
||||
// #given user has only ChatGPT subscription
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
@@ -358,19 +358,19 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// when generating config
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// then Sisyphus should use native OpenAI (fallback within native tier)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("openai/gpt-5.2")
|
||||
// then Oracle should use native OpenAI (first fallback entry)
|
||||
// #then Sisyphus is omitted (requires all fallback providers)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus).toBeUndefined()
|
||||
// #then Oracle should use native OpenAI (first fallback entry)
|
||||
expect((result.agents as Record<string, { model: string }>).oracle.model).toBe("openai/gpt-5.2")
|
||||
// then multimodal-looker should use native OpenAI (fallback within native tier)
|
||||
// #then multimodal-looker should use native OpenAI (fallback within native tier)
|
||||
expect((result.agents as Record<string, { model: string }>)["multimodal-looker"].model).toBe("openai/gpt-5.2")
|
||||
})
|
||||
|
||||
test("uses haiku for explore when Claude max20", () => {
|
||||
// given user has Claude max20
|
||||
// #given user has Claude max20
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: true,
|
||||
@@ -382,15 +382,15 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// when generating config
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// then explore should use haiku (max20 plan uses Claude quota)
|
||||
// #then explore should use haiku (max20 plan uses Claude quota)
|
||||
expect((result.agents as Record<string, { model: string }>).explore.model).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("uses haiku for explore regardless of max20 flag", () => {
|
||||
// given user has Claude but not max20
|
||||
// #given user has Claude but not max20
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: false,
|
||||
@@ -402,10 +402,10 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// when generating config
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// then explore should use haiku (isMax20 doesn't affect explore anymore)
|
||||
// #then explore should use haiku (isMax20 doesn't affect explore anymore)
|
||||
expect((result.agents as Record<string, { model: string }>).explore.model).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
})
|
||||
|
||||
+188
-100
@@ -20,103 +20,103 @@ function createConfig(overrides: Partial<InstallConfig> = {}): InstallConfig {
|
||||
describe("generateModelConfig", () => {
|
||||
describe("no providers available", () => {
|
||||
test("returns ULTIMATE_FALLBACK for all agents and categories when no providers", () => {
|
||||
// given no providers are available
|
||||
// #given no providers are available
|
||||
const config = createConfig()
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use ULTIMATE_FALLBACK for everything
|
||||
// #then should use ULTIMATE_FALLBACK for everything
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("single native provider", () => {
|
||||
test("uses Claude models when only Claude is available", () => {
|
||||
// given only Claude is available
|
||||
// #given only Claude is available
|
||||
const config = createConfig({ hasClaude: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use Claude models per NATIVE_FALLBACK_CHAINS
|
||||
// #then should use Claude models per NATIVE_FALLBACK_CHAINS
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses Claude models with isMax20 flag", () => {
|
||||
// given Claude is available with Max 20 plan
|
||||
// #given Claude is available with Max 20 plan
|
||||
const config = createConfig({ hasClaude: true, isMax20: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use higher capability models for Sisyphus
|
||||
// #then should use higher capability models for Sisyphus
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses OpenAI models when only OpenAI is available", () => {
|
||||
// given only OpenAI is available
|
||||
// #given only OpenAI is available
|
||||
const config = createConfig({ hasOpenAI: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use OpenAI models
|
||||
// #then should use OpenAI models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses OpenAI models with isMax20 flag", () => {
|
||||
// given OpenAI is available with Max 20 plan
|
||||
// #given OpenAI is available with Max 20 plan
|
||||
const config = createConfig({ hasOpenAI: true, isMax20: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use higher capability models
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses Gemini models when only Gemini is available", () => {
|
||||
// given only Gemini is available
|
||||
// #given only Gemini is available
|
||||
const config = createConfig({ hasGemini: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use Gemini models
|
||||
// #then should use Gemini models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses Gemini models with isMax20 flag", () => {
|
||||
// given Gemini is available with Max 20 plan
|
||||
// #given Gemini is available with Max 20 plan
|
||||
const config = createConfig({ hasGemini: true, isMax20: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use higher capability models
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("all native providers", () => {
|
||||
test("uses preferred models from fallback chains when all natives available", () => {
|
||||
// given all native providers are available
|
||||
// #given all native providers are available
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasOpenAI: true,
|
||||
hasGemini: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use first provider in each fallback chain
|
||||
// #then should use first provider in each fallback chain
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses preferred models with isMax20 flag when all natives available", () => {
|
||||
// given all native providers are available with Max 20 plan
|
||||
// #given all native providers are available with Max 20 plan
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasOpenAI: true,
|
||||
@@ -124,156 +124,156 @@ describe("generateModelConfig", () => {
|
||||
isMax20: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use higher capability models
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("fallback providers", () => {
|
||||
test("uses OpenCode Zen models when only OpenCode Zen is available", () => {
|
||||
// given only OpenCode Zen is available
|
||||
// #given only OpenCode Zen is available
|
||||
const config = createConfig({ hasOpencodeZen: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use OPENCODE_ZEN_MODELS
|
||||
// #then should use OPENCODE_ZEN_MODELS
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses OpenCode Zen models with isMax20 flag", () => {
|
||||
// given OpenCode Zen is available with Max 20 plan
|
||||
// #given OpenCode Zen is available with Max 20 plan
|
||||
const config = createConfig({ hasOpencodeZen: true, isMax20: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use higher capability models
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses GitHub Copilot models when only Copilot is available", () => {
|
||||
// given only GitHub Copilot is available
|
||||
// #given only GitHub Copilot is available
|
||||
const config = createConfig({ hasCopilot: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use GITHUB_COPILOT_MODELS
|
||||
// #then should use GITHUB_COPILOT_MODELS
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses GitHub Copilot models with isMax20 flag", () => {
|
||||
// given GitHub Copilot is available with Max 20 plan
|
||||
// #given GitHub Copilot is available with Max 20 plan
|
||||
const config = createConfig({ hasCopilot: true, isMax20: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use higher capability models
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses ZAI model for librarian when only ZAI is available", () => {
|
||||
// given only ZAI is available
|
||||
// #given only ZAI is available
|
||||
const config = createConfig({ hasZaiCodingPlan: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use ZAI_MODEL for librarian
|
||||
// #then should use ZAI_MODEL for librarian
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses ZAI model for librarian with isMax20 flag", () => {
|
||||
// given ZAI is available with Max 20 plan
|
||||
// #given ZAI is available with Max 20 plan
|
||||
const config = createConfig({ hasZaiCodingPlan: true, isMax20: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use ZAI_MODEL for librarian
|
||||
// #then should use ZAI_MODEL for librarian
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("mixed provider scenarios", () => {
|
||||
test("uses Claude + OpenCode Zen combination", () => {
|
||||
// given Claude and OpenCode Zen are available
|
||||
// #given Claude and OpenCode Zen are available
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasOpencodeZen: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should prefer Claude (native) over OpenCode Zen
|
||||
// #then should prefer Claude (native) over OpenCode Zen
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses OpenAI + Copilot combination", () => {
|
||||
// given OpenAI and Copilot are available
|
||||
// #given OpenAI and Copilot are available
|
||||
const config = createConfig({
|
||||
hasOpenAI: true,
|
||||
hasCopilot: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should prefer OpenAI (native) over Copilot
|
||||
// #then should prefer OpenAI (native) over Copilot
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses Claude + ZAI combination (librarian uses ZAI)", () => {
|
||||
// given Claude and ZAI are available
|
||||
// #given Claude and ZAI are available
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasZaiCodingPlan: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then librarian should use ZAI, others use Claude
|
||||
// #then librarian should use ZAI, others use Claude
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses Gemini + Claude combination (explore uses Gemini)", () => {
|
||||
// given Gemini and Claude are available
|
||||
// #given Gemini and Claude are available
|
||||
const config = createConfig({
|
||||
hasGemini: true,
|
||||
hasClaude: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then explore should use Gemini flash
|
||||
// #then explore should use Gemini flash
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses all fallback providers together", () => {
|
||||
// given all fallback providers are available
|
||||
// #given all fallback providers are available
|
||||
const config = createConfig({
|
||||
hasOpencodeZen: true,
|
||||
hasCopilot: true,
|
||||
hasZaiCodingPlan: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should prefer OpenCode Zen, but librarian uses ZAI
|
||||
// #then should prefer OpenCode Zen, but librarian uses ZAI
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses all providers together", () => {
|
||||
// given all providers are available
|
||||
// #given all providers are available
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasOpenAI: true,
|
||||
@@ -283,15 +283,15 @@ describe("generateModelConfig", () => {
|
||||
hasZaiCodingPlan: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should prefer native providers, librarian uses ZAI
|
||||
// #then should prefer native providers, librarian uses ZAI
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses all providers with isMax20 flag", () => {
|
||||
// given all providers are available with Max 20 plan
|
||||
// #given all providers are available with Max 20 plan
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasOpenAI: true,
|
||||
@@ -302,131 +302,219 @@ describe("generateModelConfig", () => {
|
||||
isMax20: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should use higher capability models
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("explore agent special cases", () => {
|
||||
test("explore uses gpt-5-nano when only Gemini available (no Claude)", () => {
|
||||
// given only Gemini is available (no Claude)
|
||||
// #given only Gemini is available (no Claude)
|
||||
const config = createConfig({ hasGemini: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then explore should use gpt-5-nano (Claude haiku not available)
|
||||
// #then explore should use gpt-5-nano (Claude haiku not available)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
|
||||
})
|
||||
|
||||
test("explore uses Claude haiku when Claude available", () => {
|
||||
// given Claude is available
|
||||
// #given Claude is available
|
||||
const config = createConfig({ hasClaude: true, isMax20: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then explore should use claude-haiku-4-5
|
||||
// #then explore should use claude-haiku-4-5
|
||||
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("explore uses Claude haiku regardless of isMax20 flag", () => {
|
||||
// given Claude is available without Max 20 plan
|
||||
// #given Claude is available without Max 20 plan
|
||||
const config = createConfig({ hasClaude: true, isMax20: false })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then explore should use claude-haiku-4-5 (isMax20 doesn't affect explore)
|
||||
// #then explore should use claude-haiku-4-5 (isMax20 doesn't affect explore)
|
||||
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("explore uses gpt-5-nano when only OpenAI available", () => {
|
||||
// given only OpenAI is available
|
||||
// #given only OpenAI is available
|
||||
const config = createConfig({ hasOpenAI: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then explore should use gpt-5-nano (fallback)
|
||||
// #then explore should use gpt-5-nano (fallback)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
|
||||
})
|
||||
|
||||
test("explore uses gpt-5-mini when only Copilot available", () => {
|
||||
// given only Copilot is available
|
||||
// #given only Copilot is available
|
||||
const config = createConfig({ hasCopilot: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then explore should use gpt-5-mini (Copilot fallback)
|
||||
// #then explore should use gpt-5-mini (Copilot fallback)
|
||||
expect(result.agents?.explore?.model).toBe("github-copilot/gpt-5-mini")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Sisyphus agent special cases", () => {
|
||||
test("Sisyphus uses sisyphus-high capability when isMax20 is true", () => {
|
||||
// given Claude is available with Max 20 plan
|
||||
test("Sisyphus is created when at least one fallback provider is available (Claude)", () => {
|
||||
// #given
|
||||
const config = createConfig({ hasClaude: true, isMax20: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then Sisyphus should use opus (sisyphus-high)
|
||||
// #then
|
||||
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("Sisyphus uses sisyphus-low capability when isMax20 is false", () => {
|
||||
// given Claude is available without Max 20 plan
|
||||
const config = createConfig({ hasClaude: true, isMax20: false })
|
||||
test("Sisyphus is created when multiple fallback providers are available", () => {
|
||||
// #given
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasKimiForCoding: true,
|
||||
hasOpencodeZen: true,
|
||||
hasZaiCodingPlan: true,
|
||||
isMax20: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then Sisyphus should use sonnet (sisyphus-low)
|
||||
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-sonnet-4-5")
|
||||
// #then
|
||||
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("Sisyphus is omitted when no fallback provider is available (OpenAI not in chain)", () => {
|
||||
// #given
|
||||
const config = createConfig({ hasOpenAI: true })
|
||||
|
||||
// #when
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then
|
||||
expect(result.agents?.sisyphus).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Hephaestus agent special cases", () => {
|
||||
test("Hephaestus is created when OpenAI is available (has gpt-5.2-codex)", () => {
|
||||
// #given
|
||||
const config = createConfig({ hasOpenAI: true })
|
||||
|
||||
// #when
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then
|
||||
expect(result.agents?.hephaestus?.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(result.agents?.hephaestus?.variant).toBe("medium")
|
||||
})
|
||||
|
||||
test("Hephaestus is created when Copilot is available (has gpt-5.2-codex)", () => {
|
||||
// #given
|
||||
const config = createConfig({ hasCopilot: true })
|
||||
|
||||
// #when
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then
|
||||
expect(result.agents?.hephaestus?.model).toBe("github-copilot/gpt-5.2-codex")
|
||||
expect(result.agents?.hephaestus?.variant).toBe("medium")
|
||||
})
|
||||
|
||||
test("Hephaestus is created when OpenCode Zen is available (has gpt-5.2-codex)", () => {
|
||||
// #given
|
||||
const config = createConfig({ hasOpencodeZen: true })
|
||||
|
||||
// #when
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then
|
||||
expect(result.agents?.hephaestus?.model).toBe("opencode/gpt-5.2-codex")
|
||||
expect(result.agents?.hephaestus?.variant).toBe("medium")
|
||||
})
|
||||
|
||||
test("Hephaestus is omitted when only Claude is available (no gpt-5.2-codex)", () => {
|
||||
// #given
|
||||
const config = createConfig({ hasClaude: true })
|
||||
|
||||
// #when
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then
|
||||
expect(result.agents?.hephaestus).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Hephaestus is omitted when only Gemini is available (no gpt-5.2-codex)", () => {
|
||||
// #given
|
||||
const config = createConfig({ hasGemini: true })
|
||||
|
||||
// #when
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then
|
||||
expect(result.agents?.hephaestus).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Hephaestus is omitted when only ZAI is available (no gpt-5.2-codex)", () => {
|
||||
// #given
|
||||
const config = createConfig({ hasZaiCodingPlan: true })
|
||||
|
||||
// #when
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then
|
||||
expect(result.agents?.hephaestus).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("librarian agent special cases", () => {
|
||||
test("librarian uses ZAI when ZAI is available regardless of other providers", () => {
|
||||
// given ZAI and Claude are available
|
||||
// #given ZAI and Claude are available
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasZaiCodingPlan: true,
|
||||
})
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then librarian should use ZAI_MODEL
|
||||
// #then librarian should use ZAI_MODEL
|
||||
expect(result.agents?.librarian?.model).toBe("zai-coding-plan/glm-4.7")
|
||||
})
|
||||
|
||||
test("librarian uses claude-sonnet when ZAI not available but Claude is", () => {
|
||||
// given only Claude is available (no ZAI)
|
||||
// #given only Claude is available (no ZAI)
|
||||
const config = createConfig({ hasClaude: true })
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then librarian should use claude-sonnet-4-5 (third in fallback chain after ZAI and opencode/glm)
|
||||
// #then librarian should use claude-sonnet-4-5 (third in fallback chain after ZAI and opencode/glm)
|
||||
expect(result.agents?.librarian?.model).toBe("anthropic/claude-sonnet-4-5")
|
||||
})
|
||||
})
|
||||
|
||||
describe("schema URL", () => {
|
||||
test("always includes correct schema URL", () => {
|
||||
// given any config
|
||||
// #given any config
|
||||
const config = createConfig()
|
||||
|
||||
// when generateModelConfig is called
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// then should include correct schema URL
|
||||
// #then should include correct schema URL
|
||||
expect(result.$schema).toBe(
|
||||
"https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json"
|
||||
)
|
||||
|
||||
+45
-20
@@ -97,19 +97,27 @@ function resolveModelFromChain(
|
||||
return null
|
||||
}
|
||||
|
||||
function getSisyphusFallbackChain(isMaxPlan: boolean): FallbackEntry[] {
|
||||
// Sisyphus uses opus when isMaxPlan, sonnet otherwise
|
||||
if (isMaxPlan) {
|
||||
return AGENT_MODEL_REQUIREMENTS.sisyphus.fallbackChain
|
||||
}
|
||||
// For non-max plan, use sonnet instead of opus
|
||||
return [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
]
|
||||
function getSisyphusFallbackChain(): FallbackEntry[] {
|
||||
return AGENT_MODEL_REQUIREMENTS.sisyphus.fallbackChain
|
||||
}
|
||||
|
||||
function isAnyFallbackEntryAvailable(
|
||||
fallbackChain: FallbackEntry[],
|
||||
avail: ProviderAvailability
|
||||
): boolean {
|
||||
return fallbackChain.some((entry) =>
|
||||
entry.providers.some((provider) => isProviderAvailable(provider, avail))
|
||||
)
|
||||
}
|
||||
|
||||
function isRequiredModelAvailable(
|
||||
requiresModel: string,
|
||||
fallbackChain: FallbackEntry[],
|
||||
avail: ProviderAvailability
|
||||
): boolean {
|
||||
const matchingEntry = fallbackChain.find((entry) => entry.model === requiresModel)
|
||||
if (!matchingEntry) return false
|
||||
return matchingEntry.providers.some((provider) => isProviderAvailable(provider, avail))
|
||||
}
|
||||
|
||||
export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
@@ -127,7 +135,9 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
return {
|
||||
$schema: SCHEMA_URL,
|
||||
agents: Object.fromEntries(
|
||||
Object.keys(AGENT_MODEL_REQUIREMENTS).map((role) => [role, { model: ULTIMATE_FALLBACK }])
|
||||
Object.entries(AGENT_MODEL_REQUIREMENTS)
|
||||
.filter(([role, req]) => !(role === "sisyphus" && req.requiresAnyModel))
|
||||
.map(([role]) => [role, { model: ULTIMATE_FALLBACK }])
|
||||
),
|
||||
categories: Object.fromEntries(
|
||||
Object.keys(CATEGORY_MODEL_REQUIREMENTS).map((cat) => [cat, { model: ULTIMATE_FALLBACK }])
|
||||
@@ -139,13 +149,11 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
const categories: Record<string, CategoryConfig> = {}
|
||||
|
||||
for (const [role, req] of Object.entries(AGENT_MODEL_REQUIREMENTS)) {
|
||||
// Special case: librarian always uses ZAI first if available
|
||||
if (role === "librarian" && avail.zai) {
|
||||
agents[role] = { model: ZAI_MODEL }
|
||||
continue
|
||||
}
|
||||
|
||||
// Special case: explore uses Claude haiku → GitHub Copilot gpt-5-mini → OpenCode gpt-5-nano
|
||||
if (role === "explore") {
|
||||
if (avail.native.claude) {
|
||||
agents[role] = { model: "anthropic/claude-haiku-4-5" }
|
||||
@@ -159,11 +167,24 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
continue
|
||||
}
|
||||
|
||||
// Special case: Sisyphus uses different fallbackChain based on isMaxPlan
|
||||
const fallbackChain =
|
||||
role === "sisyphus" ? getSisyphusFallbackChain(avail.isMaxPlan) : req.fallbackChain
|
||||
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
|
||||
agents[role] = variant ? { model: resolved.model, variant } : { model: resolved.model }
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const resolved = resolveModelFromChain(fallbackChain, avail)
|
||||
if (req.requiresModel && !isRequiredModelAvailable(req.requiresModel, req.fallbackChain, avail)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const resolved = resolveModelFromChain(req.fallbackChain, avail)
|
||||
if (resolved) {
|
||||
const variant = resolved.variant ?? req.variant
|
||||
agents[role] = variant ? { model: resolved.model, variant } : { model: resolved.model }
|
||||
@@ -179,6 +200,10 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
? CATEGORY_MODEL_REQUIREMENTS["unspecified-low"].fallbackChain
|
||||
: req.fallbackChain
|
||||
|
||||
if (req.requiresModel && !isRequiredModelAvailable(req.requiresModel, req.fallbackChain, avail)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const resolved = resolveModelFromChain(fallbackChain, avail)
|
||||
if (resolved) {
|
||||
const variant = resolved.variant ?? req.variant
|
||||
|
||||
Reference in New Issue
Block a user