From 7e5a657f06234e3b4c9775a25b1a43bfaad0d1fc Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 6 Feb 2026 15:55:28 +0900 Subject: [PATCH] feat(migration): add model version migration for gpt-5.2-codex and claude-opus-4-5 --- src/shared/migration.test.ts | 216 +++++++++++++++++++++++++++++++---- src/shared/migration.ts | 49 ++++++++ 2 files changed, 244 insertions(+), 21 deletions(-) diff --git a/src/shared/migration.test.ts b/src/shared/migration.test.ts index 2ba28dc71..c03064830 100644 --- a/src/shared/migration.test.ts +++ b/src/shared/migration.test.ts @@ -4,8 +4,10 @@ import * as path from "path" import { AGENT_NAME_MAP, HOOK_NAME_MAP, + MODEL_VERSION_MAP, migrateAgentNames, migrateHookNames, + migrateModelVersions, migrateConfigFile, migrateAgentConfigToCategory, shouldDeleteAgentConfig, @@ -369,29 +371,81 @@ describe("migrateConfigFile", () => { expect(needsWrite).toBe(false) }) - test("handles migration of all legacy items together", () => { - // given: Config with all legacy items - const rawConfig: Record = { - omo_agent: { disabled: false }, - agents: { - omo: { model: "test" }, - "OmO-Plan": { prompt: "custom" }, - }, - disabled_hooks: ["anthropic-auto-compact"], - } + test("handles migration of all legacy items together", () => { + // given: Config with all legacy items + const rawConfig: Record = { + omo_agent: { disabled: false }, + agents: { + omo: { model: "test" }, + "OmO-Plan": { prompt: "custom" }, + }, + disabled_hooks: ["anthropic-auto-compact"], + } - // when: Migrate config file - const needsWrite = migrateConfigFile(testConfigPath, rawConfig) + // when: Migrate config file + const needsWrite = migrateConfigFile(testConfigPath, rawConfig) - // then: All legacy items should be migrated - expect(needsWrite).toBe(true) - expect(rawConfig.sisyphus_agent).toEqual({ disabled: false }) - expect(rawConfig.omo_agent).toBeUndefined() - const agents = rawConfig.agents as Record - expect(agents["sisyphus"]).toBeDefined() - expect(agents["prometheus"]).toBeDefined() - expect(rawConfig.disabled_hooks).toContain("anthropic-context-window-limit-recovery") - }) + // then: All legacy items should be migrated + expect(needsWrite).toBe(true) + expect(rawConfig.sisyphus_agent).toEqual({ disabled: false }) + expect(rawConfig.omo_agent).toBeUndefined() + const agents = rawConfig.agents as Record + expect(agents["sisyphus"]).toBeDefined() + expect(agents["prometheus"]).toBeDefined() + expect(rawConfig.disabled_hooks).toContain("anthropic-context-window-limit-recovery") + }) + + test("migrates model versions in agents", () => { + // given: Config with old model version in agents + const rawConfig: Record = { + agents: { + sisyphus: { model: "openai/gpt-5.2-codex", temperature: 0.1 }, + }, + } + + // when: Migrate config file + const needsWrite = migrateConfigFile(testConfigPath, rawConfig) + + // then: Model version should be migrated + expect(needsWrite).toBe(true) + const agents = rawConfig.agents as Record> + expect(agents["sisyphus"].model).toBe("openai/gpt-5.3-codex") + }) + + test("migrates model versions in categories", () => { + // given: Config with old model version in categories + const rawConfig: Record = { + categories: { + "my-category": { model: "anthropic/claude-opus-4-5", temperature: 0.2 }, + }, + } + + // when: Migrate config file + const needsWrite = migrateConfigFile(testConfigPath, rawConfig) + + // 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") + }) + + test("does not set needsWrite when no model versions need migration", () => { + // given: Config with current model versions + const rawConfig: Record = { + agents: { + sisyphus: { model: "openai/gpt-5.3-codex" }, + }, + categories: { + "my-category": { model: "anthropic/claude-opus-4-6" }, + }, + } + + // when: Migrate config file + const needsWrite = migrateConfigFile(testConfigPath, rawConfig) + + // then: No write should be needed + expect(needsWrite).toBe(false) + }) }) describe("migration maps", () => { @@ -413,6 +467,126 @@ describe("migration maps", () => { }) }) +describe("MODEL_VERSION_MAP", () => { + test("maps openai/gpt-5.2-codex to openai/gpt-5.3-codex", () => { + // given/when: Check MODEL_VERSION_MAP + // then: Should contain correct mapping + expect(MODEL_VERSION_MAP["openai/gpt-5.2-codex"]).toBe("openai/gpt-5.3-codex") + }) + + test("maps anthropic/claude-opus-4-5 to anthropic/claude-opus-4-6", () => { + // 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") + }) +}) + +describe("migrateModelVersions", () => { + test("replaces old model string in agent config", () => { + // given: Agent config with old model version + const agents = { + sisyphus: { model: "openai/gpt-5.2-codex", temperature: 0.1 }, + } + + // when: Migrate model versions + const { migrated, changed } = migrateModelVersions(agents) + + // then: Model should be updated, other fields preserved + expect(changed).toBe(true) + const sisyphus = migrated["sisyphus"] as Record + expect(sisyphus.model).toBe("openai/gpt-5.3-codex") + expect(sisyphus.temperature).toBe(0.1) + }) + + test("replaces anthropic model version", () => { + // given: Agent config with old anthropic model + const agents = { + prometheus: { model: "anthropic/claude-opus-4-5" }, + } + + // when: Migrate model versions + const { migrated, changed } = migrateModelVersions(agents) + + // then: Model should be updated + expect(changed).toBe(true) + const prometheus = migrated["prometheus"] as Record + expect(prometheus.model).toBe("anthropic/claude-opus-4-6") + }) + + test("leaves unknown model strings untouched", () => { + // given: Agent config with unknown model + const agents = { + oracle: { model: "openai/gpt-5.2", temperature: 0.5 }, + } + + // when: Migrate model versions + const { migrated, changed } = migrateModelVersions(agents) + + // then: Config should remain unchanged + expect(changed).toBe(false) + const oracle = migrated["oracle"] as Record + expect(oracle.model).toBe("openai/gpt-5.2") + }) + + test("handles agent config with no model field", () => { + // given: Agent config without model field + const agents = { + sisyphus: { temperature: 0.1, prompt: "custom" }, + } + + // when: Migrate model versions + const { migrated, changed } = migrateModelVersions(agents) + + // then: Config should remain unchanged + expect(changed).toBe(false) + const sisyphus = migrated["sisyphus"] as Record + expect(sisyphus.temperature).toBe(0.1) + }) + + test("handles agent config with non-string model", () => { + // given: Agent config with non-string model + const agents = { + sisyphus: { model: 123, temperature: 0.1 }, + } + + // when: Migrate model versions + const { migrated, changed } = migrateModelVersions(agents) + + // then: Config should remain unchanged + expect(changed).toBe(false) + }) + + test("migrates multiple agents in one pass", () => { + // given: Multiple agents with old models + const agents = { + sisyphus: { model: "openai/gpt-5.2-codex" }, + prometheus: { model: "anthropic/claude-opus-4-5" }, + oracle: { model: "openai/gpt-5.2" }, + } + + // when: Migrate model versions + const { migrated, changed } = migrateModelVersions(agents) + + // then: Only mapped models should be updated + expect(changed).toBe(true) + expect((migrated["sisyphus"] as Record).model).toBe("openai/gpt-5.3-codex") + expect((migrated["prometheus"] as Record).model).toBe("anthropic/claude-opus-4-6") + expect((migrated["oracle"] as Record).model).toBe("openai/gpt-5.2") + }) + + test("handles empty object", () => { + // given: Empty agents object + const agents = {} + + // when: Migrate model versions + const { migrated, changed } = migrateModelVersions(agents) + + // then: Should return empty with no change + expect(changed).toBe(false) + expect(Object.keys(migrated)).toHaveLength(0) + }) +}) + describe("migrateAgentConfigToCategory", () => { test("migrates model to category when mapping exists", () => { // given: Config with a model that has a category mapping diff --git a/src/shared/migration.ts b/src/shared/migration.ts index 096a24869..fae241c40 100644 --- a/src/shared/migration.ts +++ b/src/shared/migration.ts @@ -89,6 +89,18 @@ export const MODEL_TO_CATEGORY_MAP: Record = { "anthropic/claude-sonnet-4-5": "unspecified-low", } +/** + * Model version migration map: old full model strings → new full model strings. + * Used to auto-upgrade hardcoded model versions in user configs when the plugin + * bumps to newer model versions. + * + * Keys are full "provider/model" strings. Only openai and anthropic entries needed. + */ +export const MODEL_VERSION_MAP: Record = { + "openai/gpt-5.2-codex": "openai/gpt-5.3-codex", + "anthropic/claude-opus-4-5": "anthropic/claude-opus-4-6", +} + export function migrateAgentNames(agents: Record): { migrated: Record; changed: boolean } { const migrated: Record = {} let changed = false @@ -104,6 +116,25 @@ export function migrateAgentNames(agents: Record): { migrated: return { migrated, changed } } +export function migrateModelVersions(configs: Record): { migrated: Record; changed: boolean } { + const migrated: Record = {} + let changed = false + + for (const [key, value] of Object.entries(configs)) { + if (value && typeof value === "object" && !Array.isArray(value)) { + const config = value as Record + if (typeof config.model === "string" && MODEL_VERSION_MAP[config.model]) { + migrated[key] = { ...config, model: MODEL_VERSION_MAP[config.model] } + changed = true + continue + } + } + migrated[key] = value + } + + return { migrated, changed } +} + export function migrateHookNames(hooks: string[]): { migrated: string[]; changed: boolean; removed: string[] } { const migrated: string[] = [] const removed: string[] = [] @@ -178,7 +209,25 @@ export function migrateConfigFile(configPath: string, rawConfig: Record) + if (changed) { + rawConfig.agents = migrated + needsWrite = true + log(`Migrated model versions in agents config`) + } + } + // Migrate model versions in categories + if (rawConfig.categories && typeof rawConfig.categories === "object") { + const { migrated, changed } = migrateModelVersions(rawConfig.categories as Record) + if (changed) { + rawConfig.categories = migrated + needsWrite = true + log(`Migrated model versions in categories config`) + } + } if (rawConfig.omo_agent) { rawConfig.sisyphus_agent = rawConfig.omo_agent