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 @@
11import { 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
515describe("normalizeModelSlug", () => {
616 it("maps known aliases to canonical slugs", () => {
......@@ -19,6 +29,11 @@ describe("normalizeModelSlug", () => {
1929 expect(normalizeModelSlug("gpt-5.2")).toBe("gpt-5.2");
2030 expect(normalizeModelSlug("gpt-5.2-codex")).toBe("gpt-5.2-codex");
2131 });
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 });
2237});
2338
2439describe("resolveModelSlug", () => {
......@@ -37,4 +52,20 @@ describe("resolveModelSlug", () => {
3752 expect(resolveModelSlug(model.slug)).toBe(model.slug);
3853 }
3954 });
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 });
4071});
packages/contracts/src/model.ts+87-22
......@@ -1,23 +1,72 @@
1export const MODEL_OPTIONS = [
2 { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
3 { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
4 { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
5 { slug: "gpt-5.2", name: "GPT-5.2" },
6] as const;
7
8export type BuiltInModelSlug = (typeof MODEL_OPTIONS)[number]["slug"];
9export type ModelSlug = string;
10
11export const DEFAULT_MODEL = "gpt-5.3-codex";
12
13export const MODEL_SLUG_ALIASES: Record<string, BuiltInModelSlug> = {
14 "5.3": "gpt-5.3-codex",
15 "gpt-5.3": "gpt-5.3-codex",
16 "5.3-spark": "gpt-5.3-codex-spark",
17 "gpt-5.3-spark": "gpt-5.3-codex-spark",
1import type { ProviderKind } from "./orchestration";
2
3type ModelOption = {
4 readonly slug: string;
5 readonly name: string;
6};
7
8export const MODEL_OPTIONS_BY_PROVIDER = {
9 codex: [
10 { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
11 { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
12 { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
13 { slug: "gpt-5.2", name: "GPT-5.2" },
14 ],
15 claudeCode: [
16 { slug: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" },
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)),
1856};
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 {
2170 if (typeof model !== "string") {
2271 return null;
2372 }
......@@ -27,12 +76,28 @@ export function normalizeModelSlug(model: string | null | undefined): ModelSlug
2776 return null;
2877 }
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];
3194}
3295
33export function resolveModelSlug(model: string | null | undefined): ModelSlug {
34 const normalized = normalizeModelSlug(model);
35 return normalized ?? DEFAULT_MODEL;
96export function resolveModelSlugForProvider(
97 provider: ProviderKind,
98 model: string | null | undefined,
99): ModelSlug {
100 return resolveModelSlug(model, provider);
36101}
37102
38103export const REASONING_OPTIONS = ["xhigh", "high", "medium", "low"] as const;