authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 18:43:41-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log2cd04f08c171ad69b7ca47ace875173181a054ea
tree44b19e304dbe95f8352da2150af7a2e4659897b9
parent4916dad78ef84d6e9c605d2f75b0bd7ed25afb6d

Add provider-specific model resolution and defaults

- Add per-provider model options, defaults, and slug aliases - Add provider-aware model normalization/resolution helpers - Preserve Codex-only constants/functions for backward compatibility - Extend tests to cover Claude aliases and provider-specific fallback behavior

2 files changed, 119 insertions(+), 23 deletions(-)

packages/contracts/src/model.test.ts+32-1
...@@ -1,6 +1,16 @@...@@ -1,6 +1,16 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { DEFAULT_MODEL, MODEL_OPTIONS, normalizeModelSlug, resolveModelSlug } from "./model";3import {
4 DEFAULT_MODEL,
5 DEFAULT_MODEL_BY_PROVIDER,
6 MODEL_OPTIONS,
7 MODEL_OPTIONS_BY_PROVIDER,
8 getDefaultModel,
9 getModelOptions,
10 normalizeModelSlug,
11 resolveModelSlug,
12 resolveModelSlugForProvider,
13} from "./model";
414
5describe("normalizeModelSlug", () => {15describe("normalizeModelSlug", () => {
6 it("maps known aliases to canonical slugs", () => {16 it("maps known aliases to canonical slugs", () => {
...@@ -19,6 +29,11 @@ describe("normalizeModelSlug", () => {...@@ -19,6 +29,11 @@ describe("normalizeModelSlug", () => {
19 expect(normalizeModelSlug("gpt-5.2")).toBe("gpt-5.2");29 expect(normalizeModelSlug("gpt-5.2")).toBe("gpt-5.2");
20 expect(normalizeModelSlug("gpt-5.2-codex")).toBe("gpt-5.2-codex");30 expect(normalizeModelSlug("gpt-5.2-codex")).toBe("gpt-5.2-codex");
21 });31 });
32
33 it("uses provider-specific aliases", () => {
34 expect(normalizeModelSlug("sonnet", "claudeCode")).toBe("claude-sonnet-4-5");
35 expect(normalizeModelSlug("opus-4.1", "claudeCode")).toBe("claude-opus-4-1");
36 });
22});37});
2338
24describe("resolveModelSlug", () => {39describe("resolveModelSlug", () => {
...@@ -37,4 +52,20 @@ describe("resolveModelSlug", () => {...@@ -37,4 +52,20 @@ describe("resolveModelSlug", () => {
37 expect(resolveModelSlug(model.slug)).toBe(model.slug);52 expect(resolveModelSlug(model.slug)).toBe(model.slug);
38 }53 }
39 });54 });
55
56 it("supports provider-aware resolution", () => {
57 expect(resolveModelSlugForProvider("claudeCode", undefined)).toBe(
58 DEFAULT_MODEL_BY_PROVIDER.claudeCode,
59 );
60 expect(resolveModelSlugForProvider("claudeCode", "sonnet")).toBe("claude-sonnet-4-5");
61 expect(resolveModelSlugForProvider("claudeCode", "gpt-5.3-codex")).toBe(
62 DEFAULT_MODEL_BY_PROVIDER.claudeCode,
63 );
64 });
65
66 it("keeps codex defaults for backward compatibility", () => {
67 expect(getDefaultModel()).toBe(DEFAULT_MODEL);
68 expect(getModelOptions()).toEqual(MODEL_OPTIONS);
69 expect(getModelOptions("claudeCode")).toEqual(MODEL_OPTIONS_BY_PROVIDER.claudeCode);
70 });
40});71});
packages/contracts/src/model.ts+87-22
...@@ -1,23 +1,72 @@...@@ -1,23 +1,72 @@
1export const MODEL_OPTIONS = [1import type { ProviderKind } from "./orchestration";
2 { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" },2
3 { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },3type ModelOption = {
4 { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" },4 readonly slug: string;
5 { slug: "gpt-5.2", name: "GPT-5.2" },5 readonly name: string;
6] as const;6};
77
8export type BuiltInModelSlug = (typeof MODEL_OPTIONS)[number]["slug"];8export const MODEL_OPTIONS_BY_PROVIDER = {
9export type ModelSlug = string;9 codex: [
1010 { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
11export const DEFAULT_MODEL = "gpt-5.3-codex";11 { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
1212 { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
13export const MODEL_SLUG_ALIASES: Record<string, BuiltInModelSlug> = {13 { slug: "gpt-5.2", name: "GPT-5.2" },
14 "5.3": "gpt-5.3-codex",14 ],
15 "gpt-5.3": "gpt-5.3-codex",15 claudeCode: [
16 "5.3-spark": "gpt-5.3-codex-spark",16 { slug: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" },
17 "gpt-5.3-spark": "gpt-5.3-codex-spark",17 { slug: "claude-opus-4-1", name: "Claude Opus 4.1" },
18 { slug: "claude-haiku-3-5", name: "Claude Haiku 3.5" },
19 ],
20} as const satisfies Record<ProviderKind, readonly ModelOption[]>;
21
22export type ModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)[ProviderKind][number]["slug"];
23
24export const DEFAULT_MODEL_BY_PROVIDER: Record<ProviderKind, ModelSlug> = {
25 codex: "gpt-5.3-codex",
26 claudeCode: "claude-sonnet-4-5",
27};
28
29// Backward compatibility for existing Codex-only call sites.
30export const MODEL_OPTIONS = MODEL_OPTIONS_BY_PROVIDER.codex;
31export const DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex;
32
33export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string, ModelSlug>> = {
34 codex: {
35 "5.3": "gpt-5.3-codex",
36 "gpt-5.3": "gpt-5.3-codex",
37 "5.3-spark": "gpt-5.3-codex-spark",
38 "gpt-5.3-spark": "gpt-5.3-codex-spark",
39 },
40 claudeCode: {
41 sonnet: "claude-sonnet-4-5",
42 "sonnet-4.5": "claude-sonnet-4-5",
43 "claude-sonnet-4.5": "claude-sonnet-4-5",
44 opus: "claude-opus-4-1",
45 "opus-4.1": "claude-opus-4-1",
46 "claude-opus-4.1": "claude-opus-4-1",
47 haiku: "claude-haiku-3-5",
48 "haiku-3.5": "claude-haiku-3-5",
49 "claude-haiku-3.5": "claude-haiku-3-5",
50 },
51};
52
53const MODEL_SLUG_SET_BY_PROVIDER: Record<ProviderKind, ReadonlySet<ModelSlug>> = {
54 codex: new Set(MODEL_OPTIONS_BY_PROVIDER.codex.map((option) => option.slug)),
55 claudeCode: new Set(MODEL_OPTIONS_BY_PROVIDER.claudeCode.map((option) => option.slug)),
18};56};
1957
20export function normalizeModelSlug(model: string | null | undefined): ModelSlug | null {58export function getModelOptions(provider: ProviderKind = "codex") {
59 return MODEL_OPTIONS_BY_PROVIDER[provider];
60}
61
62export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug {
63 return DEFAULT_MODEL_BY_PROVIDER[provider];
64}
65
66export function normalizeModelSlug(
67 model: string | null | undefined,
68 provider: ProviderKind = "codex",
69): ModelSlug | null {
21 if (typeof model !== "string") {70 if (typeof model !== "string") {
22 return null;71 return null;
23 }72 }
...@@ -27,12 +76,28 @@ export function normalizeModelSlug(model: string | null | undefined): ModelSlug...@@ -27,12 +76,28 @@ export function normalizeModelSlug(model: string | null | undefined): ModelSlug
27 return null;76 return null;
28 }77 }
2978
30 return MODEL_SLUG_ALIASES[trimmed] ?? trimmed;79 return MODEL_SLUG_ALIASES_BY_PROVIDER[provider][trimmed] ?? (trimmed as ModelSlug);
80}
81
82export function resolveModelSlug(
83 model: string | null | undefined,
84 provider: ProviderKind = "codex",
85): ModelSlug {
86 const normalized = normalizeModelSlug(model, provider);
87 if (!normalized) {
88 return DEFAULT_MODEL_BY_PROVIDER[provider];
89 }
90
91 return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized)
92 ? normalized
93 : DEFAULT_MODEL_BY_PROVIDER[provider];
31}94}
3295
33export function resolveModelSlug(model: string | null | undefined): ModelSlug {96export function resolveModelSlugForProvider(
34 const normalized = normalizeModelSlug(model);97 provider: ProviderKind,
35 return normalized ?? DEFAULT_MODEL;98 model: string | null | undefined,
99): ModelSlug {
100 return resolveModelSlug(model, provider);
36}101}
37102
38export const REASONING_OPTIONS = ["xhigh", "high", "medium", "low"] as const;103export const REASONING_OPTIONS = ["xhigh", "high", "medium", "low"] as const;