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:
YeonGyu-Kim
2026-03-07 02:33:32 +09:00
parent 8a1352fc9b
commit fade6740ae
62 changed files with 478 additions and 479 deletions

View File

@@ -65,7 +65,7 @@ These agents have Claude-optimized prompts — long, detailed, mechanics-driven.
| Agent | Role | Fallback Chain | Notes |
| ------------ | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **Sisyphus** | Main orchestrator | Claude Opus → GLM 5 → Big Pickle | Claude-family first. GPT-5.4 has dedicated support, but Claude/Kimi/GLM remain the preferred fit. |
| **Metis** | Plan gap analyzer | Claude Opus → GPT-5.2 → Gemini 3.1 Pro | Claude preferred, GPT acceptable fallback. |
| **Metis** | Plan gap analyzer | Claude Opus → GPT-5.4 → Gemini 3.1 Pro | Claude preferred, GPT acceptable fallback. |
### Dual-Prompt Agents → Claude preferred, GPT supported
@@ -83,7 +83,7 @@ These agents are built for GPT's principle-driven style. Their prompts assume au
| Agent | Role | Fallback Chain | Notes |
| -------------- | ----------------------- | -------------------------------------- | ------------------------------------------------ |
| **Hephaestus** | Autonomous deep worker | GPT-5.3 Codex only | No fallback. Requires GPT access. The craftsman. |
| **Oracle** | Architecture consultant | GPT-5.2 → Gemini 3.1 Pro → Claude Opus | Read-only high-IQ consultation. |
| **Oracle** | Architecture consultant | GPT-5.4 → Gemini 3.1 Pro → Claude Opus | Read-only high-IQ consultation. |
| **Momus** | Ruthless reviewer | GPT-5.4 → Claude Opus → Gemini 3.1 Pro | Verification and plan review. |
### Utility Runners → Speed over Intelligence
@@ -119,7 +119,7 @@ Principle-driven, explicit reasoning, deep technical capability. Best for agents
| Model | Strengths |
| ----------------- | ----------------------------------------------------------------------------------------------- |
| **GPT-5.3 Codex** | Deep coding powerhouse. Autonomous exploration. Required for Hephaestus. |
| **GPT-5.2** | High intelligence, strategic reasoning. Default for Oracle. |
| **GPT-5.4** | High intelligence, strategic reasoning. Default for Oracle. |
| **GPT-5.4** | Strong principle-driven reasoning. Default for Momus and a key fallback for Prometheus / Atlas. |
| **GPT-5-Nano** | Ultra-cheap, fast. Good for simple utility tasks. |
@@ -149,7 +149,7 @@ When agents delegate work, they don't pick a model name — they pick a **catego
| `visual-engineering` | Frontend, UI, CSS, design | Gemini 3.1 Pro → GLM 5 → Claude Opus |
| `ultrabrain` | Maximum reasoning needed | GPT-5.3 Codex → Gemini 3.1 Pro → Claude Opus |
| `deep` | Deep coding, complex logic | GPT-5.3 Codex → Claude Opus → Gemini 3.1 Pro |
| `artistry` | Creative, novel approaches | Gemini 3.1 Pro → Claude Opus → GPT-5.2 |
| `artistry` | Creative, novel approaches | Gemini 3.1 Pro → Claude Opus → GPT-5.4 |
| `quick` | Simple, fast tasks | Claude Haiku → Gemini Flash → GPT-5-Nano |
| `unspecified-high` | General complex work | GPT-5.4 → Claude Opus → GLM 5 → K2P5 |
| `unspecified-low` | General standard work | Claude Sonnet → GPT-5.3 Codex → Gemini Flash |
@@ -179,7 +179,7 @@ See the [Orchestration System Guide](./orchestration.md) for how agents dispatch
"explore": { "model": "github-copilot/grok-code-fast-1" },
// Architecture consultation: GPT or Claude Opus
"oracle": { "model": "openai/gpt-5.2", "variant": "high" },
"oracle": { "model": "openai/gpt-5.4", "variant": "high" },
// Prometheus inherits sisyphus model; just add prompt guidance
"prometheus": {

View File

@@ -49,7 +49,7 @@ Ask the user these questions to determine CLI options:
- If **no**`--claude=no`
2. **Do you have an OpenAI/ChatGPT Plus Subscription?**
- If **yes**`--openai=yes` (GPT-5.2 for Oracle agent)
- If **yes**`--openai=yes` (GPT-5.4 for Oracle agent)
- If **no**`--openai=no` (default)
3. **Will you integrate Gemini models?**
@@ -200,7 +200,7 @@ When GitHub Copilot is the best available provider, oh-my-opencode uses these mo
| Agent | Model |
| ------------- | --------------------------------- |
| **Sisyphus** | `github-copilot/claude-opus-4-6` |
| **Oracle** | `github-copilot/gpt-5.2` |
| **Oracle** | `github-copilot/gpt-5.4` |
| **Explore** | `github-copilot/grok-code-fast-1` |
| **Librarian** | `github-copilot/gemini-3-flash` |
@@ -228,7 +228,7 @@ When OpenCode Zen is the best available provider (no native or Copilot), these m
| Agent | Model |
| ------------- | ---------------------------------------------------- |
| **Sisyphus** | `opencode/claude-opus-4-6` |
| **Oracle** | `opencode/gpt-5.2` |
| **Oracle** | `opencode/gpt-5.4` |
| **Explore** | `opencode/gpt-5-nano` |
| **Librarian** | `opencode/minimax-m2.5-free` / `opencode/big-pickle` |
@@ -280,7 +280,7 @@ Not all models behave the same way. Understanding which models are "similar" hel
| Model | Provider(s) | Notes |
| ----------------- | -------------------------------- | ------------------------------------------------- |
| **GPT-5.3-codex** | openai, github-copilot, opencode | Deep coding powerhouse. Required for Hephaestus. |
| **GPT-5.2** | openai, github-copilot, opencode | High intelligence. Default for Oracle. |
| **GPT-5.4** | openai, github-copilot, opencode | High intelligence. Default for Oracle. |
| **GPT-5-Nano** | opencode | Ultra-cheap, fast. Good for simple utility tasks. |
**Different-Behavior Models**:
@@ -310,7 +310,7 @@ Based on your subscriptions, here's how the agents were configured:
| Agent | Role | Default Chain | What It Does |
| ------------ | ---------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **Sisyphus** | Main ultraworker | Opus (max) → Kimi K2.5 → GLM 5 → Big Pickle | Primary coding agent. Orchestrates everything. **Never use GPT — no GPT prompt exists.** |
| **Metis** | Plan review | Opus (max) → Kimi K2.5 → GPT-5.2 → Gemini 3 Pro | Reviews Prometheus plans for gaps. |
| **Metis** | Plan review | Opus (max) → Kimi K2.5 → GPT-5.4 → Gemini 3 Pro | Reviews Prometheus plans for gaps. |
**Dual-Prompt Agents** (auto-switch between Claude and GPT prompts):
@@ -320,16 +320,16 @@ Priority: **Claude > GPT > Claude-like models**
| Agent | Role | Default Chain | GPT Prompt? |
| -------------- | ----------------- | ---------------------------------------------------------- | ---------------------------------------------------------------- |
| **Prometheus** | Strategic planner | Opus (max) → **GPT-5.2 (high)** → Kimi K2.5 → Gemini 3 Pro | Yes — XML-tagged, principle-driven (~300 lines vs ~1,100 Claude) |
| **Atlas** | Todo orchestrator | **Kimi K2.5** → Sonnet → GPT-5.2 | Yes — GPT-optimized todo management |
| **Prometheus** | Strategic planner | Opus (max) → **GPT-5.4 (high)** → Kimi K2.5 → Gemini 3 Pro | Yes — XML-tagged, principle-driven (~300 lines vs ~1,100 Claude) |
| **Atlas** | Todo orchestrator | **Kimi K2.5** → Sonnet → GPT-5.4 | Yes — GPT-optimized todo management |
**GPT-Native Agents** (built for GPT, don't override to Claude):
| Agent | Role | Default Chain | Notes |
| -------------- | ---------------------- | -------------------------------------- | ------------------------------------------------------ |
| **Hephaestus** | Deep autonomous worker | GPT-5.3-codex (medium) only | "Codex on steroids." No fallback. Requires GPT access. |
| **Oracle** | Architecture/debugging | GPT-5.2 (high) → Gemini 3 Pro → Opus | High-IQ strategic backup. GPT preferred. |
| **Momus** | High-accuracy reviewer | GPT-5.2 (medium) → Opus → Gemini 3 Pro | Verification agent. GPT preferred. |
| **Oracle** | Architecture/debugging | GPT-5.4 (high) → Gemini 3 Pro → Opus | High-IQ strategic backup. GPT preferred. |
| **Momus** | High-accuracy reviewer | GPT-5.4 (medium) → Opus → Gemini 3 Pro | Verification agent. GPT preferred. |
**Utility Agents** (speed over intelligence):
@@ -339,7 +339,7 @@ These agents do search, grep, and retrieval. They intentionally use fast, cheap
| --------------------- | ------------------ | ---------------------------------------------------------------------- | -------------------------------------------------------------- |
| **Explore** | Fast codebase grep | MiniMax M2.5 Free → Grok Code Fast → MiniMax M2.5 → Haiku → GPT-5-Nano | Speed is everything. Grok is blazing fast for grep. |
| **Librarian** | Docs/code search | MiniMax M2.5 Free → Gemini Flash → Big Pickle | Entirely free-tier. Doc retrieval doesn't need deep reasoning. |
| **Multimodal Looker** | Vision/screenshots | Kimi K2.5 → Kimi Free → Gemini Flash → GPT-5.2 → GLM-4.6v | Kimi excels at multimodal understanding. |
| **Multimodal Looker** | Vision/screenshots | Kimi K2.5 → Kimi Free → Gemini Flash → GPT-5.4 → GLM-4.6v | Kimi excels at multimodal understanding. |
#### Why Different Models Need Different Prompts
@@ -388,8 +388,8 @@ GPT (5.3-codex, 5.2) > Claude Opus (decent fallback) > Gemini (acceptable)
**Safe** (same family):
- Sisyphus: Opus → Sonnet, Kimi K2.5, GLM 5
- Prometheus: Opus → GPT-5.2 (auto-switches prompt)
- Atlas: Kimi K2.5 → Sonnet, GPT-5.2 (auto-switches)
- Prometheus: Opus → GPT-5.4 (auto-switches prompt)
- Atlas: Kimi K2.5 → Sonnet, GPT-5.4 (auto-switches)
**Dangerous** (no prompt support):

View File

@@ -45,7 +45,7 @@ flowchart TB
subgraph Workers["Worker Layer (Specialized Agents)"]
Junior[" Sisyphus-Junior<br/>(Task Executor)<br/>Claude Sonnet 4.6"]
Oracle[" Oracle<br/>(Architecture)<br/>GPT-5.2"]
Oracle[" Oracle<br/>(Architecture)<br/>GPT-5.4"]
Explore[" Explore<br/>(Codebase Grep)<br/>Grok Code"]
Librarian[" Librarian<br/>(Docs/OSS)<br/>Gemini 3 Flash"]
Frontend[" Frontend<br/>(UI/UX)<br/>Gemini 3.1 Pro"]

View File

@@ -182,7 +182,7 @@ You can override specific agents or categories in your config:
"explore": { "model": "github-copilot/grok-code-fast-1" },
// Architecture consultation: GPT or Claude Opus
"oracle": { "model": "openai/gpt-5.2", "variant": "high" },
"oracle": { "model": "openai/gpt-5.4", "variant": "high" },
},
"categories": {
@@ -215,7 +215,7 @@ You can override specific agents or categories in your config:
**GPT models** (explicit reasoning, principle-driven):
- GPT-5.3-codex — deep coding powerhouse, required for Hephaestus
- GPT-5.2 — high intelligence, default for Oracle
- GPT-5.4 — high intelligence, default for Oracle
- GPT-5-Nano — ultra-cheap, fast utility tasks
**Different-behavior models**:

View File

@@ -83,8 +83,8 @@ Here's a practical starting configuration:
"librarian": { "model": "google/gemini-3-flash" },
"explore": { "model": "github-copilot/grok-code-fast-1" },
// Architecture consultation: GPT-5.2 or Claude Opus
"oracle": { "model": "openai/gpt-5.2", "variant": "high" },
// Architecture consultation: GPT-5.4 or Claude Opus
"oracle": { "model": "openai/gpt-5.4", "variant": "high" },
// Prometheus inherits sisyphus model; just add prompt guidance
"prometheus": {
@@ -268,13 +268,13 @@ Disable categories: `{ "disabled_categories": ["ultrabrain"] }`
| Agent | Default Model | Provider Priority |
| --------------------- | ------------------- | ---------------------------------------------------------------------------- |
| **Sisyphus** | `claude-opus-4-6` | `claude-opus-4-6``glm-5``big-pickle` |
| **Hephaestus** | `gpt-5.3-codex` | `gpt-5.3-codex``gpt-5.2` (GitHub Copilot fallback) |
| **oracle** | `gpt-5.2` | `gpt-5.2``gemini-3.1-pro``claude-opus-4-6` |
| **Hephaestus** | `gpt-5.3-codex` | `gpt-5.3-codex``gpt-5.4` (GitHub Copilot fallback) |
| **oracle** | `gpt-5.4` | `gpt-5.4``gemini-3.1-pro``claude-opus-4-6` |
| **librarian** | `gemini-3-flash` | `gemini-3-flash``minimax-m2.5-free``big-pickle` |
| **explore** | `grok-code-fast-1` | `grok-code-fast-1``minimax-m2.5-free``claude-haiku-4-5``gpt-5-nano` |
| **multimodal-looker** | `gpt-5.3-codex` | `gpt-5.3-codex``k2p5``gemini-3-flash``glm-4.6v``gpt-5-nano` |
| **Prometheus** | `claude-opus-4-6` | `claude-opus-4-6``gpt-5.4``gemini-3.1-pro` |
| **Metis** | `claude-opus-4-6` | `claude-opus-4-6``gpt-5.2``gemini-3.1-pro` |
| **Metis** | `claude-opus-4-6` | `claude-opus-4-6``gpt-5.4``gemini-3.1-pro` |
| **Momus** | `gpt-5.4` | `gpt-5.4``claude-opus-4-6``gemini-3.1-pro` |
| **Atlas** | `claude-sonnet-4-6` | `claude-sonnet-4-6``gpt-5.4` |
@@ -285,7 +285,7 @@ Disable categories: `{ "disabled_categories": ["ultrabrain"] }`
| **visual-engineering** | `gemini-3.1-pro` | `gemini-3.1-pro``glm-5``claude-opus-4-6` |
| **ultrabrain** | `gpt-5.3-codex` | `gpt-5.3-codex``gemini-3.1-pro``claude-opus-4-6` |
| **deep** | `gpt-5.3-codex` | `gpt-5.3-codex``claude-opus-4-6``gemini-3.1-pro` |
| **artistry** | `gemini-3.1-pro` | `gemini-3.1-pro``claude-opus-4-6``gpt-5.2` |
| **artistry** | `gemini-3.1-pro` | `gemini-3.1-pro``claude-opus-4-6``gpt-5.4` |
| **quick** | `claude-haiku-4-5` | `claude-haiku-4-5``gemini-3-flash``gpt-5-nano` |
| **unspecified-low** | `claude-sonnet-4-6` | `claude-sonnet-4-6``gpt-5.3-codex``gemini-3-flash` |
| **unspecified-high** | `gpt-5.4` | `gpt-5.4``claude-opus-4-6``glm-5``k2p5``kimi-k2.5` |

View File

@@ -9,8 +9,8 @@ Oh-My-OpenCode provides 11 specialized AI agents. Each has distinct expertise, o
| Agent | Model | Purpose |
| --------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Sisyphus** | `claude-opus-4-6` | The default orchestrator. Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Todo-driven workflow with extended thinking (32k budget). Fallback: `glm-5``big-pickle`. |
| **Hephaestus** | `gpt-5.3-codex` | The Legitimate Craftsman. Autonomous deep worker inspired by AmpCode's deep mode. Goal-oriented execution with thorough research before action. Explores codebase patterns, completes tasks end-to-end without premature stopping. Named after the Greek god of forge and craftsmanship. Fallback: `gpt-5.2` on GitHub Copilot. Requires a GPT-capable provider. |
| **Oracle** | `gpt-5.2` | Architecture decisions, code review, debugging. Read-only consultation with stellar logical reasoning and deep analysis. Inspired by AmpCode. Fallback: `gemini-3.1-pro``claude-opus-4-6`. |
| **Hephaestus** | `gpt-5.3-codex` | The Legitimate Craftsman. Autonomous deep worker inspired by AmpCode's deep mode. Goal-oriented execution with thorough research before action. Explores codebase patterns, completes tasks end-to-end without premature stopping. Named after the Greek god of forge and craftsmanship. Fallback: `gpt-5.4` on GitHub Copilot. Requires a GPT-capable provider. |
| **Oracle** | `gpt-5.4` | Architecture decisions, code review, debugging. Read-only consultation with stellar logical reasoning and deep analysis. Inspired by AmpCode. Fallback: `gemini-3.1-pro``claude-opus-4-6`. |
| **Librarian** | `gemini-3-flash` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Fallback: `minimax-m2.5-free``big-pickle`. |
| **Explore** | `grok-code-fast-1` | Fast codebase exploration and contextual grep. Fallback: `minimax-m2.5-free``claude-haiku-4-5``gpt-5-nano`. |
| **Multimodal-Looker** | `gpt-5.3-codex` | Visual content specialist. Analyzes PDFs, images, diagrams to extract information. Fallback: `k2p5``gemini-3-flash``glm-4.6v``gpt-5-nano`. |
@@ -20,7 +20,7 @@ Oh-My-OpenCode provides 11 specialized AI agents. Each has distinct expertise, o
| Agent | Model | Purpose |
| -------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Prometheus** | `claude-opus-4-6` | Strategic planner with interview mode. Creates detailed work plans through iterative questioning. Fallback: `gpt-5.4``gemini-3.1-pro`. |
| **Metis** | `claude-opus-4-6` | Plan consultant — pre-planning analysis. Identifies hidden intentions, ambiguities, and AI failure points. Fallback: `gpt-5.2``gemini-3.1-pro`. |
| **Metis** | `claude-opus-4-6` | Plan consultant — pre-planning analysis. Identifies hidden intentions, ambiguities, and AI failure points. Fallback: `gpt-5.4``gemini-3.1-pro`. |
| **Momus** | `gpt-5.4` | Plan reviewer — validates plans against clarity, verifiability, and completeness standards. Fallback: `claude-opus-4-6``gemini-3.1-pro`. |
### Orchestration Agents

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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) |

View File

@@ -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)
*/

View File

@@ -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)

View File

@@ -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);

View File

@@ -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,

View File

@@ -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)
*/

View File

@@ -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)
*/

View File

@@ -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)

View File

@@ -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);
});

View File

@@ -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,
},
}

View File

@@ -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": {

View File

@@ -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.)

View File

@@ -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")
})

View File

@@ -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")
})

View File

@@ -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"))

View File

@@ -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",

View File

@@ -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,
})

View File

@@ -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)
}
})

View File

@@ -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",
}

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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

View File

@@ -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",
]),
})

View File

@@ -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",
})
})

View File

@@ -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)
})
})
})

View File

@@ -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")
}

View File

@@ -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" })
})
// ============================================================

View File

@@ -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();
});
});

View File

@@ -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)
})

View File

@@ -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")

View File

@@ -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" },
}

View File

@@ -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)

View File

@@ -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"],
})
})

View File

@@ -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([])
})

View File

@@ -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

View File

@@ -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",
])
})

View File

@@ -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",

View File

@@ -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"])

View File

@@ -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

View File

@@ -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", () => {

View File

@@ -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",
},

View File

@@ -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,
}

View File

@@ -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",
})
})

View File

@@ -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" }],
},
],

View File

@@ -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 })
})