chore: update GPT-5.2 references to GPT-5.4
Align runtime defaults, tests, docs, and generated artifacts with the newer GPT-5.4 baseline. Keep think-mode and prompt-routing expectations consistent after the model version bump.
This commit is contained in:
@@ -11,12 +11,12 @@ Agent factories following `createXXXAgent(model) → AgentConfig` pattern. Each
|
||||
| Agent | Model | Temp | Mode | Fallback Chain | Purpose |
|
||||
|-------|-------|------|------|----------------|---------|
|
||||
| **Sisyphus** | claude-opus-4-6 max | 0.1 | all | glm-5 → big-pickle | Main orchestrator, plans + delegates |
|
||||
| **Hephaestus** | gpt-5.3-codex medium | 0.1 | all | gpt-5.2 medium (copilot) | Autonomous deep worker |
|
||||
| **Oracle** | gpt-5.2 high | 0.1 | subagent | gemini-3.1-pro high → claude-opus-4-6 max | Read-only consultation |
|
||||
| **Hephaestus** | gpt-5.3-codex medium | 0.1 | all | gpt-5.4 medium (copilot) | Autonomous deep worker |
|
||||
| **Oracle** | gpt-5.4 high | 0.1 | subagent | gemini-3.1-pro high → claude-opus-4-6 max | Read-only consultation |
|
||||
| **Librarian** | gemini-3-flash | 0.1 | subagent | minimax-m2.5-free → big-pickle | External docs/code search |
|
||||
| **Explore** | grok-code-fast-1 | 0.1 | subagent | minimax-m2.5-free → claude-haiku-4-5 → gpt-5-nano | Contextual grep |
|
||||
| **Multimodal-Looker** | gpt-5.3-codex medium | 0.1 | subagent | k2p5 → gemini-3-flash → glm-4.6v → gpt-5-nano | PDF/image analysis |
|
||||
| **Metis** | claude-opus-4-6 max | **0.3** | subagent | gpt-5.2 high → gemini-3.1-pro high | Pre-planning consultant |
|
||||
| **Metis** | claude-opus-4-6 max | **0.3** | subagent | gpt-5.4 high → gemini-3.1-pro high | Pre-planning consultant |
|
||||
| **Momus** | gpt-5.4 xhigh | 0.1 | subagent | claude-opus-4-6 max → gemini-3.1-pro high | Plan reviewer |
|
||||
| **Atlas** | claude-sonnet-4-6 | 0.1 | primary | gpt-5.4 medium | Todo-list orchestrator |
|
||||
| **Prometheus** | claude-opus-4-6 max | 0.1 | — | gpt-5.4 high → gemini-3.1-pro | Strategic planner (internal) |
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* You are the conductor of a symphony of specialized agents.
|
||||
*
|
||||
* Routing:
|
||||
* 1. GPT models (openai/*, github-copilot/gpt-*) → gpt.ts (GPT-5.2 optimized)
|
||||
* 1. GPT models (openai/*, github-copilot/gpt-*) → gpt.ts (GPT-5.4 optimized)
|
||||
* 2. Gemini models (google/*, google-vertex/*) → gemini.ts (Gemini-optimized)
|
||||
* 3. Default (Claude, etc.) → default.ts (Claude-optimized)
|
||||
*/
|
||||
|
||||
@@ -197,7 +197,7 @@ describe("buildParallelDelegationSection", () => {
|
||||
|
||||
it("#given non-Claude model with unspecified-high category #when building #then returns aggressive delegation section", () => {
|
||||
//#given
|
||||
const model = "openai/gpt-5.2"
|
||||
const model = "openai/gpt-5.4"
|
||||
const categories = [unspecifiedHighCategory, otherCategory]
|
||||
|
||||
//#when
|
||||
@@ -223,7 +223,7 @@ describe("buildParallelDelegationSection", () => {
|
||||
|
||||
it("#given non-Claude model without deep or unspecified-high category #when building #then returns empty", () => {
|
||||
//#given
|
||||
const model = "openai/gpt-5.2"
|
||||
const model = "openai/gpt-5.4"
|
||||
const categories = [otherCategory]
|
||||
|
||||
//#when
|
||||
@@ -261,7 +261,7 @@ describe("buildNonClaudePlannerSection", () => {
|
||||
|
||||
it("#given GPT model #when building #then returns plan agent section", () => {
|
||||
//#given
|
||||
const model = "openai/gpt-5.2"
|
||||
const model = "openai/gpt-5.4"
|
||||
|
||||
//#when
|
||||
const result = buildNonClaudePlannerSection(model)
|
||||
|
||||
@@ -39,8 +39,8 @@ describe("getHephaestusPromptSource", () => {
|
||||
|
||||
test("returns 'gpt' for generic GPT models", () => {
|
||||
// given
|
||||
const model1 = "openai/gpt-5.2";
|
||||
const model2 = "github-copilot/gpt-5.2";
|
||||
const model1 = "openai/gpt-4o";
|
||||
const model2 = "github-copilot/gpt-4o";
|
||||
const model3 = "openai/gpt-4o";
|
||||
|
||||
// when
|
||||
@@ -111,7 +111,7 @@ describe("getHephaestusPrompt", () => {
|
||||
|
||||
test("generic GPT model returns generic GPT prompt", () => {
|
||||
// given
|
||||
const model = "openai/gpt-5.2";
|
||||
const model = "openai/gpt-4o";
|
||||
|
||||
// when
|
||||
const prompt = getHephaestusPrompt(model);
|
||||
|
||||
@@ -522,7 +522,7 @@ export function createHephaestusAgent(
|
||||
|
||||
return {
|
||||
description:
|
||||
"Autonomous Deep Worker - goal-oriented execution with GPT 5.2 Codex. Explores thoroughly before acting, uses explore/librarian agents for comprehensive context, completes tasks end-to-end. Inspired by AmpCode deep mode. (Hephaestus - OhMyOpenCode)",
|
||||
"Autonomous Deep Worker - goal-oriented execution with GPT 5.4 Codex. Explores thoroughly before acting, uses explore/librarian agents for comprehensive context, completes tasks end-to-end. Inspired by AmpCode deep mode. (Hephaestus - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
model,
|
||||
maxTokens: 32000,
|
||||
|
||||
@@ -48,7 +48,7 @@ export function getPrometheusPromptSource(model?: string): PrometheusPromptSourc
|
||||
|
||||
/**
|
||||
* Gets the appropriate Prometheus prompt based on model.
|
||||
* GPT models → GPT-5.2 optimized prompt (XML-tagged, principle-driven)
|
||||
* GPT models → GPT-5.4 optimized prompt (XML-tagged, principle-driven)
|
||||
* Gemini models → Gemini-optimized prompt (aggressive tool-call enforcement, thinking checkpoints)
|
||||
* Default (Claude, etc.) → Claude-optimized prompt (modular sections)
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Category-spawned executor with domain-specific configurations.
|
||||
*
|
||||
* Routing:
|
||||
* 1. GPT models (openai/*, github-copilot/gpt-*) -> gpt.ts (GPT-5.2 optimized)
|
||||
* 1. GPT models (openai/*, github-copilot/gpt-*) -> gpt.ts (GPT-5.4 optimized)
|
||||
* 2. Gemini models (google/*, google-vertex/*) -> gemini.ts (Gemini-optimized)
|
||||
* 3. Default (Claude, etc.) -> default.ts (Claude-optimized)
|
||||
*/
|
||||
|
||||
@@ -10,13 +10,13 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
|
||||
describe("honored fields", () => {
|
||||
test("applies model override", () => {
|
||||
// given
|
||||
const override = { model: "openai/gpt-5.2" }
|
||||
const override = { model: "openai/gpt-5.4" }
|
||||
|
||||
// when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// then
|
||||
expect(result.model).toBe("openai/gpt-5.2")
|
||||
expect(result.model).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
test("applies temperature override", () => {
|
||||
@@ -105,7 +105,7 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
|
||||
// given
|
||||
const override = {
|
||||
disable: true,
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
temperature: 0.9,
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
|
||||
|
||||
test("useTaskSystem=true produces Task Discipline prompt for GPT", () => {
|
||||
//#given
|
||||
const override = { model: "openai/gpt-5.2" }
|
||||
const override = { model: "openai/gpt-5.4" }
|
||||
|
||||
//#when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override, undefined, true)
|
||||
@@ -253,7 +253,7 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
|
||||
|
||||
test("useTaskSystem=true includes task_create/task_update in GPT prompt", () => {
|
||||
//#given
|
||||
const override = { model: "openai/gpt-5.2" }
|
||||
const override = { model: "openai/gpt-5.4" }
|
||||
|
||||
//#when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override, undefined, true)
|
||||
@@ -303,7 +303,7 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
|
||||
|
||||
test("GPT model uses GPT-optimized prompt with Hephaestus-style sections", () => {
|
||||
// given
|
||||
const override = { model: "openai/gpt-5.2" }
|
||||
const override = { model: "openai/gpt-5.4" }
|
||||
|
||||
// when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
@@ -401,7 +401,7 @@ describe("getSisyphusJuniorPromptSource", () => {
|
||||
|
||||
test("returns 'gpt' for generic GPT models", () => {
|
||||
// given
|
||||
const model = "openai/gpt-5.2"
|
||||
const model = "openai/gpt-4o"
|
||||
|
||||
// when
|
||||
const source = getSisyphusJuniorPromptSource(model)
|
||||
@@ -473,7 +473,7 @@ describe("buildSisyphusJuniorPrompt", () => {
|
||||
|
||||
test("generic GPT model uses generic GPT prompt", () => {
|
||||
// given
|
||||
const model = "openai/gpt-5.2"
|
||||
const model = "openai/gpt-5.4"
|
||||
|
||||
// when
|
||||
const prompt = buildSisyphusJuniorPrompt(model, false)
|
||||
|
||||
@@ -12,9 +12,9 @@ describe("isGpt5_4Model", () => {
|
||||
|
||||
test("does not match other GPT models", () => {
|
||||
expect(isGpt5_4Model("openai/gpt-5.3-codex")).toBe(false);
|
||||
expect(isGpt5_4Model("openai/gpt-5.2")).toBe(false);
|
||||
expect(isGpt5_4Model("openai/gpt-5.1")).toBe(false);
|
||||
expect(isGpt5_4Model("openai/gpt-4o")).toBe(false);
|
||||
expect(isGpt5_4Model("github-copilot/gpt-5.2")).toBe(false);
|
||||
expect(isGpt5_4Model("github-copilot/gpt-4o")).toBe(false);
|
||||
});
|
||||
|
||||
test("does not match non-GPT models", () => {
|
||||
@@ -26,7 +26,7 @@ describe("isGpt5_4Model", () => {
|
||||
|
||||
describe("isGptModel", () => {
|
||||
test("standard openai provider gpt models", () => {
|
||||
expect(isGptModel("openai/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("openai/gpt-5.4")).toBe(true);
|
||||
expect(isGptModel("openai/gpt-4o")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -39,22 +39,22 @@ describe("isGptModel", () => {
|
||||
});
|
||||
|
||||
test("github copilot gpt models", () => {
|
||||
expect(isGptModel("github-copilot/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("github-copilot/gpt-5.4")).toBe(true);
|
||||
expect(isGptModel("github-copilot/gpt-4o")).toBe(true);
|
||||
});
|
||||
|
||||
test("litellm proxied gpt models", () => {
|
||||
expect(isGptModel("litellm/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("litellm/gpt-5.4")).toBe(true);
|
||||
expect(isGptModel("litellm/gpt-4o")).toBe(true);
|
||||
});
|
||||
|
||||
test("other proxied gpt models", () => {
|
||||
expect(isGptModel("ollama/gpt-4o")).toBe(true);
|
||||
expect(isGptModel("custom-provider/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("custom-provider/gpt-5.4")).toBe(true);
|
||||
});
|
||||
|
||||
test("venice provider gpt models", () => {
|
||||
expect(isGptModel("venice/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("venice/gpt-5.4")).toBe(true);
|
||||
expect(isGptModel("venice/gpt-4o")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("isGeminiModel", () => {
|
||||
});
|
||||
|
||||
test("#given gpt models #then returns false", () => {
|
||||
expect(isGeminiModel("openai/gpt-5.2")).toBe(false);
|
||||
expect(isGeminiModel("openai/gpt-5.4")).toBe(false);
|
||||
expect(isGeminiModel("openai/o3-mini")).toBe(false);
|
||||
expect(isGeminiModel("litellm/gpt-4o")).toBe(false);
|
||||
});
|
||||
|
||||
+30
-30
@@ -39,14 +39,14 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("Sisyphus with GPT model override has reasoningEffort, no thinking", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
sisyphus: { model: "github-copilot/gpt-5.2" },
|
||||
sisyphus: { model: "github-copilot/gpt-5.4" },
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
|
||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.4")
|
||||
expect(agents.sisyphus.reasoningEffort).toBe("medium")
|
||||
expect(agents.sisyphus.thinking).toBeUndefined()
|
||||
})
|
||||
@@ -54,9 +54,9 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("Atlas uses uiSelectedModel", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["openai/gpt-5.2", "anthropic/claude-sonnet-4-6"])
|
||||
new Set(["openai/gpt-5.4", "anthropic/claude-sonnet-4-6"])
|
||||
)
|
||||
const uiSelectedModel = "openai/gpt-5.2"
|
||||
const uiSelectedModel = "openai/gpt-5.4"
|
||||
|
||||
try {
|
||||
// #when
|
||||
@@ -75,7 +75,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
|
||||
// #then
|
||||
expect(agents.atlas).toBeDefined()
|
||||
expect(agents.atlas.model).toBe("openai/gpt-5.2")
|
||||
expect(agents.atlas.model).toBe("openai/gpt-5.4")
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
@@ -84,9 +84,9 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("user config model takes priority over uiSelectedModel for sisyphus", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["openai/gpt-5.2", "anthropic/claude-sonnet-4-6"])
|
||||
new Set(["openai/gpt-5.4", "anthropic/claude-sonnet-4-6"])
|
||||
)
|
||||
const uiSelectedModel = "openai/gpt-5.2"
|
||||
const uiSelectedModel = "openai/gpt-5.4"
|
||||
const overrides = {
|
||||
sisyphus: { model: "google/antigravity-claude-opus-4-5-thinking" },
|
||||
}
|
||||
@@ -117,9 +117,9 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("user config model takes priority over uiSelectedModel for atlas", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["openai/gpt-5.2", "anthropic/claude-sonnet-4-6"])
|
||||
new Set(["openai/gpt-5.4", "anthropic/claude-sonnet-4-6"])
|
||||
)
|
||||
const uiSelectedModel = "openai/gpt-5.2"
|
||||
const uiSelectedModel = "openai/gpt-5.4"
|
||||
const overrides = {
|
||||
atlas: { model: "google/antigravity-claude-opus-4-5-thinking" },
|
||||
}
|
||||
@@ -173,8 +173,8 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||
|
||||
// #then - oracle resolves via connected cache fallback to openai/gpt-5.2 (not system default)
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
// #then - oracle resolves via connected cache fallback to openai/gpt-5.4 (not system default)
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.4")
|
||||
expect(agents.oracle.reasoningEffort).toBe("medium")
|
||||
expect(agents.oracle.thinking).toBeUndefined()
|
||||
cacheSpy.mockRestore?.()
|
||||
@@ -196,14 +196,14 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("Oracle with GPT model override has reasoningEffort, no thinking", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||
|
||||
// #then
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.4")
|
||||
expect(agents.oracle.reasoningEffort).toBe("medium")
|
||||
expect(agents.oracle.textVerbosity).toBe("high")
|
||||
expect(agents.oracle.thinking).toBeUndefined()
|
||||
@@ -228,14 +228,14 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("non-model overrides are still applied after factory rebuild", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
sisyphus: { model: "github-copilot/gpt-5.2", temperature: 0.5 },
|
||||
sisyphus: { model: "github-copilot/gpt-5.4", temperature: 0.5 },
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
|
||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.4")
|
||||
expect(agents.sisyphus.temperature).toBe(0.5)
|
||||
})
|
||||
|
||||
@@ -261,7 +261,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
"opencode/kimi-k2.5-free",
|
||||
"zai-coding-plan/glm-5",
|
||||
"opencode/big-pickle",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
])
|
||||
)
|
||||
|
||||
@@ -298,7 +298,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("excludes hidden custom agents from orchestrator prompts", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
|
||||
)
|
||||
|
||||
const customAgentSummaries = [
|
||||
@@ -334,7 +334,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("excludes disabled custom agents from orchestrator prompts", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
|
||||
)
|
||||
|
||||
const customAgentSummaries = [
|
||||
@@ -370,7 +370,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("excludes custom agents when disabledAgents contains their name (case-insensitive)", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
|
||||
)
|
||||
|
||||
const disabledAgents = ["ReSeArChEr"]
|
||||
@@ -406,7 +406,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("deduplicates custom agents case-insensitively", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
|
||||
)
|
||||
|
||||
const customAgentSummaries = [
|
||||
@@ -438,7 +438,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("sanitizes custom agent strings for markdown tables", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
|
||||
)
|
||||
|
||||
const customAgentSummaries = [
|
||||
@@ -479,7 +479,7 @@ describe("createBuiltinAgents without systemDefaultModel", () => {
|
||||
|
||||
// #then - connected cache enables model resolution despite no systemDefaultModel
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.4")
|
||||
cacheSpy.mockRestore?.()
|
||||
})
|
||||
|
||||
@@ -787,7 +787,7 @@ describe("Atlas is unaffected by environment context toggle", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
|
||||
)
|
||||
})
|
||||
|
||||
@@ -891,9 +891,9 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
||||
})
|
||||
|
||||
test("sisyphus is not created when no fallback model is available and provider not connected", async () => {
|
||||
// #given - only openai/gpt-5.2 available, not in sisyphus fallback chain
|
||||
// #given - only openai/gpt-5.4 available, not in sisyphus fallback chain
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["openai/gpt-5.2"])
|
||||
new Set(["openai/gpt-5.4"])
|
||||
)
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue([])
|
||||
|
||||
@@ -913,7 +913,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
||||
// #given - user configures a model from a plugin provider (like antigravity)
|
||||
// that is NOT in the availableModels cache and NOT in the fallback chain
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["openai/gpt-5.2"])
|
||||
new Set(["openai/gpt-5.4"])
|
||||
)
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(
|
||||
["openai"]
|
||||
@@ -1021,7 +1021,7 @@ describe("buildAgent with category and skills", () => {
|
||||
|
||||
const categories = {
|
||||
"custom-category": {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
variant: "xhigh",
|
||||
},
|
||||
}
|
||||
@@ -1030,7 +1030,7 @@ describe("buildAgent with category and skills", () => {
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL, categories)
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("openai/gpt-5.2")
|
||||
expect(agent.model).toBe("openai/gpt-5.4")
|
||||
expect(agent.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
@@ -1247,7 +1247,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
||||
// #given - custom category has reasoningEffort=xhigh, direct override says "low"
|
||||
const categories = {
|
||||
"test-cat": {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
reasoningEffort: "xhigh" as const,
|
||||
},
|
||||
}
|
||||
@@ -1267,7 +1267,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
||||
// #given - custom category has reasoningEffort, no direct reasoningEffort in override
|
||||
const categories = {
|
||||
"reasoning-cat": {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
reasoningEffort: "high" as const,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"metis": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"momus": {
|
||||
@@ -217,7 +217,7 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -274,7 +274,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"metis": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"momus": {
|
||||
@@ -286,7 +286,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -476,7 +476,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -551,7 +551,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -627,7 +627,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
"model": "opencode/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -702,7 +702,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
"model": "opencode/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -773,7 +773,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
"model": "github-copilot/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -839,7 +839,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
"model": "github-copilot/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -1021,7 +1021,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
"model": "opencode/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -1096,7 +1096,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -1298,7 +1298,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
"model": "github-copilot/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -1373,7 +1373,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
@@ -1448,7 +1448,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.4",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
|
||||
@@ -40,7 +40,7 @@ Examples:
|
||||
|
||||
Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi):
|
||||
Claude Native anthropic/ models (Opus, Sonnet, Haiku)
|
||||
OpenAI Native openai/ models (GPT-5.2 for Oracle)
|
||||
OpenAI Native openai/ models (GPT-5.4 for Oracle)
|
||||
Gemini Native google/ models (Gemini 3 Pro, Flash)
|
||||
Copilot github-copilot/ models (fallback)
|
||||
OpenCode Zen opencode/ models (opencode/claude-opus-4-6, etc.)
|
||||
|
||||
@@ -252,7 +252,7 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
// #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")
|
||||
expect((result.agents as Record<string, { model: string }>).oracle.model).toBe("openai/gpt-5.4")
|
||||
// #then multimodal-looker should use native OpenAI (first fallback entry is gpt-5.3-codex)
|
||||
expect((result.agents as Record<string, { model: string }>)["multimodal-looker"].model).toBe("openai/gpt-5.3-codex")
|
||||
})
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("model-resolution check", () => {
|
||||
// given: User has override for visual-engineering category
|
||||
const mockConfig = {
|
||||
categories: {
|
||||
"visual-engineering": { model: "openai/gpt-5.2" },
|
||||
"visual-engineering": { model: "openai/gpt-5.4" },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ describe("model-resolution check", () => {
|
||||
// then: visual-engineering should show the override
|
||||
const visual = info.categories.find((c) => c.name === "visual-engineering")
|
||||
expect(visual).toBeDefined()
|
||||
expect(visual!.userOverride).toBe("openai/gpt-5.2")
|
||||
expect(visual!.effectiveResolution).toBe("User override: openai/gpt-5.2")
|
||||
expect(visual!.userOverride).toBe("openai/gpt-5.4")
|
||||
expect(visual!.effectiveResolution).toBe("User override: openai/gpt-5.4")
|
||||
})
|
||||
|
||||
it("shows provider fallback when no override exists", async () => {
|
||||
@@ -96,7 +96,7 @@ describe("model-resolution check", () => {
|
||||
//#given User has model with variant override for oracle agent
|
||||
const mockConfig = {
|
||||
agents: {
|
||||
oracle: { model: "openai/gpt-5.2", variant: "xhigh" },
|
||||
oracle: { model: "openai/gpt-5.4", variant: "xhigh" },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ describe("model-resolution check", () => {
|
||||
//#then Oracle should have userVariant set
|
||||
const oracle = info.agents.find((a) => a.name === "oracle")
|
||||
expect(oracle).toBeDefined()
|
||||
expect(oracle!.userOverride).toBe("openai/gpt-5.2")
|
||||
expect(oracle!.userOverride).toBe("openai/gpt-5.4")
|
||||
expect(oracle!.userVariant).toBe("xhigh")
|
||||
})
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export function formatConfigSummary(config: InstallConfig): string {
|
||||
|
||||
const claudeDetail = config.hasClaude ? (config.isMax20 ? "max20" : "standard") : undefined
|
||||
lines.push(formatProvider("Claude", config.hasClaude, claudeDetail))
|
||||
lines.push(formatProvider("OpenAI/ChatGPT", config.hasOpenAI, "GPT-5.2 for Oracle"))
|
||||
lines.push(formatProvider("OpenAI/ChatGPT", config.hasOpenAI, "GPT-5.4 for Oracle"))
|
||||
lines.push(formatProvider("Gemini", config.hasGemini))
|
||||
lines.push(formatProvider("GitHub Copilot", config.hasCopilot, "fallback"))
|
||||
lines.push(formatProvider("OpenCode Zen", config.hasOpencodeZen, "opencode/ models"))
|
||||
|
||||
@@ -31,7 +31,7 @@ export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
fallbackChain: [
|
||||
{
|
||||
providers: ["openai", "github-copilot", "opencode"],
|
||||
model: "gpt-5.2",
|
||||
model: "gpt-5.4",
|
||||
variant: "high",
|
||||
},
|
||||
{
|
||||
@@ -108,7 +108,7 @@ export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{
|
||||
providers: ["openai", "github-copilot", "opencode"],
|
||||
model: "gpt-5.2",
|
||||
model: "gpt-5.4",
|
||||
variant: "high",
|
||||
},
|
||||
{
|
||||
@@ -224,7 +224,7 @@ export const CLI_CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> =
|
||||
},
|
||||
{
|
||||
providers: ["openai", "github-copilot", "opencode"],
|
||||
model: "gpt-5.2",
|
||||
model: "gpt-5.4",
|
||||
},
|
||||
],
|
||||
requiresModel: "gemini-3.1-pro",
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function promptInstallConfig(detected: DetectedConfig): Promise<Ins
|
||||
message: "Do you have an OpenAI/ChatGPT Plus subscription?",
|
||||
options: [
|
||||
{ value: "no", label: "No", hint: "Oracle will use fallback models" },
|
||||
{ value: "yes", label: "Yes", hint: "GPT-5.2 for Oracle (high-IQ debugging)" },
|
||||
{ value: "yes", label: "Yes", hint: "GPT-5.4 for Oracle (high-IQ debugging)" },
|
||||
],
|
||||
initialValue: initial.openai,
|
||||
})
|
||||
@@ -74,7 +74,7 @@ export async function promptInstallConfig(detected: DetectedConfig): Promise<Ins
|
||||
message: "Do you have access to OpenCode Zen (opencode/ models)?",
|
||||
options: [
|
||||
{ value: "no", label: "No", hint: "Will use other configured providers" },
|
||||
{ value: "yes", label: "Yes", hint: "opencode/claude-opus-4-6, opencode/gpt-5.2, etc." },
|
||||
{ value: "yes", label: "Yes", hint: "opencode/claude-opus-4-6, opencode/gpt-5.4, etc." },
|
||||
],
|
||||
initialValue: initial.opencodeZen,
|
||||
})
|
||||
|
||||
@@ -266,7 +266,7 @@ describe("AgentOverrideConfigSchema", () => {
|
||||
describe("backward compatibility", () => {
|
||||
test("still accepts model field (deprecated)", () => {
|
||||
// given
|
||||
const config = { model: "openai/gpt-5.2" }
|
||||
const config = { model: "openai/gpt-5.4" }
|
||||
|
||||
// when
|
||||
const result = AgentOverrideConfigSchema.safeParse(config)
|
||||
@@ -274,14 +274,14 @@ describe("AgentOverrideConfigSchema", () => {
|
||||
// then
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.model).toBe("openai/gpt-5.2")
|
||||
expect(result.data.model).toBe("openai/gpt-5.4")
|
||||
}
|
||||
})
|
||||
|
||||
test("accepts both model and category (deprecated usage)", () => {
|
||||
// given - category should take precedence at runtime, but both should validate
|
||||
const config = {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
category: "ultrabrain"
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ describe("AgentOverrideConfigSchema", () => {
|
||||
// then
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.model).toBe("openai/gpt-5.2")
|
||||
expect(result.data.model).toBe("openai/gpt-5.4")
|
||||
expect(result.data.category).toBe("ultrabrain")
|
||||
}
|
||||
})
|
||||
@@ -343,7 +343,7 @@ describe("AgentOverrideConfigSchema", () => {
|
||||
describe("CategoryConfigSchema", () => {
|
||||
test("accepts variant as optional string", () => {
|
||||
// given
|
||||
const config = { model: "openai/gpt-5.2", variant: "xhigh" }
|
||||
const config = { model: "openai/gpt-5.4", variant: "xhigh" }
|
||||
|
||||
// when
|
||||
const result = CategoryConfigSchema.safeParse(config)
|
||||
@@ -371,7 +371,7 @@ describe("CategoryConfigSchema", () => {
|
||||
|
||||
test("rejects non-string variant", () => {
|
||||
// given
|
||||
const config = { model: "openai/gpt-5.2", variant: 123 }
|
||||
const config = { model: "openai/gpt-5.4", variant: 123 }
|
||||
|
||||
// when
|
||||
const result = CategoryConfigSchema.safeParse(config)
|
||||
@@ -413,7 +413,7 @@ describe("Sisyphus-Junior agent override", () => {
|
||||
const config = {
|
||||
agents: {
|
||||
"sisyphus-junior": {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
temperature: 0.2,
|
||||
},
|
||||
},
|
||||
@@ -426,7 +426,7 @@ describe("Sisyphus-Junior agent override", () => {
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.agents?.["sisyphus-junior"]).toBeDefined()
|
||||
expect(result.data.agents?.["sisyphus-junior"]?.model).toBe("openai/gpt-5.2")
|
||||
expect(result.data.agents?.["sisyphus-junior"]?.model).toBe("openai/gpt-5.4")
|
||||
expect(result.data.agents?.["sisyphus-junior"]?.temperature).toBe(0.2)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2078,7 +2078,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
|
||||
description: "Task 2",
|
||||
prompt: "Do something else",
|
||||
agent: "test-agent",
|
||||
model: { providerID: "openai", modelID: "gpt-5.2" },
|
||||
model: { providerID: "openai", modelID: "gpt-5.4" },
|
||||
parentSessionID: "parent-session",
|
||||
parentMessageID: "parent-message",
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ describe("createAnthropicEffortHook", () => {
|
||||
const hook = createAnthropicEffortHook()
|
||||
const { input, output } = createMockParams({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.2",
|
||||
modelID: "gpt-5.4",
|
||||
})
|
||||
|
||||
//#when chat.params hook is called
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Routing logic:
|
||||
* 1. Planner agents (prometheus, plan) → planner.ts
|
||||
* 2. GPT 5.2 models → gpt5.2.ts
|
||||
* 2. GPT 5.4 models → gpt5.4.ts
|
||||
* 3. Gemini models → gemini.ts
|
||||
* 4. Everything else (Claude, etc.) → default.ts
|
||||
*/
|
||||
|
||||
@@ -104,7 +104,7 @@ describe("no-sisyphus-gpt hook", () => {
|
||||
await hook["chat.message"]?.({
|
||||
sessionID: "ses_3",
|
||||
agent: HEPHAESTUS_DISPLAY,
|
||||
model: { providerID: "openai", modelID: "gpt-5.2" },
|
||||
model: { providerID: "openai", modelID: "gpt-5.4" },
|
||||
}, output)
|
||||
|
||||
// then - no toast
|
||||
@@ -126,7 +126,7 @@ describe("no-sisyphus-gpt hook", () => {
|
||||
// when - chat.message runs without input.agent
|
||||
await hook["chat.message"]?.({
|
||||
sessionID: "ses_4",
|
||||
model: { providerID: "openai", modelID: "gpt-5.2" },
|
||||
model: { providerID: "openai", modelID: "gpt-4o" },
|
||||
}, output)
|
||||
|
||||
// then - toast shown via session-agent fallback
|
||||
|
||||
@@ -103,7 +103,7 @@ describe("runtime-fallback", () => {
|
||||
await hook.event({
|
||||
event: {
|
||||
type: "session.created",
|
||||
properties: { info: { id: sessionID, model: "openai/gpt-5.2" } },
|
||||
properties: { info: { id: sessionID, model: "openai/gpt-5.4" } },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -202,7 +202,7 @@ describe("runtime-fallback", () => {
|
||||
test("should trigger fallback for missing API key errors when fallback models are configured", async () => {
|
||||
const hook = createRuntimeFallbackHook(createMockPluginInput(), {
|
||||
config: createMockConfig({ notify_on_fallback: false }),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.2"]),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.4"]),
|
||||
})
|
||||
const sessionID = "test-session-missing-api-key-fallback"
|
||||
SessionCategoryRegistry.register(sessionID, "test")
|
||||
@@ -230,7 +230,7 @@ describe("runtime-fallback", () => {
|
||||
|
||||
const fallbackLog = logCalls.find((c) => c.msg.includes("Preparing fallback"))
|
||||
expect(fallbackLog).toBeDefined()
|
||||
expect(fallbackLog?.data).toMatchObject({ from: "google/gemini-2.5-pro", to: "openai/gpt-5.2" })
|
||||
expect(fallbackLog?.data).toMatchObject({ from: "google/gemini-2.5-pro", to: "openai/gpt-5.4" })
|
||||
})
|
||||
|
||||
test("should detect retryable error from message pattern 'rate limit'", async () => {
|
||||
@@ -260,7 +260,7 @@ describe("runtime-fallback", () => {
|
||||
config: createMockConfig({ notify_on_fallback: false }),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback([
|
||||
"anthropic/claude-opus-4.6",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
]),
|
||||
})
|
||||
const sessionID = "test-session-model-not-found"
|
||||
@@ -302,7 +302,7 @@ describe("runtime-fallback", () => {
|
||||
|
||||
const fallbackLogs = logCalls.filter((c) => c.msg.includes("Preparing fallback"))
|
||||
expect(fallbackLogs.length).toBeGreaterThanOrEqual(2)
|
||||
expect(fallbackLogs[1]?.data).toMatchObject({ from: "anthropic/claude-opus-4.6", to: "openai/gpt-5.2" })
|
||||
expect(fallbackLogs[1]?.data).toMatchObject({ from: "anthropic/claude-opus-4.6", to: "openai/gpt-5.4" })
|
||||
|
||||
const nonRetryLog = logCalls.find(
|
||||
(c) => c.msg.includes("Error not retryable") && (c.data as { sessionID?: string } | undefined)?.sessionID === sessionID
|
||||
@@ -313,7 +313,7 @@ describe("runtime-fallback", () => {
|
||||
test("should trigger fallback on Copilot auto-retry signal in message.updated", async () => {
|
||||
const hook = createRuntimeFallbackHook(createMockPluginInput(), {
|
||||
config: createMockConfig({ notify_on_fallback: false }),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.2"]),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.4"]),
|
||||
})
|
||||
|
||||
const sessionID = "test-session-copilot-auto-retry"
|
||||
@@ -346,7 +346,7 @@ describe("runtime-fallback", () => {
|
||||
|
||||
const fallbackLog = logCalls.find((c) => c.msg.includes("Preparing fallback"))
|
||||
expect(fallbackLog).toBeDefined()
|
||||
expect(fallbackLog?.data).toMatchObject({ from: "github-copilot/claude-opus-4.6", to: "openai/gpt-5.2" })
|
||||
expect(fallbackLog?.data).toMatchObject({ from: "github-copilot/claude-opus-4.6", to: "openai/gpt-5.4" })
|
||||
})
|
||||
|
||||
test("should trigger fallback on OpenAI auto-retry signal in message.updated", async () => {
|
||||
@@ -658,7 +658,7 @@ describe("runtime-fallback", () => {
|
||||
test("should trigger fallback when message.updated has missing API key error without model", async () => {
|
||||
const hook = createRuntimeFallbackHook(createMockPluginInput(), {
|
||||
config: createMockConfig({ notify_on_fallback: false }),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.2"]),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.4"]),
|
||||
})
|
||||
const sessionID = "test-message-updated-missing-model"
|
||||
SessionCategoryRegistry.register(sessionID, "test")
|
||||
@@ -689,7 +689,7 @@ describe("runtime-fallback", () => {
|
||||
|
||||
const fallbackLog = logCalls.find((c) => c.msg.includes("Preparing fallback"))
|
||||
expect(fallbackLog).toBeDefined()
|
||||
expect(fallbackLog?.data).toMatchObject({ from: "google/gemini-2.5-pro", to: "openai/gpt-5.2" })
|
||||
expect(fallbackLog?.data).toMatchObject({ from: "google/gemini-2.5-pro", to: "openai/gpt-5.4" })
|
||||
})
|
||||
|
||||
test("should not advance fallback state from message.updated while retry is already in flight", async () => {
|
||||
@@ -709,7 +709,7 @@ describe("runtime-fallback", () => {
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback([
|
||||
"github-copilot/claude-opus-4.6",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
]),
|
||||
}
|
||||
)
|
||||
@@ -799,7 +799,7 @@ describe("runtime-fallback", () => {
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback([
|
||||
"github-copilot/claude-opus-4.6",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
]),
|
||||
}
|
||||
)
|
||||
@@ -883,7 +883,7 @@ describe("runtime-fallback", () => {
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback([
|
||||
"github-copilot/claude-opus-4.6",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
]),
|
||||
session_timeout_ms: 20,
|
||||
}
|
||||
@@ -949,7 +949,7 @@ describe("runtime-fallback", () => {
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback([
|
||||
"github-copilot/claude-opus-4.6",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
]),
|
||||
session_timeout_ms: 20,
|
||||
}
|
||||
@@ -1034,7 +1034,7 @@ describe("runtime-fallback", () => {
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback([
|
||||
"github-copilot/claude-opus-4.6",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
]),
|
||||
session_timeout_ms: 20,
|
||||
}
|
||||
@@ -1099,7 +1099,7 @@ describe("runtime-fallback", () => {
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback([
|
||||
"github-copilot/claude-opus-4.6",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
]),
|
||||
session_timeout_ms: 20,
|
||||
}
|
||||
@@ -1637,7 +1637,7 @@ describe("runtime-fallback", () => {
|
||||
}),
|
||||
{
|
||||
config: createMockConfig({ notify_on_fallback: false }),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.2"]),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.4"]),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1665,7 +1665,7 @@ describe("runtime-fallback", () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(retriedModels).toContain("openai/gpt-5.2")
|
||||
expect(retriedModels).toContain("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
test("triggers fallback when message has mixed text and error parts", async () => {
|
||||
@@ -1745,7 +1745,7 @@ describe("runtime-fallback", () => {
|
||||
}),
|
||||
{
|
||||
config: createMockConfig({ notify_on_fallback: false }),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.2"]),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.4"]),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1841,7 +1841,7 @@ describe("runtime-fallback", () => {
|
||||
test("should apply fallback model on next chat.message after error", async () => {
|
||||
const hook = createRuntimeFallbackHook(createMockPluginInput(), {
|
||||
config: createMockConfig({ notify_on_fallback: false }),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.2", "google/gemini-3.1-pro"]),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.4", "google/gemini-3.1-pro"]),
|
||||
})
|
||||
const sessionID = "test-session-switch"
|
||||
SessionCategoryRegistry.register(sessionID, "test")
|
||||
@@ -1871,13 +1871,13 @@ describe("runtime-fallback", () => {
|
||||
output
|
||||
)
|
||||
|
||||
expect(output.message.model).toEqual({ providerID: "openai", modelID: "gpt-5.2" })
|
||||
expect(output.message.model).toEqual({ providerID: "openai", modelID: "gpt-5.4" })
|
||||
})
|
||||
|
||||
test("should notify when fallback occurs", async () => {
|
||||
const hook = createRuntimeFallbackHook(createMockPluginInput(), {
|
||||
config: createMockConfig({ notify_on_fallback: true }),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.2"]),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.4"]),
|
||||
})
|
||||
const sessionID = "test-session-notify"
|
||||
SessionCategoryRegistry.register(sessionID, "test")
|
||||
@@ -1897,7 +1897,7 @@ describe("runtime-fallback", () => {
|
||||
})
|
||||
|
||||
expect(toastCalls.length).toBe(1)
|
||||
expect(toastCalls[0]?.message.includes("gpt-5.2")).toBe(true)
|
||||
expect(toastCalls[0]?.message.includes("gpt-5.4")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1916,7 +1916,7 @@ describe("runtime-fallback", () => {
|
||||
const input = createMockPluginInput()
|
||||
const hook = createRuntimeFallbackHook(input, {
|
||||
config: createMockConfig({ notify_on_fallback: false }),
|
||||
pluginConfig: createMockPluginConfigWithAgentFallback("oracle", ["openai/gpt-5.2", "google/gemini-3.1-pro"]),
|
||||
pluginConfig: createMockPluginConfigWithAgentFallback("oracle", ["openai/gpt-5.4", "google/gemini-3.1-pro"]),
|
||||
})
|
||||
const sessionID = "test-agent-fallback"
|
||||
|
||||
@@ -1936,16 +1936,16 @@ describe("runtime-fallback", () => {
|
||||
},
|
||||
})
|
||||
|
||||
//#then - should prepare fallback to openai/gpt-5.2
|
||||
//#then - should prepare fallback to openai/gpt-5.4
|
||||
const fallbackLog = logCalls.find((c) => c.msg.includes("Preparing fallback"))
|
||||
expect(fallbackLog).toBeDefined()
|
||||
expect(fallbackLog?.data).toMatchObject({ from: "anthropic/claude-opus-4-5", to: "openai/gpt-5.2" })
|
||||
expect(fallbackLog?.data).toMatchObject({ from: "anthropic/claude-opus-4-5", to: "openai/gpt-5.4" })
|
||||
})
|
||||
|
||||
test("should detect agent from sessionID pattern", async () => {
|
||||
const hook = createRuntimeFallbackHook(createMockPluginInput(), {
|
||||
config: createMockConfig({ notify_on_fallback: false }),
|
||||
pluginConfig: createMockPluginConfigWithAgentFallback("sisyphus", ["openai/gpt-5.2"]),
|
||||
pluginConfig: createMockPluginConfigWithAgentFallback("sisyphus", ["openai/gpt-5.4"]),
|
||||
})
|
||||
const sessionID = "sisyphus-session-123"
|
||||
|
||||
@@ -1966,7 +1966,7 @@ describe("runtime-fallback", () => {
|
||||
//#then - should detect sisyphus from sessionID and use its fallback
|
||||
const fallbackLog = logCalls.find((c) => c.msg.includes("Preparing fallback"))
|
||||
expect(fallbackLog).toBeDefined()
|
||||
expect(fallbackLog?.data).toMatchObject({ to: "openai/gpt-5.2" })
|
||||
expect(fallbackLog?.data).toMatchObject({ to: "openai/gpt-5.4" })
|
||||
})
|
||||
|
||||
test("should preserve resolved agent during auto-retry", async () => {
|
||||
@@ -2019,7 +2019,7 @@ describe("runtime-fallback", () => {
|
||||
const hook = createRuntimeFallbackHook(createMockPluginInput(), {
|
||||
config: createMockConfig({ cooldown_seconds: 60, notify_on_fallback: false }),
|
||||
pluginConfig: createMockPluginConfigWithCategoryFallback([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-5",
|
||||
]),
|
||||
})
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("createThinkModeHook", () => {
|
||||
const input = createHookInput({
|
||||
sessionID,
|
||||
providerID: "github-copilot",
|
||||
modelID: "gpt-5.2",
|
||||
modelID: "gpt-5.4",
|
||||
})
|
||||
const output = createHookOutput("ultrathink about this")
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("createThinkModeHook", () => {
|
||||
expect(output.message.variant).toBe("high")
|
||||
expect(output.message.model).toEqual({
|
||||
providerID: "github-copilot",
|
||||
modelID: "gpt-5-2-high",
|
||||
modelID: "gpt-5-4-high",
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -32,11 +32,11 @@ describe("think-mode switcher", () => {
|
||||
})
|
||||
|
||||
it("should handle dots in GPT version numbers", () => {
|
||||
// given a GPT model ID with dot format (gpt-5.2)
|
||||
const variant = getHighVariant("gpt-5.2")
|
||||
// given a GPT model ID with dot format (gpt-5.4)
|
||||
const variant = getHighVariant("gpt-5.4")
|
||||
|
||||
// then should return high variant
|
||||
expect(variant).toBe("gpt-5-2-high")
|
||||
expect(variant).toBe("gpt-5-4-high")
|
||||
})
|
||||
|
||||
it("should handle dots in GPT-5.1 codex variants", () => {
|
||||
@@ -60,7 +60,7 @@ describe("think-mode switcher", () => {
|
||||
it("should return null for already-high variants", () => {
|
||||
// given model IDs that are already high variants
|
||||
expect(getHighVariant("claude-opus-4-6-high")).toBeNull()
|
||||
expect(getHighVariant("gpt-5-2-high")).toBeNull()
|
||||
expect(getHighVariant("gpt-5-4-high")).toBeNull()
|
||||
expect(getHighVariant("gemini-3-1-pro-high")).toBeNull()
|
||||
})
|
||||
|
||||
@@ -76,20 +76,20 @@ describe("think-mode switcher", () => {
|
||||
it("should detect -high suffix", () => {
|
||||
// given model IDs with -high suffix
|
||||
expect(isAlreadyHighVariant("claude-opus-4-6-high")).toBe(true)
|
||||
expect(isAlreadyHighVariant("gpt-5-2-high")).toBe(true)
|
||||
expect(isAlreadyHighVariant("gpt-5-4-high")).toBe(true)
|
||||
expect(isAlreadyHighVariant("gemini-3.1-pro-high")).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect -high suffix after normalization", () => {
|
||||
// given model IDs with dots that end in -high
|
||||
expect(isAlreadyHighVariant("gpt-5.2-high")).toBe(true)
|
||||
expect(isAlreadyHighVariant("gpt-5.4-high")).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for base models", () => {
|
||||
// given base model IDs without -high suffix
|
||||
expect(isAlreadyHighVariant("claude-opus-4-6")).toBe(false)
|
||||
expect(isAlreadyHighVariant("claude-opus-4.6")).toBe(false)
|
||||
expect(isAlreadyHighVariant("gpt-5.2")).toBe(false)
|
||||
expect(isAlreadyHighVariant("gpt-5.4")).toBe(false)
|
||||
expect(isAlreadyHighVariant("gemini-3.1-pro")).toBe(false)
|
||||
})
|
||||
|
||||
@@ -111,10 +111,10 @@ describe("think-mode switcher", () => {
|
||||
|
||||
it("should preserve openai/ prefix when getting high variant", () => {
|
||||
// given a model ID with openai/ prefix
|
||||
const variant = getHighVariant("openai/gpt-5-2")
|
||||
const variant = getHighVariant("openai/gpt-5-4")
|
||||
|
||||
// then should return high variant with prefix preserved
|
||||
expect(variant).toBe("openai/gpt-5-2-high")
|
||||
expect(variant).toBe("openai/gpt-5-4-high")
|
||||
})
|
||||
|
||||
it("should handle prefixes with dots in version numbers", () => {
|
||||
@@ -141,7 +141,7 @@ describe("think-mode switcher", () => {
|
||||
it("should return null for already-high prefixed models", () => {
|
||||
// given prefixed model IDs that are already high
|
||||
expect(getHighVariant("vertex_ai/claude-opus-4-6-high")).toBeNull()
|
||||
expect(getHighVariant("openai/gpt-5-2-high")).toBeNull()
|
||||
expect(getHighVariant("openai/gpt-5-4-high")).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -149,20 +149,20 @@ describe("think-mode switcher", () => {
|
||||
it("should detect -high suffix in prefixed models", () => {
|
||||
// given prefixed model IDs with -high suffix
|
||||
expect(isAlreadyHighVariant("vertex_ai/claude-opus-4-6-high")).toBe(true)
|
||||
expect(isAlreadyHighVariant("openai/gpt-5-2-high")).toBe(true)
|
||||
expect(isAlreadyHighVariant("openai/gpt-5-4-high")).toBe(true)
|
||||
expect(isAlreadyHighVariant("custom/gemini-3.1-pro-high")).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for prefixed base models", () => {
|
||||
// given prefixed base model IDs without -high suffix
|
||||
expect(isAlreadyHighVariant("vertex_ai/claude-opus-4-6")).toBe(false)
|
||||
expect(isAlreadyHighVariant("openai/gpt-5-2")).toBe(false)
|
||||
expect(isAlreadyHighVariant("openai/gpt-5-4")).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle prefixed models with dots", () => {
|
||||
// given prefixed model IDs with dots
|
||||
expect(isAlreadyHighVariant("vertex_ai/gpt-5.2")).toBe(false)
|
||||
expect(isAlreadyHighVariant("vertex_ai/gpt-5.2-high")).toBe(true)
|
||||
expect(isAlreadyHighVariant("vertex_ai/gpt-5.4")).toBe(false)
|
||||
expect(isAlreadyHighVariant("vertex_ai/gpt-5.4-high")).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,7 +25,7 @@ import { normalizeModelID } from "../../shared"
|
||||
* @example
|
||||
* extractModelPrefix("vertex_ai/claude-sonnet-4-6") // { prefix: "vertex_ai/", base: "claude-sonnet-4-6" }
|
||||
* extractModelPrefix("claude-sonnet-4-6") // { prefix: "", base: "claude-sonnet-4-6" }
|
||||
* extractModelPrefix("openai/gpt-5.2") // { prefix: "openai/", base: "gpt-5.2" }
|
||||
* extractModelPrefix("openai/gpt-5.4") // { prefix: "openai/", base: "gpt-5.4" }
|
||||
*/
|
||||
function extractModelPrefix(modelID: string): { prefix: string; base: string } {
|
||||
const slashIndex = modelID.indexOf("/")
|
||||
@@ -61,10 +61,10 @@ const HIGH_VARIANT_MAP: Record<string, string> = {
|
||||
"gpt-5-1-codex": "gpt-5-1-codex-high",
|
||||
"gpt-5-1-codex-mini": "gpt-5-1-codex-mini-high",
|
||||
"gpt-5-1-codex-max": "gpt-5-1-codex-max-high",
|
||||
// GPT-5.2
|
||||
"gpt-5-2": "gpt-5-2-high",
|
||||
"gpt-5-2-chat-latest": "gpt-5-2-chat-latest-high",
|
||||
"gpt-5-2-pro": "gpt-5-2-pro-high",
|
||||
// GPT-5.4
|
||||
"gpt-5-4": "gpt-5-4-high",
|
||||
"gpt-5-4-chat-latest": "gpt-5-4-chat-latest-high",
|
||||
"gpt-5-4-pro": "gpt-5-4-pro-high",
|
||||
// Antigravity (Google)
|
||||
"antigravity-gemini-3-1-pro": "antigravity-gemini-3-1-pro-high",
|
||||
"antigravity-gemini-3-flash": "antigravity-gemini-3-flash-high",
|
||||
@@ -97,4 +97,3 @@ export function isAlreadyHighVariant(modelID: string): boolean {
|
||||
const { base } = extractModelPrefix(normalized)
|
||||
return ALREADY_HIGH.has(base) || base.endsWith("-high")
|
||||
}
|
||||
|
||||
|
||||
@@ -1345,8 +1345,8 @@ describe("todo-continuation-enforcer", () => {
|
||||
|
||||
// OpenCode returns assistant messages with flat modelID/providerID, not nested model object
|
||||
const mockMessagesWithAssistant = [
|
||||
{ info: { id: "msg-1", role: "user", agent: "sisyphus", model: { providerID: "openai", modelID: "gpt-5.2" } } },
|
||||
{ info: { id: "msg-2", role: "assistant", agent: "sisyphus", modelID: "gpt-5.2", providerID: "openai" } },
|
||||
{ info: { id: "msg-1", role: "user", agent: "sisyphus", model: { providerID: "openai", modelID: "gpt-5.4" } } },
|
||||
{ info: { id: "msg-2", role: "assistant", agent: "sisyphus", modelID: "gpt-5.4", providerID: "openai" } },
|
||||
]
|
||||
|
||||
const mockInput = {
|
||||
@@ -1390,7 +1390,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
|
||||
// then - model should be extracted from assistant message's flat modelID/providerID
|
||||
expect(promptCalls.length).toBe(1)
|
||||
expect(promptCalls[0].model).toEqual({ providerID: "openai", modelID: "gpt-5.2" })
|
||||
expect(promptCalls[0].model).toEqual({ providerID: "openai", modelID: "gpt-5.4" })
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
|
||||
+18
-18
@@ -12,7 +12,7 @@ describe("mergeConfigs", () => {
|
||||
const base = {
|
||||
categories: {
|
||||
general: {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
temperature: 0.5,
|
||||
},
|
||||
quick: {
|
||||
@@ -35,7 +35,7 @@ describe("mergeConfigs", () => {
|
||||
const result = mergeConfigs(base, override);
|
||||
|
||||
// then general.model should be preserved from base
|
||||
expect(result.categories?.general?.model).toBe("openai/gpt-5.2");
|
||||
expect(result.categories?.general?.model).toBe("openai/gpt-5.4");
|
||||
// then general.temperature should be overridden
|
||||
expect(result.categories?.general?.temperature).toBe(0.3);
|
||||
// then quick should be preserved from base
|
||||
@@ -48,7 +48,7 @@ describe("mergeConfigs", () => {
|
||||
const base: OhMyOpenCodeConfig = {
|
||||
categories: {
|
||||
general: {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -57,7 +57,7 @@ describe("mergeConfigs", () => {
|
||||
|
||||
const result = mergeConfigs(base, override);
|
||||
|
||||
expect(result.categories?.general?.model).toBe("openai/gpt-5.2");
|
||||
expect(result.categories?.general?.model).toBe("openai/gpt-5.4");
|
||||
});
|
||||
|
||||
it("should use override categories when base has no categories", () => {
|
||||
@@ -66,14 +66,14 @@ describe("mergeConfigs", () => {
|
||||
const override: OhMyOpenCodeConfig = {
|
||||
categories: {
|
||||
general: {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeConfigs(base, override);
|
||||
|
||||
expect(result.categories?.general?.model).toBe("openai/gpt-5.2");
|
||||
expect(result.categories?.general?.model).toBe("openai/gpt-5.4");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("mergeConfigs", () => {
|
||||
it("should deep merge agents", () => {
|
||||
const base: OhMyOpenCodeConfig = {
|
||||
agents: {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("mergeConfigs", () => {
|
||||
|
||||
const result = mergeConfigs(base, override);
|
||||
|
||||
expect(result.agents?.oracle?.model).toBe("openai/gpt-5.2");
|
||||
expect(result.agents?.oracle?.model).toBe("openai/gpt-5.4");
|
||||
expect(result.agents?.oracle?.temperature).toBe(0.5);
|
||||
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5");
|
||||
});
|
||||
@@ -127,8 +127,8 @@ describe("parseConfigPartially", () => {
|
||||
it("should return the full config when everything is valid", () => {
|
||||
const rawConfig = {
|
||||
agents: {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
momus: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
momus: { model: "openai/gpt-5.4" },
|
||||
},
|
||||
disabled_hooks: ["comment-checker"],
|
||||
};
|
||||
@@ -136,8 +136,8 @@ describe("parseConfigPartially", () => {
|
||||
const result = parseConfigPartially(rawConfig);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.2");
|
||||
expect(result!.agents?.momus?.model).toBe("openai/gpt-5.2");
|
||||
expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.4");
|
||||
expect(result!.agents?.momus?.model).toBe("openai/gpt-5.4");
|
||||
expect(result!.disabled_hooks).toEqual(["comment-checker"]);
|
||||
});
|
||||
});
|
||||
@@ -150,8 +150,8 @@ describe("parseConfigPartially", () => {
|
||||
it("should preserve valid agent overrides when another section is invalid", () => {
|
||||
const rawConfig = {
|
||||
agents: {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
momus: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
momus: { model: "openai/gpt-5.4" },
|
||||
prometheus: {
|
||||
permission: {
|
||||
edit: { "*": "ask", ".sisyphus/**": "allow" },
|
||||
@@ -171,7 +171,7 @@ describe("parseConfigPartially", () => {
|
||||
it("should preserve valid agents when a non-agent section is invalid", () => {
|
||||
const rawConfig = {
|
||||
agents: {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
},
|
||||
disabled_hooks: ["not-a-real-hook"],
|
||||
};
|
||||
@@ -179,7 +179,7 @@ describe("parseConfigPartially", () => {
|
||||
const result = parseConfigPartially(rawConfig);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.2");
|
||||
expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.4");
|
||||
expect(result!.disabled_hooks).toEqual(["not-a-real-hook"]);
|
||||
});
|
||||
});
|
||||
@@ -224,7 +224,7 @@ describe("parseConfigPartially", () => {
|
||||
it("should ignore unknown keys and return valid sections", () => {
|
||||
const rawConfig = {
|
||||
agents: {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
},
|
||||
some_future_key: { foo: "bar" },
|
||||
};
|
||||
@@ -232,7 +232,7 @@ describe("parseConfigPartially", () => {
|
||||
const result = parseConfigPartially(rawConfig);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.2");
|
||||
expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.4");
|
||||
expect((result as Record<string, unknown>)["some_future_key"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -656,7 +656,7 @@ describe("Prometheus direct override priority over category", () => {
|
||||
},
|
||||
categories: {
|
||||
"test-planning": {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
reasoningEffort: "xhigh",
|
||||
},
|
||||
},
|
||||
@@ -698,7 +698,7 @@ describe("Prometheus direct override priority over category", () => {
|
||||
},
|
||||
categories: {
|
||||
"reasoning-cat": {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
},
|
||||
@@ -739,7 +739,7 @@ describe("Prometheus direct override priority over category", () => {
|
||||
},
|
||||
categories: {
|
||||
"temp-cat": {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
temperature: 0.8,
|
||||
},
|
||||
},
|
||||
@@ -860,7 +860,7 @@ describe("Plan agent model inheritance from prometheus", () => {
|
||||
test("plan agent inherits temperature, reasoningEffort, and other model settings from prometheus", async () => {
|
||||
//#given - prometheus configured with category that has temperature and reasoningEffort
|
||||
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
provenance: "override",
|
||||
variant: "high",
|
||||
})
|
||||
@@ -871,7 +871,7 @@ describe("Plan agent model inheritance from prometheus", () => {
|
||||
},
|
||||
agents: {
|
||||
prometheus: {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
variant: "high",
|
||||
temperature: 0.3,
|
||||
top_p: 0.9,
|
||||
@@ -902,7 +902,7 @@ describe("Plan agent model inheritance from prometheus", () => {
|
||||
const agents = config.agent as Record<string, Record<string, unknown>>
|
||||
expect(agents.plan).toBeDefined()
|
||||
expect(agents.plan.mode).toBe("subagent")
|
||||
expect(agents.plan.model).toBe("openai/gpt-5.2")
|
||||
expect(agents.plan.model).toBe("openai/gpt-5.4")
|
||||
expect(agents.plan.variant).toBe("high")
|
||||
expect(agents.plan.temperature).toBe(0.3)
|
||||
expect(agents.plan.top_p).toBe(0.9)
|
||||
@@ -913,7 +913,7 @@ describe("Plan agent model inheritance from prometheus", () => {
|
||||
})
|
||||
|
||||
test("plan agent user override takes priority over prometheus inherited settings", async () => {
|
||||
//#given - prometheus resolves to opus, but user has plan override for gpt-5.2
|
||||
//#given - prometheus resolves to opus, but user has plan override for gpt-5.4
|
||||
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
provenance: "provider-fallback",
|
||||
@@ -926,7 +926,7 @@ describe("Plan agent model inheritance from prometheus", () => {
|
||||
},
|
||||
agents: {
|
||||
plan: {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
variant: "high",
|
||||
temperature: 0.5,
|
||||
},
|
||||
@@ -950,7 +950,7 @@ describe("Plan agent model inheritance from prometheus", () => {
|
||||
|
||||
//#then - plan uses its own override, not prometheus settings
|
||||
const agents = config.agent as Record<string, Record<string, unknown>>
|
||||
expect(agents.plan.model).toBe("openai/gpt-5.2")
|
||||
expect(agents.plan.model).toBe("openai/gpt-5.4")
|
||||
expect(agents.plan.variant).toBe("high")
|
||||
expect(agents.plan.temperature).toBe(0.5)
|
||||
})
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("buildPlanDemoteConfig", () => {
|
||||
reasoningEffort: "high",
|
||||
}
|
||||
const planOverride = {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
variant: "high",
|
||||
temperature: 0.5,
|
||||
reasoningEffort: "low",
|
||||
@@ -74,7 +74,7 @@ describe("buildPlanDemoteConfig", () => {
|
||||
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
|
||||
|
||||
//#then
|
||||
expect(result.model).toBe("openai/gpt-5.2")
|
||||
expect(result.model).toBe("openai/gpt-5.4")
|
||||
expect(result.variant).toBe("high")
|
||||
expect(result.temperature).toBe(0.5)
|
||||
expect(result.reasoningEffort).toBe("low")
|
||||
@@ -89,14 +89,14 @@ describe("buildPlanDemoteConfig", () => {
|
||||
reasoningEffort: "high",
|
||||
}
|
||||
const planOverride = {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
|
||||
|
||||
//#then - plan model wins, rest inherits from prometheus
|
||||
expect(result.model).toBe("openai/gpt-5.2")
|
||||
expect(result.model).toBe("openai/gpt-5.4")
|
||||
expect(result.variant).toBe("max")
|
||||
expect(result.temperature).toBe(0.1)
|
||||
expect(result.reasoningEffort).toBe("high")
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("Agent Config Integration", () => {
|
||||
// given - config with lowercase keys
|
||||
const config = {
|
||||
sisyphus: { model: "anthropic/claude-opus-4-6" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
librarian: { model: "opencode/big-pickle" },
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ describe("Agent Config Integration", () => {
|
||||
// given - config with mixed old and new format
|
||||
const mixedConfig = {
|
||||
Sisyphus: { model: "anthropic/claude-opus-4-6" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
"Prometheus (Planner)": { model: "anthropic/claude-opus-4-6" },
|
||||
librarian: { model: "opencode/big-pickle" },
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("resolveAgentVariant", () => {
|
||||
sisyphus: { category: "ultrabrain" },
|
||||
},
|
||||
categories: {
|
||||
ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" },
|
||||
ultrabrain: { model: "openai/gpt-5.4", variant: "xhigh" },
|
||||
},
|
||||
} as OhMyOpenCodeConfig
|
||||
|
||||
@@ -127,7 +127,7 @@ describe("resolveVariantForModel", () => {
|
||||
test("returns undefined for provider not in sisyphus chain", () => {
|
||||
// #given openai is not in sisyphus fallback chain anymore
|
||||
const config = {} as OhMyOpenCodeConfig
|
||||
const model = { providerID: "openai", modelID: "gpt-5.2" }
|
||||
const model = { providerID: "openai", modelID: "gpt-5.4" }
|
||||
|
||||
// when
|
||||
const variant = resolveVariantForModel(config, "sisyphus", model)
|
||||
@@ -191,7 +191,7 @@ describe("resolveVariantForModel", () => {
|
||||
test("returns correct variant for oracle agent with openai", () => {
|
||||
// given
|
||||
const config = {} as OhMyOpenCodeConfig
|
||||
const model = { providerID: "openai", modelID: "gpt-5.2" }
|
||||
const model = { providerID: "openai", modelID: "gpt-5.4" }
|
||||
|
||||
// when
|
||||
const variant = resolveVariantForModel(config, "oracle", model)
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("updateConnectedProvidersCache", () => {
|
||||
env: [],
|
||||
models: {
|
||||
"gpt-5.3-codex": { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
"gpt-5.2": { id: "gpt-5.2", name: "GPT-5.2" },
|
||||
"gpt-5.4": { id: "gpt-5.4", name: "GPT-5.4" },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -64,7 +64,7 @@ describe("updateConnectedProvidersCache", () => {
|
||||
expect(cache).not.toBeNull()
|
||||
expect(cache!.connected).toEqual(["openai", "anthropic"])
|
||||
expect(cache!.models).toEqual({
|
||||
openai: ["gpt-5.3-codex", "gpt-5.2"],
|
||||
openai: ["gpt-5.3-codex", "gpt-5.4"],
|
||||
anthropic: ["claude-opus-4-6", "claude-sonnet-4-6"],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -105,7 +105,7 @@ describe("parseJsonc", () => {
|
||||
const jsonc = `{
|
||||
// This is an example config
|
||||
"agents": {
|
||||
"oracle": { "model": "openai/gpt-5.2" }, // GPT for strategic reasoning
|
||||
"oracle": { "model": "openai/gpt-5.4" }, // GPT for strategic reasoning
|
||||
},
|
||||
/* Agent overrides */
|
||||
"disabled_agents": [],
|
||||
@@ -118,7 +118,7 @@ describe("parseJsonc", () => {
|
||||
}>(jsonc)
|
||||
|
||||
// then
|
||||
expect(result.agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(result.agents.oracle.model).toBe("openai/gpt-5.4")
|
||||
expect(result.disabled_agents).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("mergeCategories", () => {
|
||||
it("allows user to add custom categories", () => {
|
||||
//#given
|
||||
const userCategories = {
|
||||
"my-custom": { model: "openai/gpt-5.2", description: "Custom category" },
|
||||
"my-custom": { model: "openai/gpt-5.4", description: "Custom category" },
|
||||
}
|
||||
|
||||
//#when
|
||||
@@ -52,13 +52,13 @@ describe("mergeCategories", () => {
|
||||
|
||||
//#then
|
||||
expect(result["my-custom"]).toBeDefined()
|
||||
expect(result["my-custom"].model).toBe("openai/gpt-5.2")
|
||||
expect(result["my-custom"].model).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
it("allows user to disable custom categories", () => {
|
||||
//#given
|
||||
const userCategories = {
|
||||
"my-custom": { model: "openai/gpt-5.2", disable: true },
|
||||
"my-custom": { model: "openai/gpt-5.4", disable: true },
|
||||
}
|
||||
|
||||
//#when
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("migrateAgentNames", () => {
|
||||
test("preserves current agent names unchanged", () => {
|
||||
// given: Config with current agent names
|
||||
const agents = {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
librarian: { model: "google/gemini-3-flash" },
|
||||
explore: { model: "opencode/gpt-5-nano" },
|
||||
}
|
||||
@@ -47,7 +47,7 @@ describe("migrateAgentNames", () => {
|
||||
|
||||
// then: Current names should remain unchanged
|
||||
expect(changed).toBe(false)
|
||||
expect(migrated["oracle"]).toEqual({ model: "openai/gpt-5.2" })
|
||||
expect(migrated["oracle"]).toEqual({ model: "openai/gpt-5.4" })
|
||||
expect(migrated["librarian"]).toEqual({ model: "google/gemini-3-flash" })
|
||||
expect(migrated["explore"]).toEqual({ model: "opencode/gpt-5-nano" })
|
||||
})
|
||||
@@ -57,7 +57,7 @@ describe("migrateAgentNames", () => {
|
||||
const agents = {
|
||||
SISYPHUS: { model: "test" },
|
||||
"planner-sisyphus": { prompt: "test" },
|
||||
"Orchestrator-Sisyphus": { model: "openai/gpt-5.2" },
|
||||
"Orchestrator-Sisyphus": { model: "openai/gpt-5.4" },
|
||||
}
|
||||
|
||||
// when: Migrate agent names
|
||||
@@ -66,7 +66,7 @@ describe("migrateAgentNames", () => {
|
||||
// then: Case-insensitive lookup should migrate correctly
|
||||
expect(migrated["sisyphus"]).toEqual({ model: "test" })
|
||||
expect(migrated["prometheus"]).toEqual({ prompt: "test" })
|
||||
expect(migrated["atlas"]).toEqual({ model: "openai/gpt-5.2" })
|
||||
expect(migrated["atlas"]).toEqual({ model: "openai/gpt-5.4" })
|
||||
})
|
||||
|
||||
test("passes through unknown agent names unchanged", () => {
|
||||
@@ -441,11 +441,11 @@ describe("migrateConfigFile", () => {
|
||||
expect(rawConfig.disabled_hooks).toContain("anthropic-context-window-limit-recovery")
|
||||
})
|
||||
|
||||
test("does not migrate gpt-5.2-codex model versions in agents", () => {
|
||||
test("does not migrate gpt-5.4-codex model versions in agents", () => {
|
||||
// given: Config with old model version in agents
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex", temperature: 0.1 },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex", temperature: 0.1 },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ describe("migrateConfigFile", () => {
|
||||
// then: Model version should remain unchanged
|
||||
expect(needsWrite).toBe(false)
|
||||
const agents = rawConfig.agents as Record<string, Record<string, unknown>>
|
||||
expect(agents["sisyphus"].model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents["sisyphus"].model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("migrates model versions in categories", () => {
|
||||
@@ -479,7 +479,7 @@ describe("migrateConfigFile", () => {
|
||||
// given: Config with current model versions
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
},
|
||||
categories: {
|
||||
"my-category": { model: "anthropic/claude-opus-4-6" },
|
||||
@@ -514,10 +514,10 @@ describe("migration maps", () => {
|
||||
})
|
||||
|
||||
describe("MODEL_VERSION_MAP", () => {
|
||||
test("does not include openai/gpt-5.2-codex migration", () => {
|
||||
test("does not include openai/gpt-5.4-codex migration", () => {
|
||||
// given/when: Check MODEL_VERSION_MAP
|
||||
// then: openai/gpt-5.2-codex should not be migrated
|
||||
expect(MODEL_VERSION_MAP["openai/gpt-5.2-codex"]).toBeUndefined()
|
||||
// then: openai/gpt-5.4-codex should not be migrated
|
||||
expect(MODEL_VERSION_MAP["openai/gpt-5.4-codex"]).toBeUndefined()
|
||||
})
|
||||
|
||||
test("maps anthropic/claude-opus-4-5 to anthropic/claude-opus-4-6", () => {
|
||||
@@ -528,10 +528,10 @@ describe("MODEL_VERSION_MAP", () => {
|
||||
})
|
||||
|
||||
describe("migrateModelVersions", () => {
|
||||
test("#given a config with gpt-5.2-codex model #when migrating model versions #then does not overwrite with non-existent gpt-5.3-codex", () => {
|
||||
// given: Agent config with gpt-5.2-codex model
|
||||
test("#given a config with gpt-5.4-codex model #when migrating model versions #then does not overwrite with non-existent gpt-5.3-codex", () => {
|
||||
// given: Agent config with gpt-5.4-codex model
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex", temperature: 0.1 },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex", temperature: 0.1 },
|
||||
}
|
||||
|
||||
// when: Migrate model versions
|
||||
@@ -540,7 +540,7 @@ describe("migrateModelVersions", () => {
|
||||
// then: Model should remain unchanged
|
||||
expect(changed).toBe(false)
|
||||
const sisyphus = migrated["sisyphus"] as Record<string, unknown>
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
expect(sisyphus.temperature).toBe(0.1)
|
||||
})
|
||||
|
||||
@@ -562,7 +562,7 @@ describe("migrateModelVersions", () => {
|
||||
test("leaves unknown model strings untouched", () => {
|
||||
// given: Agent config with unknown model
|
||||
const agents = {
|
||||
oracle: { model: "openai/gpt-5.2", temperature: 0.5 },
|
||||
oracle: { model: "openai/gpt-5.4", temperature: 0.5 },
|
||||
}
|
||||
|
||||
// when: Migrate model versions
|
||||
@@ -571,7 +571,7 @@ describe("migrateModelVersions", () => {
|
||||
// then: Config should remain unchanged
|
||||
expect(changed).toBe(false)
|
||||
const oracle = migrated["oracle"] as Record<string, unknown>
|
||||
expect(oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(oracle.model).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
test("handles agent config with no model field", () => {
|
||||
@@ -605,9 +605,9 @@ describe("migrateModelVersions", () => {
|
||||
test("migrates multiple agents in one pass", () => {
|
||||
// given: Multiple agents with old models
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
prometheus: { model: "anthropic/claude-opus-4-5" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
}
|
||||
|
||||
// when: Migrate model versions
|
||||
@@ -615,9 +615,9 @@ describe("migrateModelVersions", () => {
|
||||
|
||||
// then: Only mapped models should be updated
|
||||
expect(changed).toBe(true)
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
expect((migrated["prometheus"] as Record<string, unknown>).model).toBe("anthropic/claude-opus-4-6")
|
||||
expect((migrated["oracle"] as Record<string, unknown>).model).toBe("openai/gpt-5.2")
|
||||
expect((migrated["oracle"] as Record<string, unknown>).model).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
test("handles empty object", () => {
|
||||
@@ -635,9 +635,9 @@ describe("migrateModelVersions", () => {
|
||||
test("skips already-applied migrations", () => {
|
||||
// given: Agent config with old model, but migration already applied
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex", temperature: 0.1 },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex", temperature: 0.1 },
|
||||
}
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"])
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"])
|
||||
|
||||
// when: Migrate with applied migrations
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(agents, appliedMigrations)
|
||||
@@ -646,32 +646,32 @@ describe("migrateModelVersions", () => {
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toHaveLength(0)
|
||||
const sisyphus = migrated["sisyphus"] as Record<string, unknown>
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("applies new migrations and records them", () => {
|
||||
// given: Agent config with old model, no prior migrations
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
|
||||
// when: Migrate without applied migrations
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(agents)
|
||||
|
||||
// then: No migration should be applied for gpt-5.2-codex
|
||||
// then: No migration should be applied for gpt-5.4-codex
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toEqual([])
|
||||
const sisyphus = migrated["sisyphus"] as Record<string, unknown>
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("handles mixed: some applied, some new", () => {
|
||||
// given: Multiple agents, one migration already applied
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
prometheus: { model: "anthropic/claude-opus-4-5" },
|
||||
}
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"])
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"])
|
||||
|
||||
// when: Migrate with partial history
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(agents, appliedMigrations)
|
||||
@@ -679,23 +679,23 @@ describe("migrateModelVersions", () => {
|
||||
// then: Only prometheus should be migrated
|
||||
expect(changed).toBe(true)
|
||||
expect(newMigrations).toEqual(["model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6"])
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
expect((migrated["prometheus"] as Record<string, unknown>).model).toBe("anthropic/claude-opus-4-6")
|
||||
})
|
||||
|
||||
test("backward compatible without appliedMigrations param", () => {
|
||||
// given: Agent config with old model, no appliedMigrations param
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
|
||||
// when: Migrate without the param (backward compat)
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(agents)
|
||||
|
||||
// then: Should keep gpt-5.2-codex unchanged
|
||||
// then: Should keep gpt-5.4-codex unchanged
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toHaveLength(0)
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -706,14 +706,14 @@ describe("migrateConfigFile _migrations tracking", () => {
|
||||
const configPath = `${tmpDir}/oh-my-opencode.json`
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
},
|
||||
}
|
||||
|
||||
// when: Migrate config file
|
||||
const result = migrateConfigFile(configPath, rawConfig)
|
||||
|
||||
// then: gpt-5.2-codex should not produce migrations
|
||||
// then: gpt-5.4-codex should not produce migrations
|
||||
expect(result).toBe(false)
|
||||
expect(rawConfig._migrations).toBeUndefined()
|
||||
|
||||
@@ -727,9 +727,9 @@ describe("migrateConfigFile _migrations tracking", () => {
|
||||
const configPath = `${tmpDir}/oh-my-opencode.json`
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
},
|
||||
_migrations: ["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"],
|
||||
_migrations: ["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"],
|
||||
}
|
||||
|
||||
// when: Migrate config file
|
||||
@@ -738,7 +738,7 @@ describe("migrateConfigFile _migrations tracking", () => {
|
||||
// then: Should NOT rewrite (model stays as user set it)
|
||||
// Note: result may be true due to other migrations, but model should NOT change
|
||||
const sisyphus = (rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
|
||||
// cleanup
|
||||
fs.rmSync(tmpDir, { recursive: true })
|
||||
@@ -752,7 +752,7 @@ describe("migrateConfigFile _migrations tracking", () => {
|
||||
agents: {
|
||||
prometheus: { model: "anthropic/claude-opus-4-5" },
|
||||
},
|
||||
_migrations: ["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"],
|
||||
_migrations: ["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"],
|
||||
}
|
||||
|
||||
// when: Migrate config file
|
||||
@@ -761,7 +761,7 @@ describe("migrateConfigFile _migrations tracking", () => {
|
||||
// then: New migration appended, old one preserved
|
||||
expect(result).toBe(true)
|
||||
expect(rawConfig._migrations).toEqual([
|
||||
"model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex",
|
||||
"model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex",
|
||||
"model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6",
|
||||
])
|
||||
|
||||
@@ -825,7 +825,7 @@ describe("migrateAgentConfigToCategory", () => {
|
||||
const configs = [
|
||||
{ model: "google/gemini-3.1-pro" },
|
||||
{ model: "google/gemini-3-flash" },
|
||||
{ model: "openai/gpt-5.2" },
|
||||
{ model: "openai/gpt-5.4" },
|
||||
{ model: "anthropic/claude-haiku-4-5" },
|
||||
{ model: "anthropic/claude-opus-4-6" },
|
||||
{ model: "anthropic/claude-sonnet-4-6" },
|
||||
@@ -847,7 +847,7 @@ describe("migrateAgentConfigToCategory", () => {
|
||||
test("preserves non-model fields during migration", () => {
|
||||
// given: Config with multiple fields
|
||||
const config = {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
temperature: 0.1,
|
||||
top_p: 0.95,
|
||||
maxTokens: 4096,
|
||||
@@ -1020,7 +1020,7 @@ describe("migrateConfigFile with backup", () => {
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
"multimodal-looker": { model: "anthropic/claude-haiku-4-5" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
"my-custom-agent": { model: "google/gemini-3.1-pro" },
|
||||
},
|
||||
}
|
||||
@@ -1036,7 +1036,7 @@ describe("migrateConfigFile with backup", () => {
|
||||
|
||||
const agents = rawConfig.agents as Record<string, Record<string, unknown>>
|
||||
expect(agents["multimodal-looker"].model).toBe("anthropic/claude-haiku-4-5")
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.4")
|
||||
expect(agents["my-custom-agent"].model).toBe("google/gemini-3.1-pro")
|
||||
})
|
||||
|
||||
@@ -1140,9 +1140,9 @@ describe("migrateModelVersions with applied migrations", () => {
|
||||
test("skips already-applied migrations", () => {
|
||||
// given: Config with old model and migration already applied
|
||||
const configs = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"])
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"])
|
||||
|
||||
// when: Migrate model versions
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(configs, appliedMigrations)
|
||||
@@ -1150,32 +1150,32 @@ describe("migrateModelVersions with applied migrations", () => {
|
||||
// then: Migration should be skipped (user reverted)
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toEqual([])
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("applies new migrations not in history", () => {
|
||||
// given: Config with old model, no migration history
|
||||
const configs = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
const appliedMigrations = new Set<string>()
|
||||
|
||||
// when: Migrate model versions
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(configs, appliedMigrations)
|
||||
|
||||
// then: gpt-5.2-codex should not be migrated
|
||||
// then: gpt-5.4-codex should not be migrated
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toEqual([])
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("handles mixed: skip applied, apply new", () => {
|
||||
// given: Config with 2 old models, 1 already migrated
|
||||
const configs = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
oracle: { model: "anthropic/claude-opus-4-5" },
|
||||
}
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"])
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"])
|
||||
|
||||
// when: Migrate model versions
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(configs, appliedMigrations)
|
||||
@@ -1183,29 +1183,29 @@ describe("migrateModelVersions with applied migrations", () => {
|
||||
// then: Skip sisyphus (already applied), apply oracle
|
||||
expect(changed).toBe(true)
|
||||
expect(newMigrations).toEqual(["model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6"])
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
expect((migrated.oracle as Record<string, unknown>).model).toBe("anthropic/claude-opus-4-6")
|
||||
})
|
||||
|
||||
test("backward compatible: no appliedMigrations param", () => {
|
||||
// given: Config with old model, no appliedMigrations param (legacy call)
|
||||
const configs = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
|
||||
// when: Migrate model versions (without appliedMigrations)
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(configs)
|
||||
|
||||
// then: gpt-5.2-codex remains unchanged
|
||||
// then: gpt-5.4-codex remains unchanged
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toEqual([])
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("returns empty newMigrations when no migrations applied", () => {
|
||||
// given: Config with no old models
|
||||
const configs = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
|
||||
// when: Migrate model versions
|
||||
@@ -1235,7 +1235,7 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
const testConfigPath = "/tmp/test-config-migrations-1.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
},
|
||||
}
|
||||
fs.writeFileSync(testConfigPath, JSON.stringify(rawConfig, null, 2))
|
||||
@@ -1244,10 +1244,10 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
// when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// then: gpt-5.2-codex should not create migration history
|
||||
// then: gpt-5.4-codex should not create migration history
|
||||
expect(needsWrite).toBe(false)
|
||||
expect(rawConfig._migrations).toBeUndefined()
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("skips re-applying already-recorded migrations", () => {
|
||||
@@ -1255,9 +1255,9 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
const testConfigPath = "/tmp/test-config-migrations-2.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
},
|
||||
_migrations: ["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"],
|
||||
_migrations: ["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"],
|
||||
}
|
||||
fs.writeFileSync(testConfigPath, JSON.stringify(rawConfig, null, 2))
|
||||
cleanupPaths.push(testConfigPath)
|
||||
@@ -1267,8 +1267,8 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
|
||||
// then: Should not migrate (user reverted)
|
||||
expect(needsWrite).toBe(false)
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(rawConfig._migrations).toEqual(["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"])
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
expect(rawConfig._migrations).toEqual(["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"])
|
||||
})
|
||||
|
||||
test("preserves existing _migrations and appends new ones", () => {
|
||||
@@ -1276,10 +1276,10 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
const testConfigPath = "/tmp/test-config-migrations-3.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
oracle: { model: "anthropic/claude-opus-4-5" },
|
||||
},
|
||||
_migrations: ["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"],
|
||||
_migrations: ["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"],
|
||||
}
|
||||
fs.writeFileSync(testConfigPath, JSON.stringify(rawConfig, null, 2))
|
||||
cleanupPaths.push(testConfigPath)
|
||||
@@ -1289,10 +1289,10 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
|
||||
// then: Should skip sisyphus, migrate oracle, append to _migrations
|
||||
expect(needsWrite).toBe(true)
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).oracle.model).toBe("anthropic/claude-opus-4-6")
|
||||
expect(rawConfig._migrations).toEqual([
|
||||
"model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex",
|
||||
"model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex",
|
||||
"model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6",
|
||||
])
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
export const MODEL_TO_CATEGORY_MAP: Record<string, string> = {
|
||||
"google/gemini-3.1-pro": "visual-engineering",
|
||||
"google/gemini-3-flash": "writing",
|
||||
"openai/gpt-5.2": "ultrabrain",
|
||||
"openai/gpt-5.4": "ultrabrain",
|
||||
"anthropic/claude-haiku-4-5": "quick",
|
||||
"anthropic/claude-opus-4-6": "unspecified-high",
|
||||
"anthropic/claude-sonnet-4-6": "unspecified-low",
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("fetchAvailableModels", () => {
|
||||
|
||||
it("#given cache file with models #when fetchAvailableModels called with connectedProviders #then returns Set of model IDs", async () => {
|
||||
writeModelsCache({
|
||||
openai: { id: "openai", models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { id: "openai", models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { id: "anthropic", models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
google: { id: "google", models: { "gemini-3.1-pro": { id: "gemini-3.1-pro" } } },
|
||||
})
|
||||
@@ -72,14 +72,14 @@ describe("fetchAvailableModels", () => {
|
||||
|
||||
expect(result).toBeInstanceOf(Set)
|
||||
expect(result.size).toBe(3)
|
||||
expect(result.has("openai/gpt-5.2")).toBe(true)
|
||||
expect(result.has("openai/gpt-5.4")).toBe(true)
|
||||
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
|
||||
expect(result.has("google/gemini-3.1-pro")).toBe(true)
|
||||
})
|
||||
|
||||
it("#given connectedProviders unknown #when fetchAvailableModels called without options #then returns empty Set", async () => {
|
||||
writeModelsCache({
|
||||
openai: { id: "openai", models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { id: "openai", models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
})
|
||||
|
||||
const result = await fetchAvailableModels()
|
||||
@@ -141,7 +141,7 @@ describe("fetchAvailableModels", () => {
|
||||
|
||||
it("#given cache read twice #when second call made with same providers #then reads fresh each time", async () => {
|
||||
writeModelsCache({
|
||||
openai: { id: "openai", models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { id: "openai", models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { id: "anthropic", models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
})
|
||||
|
||||
@@ -149,7 +149,7 @@ describe("fetchAvailableModels", () => {
|
||||
const result2 = await fetchAvailableModels(undefined, { connectedProviders: ["openai"] })
|
||||
|
||||
expect(result1.size).toBe(result2.size)
|
||||
expect(result1.has("openai/gpt-5.2")).toBe(true)
|
||||
expect(result1.has("openai/gpt-5.4")).toBe(true)
|
||||
})
|
||||
|
||||
it("#given empty providers in cache #when fetchAvailableModels called with connectedProviders #then returns empty Set", async () => {
|
||||
@@ -187,12 +187,12 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return the matching model
|
||||
it("should match substring in model name", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.3-codex",
|
||||
"anthropic/claude-opus-4-6",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt-5.2", available)
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
const result = fuzzyMatchModel("gpt-5.4", available)
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available model with preview suffix
|
||||
@@ -213,12 +213,12 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return exact match if it exists
|
||||
it("should prefer exact match over substring match", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.3-codex",
|
||||
"openai/gpt-5.2-ultra",
|
||||
"openai/gpt-5.4-ultra",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt-5.2", available)
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
const result = fuzzyMatchModel("gpt-5.4", available)
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available models with multiple substring matches
|
||||
@@ -226,11 +226,11 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return the shorter model name (more specific)
|
||||
it("should prefer shorter model name when multiple matches exist", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2-ultra",
|
||||
"openai/gpt-5.2-ultra-mega",
|
||||
"openai/gpt-5.4-ultra",
|
||||
"openai/gpt-5.4-ultra-mega",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt-5.2", available)
|
||||
expect(result).toBe("openai/gpt-5.2-ultra")
|
||||
const result = fuzzyMatchModel("gpt-5.4", available)
|
||||
expect(result).toBe("openai/gpt-5.4-ultra")
|
||||
})
|
||||
|
||||
// given available models with claude variants
|
||||
@@ -271,12 +271,12 @@ describe("fuzzyMatchModel", () => {
|
||||
// then only search models from specified providers
|
||||
it("should filter by provider when providers array is given", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"google/gemini-3",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt", available, ["openai"])
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available models from multiple providers
|
||||
@@ -284,7 +284,7 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return null
|
||||
it("should return null when provider filter excludes all matches", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
])
|
||||
const result = fuzzyMatchModel("claude", available, ["openai"])
|
||||
@@ -296,7 +296,7 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return null
|
||||
it("should return null when no match found", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
])
|
||||
const result = fuzzyMatchModel("gemini", available)
|
||||
@@ -308,11 +308,11 @@ describe("fuzzyMatchModel", () => {
|
||||
// then match case-insensitively
|
||||
it("should match case-insensitively", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
])
|
||||
const result = fuzzyMatchModel("GPT-5.2", available)
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
const result = fuzzyMatchModel("GPT-5.4", available)
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available models with exact match and longer variants
|
||||
@@ -356,11 +356,11 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return shortest full string (preserves tie-break behavior)
|
||||
it("should use shortest tie-break when multiple providers have same model ID", () => {
|
||||
const available = new Set([
|
||||
"opencode/gpt-5.2",
|
||||
"openai/gpt-5.2",
|
||||
"opencode/gpt-5.4",
|
||||
"openai/gpt-5.4",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt-5.2", available)
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
const result = fuzzyMatchModel("gpt-5.4", available)
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available models with multiple providers
|
||||
@@ -368,12 +368,12 @@ describe("fuzzyMatchModel", () => {
|
||||
// then search all specified providers
|
||||
it("should search all specified providers", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"google/gemini-3",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt", available, ["openai", "google"])
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available models with provider prefix
|
||||
@@ -381,11 +381,11 @@ describe("fuzzyMatchModel", () => {
|
||||
// then only match models with correct provider prefix
|
||||
it("should only match models with correct provider prefix", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/gpt-something",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt", available, ["openai"])
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given empty available set
|
||||
@@ -513,7 +513,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then only returns models from that provider
|
||||
it("should filter models by connected providers", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
google: { models: { "gemini-3.1-pro": { id: "gemini-3.1-pro" } } },
|
||||
})
|
||||
@@ -524,7 +524,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
|
||||
expect(result.size).toBe(1)
|
||||
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
|
||||
expect(result.has("openai/gpt-5.2")).toBe(false)
|
||||
expect(result.has("openai/gpt-5.4")).toBe(false)
|
||||
expect(result.has("google/gemini-3.1-pro")).toBe(false)
|
||||
})
|
||||
|
||||
@@ -533,7 +533,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then returns models from all specified providers
|
||||
it("should filter models by multiple connected providers", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
google: { models: { "gemini-3.1-pro": { id: "gemini-3.1-pro" } } },
|
||||
})
|
||||
@@ -545,7 +545,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
expect(result.size).toBe(2)
|
||||
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
|
||||
expect(result.has("google/gemini-3.1-pro")).toBe(true)
|
||||
expect(result.has("openai/gpt-5.2")).toBe(false)
|
||||
expect(result.has("openai/gpt-5.4")).toBe(false)
|
||||
})
|
||||
|
||||
// given cache with models
|
||||
@@ -553,7 +553,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then returns empty set
|
||||
it("should return empty set when connectedProviders is empty", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
})
|
||||
|
||||
@@ -569,7 +569,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then returns empty set (triggers fallback in resolver)
|
||||
it("should return empty set when connectedProviders not specified", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
})
|
||||
|
||||
@@ -583,7 +583,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then returns empty set for that provider
|
||||
it("should handle provider not in cache gracefully", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
})
|
||||
|
||||
const result = await fetchAvailableModels(undefined, {
|
||||
@@ -598,7 +598,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then returns models only from matching providers
|
||||
it("should return models from providers that exist in both cache and connected list", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
})
|
||||
|
||||
@@ -615,7 +615,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then does NOT use cache (dynamic per-session)
|
||||
it("should not cache filtered results", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
})
|
||||
|
||||
@@ -630,7 +630,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
connectedProviders: ["openai"]
|
||||
})
|
||||
expect(result2.size).toBe(1)
|
||||
expect(result2.has("openai/gpt-5.2")).toBe(true)
|
||||
expect(result2.has("openai/gpt-5.4")).toBe(true)
|
||||
})
|
||||
|
||||
// given connectedProviders unknown
|
||||
@@ -638,7 +638,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then always returns empty set (triggers fallback)
|
||||
it("should return empty set when connectedProviders unknown", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
})
|
||||
|
||||
const result1 = await fetchAvailableModels()
|
||||
@@ -696,7 +696,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
|
||||
connected: ["opencode", "anthropic"]
|
||||
})
|
||||
writeModelsCache({
|
||||
opencode: { models: { "big-pickle": {}, "gpt-5-nano": {}, "gpt-5.2": {} } },
|
||||
opencode: { models: { "big-pickle": {}, "gpt-5-nano": {}, "gpt-5.4": {} } },
|
||||
anthropic: { models: { "claude-opus-4-6": {}, "claude-sonnet-4-6": {} } }
|
||||
})
|
||||
|
||||
@@ -708,7 +708,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
|
||||
expect(result.has("opencode/big-pickle")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5-nano")).toBe(true)
|
||||
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5.2")).toBe(false)
|
||||
expect(result.has("opencode/gpt-5.4")).toBe(false)
|
||||
expect(result.has("anthropic/claude-sonnet-4-6")).toBe(false)
|
||||
})
|
||||
|
||||
@@ -738,7 +738,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
|
||||
// then falls back to models.json (no whitelist filtering)
|
||||
it("should fallback to models.json when provider-models cache not found", async () => {
|
||||
writeModelsCache({
|
||||
opencode: { models: { "big-pickle": {}, "gpt-5-nano": {}, "gpt-5.2": {} } },
|
||||
opencode: { models: { "big-pickle": {}, "gpt-5-nano": {}, "gpt-5.4": {} } },
|
||||
})
|
||||
|
||||
const result = await fetchAvailableModels(undefined, {
|
||||
@@ -748,7 +748,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
|
||||
expect(result.size).toBe(3)
|
||||
expect(result.has("opencode/big-pickle")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5-nano")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5.2")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5.4")).toBe(true)
|
||||
})
|
||||
|
||||
// given provider-models cache with whitelist
|
||||
@@ -907,7 +907,7 @@ describe("fallback model availability", () => {
|
||||
|
||||
it("returns null for completely unknown model", () => {
|
||||
// given
|
||||
const available = new Set(["openai/gpt-5.2", "anthropic/claude-opus-4-6"])
|
||||
const available = new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-6"])
|
||||
|
||||
// when
|
||||
const result = fuzzyMatchModel("non-existent-model-family", available)
|
||||
@@ -918,7 +918,7 @@ describe("fallback model availability", () => {
|
||||
|
||||
it("returns true when models do not match but provider is connected", () => {
|
||||
// given
|
||||
const fallbackChain = [{ providers: ["openai"], model: "gpt-5.2" }]
|
||||
const fallbackChain = [{ providers: ["openai"], model: "gpt-5.4" }]
|
||||
const availableModels = new Set(["anthropic/claude-opus-4-6"])
|
||||
writeConnectedProvidersCache(["openai"])
|
||||
|
||||
@@ -932,25 +932,25 @@ describe("fallback model availability", () => {
|
||||
it("returns first resolved fallback model from chain", () => {
|
||||
// given
|
||||
const fallbackChain = [
|
||||
{ providers: ["openai"], model: "gpt-5.2" },
|
||||
{ providers: ["openai"], model: "gpt-5.4" },
|
||||
{ providers: ["anthropic"], model: "claude-opus-4-6" },
|
||||
]
|
||||
const availableModels = new Set([
|
||||
"anthropic/claude-opus-4-6",
|
||||
"openai/gpt-5.2-preview",
|
||||
"openai/gpt-5.4-preview",
|
||||
])
|
||||
|
||||
// when
|
||||
const result = resolveFirstAvailableFallback(fallbackChain, availableModels)
|
||||
|
||||
// then
|
||||
expect(result).toEqual({ provider: "openai", model: "openai/gpt-5.2-preview" })
|
||||
expect(result).toEqual({ provider: "openai", model: "openai/gpt-5.4-preview" })
|
||||
})
|
||||
|
||||
it("returns null when no fallback model resolves", () => {
|
||||
// given
|
||||
const fallbackChain = [
|
||||
{ providers: ["openai"], model: "gpt-5.2" },
|
||||
{ providers: ["openai"], model: "gpt-5.4" },
|
||||
{ providers: ["anthropic"], model: "claude-opus-4-6" },
|
||||
]
|
||||
const availableModels = new Set(["google/gemini-3.1-pro"])
|
||||
|
||||
@@ -8,7 +8,7 @@ import { normalizeSDKResponse } from "./normalize-sdk-response"
|
||||
/**
|
||||
* Fuzzy match a target model name against available models
|
||||
*
|
||||
* @param target - The model name or substring to search for (e.g., "gpt-5.2", "claude-opus")
|
||||
* @param target - The model name or substring to search for (e.g., "gpt-5.4", "claude-opus")
|
||||
* @param available - Set of available model names in format "provider/model-name"
|
||||
* @param providers - Optional array of provider names to filter by (e.g., ["openai", "anthropic"])
|
||||
* @returns The matched model name or null if no match found
|
||||
@@ -21,8 +21,8 @@ import { normalizeSDKResponse } from "./normalize-sdk-response"
|
||||
* If providers array is given, only models starting with "provider/" are considered.
|
||||
*
|
||||
* @example
|
||||
* const available = new Set(["openai/gpt-5.2", "openai/gpt-5.3-codex", "anthropic/claude-opus-4-6"])
|
||||
* fuzzyMatchModel("gpt-5.2", available) // → "openai/gpt-5.2"
|
||||
* const available = new Set(["openai/gpt-5.4", "openai/gpt-5.3-codex", "anthropic/claude-opus-4-6"])
|
||||
* fuzzyMatchModel("gpt-5.4", available) // → "openai/gpt-5.4"
|
||||
* fuzzyMatchModel("claude", available, ["openai"]) // → null (provider filter excludes anthropic)
|
||||
*/
|
||||
function normalizeModelName(name: string): string {
|
||||
@@ -82,7 +82,7 @@ export function fuzzyMatchModel(
|
||||
|
||||
// Priority 2: Exact model ID match (part after provider/)
|
||||
// This ensures "big-pickle" matches "zai-coding-plan/big-pickle" over "zai-coding-plan/glm-5"
|
||||
// Use filter + shortest to handle multi-provider cases (e.g., openai/gpt-5.2 + opencode/gpt-5.2)
|
||||
// Use filter + shortest to handle multi-provider cases (e.g., openai/gpt-5.4 + opencode/gpt-5.4)
|
||||
const exactModelIdMatches = matches.filter((model) => {
|
||||
const modelId = model.split("/").slice(1).join("/")
|
||||
return normalizeModelName(modelId) === targetNormalized
|
||||
|
||||
@@ -7,19 +7,19 @@ import {
|
||||
} from "./model-requirements"
|
||||
|
||||
describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
test("oracle has valid fallbackChain with gpt-5.2 as primary", () => {
|
||||
test("oracle has valid fallbackChain with gpt-5.4 as primary", () => {
|
||||
// given - oracle agent requirement
|
||||
const oracle = AGENT_MODEL_REQUIREMENTS["oracle"]
|
||||
|
||||
// when - accessing oracle requirement
|
||||
// then - fallbackChain exists with gpt-5.2 as first entry
|
||||
// then - fallbackChain exists with gpt-5.4 as first entry
|
||||
expect(oracle).toBeDefined()
|
||||
expect(oracle.fallbackChain).toBeArray()
|
||||
expect(oracle.fallbackChain.length).toBeGreaterThan(0)
|
||||
|
||||
const primary = oracle.fallbackChain[0]
|
||||
expect(primary.providers).toContain("openai")
|
||||
expect(primary.model).toBe("gpt-5.2")
|
||||
expect(primary.model).toBe("gpt-5.4")
|
||||
expect(primary.variant).toBe("high")
|
||||
})
|
||||
|
||||
@@ -435,7 +435,7 @@ describe("ModelRequirement type", () => {
|
||||
const requirement: ModelRequirement = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-6", variant: "max" },
|
||||
{ providers: ["openai", "github-copilot"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["openai", "github-copilot"], model: "gpt-5.4", variant: "high" },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ describe("ModelRequirement type", () => {
|
||||
expect(requirement.fallbackChain).toBeArray()
|
||||
expect(requirement.fallbackChain).toHaveLength(2)
|
||||
expect(requirement.fallbackChain[0].model).toBe("claude-opus-4-6")
|
||||
expect(requirement.fallbackChain[1].model).toBe("gpt-5.2")
|
||||
expect(requirement.fallbackChain[1].model).toBe("gpt-5.4")
|
||||
})
|
||||
|
||||
test("ModelRequirement variant is optional", () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
model: "gpt-5.3-codex",
|
||||
variant: "medium",
|
||||
},
|
||||
{ providers: ["github-copilot"], model: "gpt-5.2", variant: "medium" },
|
||||
{ providers: ["github-copilot"], model: "gpt-5.4", variant: "medium" },
|
||||
],
|
||||
requiresProvider: ["openai", "github-copilot", "venice", "opencode"],
|
||||
},
|
||||
@@ -40,7 +40,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
fallbackChain: [
|
||||
{
|
||||
providers: ["openai", "github-copilot", "opencode"],
|
||||
model: "gpt-5.2",
|
||||
model: "gpt-5.4",
|
||||
variant: "high",
|
||||
},
|
||||
{
|
||||
@@ -119,7 +119,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
},
|
||||
{
|
||||
providers: ["openai", "github-copilot", "opencode"],
|
||||
model: "gpt-5.2",
|
||||
model: "gpt-5.4",
|
||||
variant: "high",
|
||||
},
|
||||
{
|
||||
@@ -226,7 +226,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
model: "claude-opus-4-6",
|
||||
variant: "max",
|
||||
},
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4" },
|
||||
],
|
||||
requiresModel: "gemini-3.1-pro",
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("resolveModel", () => {
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: "anthropic/claude-opus-4-6",
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
inheritedModel: "openai/gpt-5.4",
|
||||
systemDefault: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("resolveModel", () => {
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: undefined,
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
inheritedModel: "openai/gpt-5.4",
|
||||
systemDefault: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("resolveModel", () => {
|
||||
const result = resolveModel(input)
|
||||
|
||||
// then
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
test("returns systemDefault when both userModel and inheritedModel are undefined", () => {
|
||||
@@ -56,7 +56,7 @@ describe("resolveModel", () => {
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: "",
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
inheritedModel: "openai/gpt-5.4",
|
||||
systemDefault: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("resolveModel", () => {
|
||||
const result = resolveModel(input)
|
||||
|
||||
// then
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
test("treats whitespace-only string as unset, uses fallback", () => {
|
||||
@@ -88,7 +88,7 @@ describe("resolveModel", () => {
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: "anthropic/claude-opus-4-6",
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
inheritedModel: "openai/gpt-5.4",
|
||||
systemDefault: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -292,9 +292,9 @@ describe("resolveModelWithFallback", () => {
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "anthropic", "google"], model: "gpt-5.2" },
|
||||
{ providers: ["openai", "anthropic", "google"], model: "gpt-5.4" },
|
||||
],
|
||||
availableModels: new Set(["openai/gpt-5.2", "anthropic/claude-opus-4-6", "google/gemini-3.1-pro"]),
|
||||
availableModels: new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-6", "google/gemini-3.1-pro"]),
|
||||
systemDefaultModel: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("resolveModelWithFallback", () => {
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// then
|
||||
expect(result!.model).toBe("openai/gpt-5.2")
|
||||
expect(result!.model).toBe("openai/gpt-5.4")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
@@ -476,7 +476,7 @@ describe("resolveModelWithFallback", () => {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic"], model: "nonexistent-model" },
|
||||
],
|
||||
availableModels: new Set(["openai/gpt-5.2", "anthropic/claude-opus-4-6"]),
|
||||
availableModels: new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-6"]),
|
||||
systemDefaultModel: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -592,7 +592,7 @@ describe("resolveModelWithFallback", () => {
|
||||
test("returns system default when fallbackChain is not provided", () => {
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
availableModels: new Set(["openai/gpt-5.2"]),
|
||||
availableModels: new Set(["openai/gpt-5.4"]),
|
||||
systemDefaultModel: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -613,7 +613,7 @@ describe("resolveModelWithFallback", () => {
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4", variant: "high" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||
],
|
||||
availableModels,
|
||||
@@ -632,7 +632,7 @@ describe("resolveModelWithFallback", () => {
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "anthropic"], model: "gpt-5.2" },
|
||||
{ providers: ["openai", "anthropic"], model: "gpt-5.4" },
|
||||
{ providers: ["google"], model: "gemini-3.1-pro" },
|
||||
],
|
||||
availableModels,
|
||||
@@ -647,14 +647,14 @@ describe("resolveModelWithFallback", () => {
|
||||
test("returns first matching entry even if later entries have better matches", () => {
|
||||
// given
|
||||
const availableModels = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
])
|
||||
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai"], model: "gpt-5.2" },
|
||||
{ providers: ["openai"], model: "gpt-5.4" },
|
||||
{ providers: ["anthropic"], model: "claude-opus-4-6" },
|
||||
],
|
||||
availableModels,
|
||||
@@ -662,7 +662,7 @@ describe("resolveModelWithFallback", () => {
|
||||
})
|
||||
|
||||
// then
|
||||
expect(result!.model).toBe("openai/gpt-5.2")
|
||||
expect(result!.model).toBe("openai/gpt-5.4")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
@@ -673,7 +673,7 @@ describe("resolveModelWithFallback", () => {
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai"], model: "gpt-5.2" },
|
||||
{ providers: ["openai"], model: "gpt-5.4" },
|
||||
{ providers: ["anthropic"], model: "claude-opus-4-6" },
|
||||
{ providers: ["google"], model: "gemini-3.1-pro" },
|
||||
],
|
||||
@@ -884,7 +884,7 @@ describe("resolveModelWithFallback", () => {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic"], model: "nonexistent-model" },
|
||||
],
|
||||
availableModels: new Set(["openai/gpt-5.2"]),
|
||||
availableModels: new Set(["openai/gpt-5.4"]),
|
||||
systemDefaultModel: undefined,
|
||||
}
|
||||
|
||||
@@ -898,7 +898,7 @@ describe("resolveModelWithFallback", () => {
|
||||
test("returns undefined when no fallbackChain and systemDefaultModel is undefined", () => {
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
availableModels: new Set(["openai/gpt-5.2"]),
|
||||
availableModels: new Set(["openai/gpt-5.4"]),
|
||||
systemDefaultModel: undefined,
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("parseModelSuggestion", () => {
|
||||
data: {
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5",
|
||||
suggestions: ["gpt-5.2"],
|
||||
suggestions: ["gpt-5.4"],
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -82,7 +82,7 @@ describe("parseModelSuggestion", () => {
|
||||
expect(result).toEqual({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5",
|
||||
suggestion: "gpt-5.2",
|
||||
suggestion: "gpt-5.4",
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
data: [
|
||||
{ info: { id: "msg_001", role: "user", time: { created: 1000 }, agent: "oracle" } },
|
||||
{
|
||||
info: { id: "msg_002", role: "assistant", time: { created: 2000 }, finish: "end_turn", agent: "oracle", providerID: "openai", modelID: "gpt-5.2" },
|
||||
info: { id: "msg_002", role: "assistant", time: { created: 2000 }, finish: "end_turn", agent: "oracle", providerID: "openai", modelID: "gpt-5.4" },
|
||||
parts: [{ type: "text", text: "Response" }],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -19,7 +19,7 @@ const TEST_AVAILABLE_MODELS = new Set([
|
||||
"anthropic/claude-haiku-4-5",
|
||||
"google/gemini-3.1-pro",
|
||||
"google/gemini-3-flash",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.3-codex",
|
||||
])
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("sisyphus-task", () => {
|
||||
models: {
|
||||
anthropic: ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"],
|
||||
google: ["gemini-3.1-pro", "gemini-3-flash"],
|
||||
openai: ["gpt-5.2", "gpt-5.3-codex"],
|
||||
openai: ["gpt-5.4", "gpt-5.3-codex"],
|
||||
},
|
||||
connected: ["anthropic", "google", "openai"],
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
@@ -824,7 +824,7 @@ describe("sisyphus-task", () => {
|
||||
const categoryName = "my-custom"
|
||||
const userCategories = {
|
||||
"my-custom": {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
temperature: 0.5,
|
||||
prompt_append: "You are a custom agent",
|
||||
},
|
||||
@@ -835,7 +835,7 @@ describe("sisyphus-task", () => {
|
||||
|
||||
// then
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.config.model).toBe("openai/gpt-5.2")
|
||||
expect(result!.config.model).toBe("openai/gpt-5.4")
|
||||
expect(result!.config.temperature).toBe(0.5)
|
||||
expect(result!.promptAppend).toBe("You are a custom agent")
|
||||
})
|
||||
@@ -948,7 +948,7 @@ describe("sisyphus-task", () => {
|
||||
manager: mockManager,
|
||||
client: mockClient,
|
||||
userCategories: {
|
||||
ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" },
|
||||
ultrabrain: { model: "openai/gpt-5.4", variant: "xhigh" },
|
||||
},
|
||||
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
|
||||
availableModelsOverride: createTestAvailableModels(),
|
||||
@@ -976,7 +976,7 @@ describe("sisyphus-task", () => {
|
||||
// then
|
||||
expect(launchInput.model).toEqual({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.2",
|
||||
modelID: "gpt-5.4",
|
||||
variant: "xhigh",
|
||||
})
|
||||
})
|
||||
@@ -1040,9 +1040,9 @@ describe("sisyphus-task", () => {
|
||||
|
||||
// then - variant MUST be "max" from DEFAULT_CATEGORIES
|
||||
expect(launchInput.model).toEqual({
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-opus-4-6",
|
||||
variant: "max",
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.4",
|
||||
variant: "high",
|
||||
})
|
||||
}, { timeout: 20000 })
|
||||
|
||||
@@ -1101,10 +1101,10 @@ describe("sisyphus-task", () => {
|
||||
|
||||
// then - variant MUST be "max" from DEFAULT_CATEGORIES (passed as separate field)
|
||||
expect(promptBody.model).toEqual({
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-opus-4-6",
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.4",
|
||||
})
|
||||
expect(promptBody.variant).toBe("max")
|
||||
expect(promptBody.variant).toBe("high")
|
||||
}, { timeout: 20000 })
|
||||
})
|
||||
|
||||
@@ -1976,7 +1976,7 @@ describe("sisyphus-task", () => {
|
||||
},
|
||||
}
|
||||
|
||||
// Use ultrabrain which uses gpt-5.2 (non-gemini)
|
||||
// Use ultrabrain which uses gpt-5.4 (non-gemini)
|
||||
const tool = createDelegateTask({
|
||||
manager: mockManager,
|
||||
client: mockClient,
|
||||
@@ -2185,7 +2185,7 @@ describe("sisyphus-task", () => {
|
||||
client: mockClient,
|
||||
userCategories: {
|
||||
"my-unstable-cat": {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
is_unstable_agent: true,
|
||||
},
|
||||
},
|
||||
@@ -2568,7 +2568,7 @@ describe("sisyphus-task", () => {
|
||||
const tool = createDelegateTask({
|
||||
manager: mockManager,
|
||||
client: mockClient,
|
||||
sisyphusJuniorModel: "openai/gpt-5.2",
|
||||
sisyphusJuniorModel: "openai/gpt-5.4",
|
||||
userCategories: {
|
||||
"my-custom": { temperature: 0.5 },
|
||||
},
|
||||
@@ -2595,7 +2595,7 @@ describe("sisyphus-task", () => {
|
||||
|
||||
// then - sisyphus-junior override model should be used as fallback
|
||||
expect(launchInput.model.providerID).toBe("openai")
|
||||
expect(launchInput.model.modelID).toBe("gpt-5.2")
|
||||
expect(launchInput.model.modelID).toBe("gpt-5.4")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3406,7 +3406,7 @@ describe("sisyphus-task", () => {
|
||||
app: {
|
||||
agents: async () => ({
|
||||
data: [
|
||||
{ name: "oracle", mode: "subagent", model: { providerID: "openai", modelID: "gpt-5.2" } },
|
||||
{ name: "oracle", mode: "subagent", model: { providerID: "openai", modelID: "gpt-5.4" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
@@ -3473,7 +3473,7 @@ describe("sisyphus-task", () => {
|
||||
app: {
|
||||
agents: async () => ({
|
||||
data: [
|
||||
{ name: "oracle", mode: "subagent", model: { providerID: "openai", modelID: "gpt-5.2" } },
|
||||
{ name: "oracle", mode: "subagent", model: { providerID: "openai", modelID: "gpt-5.4" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
@@ -3582,11 +3582,11 @@ describe("sisyphus-task", () => {
|
||||
)
|
||||
|
||||
// then - should resolve via AGENT_MODEL_REQUIREMENTS fallback chain for oracle
|
||||
// oracle fallback chain: gpt-5.2 (openai) > gemini-3.1-pro (google) > claude-opus-4-6 (anthropic)
|
||||
// Since openai is in connectedProviders, should resolve to openai/gpt-5.2
|
||||
// oracle fallback chain: gpt-5.4 (openai) > gemini-3.1-pro (google) > claude-opus-4-6 (anthropic)
|
||||
// Since openai is in connectedProviders, should resolve to openai/gpt-5.4
|
||||
expect(promptBody.model).toBeDefined()
|
||||
expect(promptBody.model.providerID).toBe("openai")
|
||||
expect(promptBody.model.modelID).toContain("gpt-5.2")
|
||||
expect(promptBody.model.modelID).toContain("gpt-5.4")
|
||||
}, { timeout: 20000 })
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user