diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 87d6db761..b0280f52e 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -966,6 +966,28 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => { cacheSpy.mockRestore() } }) + + test("atlas and metis resolve to OpenAI in an OpenAI-only environment without a system default", async () => { + // #given + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set(["openai/gpt-5.4"])) + const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"]) + + try { + // #when + const agents = await createBuiltinAgents([], {}, undefined, undefined, undefined, undefined, [], {}) + + // #then + expect(agents.atlas).toBeDefined() + expect(agents.atlas.model).toBe("openai/gpt-5.4") + expect(agents.atlas.variant).toBe("medium") + expect(agents.metis).toBeDefined() + expect(agents.metis.model).toBe("openai/gpt-5.4") + expect(agents.metis.variant).toBe("high") + } finally { + fetchSpy.mockRestore() + cacheSpy.mockRestore() + } + }) }) describe("buildAgent with category and skills", () => { diff --git a/src/cli/__snapshots__/model-fallback.test.ts.snap b/src/cli/__snapshots__/model-fallback.test.ts.snap index 8602b7377..e20e589fc 100644 --- a/src/cli/__snapshots__/model-fallback.test.ts.snap +++ b/src/cli/__snapshots__/model-fallback.test.ts.snap @@ -69,7 +69,7 @@ exports[`generateModelConfig single native provider uses Claude models when only "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "explore": { "model": "anthropic/claude-haiku-4-5", @@ -110,17 +110,17 @@ exports[`generateModelConfig single native provider uses Claude models when only "variant": "max", }, "unspecified-high": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "unspecified-low": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { "model": "anthropic/claude-opus-4-6", "variant": "max", }, "writing": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, }, } @@ -131,7 +131,7 @@ exports[`generateModelConfig single native provider uses Claude models with isMa "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "explore": { "model": "anthropic/claude-haiku-4-5", @@ -176,14 +176,14 @@ exports[`generateModelConfig single native provider uses Claude models with isMa "variant": "max", }, "unspecified-low": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { "model": "anthropic/claude-opus-4-6", "variant": "max", }, "writing": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, }, } @@ -194,7 +194,8 @@ exports[`generateModelConfig single native provider uses OpenAI models when only "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "opencode/glm-4.7-free", + "model": "openai/gpt-5.4", + "variant": "medium", }, "explore": { "model": "openai/gpt-5.4", @@ -209,7 +210,8 @@ exports[`generateModelConfig single native provider uses OpenAI models when only "variant": "medium", }, "metis": { - "model": "opencode/glm-4.7-free", + "model": "openai/gpt-5.4", + "variant": "high", }, "momus": { "model": "openai/gpt-5.4", @@ -232,7 +234,8 @@ exports[`generateModelConfig single native provider uses OpenAI models when only "variant": "medium", }, "sisyphus-junior": { - "model": "opencode/glm-4.7-free", + "model": "openai/gpt-5.4", + "variant": "medium", }, }, "categories": { @@ -249,7 +252,7 @@ exports[`generateModelConfig single native provider uses OpenAI models when only "variant": "low", }, "ultrabrain": { - "model": "openai/gpt-5.3-codex", + "model": "openai/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { @@ -277,7 +280,8 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "opencode/glm-4.7-free", + "model": "openai/gpt-5.4", + "variant": "medium", }, "explore": { "model": "openai/gpt-5.4", @@ -292,7 +296,8 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa "variant": "medium", }, "metis": { - "model": "opencode/glm-4.7-free", + "model": "openai/gpt-5.4", + "variant": "high", }, "momus": { "model": "openai/gpt-5.4", @@ -315,7 +320,8 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa "variant": "medium", }, "sisyphus-junior": { - "model": "opencode/glm-4.7-free", + "model": "openai/gpt-5.4", + "variant": "medium", }, }, "categories": { @@ -332,7 +338,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa "variant": "low", }, "ultrabrain": { - "model": "openai/gpt-5.3-codex", + "model": "openai/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { @@ -480,7 +486,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "explore": { "model": "anthropic/claude-haiku-4-5", @@ -530,14 +536,14 @@ exports[`generateModelConfig all native providers uses preferred models from fal "model": "anthropic/claude-haiku-4-5", }, "ultrabrain": { - "model": "openai/gpt-5.3-codex", + "model": "openai/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "unspecified-low": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { "model": "google/gemini-3.1-pro-preview", @@ -555,7 +561,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "explore": { "model": "anthropic/claude-haiku-4-5", @@ -605,15 +611,15 @@ exports[`generateModelConfig all native providers uses preferred models with isM "model": "anthropic/claude-haiku-4-5", }, "ultrabrain": { - "model": "openai/gpt-5.3-codex", + "model": "openai/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { - "model": "openai/gpt-5.4", - "variant": "high", + "model": "anthropic/claude-opus-4-6", + "variant": "max", }, "unspecified-low": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { "model": "google/gemini-3.1-pro-preview", @@ -631,7 +637,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "opencode/claude-sonnet-4-5", + "model": "opencode/claude-sonnet-4-6", }, "explore": { "model": "opencode/claude-haiku-4-5", @@ -681,14 +687,14 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on "model": "opencode/claude-haiku-4-5", }, "ultrabrain": { - "model": "opencode/gpt-5.3-codex", + "model": "opencode/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { - "model": "opencode/claude-sonnet-4-5", + "model": "opencode/claude-sonnet-4-6", }, "unspecified-low": { - "model": "opencode/claude-sonnet-4-5", + "model": "opencode/claude-sonnet-4-6", }, "visual-engineering": { "model": "opencode/gemini-3.1-pro", @@ -706,7 +712,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "opencode/claude-sonnet-4-5", + "model": "opencode/claude-sonnet-4-6", }, "explore": { "model": "opencode/claude-haiku-4-5", @@ -756,15 +762,15 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is "model": "opencode/claude-haiku-4-5", }, "ultrabrain": { - "model": "opencode/gpt-5.3-codex", + "model": "opencode/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { - "model": "opencode/gpt-5.4", - "variant": "high", + "model": "opencode/claude-opus-4-6", + "variant": "max", }, "unspecified-low": { - "model": "opencode/claude-sonnet-4-5", + "model": "opencode/claude-sonnet-4-6", }, "visual-engineering": { "model": "opencode/gemini-3.1-pro", @@ -782,11 +788,15 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "explore": { "model": "github-copilot/gpt-5-mini", }, + "hephaestus": { + "model": "github-copilot/gpt-5.4", + "variant": "medium", + }, "metis": { "model": "github-copilot/claude-opus-4.6", "variant": "max", @@ -796,7 +806,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when "variant": "xhigh", }, "multimodal-looker": { - "model": "opencode/glm-4.7-free", + "model": "github-copilot/gpt-5-nano", }, "oracle": { "model": "github-copilot/gpt-5.4", @@ -827,10 +837,10 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when "variant": "high", }, "unspecified-high": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "unspecified-low": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "visual-engineering": { "model": "github-copilot/gemini-3.1-pro-preview", @@ -848,11 +858,15 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "explore": { "model": "github-copilot/gpt-5-mini", }, + "hephaestus": { + "model": "github-copilot/gpt-5.4", + "variant": "medium", + }, "metis": { "model": "github-copilot/claude-opus-4.6", "variant": "max", @@ -862,7 +876,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with "variant": "xhigh", }, "multimodal-looker": { - "model": "opencode/glm-4.7-free", + "model": "github-copilot/gpt-5-nano", }, "oracle": { "model": "github-copilot/gpt-5.4", @@ -893,11 +907,11 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with "variant": "high", }, "unspecified-high": { - "model": "github-copilot/gpt-5.4", - "variant": "high", + "model": "github-copilot/claude-opus-4.6", + "variant": "max", }, "unspecified-low": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "visual-engineering": { "model": "github-copilot/gemini-3.1-pro-preview", @@ -1031,7 +1045,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "explore": { "model": "anthropic/claude-haiku-4-5", @@ -1081,14 +1095,14 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen "model": "anthropic/claude-haiku-4-5", }, "ultrabrain": { - "model": "opencode/gpt-5.3-codex", + "model": "opencode/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "unspecified-low": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { "model": "opencode/gemini-3.1-pro", @@ -1106,7 +1120,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "explore": { "model": "github-copilot/gpt-5-mini", @@ -1156,14 +1170,14 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb "model": "github-copilot/claude-haiku-4.5", }, "ultrabrain": { - "model": "openai/gpt-5.3-codex", + "model": "openai/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "unspecified-low": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "visual-engineering": { "model": "github-copilot/gemini-3.1-pro-preview", @@ -1181,7 +1195,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "explore": { "model": "anthropic/claude-haiku-4-5", @@ -1225,16 +1239,16 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat "variant": "max", }, "unspecified-high": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "unspecified-low": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { "model": "zai-coding-plan/glm-5", }, "writing": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, }, } @@ -1245,7 +1259,7 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "explore": { "model": "anthropic/claude-haiku-4-5", @@ -1290,10 +1304,10 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi "variant": "high", }, "unspecified-high": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "unspecified-low": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { "model": "google/gemini-3.1-pro-preview", @@ -1311,7 +1325,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "explore": { "model": "opencode/claude-haiku-4-5", @@ -1364,14 +1378,14 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider "model": "github-copilot/claude-haiku-4.5", }, "ultrabrain": { - "model": "opencode/gpt-5.3-codex", + "model": "opencode/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "unspecified-low": { - "model": "github-copilot/claude-sonnet-4.5", + "model": "github-copilot/claude-sonnet-4.6", }, "visual-engineering": { "model": "github-copilot/gemini-3.1-pro-preview", @@ -1389,7 +1403,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "explore": { "model": "anthropic/claude-haiku-4-5", @@ -1442,14 +1456,14 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe "model": "anthropic/claude-haiku-4-5", }, "ultrabrain": { - "model": "openai/gpt-5.3-codex", + "model": "openai/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "unspecified-low": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { "model": "google/gemini-3.1-pro-preview", @@ -1467,7 +1481,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", "agents": { "atlas": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "explore": { "model": "anthropic/claude-haiku-4-5", @@ -1520,15 +1534,15 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is "model": "anthropic/claude-haiku-4-5", }, "ultrabrain": { - "model": "openai/gpt-5.3-codex", + "model": "openai/gpt-5.4", "variant": "xhigh", }, "unspecified-high": { - "model": "openai/gpt-5.4", - "variant": "high", + "model": "anthropic/claude-opus-4-6", + "variant": "max", }, "unspecified-low": { - "model": "anthropic/claude-sonnet-4-5", + "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { "model": "google/gemini-3.1-pro-preview", diff --git a/src/cli/model-fallback-requirements.test.ts b/src/cli/model-fallback-requirements.test.ts new file mode 100644 index 000000000..4bcfd1511 --- /dev/null +++ b/src/cli/model-fallback-requirements.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "bun:test" + +import { + CLI_AGENT_MODEL_REQUIREMENTS, + CLI_CATEGORY_MODEL_REQUIREMENTS, +} from "./model-fallback-requirements" +import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "../shared/model-requirements" + +describe("CLI model fallback requirements", () => { + test("agent requirements stay aligned with runtime requirements", () => { + // #given + const runtimeAgents = AGENT_MODEL_REQUIREMENTS + + // #when + const cliAgents = CLI_AGENT_MODEL_REQUIREMENTS + + // #then + expect(cliAgents).toEqual(runtimeAgents) + }) + + test("category requirements stay aligned with runtime requirements", () => { + // #given + const runtimeCategories = CATEGORY_MODEL_REQUIREMENTS + + // #when + const cliCategories = CLI_CATEGORY_MODEL_REQUIREMENTS + + // #then + expect(cliCategories).toEqual(runtimeCategories) + }) +}) diff --git a/src/cli/model-fallback-requirements.ts b/src/cli/model-fallback-requirements.ts index 2178aa920..a13b6f035 100644 --- a/src/cli/model-fallback-requirements.ts +++ b/src/cli/model-fallback-requirements.ts @@ -1,283 +1,9 @@ -import type { ModelRequirement } from "../shared/model-requirements"; +import { + AGENT_MODEL_REQUIREMENTS, + CATEGORY_MODEL_REQUIREMENTS, + type ModelRequirement, +} from "../shared/model-requirements" -// NOTE: These requirements are used by the CLI config generator (`generateModelConfig`). -// They intentionally use "install-time" provider IDs (anthropic/openai/google/opencode/etc), -// not runtime-only providers like `nvidia`. +export const CLI_AGENT_MODEL_REQUIREMENTS: Record = AGENT_MODEL_REQUIREMENTS -export const CLI_AGENT_MODEL_REQUIREMENTS: Record = { - sisyphus: { - fallbackChain: [ - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-opus-4-6", - variant: "max", - }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, - { providers: ["kimi-for-coding"], model: "k2p5" }, - { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4", variant: "medium" }, - { providers: ["zai-coding-plan", "opencode"], model: "glm-5" }, - ], - requiresAnyModel: true, - }, - hephaestus: { - fallbackChain: [ - { - providers: ["openai", "opencode"], - model: "gpt-5.3-codex", - variant: "medium", - }, - ], - requiresProvider: ["openai", "opencode"], - }, - oracle: { - fallbackChain: [ - { - providers: ["openai", "github-copilot", "opencode"], - model: "gpt-5.4", - variant: "high", - }, - { - providers: ["google", "github-copilot", "opencode"], - model: "gemini-3.1-pro", - variant: "high", - }, - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-opus-4-6", - variant: "max", - }, - { providers: ["opencode-go"], model: "glm-5" }, - ], - }, - librarian: { - fallbackChain: [ - { providers: ["opencode-go"], model: "minimax-m2.5" }, - { providers: ["opencode"], model: "minimax-m2.5-free" }, - { providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" }, - { providers: ["opencode"], model: "gpt-5-nano" }, - ], - }, - explore: { - fallbackChain: [ - { providers: ["github-copilot"], model: "grok-code-fast-1" }, - { providers: ["opencode-go"], model: "minimax-m2.5" }, - { providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" }, - { providers: ["opencode"], model: "gpt-5-nano" }, - ], - }, - "multimodal-looker": { - fallbackChain: [ - { providers: ["openai", "opencode"], model: "gpt-5.4", variant: "medium" }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, - { providers: ["zai-coding-plan"], model: "glm-4.6v" }, - { providers: ["opencode"], model: "gpt-5-nano" }, - ], - }, - prometheus: { - fallbackChain: [ - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-opus-4-6", - variant: "max", - }, - { providers: ["kimi-for-coding"], model: "k2p5" }, - { - providers: ["openai", "github-copilot", "opencode"], - model: "gpt-5.4", - variant: "high", - }, - { providers: ["opencode-go"], model: "glm-5" }, - { - providers: ["google", "github-copilot", "opencode"], - model: "gemini-3.1-pro", - }, - ], - }, - metis: { - fallbackChain: [ - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-opus-4-6", - variant: "max", - }, - { providers: ["opencode-go"], model: "glm-5" }, - { providers: ["kimi-for-coding"], model: "k2p5" }, - ], - }, - momus: { - fallbackChain: [ - { - providers: ["openai", "github-copilot", "opencode"], - model: "gpt-5.4", - variant: "xhigh", - }, - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-opus-4-6", - variant: "max", - }, - { - providers: ["google", "github-copilot", "opencode"], - model: "gemini-3.1-pro", - variant: "high", - }, - { providers: ["opencode-go"], model: "glm-5" }, - ], - }, - atlas: { - fallbackChain: [ - { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, - ], - }, - "sisyphus-junior": { - fallbackChain: [ - { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, - { providers: ["opencode"], model: "big-pickle" }, - ], - }, -}; - -export const CLI_CATEGORY_MODEL_REQUIREMENTS: Record = { - "visual-engineering": { - fallbackChain: [ - { - providers: ["google", "github-copilot", "opencode"], - model: "gemini-3.1-pro", - variant: "high", - }, - { providers: ["zai-coding-plan", "opencode"], model: "glm-5" }, - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-opus-4-6", - variant: "max", - }, - { providers: ["kimi-for-coding"], model: "k2p5" }, - { providers: ["opencode-go"], model: "glm-5" }, - ], - }, - ultrabrain: { - fallbackChain: [ - { - providers: ["openai", "opencode"], - model: "gpt-5.3-codex", - variant: "xhigh", - }, - { - providers: ["google", "github-copilot", "opencode"], - model: "gemini-3.1-pro", - variant: "high", - }, - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-opus-4-6", - variant: "max", - }, - { providers: ["opencode-go"], model: "glm-5" }, - ], - }, - deep: { - fallbackChain: [ - { - providers: ["openai", "opencode"], - model: "gpt-5.3-codex", - variant: "medium", - }, - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-opus-4-6", - variant: "max", - }, - { - providers: ["google", "github-copilot", "opencode"], - model: "gemini-3.1-pro", - variant: "high", - }, - ], - requiresModel: "gpt-5.3-codex", - }, - artistry: { - fallbackChain: [ - { - providers: ["google", "github-copilot", "opencode"], - model: "gemini-3.1-pro", - variant: "high", - }, - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-opus-4-6", - variant: "max", - }, - { - providers: ["openai", "github-copilot", "opencode"], - model: "gpt-5.4", - }, - ], - requiresModel: "gemini-3.1-pro", - }, - quick: { - fallbackChain: [ - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-haiku-4-5", - }, - { - providers: ["google", "github-copilot", "opencode"], - model: "gemini-3-flash", - }, - { providers: ["opencode-go"], model: "minimax-m2.5" }, - { providers: ["opencode"], model: "gpt-5-nano" }, - ], - }, - "unspecified-low": { - fallbackChain: [ - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-sonnet-4-5", - }, - { - providers: ["openai", "opencode"], - model: "gpt-5.3-codex", - variant: "medium", - }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, - { - providers: ["google", "github-copilot", "opencode"], - model: "gemini-3-flash", - }, - ], - }, - "unspecified-high": { - fallbackChain: [ - { - providers: ["openai", "github-copilot", "opencode"], - model: "gpt-5.4", - variant: "high", - }, - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-opus-4-6", - variant: "max", - }, - { providers: ["zai-coding-plan", "opencode"], model: "glm-5" }, - { providers: ["kimi-for-coding"], model: "k2p5" }, - { providers: ["opencode"], model: "kimi-k2.5" }, - { providers: ["opencode-go"], model: "glm-5" }, - ], - }, - writing: { - fallbackChain: [ - { providers: ["kimi-for-coding"], model: "k2p5" }, - { - providers: ["google", "github-copilot", "opencode"], - model: "gemini-3-flash", - }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, - { - providers: ["anthropic", "github-copilot", "opencode"], - model: "claude-sonnet-4-5", - }, - ], - }, -}; +export const CLI_CATEGORY_MODEL_REQUIREMENTS: Record = CATEGORY_MODEL_REQUIREMENTS diff --git a/src/cli/model-fallback.test.ts b/src/cli/model-fallback.test.ts index bd3e38447..cb6192f55 100644 --- a/src/cli/model-fallback.test.ts +++ b/src/cli/model-fallback.test.ts @@ -13,6 +13,7 @@ function createConfig(overrides: Partial = {}): InstallConfig { hasOpencodeZen: false, hasZaiCodingPlan: false, hasKimiForCoding: false, + hasOpencodeGo: false, ...overrides, } } @@ -410,6 +411,44 @@ describe("generateModelConfig", () => { }) }) + describe("OpenAI fallback coverage", () => { + test("Atlas resolves to OpenAI when only OpenAI is available", () => { + // #given + const config = createConfig({ hasOpenAI: true }) + + // #when + const result = generateModelConfig(config) + + // #then + expect(result.agents?.atlas?.model).toBe("openai/gpt-5.4") + expect(result.agents?.atlas?.variant).toBe("medium") + }) + + test("Metis resolves to OpenAI when only OpenAI is available", () => { + // #given + const config = createConfig({ hasOpenAI: true }) + + // #when + const result = generateModelConfig(config) + + // #then + expect(result.agents?.metis?.model).toBe("openai/gpt-5.4") + expect(result.agents?.metis?.variant).toBe("high") + }) + + test("Sisyphus-Junior resolves to OpenAI when only OpenAI is available", () => { + // #given + const config = createConfig({ hasOpenAI: true }) + + // #when + const result = generateModelConfig(config) + + // #then + expect(result.agents?.["sisyphus-junior"]?.model).toBe("openai/gpt-5.4") + expect(result.agents?.["sisyphus-junior"]?.variant).toBe("medium") + }) + }) + describe("Hephaestus agent special cases", () => { test("Hephaestus is created when OpenAI is available (openai provider connected)", () => { // #given @@ -423,15 +462,18 @@ describe("generateModelConfig", () => { expect(result.agents?.hephaestus?.variant).toBe("medium") }) - test("Hephaestus is NOT created when only Copilot is available (gpt-5.3-codex unavailable on github-copilot)", () => { + test("Hephaestus falls back to Copilot GPT-5.4 when only Copilot is available", () => { // #given const config = createConfig({ hasCopilot: true }) // #when const result = generateModelConfig(config) - // #then - hephaestus is omitted because gpt-5.3-codex is not available on github-copilot - expect(result.agents?.hephaestus).toBeUndefined() + // #then + expect(result.agents?.hephaestus).toEqual({ + model: "github-copilot/gpt-5.4", + variant: "medium", + }) }) test("Hephaestus is created when OpenCode Zen is available (opencode provider connected)", () => { diff --git a/src/cli/openai-only-model-catalog.test.ts b/src/cli/openai-only-model-catalog.test.ts index ee56d2d06..eebb0e8fa 100644 --- a/src/cli/openai-only-model-catalog.test.ts +++ b/src/cli/openai-only-model-catalog.test.ts @@ -13,6 +13,7 @@ function createConfig(overrides: Partial = {}): InstallConfig { hasOpencodeZen: false, hasZaiCodingPlan: false, hasKimiForCoding: false, + hasOpencodeGo: false, ...overrides, } } @@ -43,4 +44,17 @@ describe("generateModelConfig OpenAI-only model catalog", () => { expect(result.categories?.["visual-engineering"]).toEqual({ model: "openai/gpt-5.4", variant: "high" }) expect(result.categories?.writing).toEqual({ model: "openai/gpt-5.4", variant: "medium" }) }) + + test("does not apply OpenAI-only overrides when OpenCode Go is also available", () => { + // #given + const config = createConfig({ hasOpenAI: true, hasOpencodeGo: true }) + + // #when + const result = generateModelConfig(config) + + // #then + expect(result.agents?.explore).toEqual({ model: "opencode-go/minimax-m2.5" }) + expect(result.agents?.librarian).toEqual({ model: "opencode-go/minimax-m2.5" }) + expect(result.categories?.quick).toEqual({ model: "opencode-go/minimax-m2.5" }) + }) }) diff --git a/src/cli/openai-only-model-catalog.ts b/src/cli/openai-only-model-catalog.ts index 52e8484e1..a6428a1ef 100644 --- a/src/cli/openai-only-model-catalog.ts +++ b/src/cli/openai-only-model-catalog.ts @@ -17,6 +17,7 @@ export function isOpenAiOnlyAvailability(availability: ProviderAvailability): bo availability.native.openai && !availability.native.claude && !availability.native.gemini && + !availability.opencodeGo && !availability.opencodeZen && !availability.copilot && !availability.zai && diff --git a/src/plugin-handlers/agent-config-handler.test.ts b/src/plugin-handlers/agent-config-handler.test.ts index 064e86116..d0d01a897 100644 --- a/src/plugin-handlers/agent-config-handler.test.ts +++ b/src/plugin-handlers/agent-config-handler.test.ts @@ -74,6 +74,13 @@ describe("applyAgentConfig builtin override protection", () => { mode: "subagent", } + const builtinAtlasConfig: AgentConfig = { + name: "atlas", + prompt: "atlas prompt", + mode: "all", + model: "openai/gpt-5.4", + } + const sisyphusJuniorConfig: AgentConfig = { name: "Sisyphus-Junior", prompt: "junior prompt", @@ -85,6 +92,7 @@ describe("applyAgentConfig builtin override protection", () => { sisyphus: builtinSisyphusConfig, oracle: builtinOracleConfig, "multimodal-looker": builtinMultimodalLookerConfig, + atlas: builtinAtlasConfig, }) createSisyphusJuniorAgentSpy = spyOn( @@ -255,4 +263,19 @@ describe("applyAgentConfig builtin override protection", () => { }) }) }) + + test("passes the resolved Atlas model to Sisyphus-Junior as its fallback default", async () => { + // given + + // when + await applyAgentConfig({ + config: createBaseConfig(), + pluginConfig: createPluginConfig(), + ctx: { directory: "/tmp" }, + pluginComponents: createPluginComponents(), + }) + + // then + expect(createSisyphusJuniorAgentSpy).toHaveBeenCalledWith(undefined, "openai/gpt-5.4", false) + }) }) diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index e078b0688..de316ffb7 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -158,7 +158,7 @@ export async function applyAgentConfig(params: { agentConfig["sisyphus-junior"] = createSisyphusJuniorAgentWithOverrides( params.pluginConfig.agents?.["sisyphus-junior"], - undefined, + (builtinAgents.atlas as { model?: string } | undefined)?.model, useTaskSystem, ); diff --git a/src/shared/model-requirements.test.ts b/src/shared/model-requirements.test.ts index 45055ad62..63278e57f 100644 --- a/src/shared/model-requirements.test.ts +++ b/src/shared/model-requirements.test.ts @@ -178,6 +178,13 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { expect(primary.model).toBe("claude-opus-4-6") expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode"]) expect(primary.variant).toBe("max") + + const openAiFallback = metis.fallbackChain.find((entry) => entry.providers.includes("openai")) + expect(openAiFallback).toEqual({ + providers: ["openai", "github-copilot", "opencode"], + model: "gpt-5.4", + variant: "high", + }) }) test("momus has valid fallbackChain with gpt-5.4 as primary", () => { @@ -213,6 +220,32 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { const secondary = atlas.fallbackChain[1] expect(secondary.model).toBe("kimi-k2.5") expect(secondary.providers[0]).toBe("opencode-go") + + const tertiary = atlas.fallbackChain[2] + expect(tertiary).toEqual({ + providers: ["openai", "github-copilot", "opencode"], + model: "gpt-5.4", + variant: "medium", + }) + }) + + test("sisyphus-junior has an OpenAI fallback before big-pickle", () => { + // given - sisyphus-junior agent requirement + const sisyphusJunior = AGENT_MODEL_REQUIREMENTS["sisyphus-junior"] + + // when - locating the OpenAI fallback entry + const openAiFallback = sisyphusJunior.fallbackChain.find((entry) => entry.providers.includes("openai")) + const openAiFallbackIndex = sisyphusJunior.fallbackChain.findIndex((entry) => entry.providers.includes("openai")) + const bigPickleIndex = sisyphusJunior.fallbackChain.findIndex((entry) => entry.model === "big-pickle") + + // then + expect(openAiFallback).toEqual({ + providers: ["openai", "github-copilot", "opencode"], + model: "gpt-5.4", + variant: "medium", + }) + expect(openAiFallbackIndex).toBeGreaterThan(-1) + expect(bigPickleIndex).toBeGreaterThan(openAiFallbackIndex) }) test("hephaestus supports openai, github-copilot, venice, and opencode providers", () => { diff --git a/src/shared/model-requirements.ts b/src/shared/model-requirements.ts index 2e59eb3bf..56863e658 100644 --- a/src/shared/model-requirements.ts +++ b/src/shared/model-requirements.ts @@ -121,6 +121,11 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { model: "claude-opus-4-6", variant: "max", }, + { + providers: ["openai", "github-copilot", "opencode"], + model: "gpt-5.4", + variant: "high", + }, { providers: ["opencode-go"], model: "glm-5" }, { providers: ["kimi-for-coding"], model: "k2p5" }, ], @@ -149,12 +154,22 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { fallbackChain: [ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" }, { providers: ["opencode-go"], model: "kimi-k2.5" }, + { + providers: ["openai", "github-copilot", "opencode"], + model: "gpt-5.4", + variant: "medium", + }, ], }, "sisyphus-junior": { fallbackChain: [ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" }, { providers: ["opencode-go"], model: "kimi-k2.5" }, + { + providers: ["openai", "github-copilot", "opencode"], + model: "gpt-5.4", + variant: "medium", + }, { providers: ["opencode"], model: "big-pickle" }, ], },