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:
@@ -45,7 +45,7 @@ describe("Agent Config Integration", () => {
|
||||
// given - config with lowercase keys
|
||||
const config = {
|
||||
sisyphus: { model: "anthropic/claude-opus-4-6" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
librarian: { model: "opencode/big-pickle" },
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ describe("Agent Config Integration", () => {
|
||||
// given - config with mixed old and new format
|
||||
const mixedConfig = {
|
||||
Sisyphus: { model: "anthropic/claude-opus-4-6" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
"Prometheus (Planner)": { model: "anthropic/claude-opus-4-6" },
|
||||
librarian: { model: "opencode/big-pickle" },
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("resolveAgentVariant", () => {
|
||||
sisyphus: { category: "ultrabrain" },
|
||||
},
|
||||
categories: {
|
||||
ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" },
|
||||
ultrabrain: { model: "openai/gpt-5.4", variant: "xhigh" },
|
||||
},
|
||||
} as OhMyOpenCodeConfig
|
||||
|
||||
@@ -127,7 +127,7 @@ describe("resolveVariantForModel", () => {
|
||||
test("returns undefined for provider not in sisyphus chain", () => {
|
||||
// #given openai is not in sisyphus fallback chain anymore
|
||||
const config = {} as OhMyOpenCodeConfig
|
||||
const model = { providerID: "openai", modelID: "gpt-5.2" }
|
||||
const model = { providerID: "openai", modelID: "gpt-5.4" }
|
||||
|
||||
// when
|
||||
const variant = resolveVariantForModel(config, "sisyphus", model)
|
||||
@@ -191,7 +191,7 @@ describe("resolveVariantForModel", () => {
|
||||
test("returns correct variant for oracle agent with openai", () => {
|
||||
// given
|
||||
const config = {} as OhMyOpenCodeConfig
|
||||
const model = { providerID: "openai", modelID: "gpt-5.2" }
|
||||
const model = { providerID: "openai", modelID: "gpt-5.4" }
|
||||
|
||||
// when
|
||||
const variant = resolveVariantForModel(config, "oracle", model)
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("updateConnectedProvidersCache", () => {
|
||||
env: [],
|
||||
models: {
|
||||
"gpt-5.3-codex": { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
"gpt-5.2": { id: "gpt-5.2", name: "GPT-5.2" },
|
||||
"gpt-5.4": { id: "gpt-5.4", name: "GPT-5.4" },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -64,7 +64,7 @@ describe("updateConnectedProvidersCache", () => {
|
||||
expect(cache).not.toBeNull()
|
||||
expect(cache!.connected).toEqual(["openai", "anthropic"])
|
||||
expect(cache!.models).toEqual({
|
||||
openai: ["gpt-5.3-codex", "gpt-5.2"],
|
||||
openai: ["gpt-5.3-codex", "gpt-5.4"],
|
||||
anthropic: ["claude-opus-4-6", "claude-sonnet-4-6"],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -105,7 +105,7 @@ describe("parseJsonc", () => {
|
||||
const jsonc = `{
|
||||
// This is an example config
|
||||
"agents": {
|
||||
"oracle": { "model": "openai/gpt-5.2" }, // GPT for strategic reasoning
|
||||
"oracle": { "model": "openai/gpt-5.4" }, // GPT for strategic reasoning
|
||||
},
|
||||
/* Agent overrides */
|
||||
"disabled_agents": [],
|
||||
@@ -118,7 +118,7 @@ describe("parseJsonc", () => {
|
||||
}>(jsonc)
|
||||
|
||||
// then
|
||||
expect(result.agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(result.agents.oracle.model).toBe("openai/gpt-5.4")
|
||||
expect(result.disabled_agents).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("mergeCategories", () => {
|
||||
it("allows user to add custom categories", () => {
|
||||
//#given
|
||||
const userCategories = {
|
||||
"my-custom": { model: "openai/gpt-5.2", description: "Custom category" },
|
||||
"my-custom": { model: "openai/gpt-5.4", description: "Custom category" },
|
||||
}
|
||||
|
||||
//#when
|
||||
@@ -52,13 +52,13 @@ describe("mergeCategories", () => {
|
||||
|
||||
//#then
|
||||
expect(result["my-custom"]).toBeDefined()
|
||||
expect(result["my-custom"].model).toBe("openai/gpt-5.2")
|
||||
expect(result["my-custom"].model).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
it("allows user to disable custom categories", () => {
|
||||
//#given
|
||||
const userCategories = {
|
||||
"my-custom": { model: "openai/gpt-5.2", disable: true },
|
||||
"my-custom": { model: "openai/gpt-5.4", disable: true },
|
||||
}
|
||||
|
||||
//#when
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("migrateAgentNames", () => {
|
||||
test("preserves current agent names unchanged", () => {
|
||||
// given: Config with current agent names
|
||||
const agents = {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
librarian: { model: "google/gemini-3-flash" },
|
||||
explore: { model: "opencode/gpt-5-nano" },
|
||||
}
|
||||
@@ -47,7 +47,7 @@ describe("migrateAgentNames", () => {
|
||||
|
||||
// then: Current names should remain unchanged
|
||||
expect(changed).toBe(false)
|
||||
expect(migrated["oracle"]).toEqual({ model: "openai/gpt-5.2" })
|
||||
expect(migrated["oracle"]).toEqual({ model: "openai/gpt-5.4" })
|
||||
expect(migrated["librarian"]).toEqual({ model: "google/gemini-3-flash" })
|
||||
expect(migrated["explore"]).toEqual({ model: "opencode/gpt-5-nano" })
|
||||
})
|
||||
@@ -57,7 +57,7 @@ describe("migrateAgentNames", () => {
|
||||
const agents = {
|
||||
SISYPHUS: { model: "test" },
|
||||
"planner-sisyphus": { prompt: "test" },
|
||||
"Orchestrator-Sisyphus": { model: "openai/gpt-5.2" },
|
||||
"Orchestrator-Sisyphus": { model: "openai/gpt-5.4" },
|
||||
}
|
||||
|
||||
// when: Migrate agent names
|
||||
@@ -66,7 +66,7 @@ describe("migrateAgentNames", () => {
|
||||
// then: Case-insensitive lookup should migrate correctly
|
||||
expect(migrated["sisyphus"]).toEqual({ model: "test" })
|
||||
expect(migrated["prometheus"]).toEqual({ prompt: "test" })
|
||||
expect(migrated["atlas"]).toEqual({ model: "openai/gpt-5.2" })
|
||||
expect(migrated["atlas"]).toEqual({ model: "openai/gpt-5.4" })
|
||||
})
|
||||
|
||||
test("passes through unknown agent names unchanged", () => {
|
||||
@@ -441,11 +441,11 @@ describe("migrateConfigFile", () => {
|
||||
expect(rawConfig.disabled_hooks).toContain("anthropic-context-window-limit-recovery")
|
||||
})
|
||||
|
||||
test("does not migrate gpt-5.2-codex model versions in agents", () => {
|
||||
test("does not migrate gpt-5.4-codex model versions in agents", () => {
|
||||
// given: Config with old model version in agents
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex", temperature: 0.1 },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex", temperature: 0.1 },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ describe("migrateConfigFile", () => {
|
||||
// then: Model version should remain unchanged
|
||||
expect(needsWrite).toBe(false)
|
||||
const agents = rawConfig.agents as Record<string, Record<string, unknown>>
|
||||
expect(agents["sisyphus"].model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents["sisyphus"].model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("migrates model versions in categories", () => {
|
||||
@@ -479,7 +479,7 @@ describe("migrateConfigFile", () => {
|
||||
// given: Config with current model versions
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
},
|
||||
categories: {
|
||||
"my-category": { model: "anthropic/claude-opus-4-6" },
|
||||
@@ -514,10 +514,10 @@ describe("migration maps", () => {
|
||||
})
|
||||
|
||||
describe("MODEL_VERSION_MAP", () => {
|
||||
test("does not include openai/gpt-5.2-codex migration", () => {
|
||||
test("does not include openai/gpt-5.4-codex migration", () => {
|
||||
// given/when: Check MODEL_VERSION_MAP
|
||||
// then: openai/gpt-5.2-codex should not be migrated
|
||||
expect(MODEL_VERSION_MAP["openai/gpt-5.2-codex"]).toBeUndefined()
|
||||
// then: openai/gpt-5.4-codex should not be migrated
|
||||
expect(MODEL_VERSION_MAP["openai/gpt-5.4-codex"]).toBeUndefined()
|
||||
})
|
||||
|
||||
test("maps anthropic/claude-opus-4-5 to anthropic/claude-opus-4-6", () => {
|
||||
@@ -528,10 +528,10 @@ describe("MODEL_VERSION_MAP", () => {
|
||||
})
|
||||
|
||||
describe("migrateModelVersions", () => {
|
||||
test("#given a config with gpt-5.2-codex model #when migrating model versions #then does not overwrite with non-existent gpt-5.3-codex", () => {
|
||||
// given: Agent config with gpt-5.2-codex model
|
||||
test("#given a config with gpt-5.4-codex model #when migrating model versions #then does not overwrite with non-existent gpt-5.3-codex", () => {
|
||||
// given: Agent config with gpt-5.4-codex model
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex", temperature: 0.1 },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex", temperature: 0.1 },
|
||||
}
|
||||
|
||||
// when: Migrate model versions
|
||||
@@ -540,7 +540,7 @@ describe("migrateModelVersions", () => {
|
||||
// then: Model should remain unchanged
|
||||
expect(changed).toBe(false)
|
||||
const sisyphus = migrated["sisyphus"] as Record<string, unknown>
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
expect(sisyphus.temperature).toBe(0.1)
|
||||
})
|
||||
|
||||
@@ -562,7 +562,7 @@ describe("migrateModelVersions", () => {
|
||||
test("leaves unknown model strings untouched", () => {
|
||||
// given: Agent config with unknown model
|
||||
const agents = {
|
||||
oracle: { model: "openai/gpt-5.2", temperature: 0.5 },
|
||||
oracle: { model: "openai/gpt-5.4", temperature: 0.5 },
|
||||
}
|
||||
|
||||
// when: Migrate model versions
|
||||
@@ -571,7 +571,7 @@ describe("migrateModelVersions", () => {
|
||||
// then: Config should remain unchanged
|
||||
expect(changed).toBe(false)
|
||||
const oracle = migrated["oracle"] as Record<string, unknown>
|
||||
expect(oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(oracle.model).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
test("handles agent config with no model field", () => {
|
||||
@@ -605,9 +605,9 @@ describe("migrateModelVersions", () => {
|
||||
test("migrates multiple agents in one pass", () => {
|
||||
// given: Multiple agents with old models
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
prometheus: { model: "anthropic/claude-opus-4-5" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
}
|
||||
|
||||
// when: Migrate model versions
|
||||
@@ -615,9 +615,9 @@ describe("migrateModelVersions", () => {
|
||||
|
||||
// then: Only mapped models should be updated
|
||||
expect(changed).toBe(true)
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
expect((migrated["prometheus"] as Record<string, unknown>).model).toBe("anthropic/claude-opus-4-6")
|
||||
expect((migrated["oracle"] as Record<string, unknown>).model).toBe("openai/gpt-5.2")
|
||||
expect((migrated["oracle"] as Record<string, unknown>).model).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
test("handles empty object", () => {
|
||||
@@ -635,9 +635,9 @@ describe("migrateModelVersions", () => {
|
||||
test("skips already-applied migrations", () => {
|
||||
// given: Agent config with old model, but migration already applied
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex", temperature: 0.1 },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex", temperature: 0.1 },
|
||||
}
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"])
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"])
|
||||
|
||||
// when: Migrate with applied migrations
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(agents, appliedMigrations)
|
||||
@@ -646,32 +646,32 @@ describe("migrateModelVersions", () => {
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toHaveLength(0)
|
||||
const sisyphus = migrated["sisyphus"] as Record<string, unknown>
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("applies new migrations and records them", () => {
|
||||
// given: Agent config with old model, no prior migrations
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
|
||||
// when: Migrate without applied migrations
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(agents)
|
||||
|
||||
// then: No migration should be applied for gpt-5.2-codex
|
||||
// then: No migration should be applied for gpt-5.4-codex
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toEqual([])
|
||||
const sisyphus = migrated["sisyphus"] as Record<string, unknown>
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("handles mixed: some applied, some new", () => {
|
||||
// given: Multiple agents, one migration already applied
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
prometheus: { model: "anthropic/claude-opus-4-5" },
|
||||
}
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"])
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"])
|
||||
|
||||
// when: Migrate with partial history
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(agents, appliedMigrations)
|
||||
@@ -679,23 +679,23 @@ describe("migrateModelVersions", () => {
|
||||
// then: Only prometheus should be migrated
|
||||
expect(changed).toBe(true)
|
||||
expect(newMigrations).toEqual(["model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6"])
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
expect((migrated["prometheus"] as Record<string, unknown>).model).toBe("anthropic/claude-opus-4-6")
|
||||
})
|
||||
|
||||
test("backward compatible without appliedMigrations param", () => {
|
||||
// given: Agent config with old model, no appliedMigrations param
|
||||
const agents = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
|
||||
// when: Migrate without the param (backward compat)
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(agents)
|
||||
|
||||
// then: Should keep gpt-5.2-codex unchanged
|
||||
// then: Should keep gpt-5.4-codex unchanged
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toHaveLength(0)
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated["sisyphus"] as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -706,14 +706,14 @@ describe("migrateConfigFile _migrations tracking", () => {
|
||||
const configPath = `${tmpDir}/oh-my-opencode.json`
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
},
|
||||
}
|
||||
|
||||
// when: Migrate config file
|
||||
const result = migrateConfigFile(configPath, rawConfig)
|
||||
|
||||
// then: gpt-5.2-codex should not produce migrations
|
||||
// then: gpt-5.4-codex should not produce migrations
|
||||
expect(result).toBe(false)
|
||||
expect(rawConfig._migrations).toBeUndefined()
|
||||
|
||||
@@ -727,9 +727,9 @@ describe("migrateConfigFile _migrations tracking", () => {
|
||||
const configPath = `${tmpDir}/oh-my-opencode.json`
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
},
|
||||
_migrations: ["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"],
|
||||
_migrations: ["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"],
|
||||
}
|
||||
|
||||
// when: Migrate config file
|
||||
@@ -738,7 +738,7 @@ describe("migrateConfigFile _migrations tracking", () => {
|
||||
// then: Should NOT rewrite (model stays as user set it)
|
||||
// Note: result may be true due to other migrations, but model should NOT change
|
||||
const sisyphus = (rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
|
||||
// cleanup
|
||||
fs.rmSync(tmpDir, { recursive: true })
|
||||
@@ -752,7 +752,7 @@ describe("migrateConfigFile _migrations tracking", () => {
|
||||
agents: {
|
||||
prometheus: { model: "anthropic/claude-opus-4-5" },
|
||||
},
|
||||
_migrations: ["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"],
|
||||
_migrations: ["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"],
|
||||
}
|
||||
|
||||
// when: Migrate config file
|
||||
@@ -761,7 +761,7 @@ describe("migrateConfigFile _migrations tracking", () => {
|
||||
// then: New migration appended, old one preserved
|
||||
expect(result).toBe(true)
|
||||
expect(rawConfig._migrations).toEqual([
|
||||
"model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex",
|
||||
"model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex",
|
||||
"model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6",
|
||||
])
|
||||
|
||||
@@ -825,7 +825,7 @@ describe("migrateAgentConfigToCategory", () => {
|
||||
const configs = [
|
||||
{ model: "google/gemini-3.1-pro" },
|
||||
{ model: "google/gemini-3-flash" },
|
||||
{ model: "openai/gpt-5.2" },
|
||||
{ model: "openai/gpt-5.4" },
|
||||
{ model: "anthropic/claude-haiku-4-5" },
|
||||
{ model: "anthropic/claude-opus-4-6" },
|
||||
{ model: "anthropic/claude-sonnet-4-6" },
|
||||
@@ -847,7 +847,7 @@ describe("migrateAgentConfigToCategory", () => {
|
||||
test("preserves non-model fields during migration", () => {
|
||||
// given: Config with multiple fields
|
||||
const config = {
|
||||
model: "openai/gpt-5.2",
|
||||
model: "openai/gpt-5.4",
|
||||
temperature: 0.1,
|
||||
top_p: 0.95,
|
||||
maxTokens: 4096,
|
||||
@@ -1020,7 +1020,7 @@ describe("migrateConfigFile with backup", () => {
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
"multimodal-looker": { model: "anthropic/claude-haiku-4-5" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
oracle: { model: "openai/gpt-5.4" },
|
||||
"my-custom-agent": { model: "google/gemini-3.1-pro" },
|
||||
},
|
||||
}
|
||||
@@ -1036,7 +1036,7 @@ describe("migrateConfigFile with backup", () => {
|
||||
|
||||
const agents = rawConfig.agents as Record<string, Record<string, unknown>>
|
||||
expect(agents["multimodal-looker"].model).toBe("anthropic/claude-haiku-4-5")
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.4")
|
||||
expect(agents["my-custom-agent"].model).toBe("google/gemini-3.1-pro")
|
||||
})
|
||||
|
||||
@@ -1140,9 +1140,9 @@ describe("migrateModelVersions with applied migrations", () => {
|
||||
test("skips already-applied migrations", () => {
|
||||
// given: Config with old model and migration already applied
|
||||
const configs = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"])
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"])
|
||||
|
||||
// when: Migrate model versions
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(configs, appliedMigrations)
|
||||
@@ -1150,32 +1150,32 @@ describe("migrateModelVersions with applied migrations", () => {
|
||||
// then: Migration should be skipped (user reverted)
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toEqual([])
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("applies new migrations not in history", () => {
|
||||
// given: Config with old model, no migration history
|
||||
const configs = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
const appliedMigrations = new Set<string>()
|
||||
|
||||
// when: Migrate model versions
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(configs, appliedMigrations)
|
||||
|
||||
// then: gpt-5.2-codex should not be migrated
|
||||
// then: gpt-5.4-codex should not be migrated
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toEqual([])
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("handles mixed: skip applied, apply new", () => {
|
||||
// given: Config with 2 old models, 1 already migrated
|
||||
const configs = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
oracle: { model: "anthropic/claude-opus-4-5" },
|
||||
}
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"])
|
||||
const appliedMigrations = new Set(["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"])
|
||||
|
||||
// when: Migrate model versions
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(configs, appliedMigrations)
|
||||
@@ -1183,29 +1183,29 @@ describe("migrateModelVersions with applied migrations", () => {
|
||||
// then: Skip sisyphus (already applied), apply oracle
|
||||
expect(changed).toBe(true)
|
||||
expect(newMigrations).toEqual(["model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6"])
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
expect((migrated.oracle as Record<string, unknown>).model).toBe("anthropic/claude-opus-4-6")
|
||||
})
|
||||
|
||||
test("backward compatible: no appliedMigrations param", () => {
|
||||
// given: Config with old model, no appliedMigrations param (legacy call)
|
||||
const configs = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
|
||||
// when: Migrate model versions (without appliedMigrations)
|
||||
const { migrated, changed, newMigrations } = migrateModelVersions(configs)
|
||||
|
||||
// then: gpt-5.2-codex remains unchanged
|
||||
// then: gpt-5.4-codex remains unchanged
|
||||
expect(changed).toBe(false)
|
||||
expect(newMigrations).toEqual([])
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.2-codex")
|
||||
expect((migrated.sisyphus as Record<string, unknown>).model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("returns empty newMigrations when no migrations applied", () => {
|
||||
// given: Config with no old models
|
||||
const configs = {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
}
|
||||
|
||||
// when: Migrate model versions
|
||||
@@ -1235,7 +1235,7 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
const testConfigPath = "/tmp/test-config-migrations-1.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
},
|
||||
}
|
||||
fs.writeFileSync(testConfigPath, JSON.stringify(rawConfig, null, 2))
|
||||
@@ -1244,10 +1244,10 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
// when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// then: gpt-5.2-codex should not create migration history
|
||||
// then: gpt-5.4-codex should not create migration history
|
||||
expect(needsWrite).toBe(false)
|
||||
expect(rawConfig._migrations).toBeUndefined()
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
})
|
||||
|
||||
test("skips re-applying already-recorded migrations", () => {
|
||||
@@ -1255,9 +1255,9 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
const testConfigPath = "/tmp/test-config-migrations-2.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
},
|
||||
_migrations: ["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"],
|
||||
_migrations: ["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"],
|
||||
}
|
||||
fs.writeFileSync(testConfigPath, JSON.stringify(rawConfig, null, 2))
|
||||
cleanupPaths.push(testConfigPath)
|
||||
@@ -1267,8 +1267,8 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
|
||||
// then: Should not migrate (user reverted)
|
||||
expect(needsWrite).toBe(false)
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(rawConfig._migrations).toEqual(["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"])
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
expect(rawConfig._migrations).toEqual(["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"])
|
||||
})
|
||||
|
||||
test("preserves existing _migrations and appends new ones", () => {
|
||||
@@ -1276,10 +1276,10 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
const testConfigPath = "/tmp/test-config-migrations-3.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
sisyphus: { model: "openai/gpt-5.2-codex" },
|
||||
sisyphus: { model: "openai/gpt-5.4-codex" },
|
||||
oracle: { model: "anthropic/claude-opus-4-5" },
|
||||
},
|
||||
_migrations: ["model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex"],
|
||||
_migrations: ["model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex"],
|
||||
}
|
||||
fs.writeFileSync(testConfigPath, JSON.stringify(rawConfig, null, 2))
|
||||
cleanupPaths.push(testConfigPath)
|
||||
@@ -1289,10 +1289,10 @@ describe("migrateConfigFile with _migrations tracking", () => {
|
||||
|
||||
// then: Should skip sisyphus, migrate oracle, append to _migrations
|
||||
expect(needsWrite).toBe(true)
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).sisyphus.model).toBe("openai/gpt-5.4-codex")
|
||||
expect((rawConfig.agents as Record<string, Record<string, unknown>>).oracle.model).toBe("anthropic/claude-opus-4-6")
|
||||
expect(rawConfig._migrations).toEqual([
|
||||
"model-version:openai/gpt-5.2-codex->openai/gpt-5.3-codex",
|
||||
"model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex",
|
||||
"model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6",
|
||||
])
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
export const MODEL_TO_CATEGORY_MAP: Record<string, string> = {
|
||||
"google/gemini-3.1-pro": "visual-engineering",
|
||||
"google/gemini-3-flash": "writing",
|
||||
"openai/gpt-5.2": "ultrabrain",
|
||||
"openai/gpt-5.4": "ultrabrain",
|
||||
"anthropic/claude-haiku-4-5": "quick",
|
||||
"anthropic/claude-opus-4-6": "unspecified-high",
|
||||
"anthropic/claude-sonnet-4-6": "unspecified-low",
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("fetchAvailableModels", () => {
|
||||
|
||||
it("#given cache file with models #when fetchAvailableModels called with connectedProviders #then returns Set of model IDs", async () => {
|
||||
writeModelsCache({
|
||||
openai: { id: "openai", models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { id: "openai", models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { id: "anthropic", models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
google: { id: "google", models: { "gemini-3.1-pro": { id: "gemini-3.1-pro" } } },
|
||||
})
|
||||
@@ -72,14 +72,14 @@ describe("fetchAvailableModels", () => {
|
||||
|
||||
expect(result).toBeInstanceOf(Set)
|
||||
expect(result.size).toBe(3)
|
||||
expect(result.has("openai/gpt-5.2")).toBe(true)
|
||||
expect(result.has("openai/gpt-5.4")).toBe(true)
|
||||
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
|
||||
expect(result.has("google/gemini-3.1-pro")).toBe(true)
|
||||
})
|
||||
|
||||
it("#given connectedProviders unknown #when fetchAvailableModels called without options #then returns empty Set", async () => {
|
||||
writeModelsCache({
|
||||
openai: { id: "openai", models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { id: "openai", models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
})
|
||||
|
||||
const result = await fetchAvailableModels()
|
||||
@@ -141,7 +141,7 @@ describe("fetchAvailableModels", () => {
|
||||
|
||||
it("#given cache read twice #when second call made with same providers #then reads fresh each time", async () => {
|
||||
writeModelsCache({
|
||||
openai: { id: "openai", models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { id: "openai", models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { id: "anthropic", models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
})
|
||||
|
||||
@@ -149,7 +149,7 @@ describe("fetchAvailableModels", () => {
|
||||
const result2 = await fetchAvailableModels(undefined, { connectedProviders: ["openai"] })
|
||||
|
||||
expect(result1.size).toBe(result2.size)
|
||||
expect(result1.has("openai/gpt-5.2")).toBe(true)
|
||||
expect(result1.has("openai/gpt-5.4")).toBe(true)
|
||||
})
|
||||
|
||||
it("#given empty providers in cache #when fetchAvailableModels called with connectedProviders #then returns empty Set", async () => {
|
||||
@@ -187,12 +187,12 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return the matching model
|
||||
it("should match substring in model name", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.3-codex",
|
||||
"anthropic/claude-opus-4-6",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt-5.2", available)
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
const result = fuzzyMatchModel("gpt-5.4", available)
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available model with preview suffix
|
||||
@@ -213,12 +213,12 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return exact match if it exists
|
||||
it("should prefer exact match over substring match", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.3-codex",
|
||||
"openai/gpt-5.2-ultra",
|
||||
"openai/gpt-5.4-ultra",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt-5.2", available)
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
const result = fuzzyMatchModel("gpt-5.4", available)
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available models with multiple substring matches
|
||||
@@ -226,11 +226,11 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return the shorter model name (more specific)
|
||||
it("should prefer shorter model name when multiple matches exist", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2-ultra",
|
||||
"openai/gpt-5.2-ultra-mega",
|
||||
"openai/gpt-5.4-ultra",
|
||||
"openai/gpt-5.4-ultra-mega",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt-5.2", available)
|
||||
expect(result).toBe("openai/gpt-5.2-ultra")
|
||||
const result = fuzzyMatchModel("gpt-5.4", available)
|
||||
expect(result).toBe("openai/gpt-5.4-ultra")
|
||||
})
|
||||
|
||||
// given available models with claude variants
|
||||
@@ -271,12 +271,12 @@ describe("fuzzyMatchModel", () => {
|
||||
// then only search models from specified providers
|
||||
it("should filter by provider when providers array is given", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"google/gemini-3",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt", available, ["openai"])
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available models from multiple providers
|
||||
@@ -284,7 +284,7 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return null
|
||||
it("should return null when provider filter excludes all matches", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
])
|
||||
const result = fuzzyMatchModel("claude", available, ["openai"])
|
||||
@@ -296,7 +296,7 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return null
|
||||
it("should return null when no match found", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
])
|
||||
const result = fuzzyMatchModel("gemini", available)
|
||||
@@ -308,11 +308,11 @@ describe("fuzzyMatchModel", () => {
|
||||
// then match case-insensitively
|
||||
it("should match case-insensitively", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
])
|
||||
const result = fuzzyMatchModel("GPT-5.2", available)
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
const result = fuzzyMatchModel("GPT-5.4", available)
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available models with exact match and longer variants
|
||||
@@ -356,11 +356,11 @@ describe("fuzzyMatchModel", () => {
|
||||
// then return shortest full string (preserves tie-break behavior)
|
||||
it("should use shortest tie-break when multiple providers have same model ID", () => {
|
||||
const available = new Set([
|
||||
"opencode/gpt-5.2",
|
||||
"openai/gpt-5.2",
|
||||
"opencode/gpt-5.4",
|
||||
"openai/gpt-5.4",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt-5.2", available)
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
const result = fuzzyMatchModel("gpt-5.4", available)
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available models with multiple providers
|
||||
@@ -368,12 +368,12 @@ describe("fuzzyMatchModel", () => {
|
||||
// then search all specified providers
|
||||
it("should search all specified providers", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"google/gemini-3",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt", available, ["openai", "google"])
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given available models with provider prefix
|
||||
@@ -381,11 +381,11 @@ describe("fuzzyMatchModel", () => {
|
||||
// then only match models with correct provider prefix
|
||||
it("should only match models with correct provider prefix", () => {
|
||||
const available = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/gpt-something",
|
||||
])
|
||||
const result = fuzzyMatchModel("gpt", available, ["openai"])
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
// given empty available set
|
||||
@@ -513,7 +513,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then only returns models from that provider
|
||||
it("should filter models by connected providers", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
google: { models: { "gemini-3.1-pro": { id: "gemini-3.1-pro" } } },
|
||||
})
|
||||
@@ -524,7 +524,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
|
||||
expect(result.size).toBe(1)
|
||||
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
|
||||
expect(result.has("openai/gpt-5.2")).toBe(false)
|
||||
expect(result.has("openai/gpt-5.4")).toBe(false)
|
||||
expect(result.has("google/gemini-3.1-pro")).toBe(false)
|
||||
})
|
||||
|
||||
@@ -533,7 +533,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then returns models from all specified providers
|
||||
it("should filter models by multiple connected providers", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
google: { models: { "gemini-3.1-pro": { id: "gemini-3.1-pro" } } },
|
||||
})
|
||||
@@ -545,7 +545,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
expect(result.size).toBe(2)
|
||||
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
|
||||
expect(result.has("google/gemini-3.1-pro")).toBe(true)
|
||||
expect(result.has("openai/gpt-5.2")).toBe(false)
|
||||
expect(result.has("openai/gpt-5.4")).toBe(false)
|
||||
})
|
||||
|
||||
// given cache with models
|
||||
@@ -553,7 +553,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then returns empty set
|
||||
it("should return empty set when connectedProviders is empty", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
})
|
||||
|
||||
@@ -569,7 +569,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then returns empty set (triggers fallback in resolver)
|
||||
it("should return empty set when connectedProviders not specified", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
})
|
||||
|
||||
@@ -583,7 +583,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then returns empty set for that provider
|
||||
it("should handle provider not in cache gracefully", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
})
|
||||
|
||||
const result = await fetchAvailableModels(undefined, {
|
||||
@@ -598,7 +598,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then returns models only from matching providers
|
||||
it("should return models from providers that exist in both cache and connected list", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
})
|
||||
|
||||
@@ -615,7 +615,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then does NOT use cache (dynamic per-session)
|
||||
it("should not cache filtered results", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
anthropic: { models: { "claude-opus-4-6": { id: "claude-opus-4-6" } } },
|
||||
})
|
||||
|
||||
@@ -630,7 +630,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
connectedProviders: ["openai"]
|
||||
})
|
||||
expect(result2.size).toBe(1)
|
||||
expect(result2.has("openai/gpt-5.2")).toBe(true)
|
||||
expect(result2.has("openai/gpt-5.4")).toBe(true)
|
||||
})
|
||||
|
||||
// given connectedProviders unknown
|
||||
@@ -638,7 +638,7 @@ describe("fetchAvailableModels with connected providers filtering", () => {
|
||||
// then always returns empty set (triggers fallback)
|
||||
it("should return empty set when connectedProviders unknown", async () => {
|
||||
writeModelsCache({
|
||||
openai: { models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
||||
openai: { models: { "gpt-5.4": { id: "gpt-5.4" } } },
|
||||
})
|
||||
|
||||
const result1 = await fetchAvailableModels()
|
||||
@@ -696,7 +696,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
|
||||
connected: ["opencode", "anthropic"]
|
||||
})
|
||||
writeModelsCache({
|
||||
opencode: { models: { "big-pickle": {}, "gpt-5-nano": {}, "gpt-5.2": {} } },
|
||||
opencode: { models: { "big-pickle": {}, "gpt-5-nano": {}, "gpt-5.4": {} } },
|
||||
anthropic: { models: { "claude-opus-4-6": {}, "claude-sonnet-4-6": {} } }
|
||||
})
|
||||
|
||||
@@ -708,7 +708,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
|
||||
expect(result.has("opencode/big-pickle")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5-nano")).toBe(true)
|
||||
expect(result.has("anthropic/claude-opus-4-6")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5.2")).toBe(false)
|
||||
expect(result.has("opencode/gpt-5.4")).toBe(false)
|
||||
expect(result.has("anthropic/claude-sonnet-4-6")).toBe(false)
|
||||
})
|
||||
|
||||
@@ -738,7 +738,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
|
||||
// then falls back to models.json (no whitelist filtering)
|
||||
it("should fallback to models.json when provider-models cache not found", async () => {
|
||||
writeModelsCache({
|
||||
opencode: { models: { "big-pickle": {}, "gpt-5-nano": {}, "gpt-5.2": {} } },
|
||||
opencode: { models: { "big-pickle": {}, "gpt-5-nano": {}, "gpt-5.4": {} } },
|
||||
})
|
||||
|
||||
const result = await fetchAvailableModels(undefined, {
|
||||
@@ -748,7 +748,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
|
||||
expect(result.size).toBe(3)
|
||||
expect(result.has("opencode/big-pickle")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5-nano")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5.2")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5.4")).toBe(true)
|
||||
})
|
||||
|
||||
// given provider-models cache with whitelist
|
||||
@@ -907,7 +907,7 @@ describe("fallback model availability", () => {
|
||||
|
||||
it("returns null for completely unknown model", () => {
|
||||
// given
|
||||
const available = new Set(["openai/gpt-5.2", "anthropic/claude-opus-4-6"])
|
||||
const available = new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-6"])
|
||||
|
||||
// when
|
||||
const result = fuzzyMatchModel("non-existent-model-family", available)
|
||||
@@ -918,7 +918,7 @@ describe("fallback model availability", () => {
|
||||
|
||||
it("returns true when models do not match but provider is connected", () => {
|
||||
// given
|
||||
const fallbackChain = [{ providers: ["openai"], model: "gpt-5.2" }]
|
||||
const fallbackChain = [{ providers: ["openai"], model: "gpt-5.4" }]
|
||||
const availableModels = new Set(["anthropic/claude-opus-4-6"])
|
||||
writeConnectedProvidersCache(["openai"])
|
||||
|
||||
@@ -932,25 +932,25 @@ describe("fallback model availability", () => {
|
||||
it("returns first resolved fallback model from chain", () => {
|
||||
// given
|
||||
const fallbackChain = [
|
||||
{ providers: ["openai"], model: "gpt-5.2" },
|
||||
{ providers: ["openai"], model: "gpt-5.4" },
|
||||
{ providers: ["anthropic"], model: "claude-opus-4-6" },
|
||||
]
|
||||
const availableModels = new Set([
|
||||
"anthropic/claude-opus-4-6",
|
||||
"openai/gpt-5.2-preview",
|
||||
"openai/gpt-5.4-preview",
|
||||
])
|
||||
|
||||
// when
|
||||
const result = resolveFirstAvailableFallback(fallbackChain, availableModels)
|
||||
|
||||
// then
|
||||
expect(result).toEqual({ provider: "openai", model: "openai/gpt-5.2-preview" })
|
||||
expect(result).toEqual({ provider: "openai", model: "openai/gpt-5.4-preview" })
|
||||
})
|
||||
|
||||
it("returns null when no fallback model resolves", () => {
|
||||
// given
|
||||
const fallbackChain = [
|
||||
{ providers: ["openai"], model: "gpt-5.2" },
|
||||
{ providers: ["openai"], model: "gpt-5.4" },
|
||||
{ providers: ["anthropic"], model: "claude-opus-4-6" },
|
||||
]
|
||||
const availableModels = new Set(["google/gemini-3.1-pro"])
|
||||
|
||||
@@ -8,7 +8,7 @@ import { normalizeSDKResponse } from "./normalize-sdk-response"
|
||||
/**
|
||||
* Fuzzy match a target model name against available models
|
||||
*
|
||||
* @param target - The model name or substring to search for (e.g., "gpt-5.2", "claude-opus")
|
||||
* @param target - The model name or substring to search for (e.g., "gpt-5.4", "claude-opus")
|
||||
* @param available - Set of available model names in format "provider/model-name"
|
||||
* @param providers - Optional array of provider names to filter by (e.g., ["openai", "anthropic"])
|
||||
* @returns The matched model name or null if no match found
|
||||
@@ -21,8 +21,8 @@ import { normalizeSDKResponse } from "./normalize-sdk-response"
|
||||
* If providers array is given, only models starting with "provider/" are considered.
|
||||
*
|
||||
* @example
|
||||
* const available = new Set(["openai/gpt-5.2", "openai/gpt-5.3-codex", "anthropic/claude-opus-4-6"])
|
||||
* fuzzyMatchModel("gpt-5.2", available) // → "openai/gpt-5.2"
|
||||
* const available = new Set(["openai/gpt-5.4", "openai/gpt-5.3-codex", "anthropic/claude-opus-4-6"])
|
||||
* fuzzyMatchModel("gpt-5.4", available) // → "openai/gpt-5.4"
|
||||
* fuzzyMatchModel("claude", available, ["openai"]) // → null (provider filter excludes anthropic)
|
||||
*/
|
||||
function normalizeModelName(name: string): string {
|
||||
@@ -82,7 +82,7 @@ export function fuzzyMatchModel(
|
||||
|
||||
// Priority 2: Exact model ID match (part after provider/)
|
||||
// This ensures "big-pickle" matches "zai-coding-plan/big-pickle" over "zai-coding-plan/glm-5"
|
||||
// Use filter + shortest to handle multi-provider cases (e.g., openai/gpt-5.2 + opencode/gpt-5.2)
|
||||
// Use filter + shortest to handle multi-provider cases (e.g., openai/gpt-5.4 + opencode/gpt-5.4)
|
||||
const exactModelIdMatches = matches.filter((model) => {
|
||||
const modelId = model.split("/").slice(1).join("/")
|
||||
return normalizeModelName(modelId) === targetNormalized
|
||||
|
||||
@@ -7,19 +7,19 @@ import {
|
||||
} from "./model-requirements"
|
||||
|
||||
describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
test("oracle has valid fallbackChain with gpt-5.2 as primary", () => {
|
||||
test("oracle has valid fallbackChain with gpt-5.4 as primary", () => {
|
||||
// given - oracle agent requirement
|
||||
const oracle = AGENT_MODEL_REQUIREMENTS["oracle"]
|
||||
|
||||
// when - accessing oracle requirement
|
||||
// then - fallbackChain exists with gpt-5.2 as first entry
|
||||
// then - fallbackChain exists with gpt-5.4 as first entry
|
||||
expect(oracle).toBeDefined()
|
||||
expect(oracle.fallbackChain).toBeArray()
|
||||
expect(oracle.fallbackChain.length).toBeGreaterThan(0)
|
||||
|
||||
const primary = oracle.fallbackChain[0]
|
||||
expect(primary.providers).toContain("openai")
|
||||
expect(primary.model).toBe("gpt-5.2")
|
||||
expect(primary.model).toBe("gpt-5.4")
|
||||
expect(primary.variant).toBe("high")
|
||||
})
|
||||
|
||||
@@ -435,7 +435,7 @@ describe("ModelRequirement type", () => {
|
||||
const requirement: ModelRequirement = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-6", variant: "max" },
|
||||
{ providers: ["openai", "github-copilot"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["openai", "github-copilot"], model: "gpt-5.4", variant: "high" },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ describe("ModelRequirement type", () => {
|
||||
expect(requirement.fallbackChain).toBeArray()
|
||||
expect(requirement.fallbackChain).toHaveLength(2)
|
||||
expect(requirement.fallbackChain[0].model).toBe("claude-opus-4-6")
|
||||
expect(requirement.fallbackChain[1].model).toBe("gpt-5.2")
|
||||
expect(requirement.fallbackChain[1].model).toBe("gpt-5.4")
|
||||
})
|
||||
|
||||
test("ModelRequirement variant is optional", () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
model: "gpt-5.3-codex",
|
||||
variant: "medium",
|
||||
},
|
||||
{ providers: ["github-copilot"], model: "gpt-5.2", variant: "medium" },
|
||||
{ providers: ["github-copilot"], model: "gpt-5.4", variant: "medium" },
|
||||
],
|
||||
requiresProvider: ["openai", "github-copilot", "venice", "opencode"],
|
||||
},
|
||||
@@ -40,7 +40,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
fallbackChain: [
|
||||
{
|
||||
providers: ["openai", "github-copilot", "opencode"],
|
||||
model: "gpt-5.2",
|
||||
model: "gpt-5.4",
|
||||
variant: "high",
|
||||
},
|
||||
{
|
||||
@@ -119,7 +119,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
},
|
||||
{
|
||||
providers: ["openai", "github-copilot", "opencode"],
|
||||
model: "gpt-5.2",
|
||||
model: "gpt-5.4",
|
||||
variant: "high",
|
||||
},
|
||||
{
|
||||
@@ -226,7 +226,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
model: "claude-opus-4-6",
|
||||
variant: "max",
|
||||
},
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4" },
|
||||
],
|
||||
requiresModel: "gemini-3.1-pro",
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("resolveModel", () => {
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: "anthropic/claude-opus-4-6",
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
inheritedModel: "openai/gpt-5.4",
|
||||
systemDefault: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("resolveModel", () => {
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: undefined,
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
inheritedModel: "openai/gpt-5.4",
|
||||
systemDefault: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("resolveModel", () => {
|
||||
const result = resolveModel(input)
|
||||
|
||||
// then
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
test("returns systemDefault when both userModel and inheritedModel are undefined", () => {
|
||||
@@ -56,7 +56,7 @@ describe("resolveModel", () => {
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: "",
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
inheritedModel: "openai/gpt-5.4",
|
||||
systemDefault: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("resolveModel", () => {
|
||||
const result = resolveModel(input)
|
||||
|
||||
// then
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
expect(result).toBe("openai/gpt-5.4")
|
||||
})
|
||||
|
||||
test("treats whitespace-only string as unset, uses fallback", () => {
|
||||
@@ -88,7 +88,7 @@ describe("resolveModel", () => {
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: "anthropic/claude-opus-4-6",
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
inheritedModel: "openai/gpt-5.4",
|
||||
systemDefault: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -292,9 +292,9 @@ describe("resolveModelWithFallback", () => {
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "anthropic", "google"], model: "gpt-5.2" },
|
||||
{ providers: ["openai", "anthropic", "google"], model: "gpt-5.4" },
|
||||
],
|
||||
availableModels: new Set(["openai/gpt-5.2", "anthropic/claude-opus-4-6", "google/gemini-3.1-pro"]),
|
||||
availableModels: new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-6", "google/gemini-3.1-pro"]),
|
||||
systemDefaultModel: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("resolveModelWithFallback", () => {
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// then
|
||||
expect(result!.model).toBe("openai/gpt-5.2")
|
||||
expect(result!.model).toBe("openai/gpt-5.4")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
@@ -476,7 +476,7 @@ describe("resolveModelWithFallback", () => {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic"], model: "nonexistent-model" },
|
||||
],
|
||||
availableModels: new Set(["openai/gpt-5.2", "anthropic/claude-opus-4-6"]),
|
||||
availableModels: new Set(["openai/gpt-5.4", "anthropic/claude-opus-4-6"]),
|
||||
systemDefaultModel: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -592,7 +592,7 @@ describe("resolveModelWithFallback", () => {
|
||||
test("returns system default when fallbackChain is not provided", () => {
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
availableModels: new Set(["openai/gpt-5.2"]),
|
||||
availableModels: new Set(["openai/gpt-5.4"]),
|
||||
systemDefaultModel: "google/gemini-3.1-pro",
|
||||
}
|
||||
|
||||
@@ -613,7 +613,7 @@ describe("resolveModelWithFallback", () => {
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4", variant: "high" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||
],
|
||||
availableModels,
|
||||
@@ -632,7 +632,7 @@ describe("resolveModelWithFallback", () => {
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "anthropic"], model: "gpt-5.2" },
|
||||
{ providers: ["openai", "anthropic"], model: "gpt-5.4" },
|
||||
{ providers: ["google"], model: "gemini-3.1-pro" },
|
||||
],
|
||||
availableModels,
|
||||
@@ -647,14 +647,14 @@ describe("resolveModelWithFallback", () => {
|
||||
test("returns first matching entry even if later entries have better matches", () => {
|
||||
// given
|
||||
const availableModels = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4-6",
|
||||
])
|
||||
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai"], model: "gpt-5.2" },
|
||||
{ providers: ["openai"], model: "gpt-5.4" },
|
||||
{ providers: ["anthropic"], model: "claude-opus-4-6" },
|
||||
],
|
||||
availableModels,
|
||||
@@ -662,7 +662,7 @@ describe("resolveModelWithFallback", () => {
|
||||
})
|
||||
|
||||
// then
|
||||
expect(result!.model).toBe("openai/gpt-5.2")
|
||||
expect(result!.model).toBe("openai/gpt-5.4")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
@@ -673,7 +673,7 @@ describe("resolveModelWithFallback", () => {
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai"], model: "gpt-5.2" },
|
||||
{ providers: ["openai"], model: "gpt-5.4" },
|
||||
{ providers: ["anthropic"], model: "claude-opus-4-6" },
|
||||
{ providers: ["google"], model: "gemini-3.1-pro" },
|
||||
],
|
||||
@@ -884,7 +884,7 @@ describe("resolveModelWithFallback", () => {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic"], model: "nonexistent-model" },
|
||||
],
|
||||
availableModels: new Set(["openai/gpt-5.2"]),
|
||||
availableModels: new Set(["openai/gpt-5.4"]),
|
||||
systemDefaultModel: undefined,
|
||||
}
|
||||
|
||||
@@ -898,7 +898,7 @@ describe("resolveModelWithFallback", () => {
|
||||
test("returns undefined when no fallbackChain and systemDefaultModel is undefined", () => {
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
availableModels: new Set(["openai/gpt-5.2"]),
|
||||
availableModels: new Set(["openai/gpt-5.4"]),
|
||||
systemDefaultModel: undefined,
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("parseModelSuggestion", () => {
|
||||
data: {
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5",
|
||||
suggestions: ["gpt-5.2"],
|
||||
suggestions: ["gpt-5.4"],
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -82,7 +82,7 @@ describe("parseModelSuggestion", () => {
|
||||
expect(result).toEqual({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5",
|
||||
suggestion: "gpt-5.2",
|
||||
suggestion: "gpt-5.4",
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user