diff --git a/packages/contracts/src/model.test.ts b/packages/contracts/src/model.test.ts index 2f31b6ebe35a5c12c7f6c9dbdec74a051fb39d63..137167daa577a9b3aff5e77d9201a805a483c203 100644 --- a/packages/contracts/src/model.test.ts +++ b/packages/contracts/src/model.test.ts @@ -1,6 +1,16 @@ import { describe, expect, it } from "vitest"; -import { DEFAULT_MODEL, MODEL_OPTIONS, normalizeModelSlug, resolveModelSlug } from "./model"; +import { + DEFAULT_MODEL, + DEFAULT_MODEL_BY_PROVIDER, + MODEL_OPTIONS, + MODEL_OPTIONS_BY_PROVIDER, + getDefaultModel, + getModelOptions, + normalizeModelSlug, + resolveModelSlug, + resolveModelSlugForProvider, +} from "./model"; describe("normalizeModelSlug", () => { it("maps known aliases to canonical slugs", () => { @@ -19,6 +29,11 @@ describe("normalizeModelSlug", () => { expect(normalizeModelSlug("gpt-5.2")).toBe("gpt-5.2"); expect(normalizeModelSlug("gpt-5.2-codex")).toBe("gpt-5.2-codex"); }); + + it("uses provider-specific aliases", () => { + expect(normalizeModelSlug("sonnet", "claudeCode")).toBe("claude-sonnet-4-5"); + expect(normalizeModelSlug("opus-4.1", "claudeCode")).toBe("claude-opus-4-1"); + }); }); describe("resolveModelSlug", () => { @@ -37,4 +52,20 @@ describe("resolveModelSlug", () => { expect(resolveModelSlug(model.slug)).toBe(model.slug); } }); + + it("supports provider-aware resolution", () => { + expect(resolveModelSlugForProvider("claudeCode", undefined)).toBe( + DEFAULT_MODEL_BY_PROVIDER.claudeCode, + ); + expect(resolveModelSlugForProvider("claudeCode", "sonnet")).toBe("claude-sonnet-4-5"); + expect(resolveModelSlugForProvider("claudeCode", "gpt-5.3-codex")).toBe( + DEFAULT_MODEL_BY_PROVIDER.claudeCode, + ); + }); + + it("keeps codex defaults for backward compatibility", () => { + expect(getDefaultModel()).toBe(DEFAULT_MODEL); + expect(getModelOptions()).toEqual(MODEL_OPTIONS); + expect(getModelOptions("claudeCode")).toEqual(MODEL_OPTIONS_BY_PROVIDER.claudeCode); + }); }); diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index cf019e98bd611795c3fab42d257507c64c1f9bfb..3626e4c5a78e256c5c9267148e3227fa9350b84b 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -1,23 +1,72 @@ -export const MODEL_OPTIONS = [ - { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, - { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" }, - { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, - { slug: "gpt-5.2", name: "GPT-5.2" }, -] as const; - -export type BuiltInModelSlug = (typeof MODEL_OPTIONS)[number]["slug"]; -export type ModelSlug = string; - -export const DEFAULT_MODEL = "gpt-5.3-codex"; - -export const MODEL_SLUG_ALIASES: Record = { - "5.3": "gpt-5.3-codex", - "gpt-5.3": "gpt-5.3-codex", - "5.3-spark": "gpt-5.3-codex-spark", - "gpt-5.3-spark": "gpt-5.3-codex-spark", +import type { ProviderKind } from "./orchestration"; + +type ModelOption = { + readonly slug: string; + readonly name: string; +}; + +export const MODEL_OPTIONS_BY_PROVIDER = { + codex: [ + { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, + { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" }, + { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, + { slug: "gpt-5.2", name: "GPT-5.2" }, + ], + claudeCode: [ + { slug: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" }, + { slug: "claude-opus-4-1", name: "Claude Opus 4.1" }, + { slug: "claude-haiku-3-5", name: "Claude Haiku 3.5" }, + ], +} as const satisfies Record; + +export type ModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)[ProviderKind][number]["slug"]; + +export const DEFAULT_MODEL_BY_PROVIDER: Record = { + codex: "gpt-5.3-codex", + claudeCode: "claude-sonnet-4-5", +}; + +// Backward compatibility for existing Codex-only call sites. +export const MODEL_OPTIONS = MODEL_OPTIONS_BY_PROVIDER.codex; +export const DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex; + +export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record> = { + codex: { + "5.3": "gpt-5.3-codex", + "gpt-5.3": "gpt-5.3-codex", + "5.3-spark": "gpt-5.3-codex-spark", + "gpt-5.3-spark": "gpt-5.3-codex-spark", + }, + claudeCode: { + sonnet: "claude-sonnet-4-5", + "sonnet-4.5": "claude-sonnet-4-5", + "claude-sonnet-4.5": "claude-sonnet-4-5", + opus: "claude-opus-4-1", + "opus-4.1": "claude-opus-4-1", + "claude-opus-4.1": "claude-opus-4-1", + haiku: "claude-haiku-3-5", + "haiku-3.5": "claude-haiku-3-5", + "claude-haiku-3.5": "claude-haiku-3-5", + }, +}; + +const MODEL_SLUG_SET_BY_PROVIDER: Record> = { + codex: new Set(MODEL_OPTIONS_BY_PROVIDER.codex.map((option) => option.slug)), + claudeCode: new Set(MODEL_OPTIONS_BY_PROVIDER.claudeCode.map((option) => option.slug)), }; -export function normalizeModelSlug(model: string | null | undefined): ModelSlug | null { +export function getModelOptions(provider: ProviderKind = "codex") { + return MODEL_OPTIONS_BY_PROVIDER[provider]; +} + +export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug { + return DEFAULT_MODEL_BY_PROVIDER[provider]; +} + +export function normalizeModelSlug( + model: string | null | undefined, + provider: ProviderKind = "codex", +): ModelSlug | null { if (typeof model !== "string") { return null; } @@ -27,12 +76,28 @@ export function normalizeModelSlug(model: string | null | undefined): ModelSlug return null; } - return MODEL_SLUG_ALIASES[trimmed] ?? trimmed; + return MODEL_SLUG_ALIASES_BY_PROVIDER[provider][trimmed] ?? (trimmed as ModelSlug); } -export function resolveModelSlug(model: string | null | undefined): ModelSlug { - const normalized = normalizeModelSlug(model); - return normalized ?? DEFAULT_MODEL; +export function resolveModelSlug( + model: string | null | undefined, + provider: ProviderKind = "codex", +): ModelSlug { + const normalized = normalizeModelSlug(model, provider); + if (!normalized) { + return DEFAULT_MODEL_BY_PROVIDER[provider]; + } + + return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized) + ? normalized + : DEFAULT_MODEL_BY_PROVIDER[provider]; +} + +export function resolveModelSlugForProvider( + provider: ProviderKind, + model: string | null | undefined, +): ModelSlug { + return resolveModelSlug(model, provider); } export const REASONING_OPTIONS = ["xhigh", "high", "medium", "low"] as const;