From 4ca4c06698f52a7664a25cc1c2248ddddcbb455e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 17 Apr 2026 14:52:02 +0900 Subject: [PATCH] refactor(migration): auto-upgrade claude-opus-4-5 and 4-6 to claude-opus-4-7 MODEL_VERSION_MAP now chains the legacy claude-opus-4-5 entry straight to claude-opus-4-7 and adds an explicit claude-opus-4-6 to 4-7 bump path, letting existing user configs upgrade on next load without an intermediate 4-6 stop. MODEL_TO_CATEGORY_MAP picks up claude-opus-4-7 as the canonical unspecified-high model (prior 4-6 entry is covered by the chained version map above, so legacy hardcoded configs still resolve). Migration tests rewritten to reflect the chained 4-5 to 4-7 behavior and the new 4-6 to 4-7 bump path, including the sidecar-union scenario. --- src/shared/migration.test.ts | 54 +++++++++---------- src/shared/migration/agent-category.ts | 2 +- src/shared/migration/config-migration.test.ts | 12 ++--- .../migration/migrations-sidecar.test.ts | 6 +-- src/shared/migration/migrations-sidecar.ts | 2 +- src/shared/migration/model-versions.ts | 3 +- 6 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/shared/migration.test.ts b/src/shared/migration.test.ts index e0b5f2808..072858d7c 100644 --- a/src/shared/migration.test.ts +++ b/src/shared/migration.test.ts @@ -19,7 +19,7 @@ describe("migrateAgentNames", () => { test("migrates legacy OmO names to lowercase", () => { // given: Config with legacy OmO agent names const agents = { - omo: { model: "anthropic/claude-opus-4-6" }, + omo: { model: "anthropic/claude-opus-4-7" }, OmO: { temperature: 0.5 }, "OmO-Plan": { prompt: "custom prompt" }, } @@ -88,7 +88,7 @@ describe("migrateAgentNames", () => { test("migrates orchestrator-sisyphus to atlas", () => { // given: Config with legacy orchestrator-sisyphus agent name const agents = { - "orchestrator-sisyphus": { model: "anthropic/claude-opus-4-6" }, + "orchestrator-sisyphus": { model: "anthropic/claude-opus-4-7" }, } // when: Migrate agent names @@ -96,14 +96,14 @@ describe("migrateAgentNames", () => { // then: orchestrator-sisyphus should be migrated to atlas expect(changed).toBe(true) - expect(migrated["atlas"]).toEqual({ model: "anthropic/claude-opus-4-6" }) + expect(migrated["atlas"]).toEqual({ model: "anthropic/claude-opus-4-7" }) expect(migrated["orchestrator-sisyphus"]).toBeUndefined() }) test("migrates lowercase atlas to atlas", () => { // given: Config with lowercase atlas agent name const agents = { - atlas: { model: "anthropic/claude-opus-4-6" }, + atlas: { model: "anthropic/claude-opus-4-7" }, } // when: Migrate agent names @@ -111,7 +111,7 @@ describe("migrateAgentNames", () => { // then: lowercase atlas should remain atlas (no change needed) expect(changed).toBe(false) - expect(migrated["atlas"]).toEqual({ model: "anthropic/claude-opus-4-6" }) + expect(migrated["atlas"]).toEqual({ model: "anthropic/claude-opus-4-7" }) }) test("migrates Sisyphus variants to lowercase", () => { @@ -524,7 +524,7 @@ describe("migrateConfigFile", () => { // then: Model version should be migrated expect(needsWrite).toBe(true) const categories = rawConfig.categories as Record> - expect(categories["my-category"].model).toBe("anthropic/claude-opus-4-6") + expect(categories["my-category"].model).toBe("anthropic/claude-opus-4-7") }) test("does not set needsWrite when no model versions need migration", () => { @@ -534,7 +534,7 @@ describe("migrateConfigFile", () => { sisyphus: { model: "openai/gpt-5.4-codex" }, }, categories: { - "my-category": { model: "anthropic/claude-opus-4-6" }, + "my-category": { model: "anthropic/claude-opus-4-7" }, }, } @@ -572,10 +572,10 @@ describe("MODEL_VERSION_MAP", () => { expect(MODEL_VERSION_MAP["openai/gpt-5.4-codex"]).toBeUndefined() }) - test("maps anthropic/claude-opus-4-5 to anthropic/claude-opus-4-6", () => { + test("maps anthropic/claude-opus-4-5 to anthropic/claude-opus-4-7", () => { // given/when: Check MODEL_VERSION_MAP // then: Should contain correct mapping - expect(MODEL_VERSION_MAP["anthropic/claude-opus-4-5"]).toBe("anthropic/claude-opus-4-6") + expect(MODEL_VERSION_MAP["anthropic/claude-opus-4-5"]).toBe("anthropic/claude-opus-4-7") }) test("maps openai/gpt-5.3-codex to openai/gpt-5.4 for deep category migration", () => { @@ -614,7 +614,7 @@ describe("migrateModelVersions", () => { // then: Model should be updated expect(changed).toBe(true) const prometheus = migrated["prometheus"] as Record - expect(prometheus.model).toBe("anthropic/claude-opus-4-6") + expect(prometheus.model).toBe("anthropic/claude-opus-4-7") }) test("leaves unknown model strings untouched", () => { @@ -674,7 +674,7 @@ describe("migrateModelVersions", () => { // then: Only mapped models should be updated expect(changed).toBe(true) expect((migrated["sisyphus"] as Record).model).toBe("openai/gpt-5.4-codex") - expect((migrated["prometheus"] as Record).model).toBe("anthropic/claude-opus-4-6") + expect((migrated["prometheus"] as Record).model).toBe("anthropic/claude-opus-4-7") expect((migrated["oracle"] as Record).model).toBe("openai/gpt-5.4") }) @@ -736,9 +736,9 @@ 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(newMigrations).toEqual(["model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7"]) expect((migrated["sisyphus"] as Record).model).toBe("openai/gpt-5.4-codex") - expect((migrated["prometheus"] as Record).model).toBe("anthropic/claude-opus-4-6") + expect((migrated["prometheus"] as Record).model).toBe("anthropic/claude-opus-4-7") }) test("backward compatible without appliedMigrations param", () => { @@ -820,12 +820,12 @@ describe("migrateConfigFile _migrations tracking", () => { // (legacy + new) is written to the sidecar file exactly once. expect(result).toBe(true) expect(rawConfig._migrations).toBeUndefined() - expect((rawConfig.agents as Record>).prometheus.model).toBe("anthropic/claude-opus-4-6") + expect((rawConfig.agents as Record>).prometheus.model).toBe("anthropic/claude-opus-4-7") const sidecar = JSON.parse(fs.readFileSync(`${configPath}.migrations.json`, "utf-8")) expect(new Set(sidecar.appliedMigrations)).toEqual(new Set([ "model-version:openai/gpt-5.4-codex->openai/gpt-5.3-codex", - "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6", + "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7", ])) // cleanup @@ -890,7 +890,7 @@ describe("migrateAgentConfigToCategory", () => { { model: "google/gemini-3-flash" }, { model: "openai/gpt-5.4" }, { model: "anthropic/claude-haiku-4-5" }, - { model: "anthropic/claude-opus-4-6" }, + { model: "anthropic/claude-opus-4-7" }, { model: "anthropic/claude-sonnet-4-6" }, ] @@ -970,7 +970,7 @@ describe("shouldDeleteAgentConfig", () => { // given: Config with custom model override const config = { category: "visual-engineering", - model: "anthropic/claude-opus-4-6", + model: "anthropic/claude-opus-4-7", } // when: Check if config should be deleted @@ -1245,9 +1245,9 @@ 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(newMigrations).toEqual(["model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7"]) expect((migrated.sisyphus as Record).model).toBe("openai/gpt-5.4-codex") - expect((migrated.oracle as Record).model).toBe("anthropic/claude-opus-4-6") + expect((migrated.oracle as Record).model).toBe("anthropic/claude-opus-4-7") }) test("backward compatible: no appliedMigrations param", () => { @@ -1334,12 +1334,12 @@ describe("migrateConfigFile with migration tracking via sidecar (#3263)", () => const needsWrite = migrateConfigFile(testConfigPath, rawConfig) expect(needsWrite).toBe(true) - expect((rawConfig.agents as Record>).oracle.model).toBe("anthropic/claude-opus-4-6") + expect((rawConfig.agents as Record>).oracle.model).toBe("anthropic/claude-opus-4-7") expect(rawConfig._migrations).toBeUndefined() const sidecar = JSON.parse(fs.readFileSync(sidecarPath(testConfigPath), "utf-8")) expect(sidecar.appliedMigrations).toEqual([ - "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6", + "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7", ]) }) @@ -1408,7 +1408,7 @@ describe("migrateConfigFile with migration tracking via sidecar (#3263)", () => JSON.stringify({ appliedMigrations: [ "model-version:openai/gpt-5.3-codex->openai/gpt-5.4", - "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6", + "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7", ], }), ) @@ -1416,7 +1416,7 @@ describe("migrateConfigFile with migration tracking via sidecar (#3263)", () => agents: { oracle: { model: "anthropic/claude-opus-4-5" }, }, - _migrations: ["model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6"], + _migrations: ["model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7"], } fs.writeFileSync(testConfigPath, JSON.stringify(rawConfig, null, 2)) @@ -1430,7 +1430,7 @@ describe("migrateConfigFile with migration tracking via sidecar (#3263)", () => const sidecar = JSON.parse(fs.readFileSync(sidecarPath(testConfigPath), "utf-8")) expect(sidecar.appliedMigrations).toEqual([ - "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6", + "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7", "model-version:openai/gpt-5.3-codex->openai/gpt-5.4", ]) }) @@ -1460,13 +1460,13 @@ describe("migrateConfigFile with migration tracking via sidecar (#3263)", () => // codex was reverted, must stay expect((rawConfig.agents as Record>).codex.model).toBe("openai/gpt-5.3-codex") // claude migrates - expect((rawConfig.agents as Record>).claude.model).toBe("anthropic/claude-opus-4-6") + expect((rawConfig.agents as Record>).claude.model).toBe("anthropic/claude-opus-4-7") expect(rawConfig._migrations).toBeUndefined() const sidecar = JSON.parse(fs.readFileSync(sidecarPath(testConfigPath), "utf-8")) expect(new Set(sidecar.appliedMigrations)).toEqual(new Set([ "model-version:openai/gpt-5.3-codex->openai/gpt-5.4", - "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6", + "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7", ])) }) @@ -1494,7 +1494,7 @@ describe("migrateConfigFile with migration tracking via sidecar (#3263)", () => expect(Array.isArray(migrations)).toBe(true) expect(migrations).toContain("model-version:openai/gpt-5.3-codex->openai/gpt-5.4") expect(migrations.length).toBeGreaterThanOrEqual(1) - expect((rawConfig.agents as Record>).oracle.model).toBe("anthropic/claude-opus-4-6") + expect((rawConfig.agents as Record>).oracle.model).toBe("anthropic/claude-opus-4-7") // Sidecar should not exist because write failed expect(fs.existsSync(sidecarPath(testConfigPath))).toBe(false) diff --git a/src/shared/migration/agent-category.ts b/src/shared/migration/agent-category.ts index 6f75682ca..9960cfca4 100644 --- a/src/shared/migration/agent-category.ts +++ b/src/shared/migration/agent-category.ts @@ -16,7 +16,7 @@ export const MODEL_TO_CATEGORY_MAP: Record = { "google/gemini-3-flash": "writing", "openai/gpt-5.4": "ultrabrain", "anthropic/claude-haiku-4-5": "quick", - "anthropic/claude-opus-4-6": "unspecified-high", + "anthropic/claude-opus-4-7": "unspecified-high", "anthropic/claude-sonnet-4-6": "unspecified-low", } diff --git a/src/shared/migration/config-migration.test.ts b/src/shared/migration/config-migration.test.ts index 35f8b574a..ff59d7ca3 100644 --- a/src/shared/migration/config-migration.test.ts +++ b/src/shared/migration/config-migration.test.ts @@ -8,7 +8,7 @@ import { migrateConfigFile } from "./config-migration" import { getSidecarPath } from "./migrations-sidecar" const createdDirectories: string[] = [] -const MIGRATION_KEY = "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6" +const MIGRATION_KEY = "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7" function createWorkdir(): string { const workdir = mkdtempSync(join(tmpdir(), "omo-config-migration-")) @@ -46,13 +46,13 @@ describe("migrateConfigFile sidecar write ordering", () => { expect(needsWrite).toBe(true) expect(rawConfig._migrations).toBeUndefined() expect((rawConfig.agents as Record>).prometheus.model).toBe( - "anthropic/claude-opus-4-6", + "anthropic/claude-opus-4-7", ) const persistedConfig = JSON.parse(readFileSync(configPath, "utf-8")) as Record expect(persistedConfig._migrations).toBeUndefined() expect((persistedConfig.agents as Record>).prometheus.model).toBe( - "anthropic/claude-opus-4-6", + "anthropic/claude-opus-4-7", ) const sidecar = JSON.parse(readFileSync(getSidecarPath(configPath), "utf-8")) as { @@ -87,7 +87,7 @@ describe("migrateConfigFile sidecar write ordering", () => { expect(retriedNeedsWrite).toBe(true) expect(retriedConfig._migrations).toBeUndefined() expect((retriedConfig.agents as Record>).prometheus.model).toBe( - "anthropic/claude-opus-4-6", + "anthropic/claude-opus-4-7", ) expect(existsSync(getSidecarPath(configPath))).toBe(true) }) @@ -108,13 +108,13 @@ describe("migrateConfigFile sidecar write ordering", () => { expect(needsWrite).toBe(true) expect(rawConfig._migrations).toEqual([MIGRATION_KEY]) expect((rawConfig.agents as Record>).prometheus.model).toBe( - "anthropic/claude-opus-4-6", + "anthropic/claude-opus-4-7", ) const persistedConfig = JSON.parse(readFileSync(configPath, "utf-8")) as Record expect(persistedConfig._migrations).toEqual([MIGRATION_KEY]) expect((persistedConfig.agents as Record>).prometheus.model).toBe( - "anthropic/claude-opus-4-6", + "anthropic/claude-opus-4-7", ) expect(statSync(getSidecarPath(configPath)).isDirectory()).toBe(true) }) diff --git a/src/shared/migration/migrations-sidecar.test.ts b/src/shared/migration/migrations-sidecar.test.ts index 5809bde94..503ea9105 100644 --- a/src/shared/migration/migrations-sidecar.test.ts +++ b/src/shared/migration/migrations-sidecar.test.ts @@ -42,7 +42,7 @@ describe("migrations sidecar", () => { JSON.stringify({ appliedMigrations: [ "model-version:openai/gpt-5.3-codex->openai/gpt-5.4", - "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6", + "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7", ], }), ) @@ -51,7 +51,7 @@ describe("migrations sidecar", () => { expect(applied.size).toBe(2) expect(applied.has("model-version:openai/gpt-5.3-codex->openai/gpt-5.4")).toBe(true) - expect(applied.has("model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6")).toBe(true) + expect(applied.has("model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7")).toBe(true) }) test("returns an empty set on malformed JSON instead of throwing", () => { @@ -134,7 +134,7 @@ describe("migrations sidecar", () => { const configPath = join(workdir, "oh-my-openagent.jsonc") const original = new Set([ "model-version:openai/gpt-5.3-codex->openai/gpt-5.4", - "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6", + "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7", ]) writeAppliedMigrations(configPath, original) diff --git a/src/shared/migration/migrations-sidecar.ts b/src/shared/migration/migrations-sidecar.ts index cd0088922..0cbac7db1 100644 --- a/src/shared/migration/migrations-sidecar.ts +++ b/src/shared/migration/migrations-sidecar.ts @@ -22,7 +22,7 @@ import { writeFileAtomically } from "../write-file-atomically" * { * "appliedMigrations": [ * "model-version:openai/gpt-5.3-codex->openai/gpt-5.4", - * "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-6" + * "model-version:anthropic/claude-opus-4-5->anthropic/claude-opus-4-7" * ] * } */ diff --git a/src/shared/migration/model-versions.ts b/src/shared/migration/model-versions.ts index 13731dcaa..40aee07b3 100644 --- a/src/shared/migration/model-versions.ts +++ b/src/shared/migration/model-versions.ts @@ -6,7 +6,8 @@ * Keys are full "provider/model" strings. Only openai and anthropic entries needed. */ export const MODEL_VERSION_MAP: Record = { - "anthropic/claude-opus-4-5": "anthropic/claude-opus-4-6", + "anthropic/claude-opus-4-5": "anthropic/claude-opus-4-7", + "anthropic/claude-opus-4-6": "anthropic/claude-opus-4-7", "anthropic/claude-sonnet-4-5": "anthropic/claude-sonnet-4-6", "openai/gpt-5.3-codex": "openai/gpt-5.4", }