Skip to content

Commit 03beb30

Browse files
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
1 parent 041acf1 commit 03beb30

File tree

2 files changed

+116
-23
lines changed

2 files changed

+116
-23
lines changed

packages/contracts/src/model.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { describe, expect, it } from "vitest";
22

3-
import { DEFAULT_MODEL, MODEL_OPTIONS, normalizeModelSlug, resolveModelSlug } from "./model";
3+
import {
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

515
describe("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

2439
describe("resolveModelSlug", () => {
@@ -32,4 +47,20 @@ describe("resolveModelSlug", () => {
3247
expect(resolveModelSlug(model.slug)).toBe(model.slug);
3348
}
3449
});
50+
51+
it("supports provider-aware resolution", () => {
52+
expect(resolveModelSlugForProvider("claudeCode", undefined)).toBe(
53+
DEFAULT_MODEL_BY_PROVIDER.claudeCode,
54+
);
55+
expect(resolveModelSlugForProvider("claudeCode", "sonnet")).toBe("claude-sonnet-4-5");
56+
expect(resolveModelSlugForProvider("claudeCode", "gpt-5.3-codex")).toBe(
57+
DEFAULT_MODEL_BY_PROVIDER.claudeCode,
58+
);
59+
});
60+
61+
it("keeps codex defaults for backward compatibility", () => {
62+
expect(getDefaultModel()).toBe(DEFAULT_MODEL);
63+
expect(getModelOptions()).toEqual(MODEL_OPTIONS);
64+
expect(getModelOptions("claudeCode")).toEqual(MODEL_OPTIONS_BY_PROVIDER.claudeCode);
65+
});
3566
});

packages/contracts/src/model.ts

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,72 @@
1-
export 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-
8-
export type ModelSlug = (typeof MODEL_OPTIONS)[number]["slug"];
9-
10-
export const DEFAULT_MODEL = "gpt-5.3-codex";
11-
12-
export const MODEL_SLUG_ALIASES: Record<string, ModelSlug> = {
13-
"5.3": "gpt-5.3-codex",
14-
"gpt-5.3": "gpt-5.3-codex",
15-
"5.3-spark": "gpt-5.3-codex-spark",
16-
"gpt-5.3-spark": "gpt-5.3-codex-spark",
1+
import type { ProviderKind } from "./orchestration";
2+
3+
type ModelOption = {
4+
readonly slug: string;
5+
readonly name: string;
6+
};
7+
8+
export 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+
22+
export type ModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)[ProviderKind][number]["slug"];
23+
24+
export 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.
30+
export const MODEL_OPTIONS = MODEL_OPTIONS_BY_PROVIDER.codex;
31+
export const DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex;
32+
33+
export 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+
},
1751
};
1852

19-
export function normalizeModelSlug(model: string | null | undefined): ModelSlug | null {
53+
const 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)),
56+
};
57+
58+
export function getModelOptions(provider: ProviderKind = "codex") {
59+
return MODEL_OPTIONS_BY_PROVIDER[provider];
60+
}
61+
62+
export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug {
63+
return DEFAULT_MODEL_BY_PROVIDER[provider];
64+
}
65+
66+
export function normalizeModelSlug(
67+
model: string | null | undefined,
68+
provider: ProviderKind = "codex",
69+
): ModelSlug | null {
2070
if (typeof model !== "string") {
2171
return null;
2272
}
@@ -26,16 +76,28 @@ export function normalizeModelSlug(model: string | null | undefined): ModelSlug
2676
return null;
2777
}
2878

29-
return MODEL_SLUG_ALIASES[trimmed] ?? (trimmed as ModelSlug);
79+
return MODEL_SLUG_ALIASES_BY_PROVIDER[provider][trimmed] ?? (trimmed as ModelSlug);
3080
}
3181

32-
export function resolveModelSlug(model: string | null | undefined): ModelSlug {
33-
const normalized = normalizeModelSlug(model);
82+
export function resolveModelSlug(
83+
model: string | null | undefined,
84+
provider: ProviderKind = "codex",
85+
): ModelSlug {
86+
const normalized = normalizeModelSlug(model, provider);
3487
if (!normalized) {
35-
return DEFAULT_MODEL;
88+
return DEFAULT_MODEL_BY_PROVIDER[provider];
3689
}
3790

38-
return MODEL_OPTIONS.some((option) => option.slug === normalized) ? normalized : DEFAULT_MODEL;
91+
return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized)
92+
? normalized
93+
: DEFAULT_MODEL_BY_PROVIDER[provider];
94+
}
95+
96+
export function resolveModelSlugForProvider(
97+
provider: ProviderKind,
98+
model: string | null | undefined,
99+
): ModelSlug {
100+
return resolveModelSlug(model, provider);
39101
}
40102

41103
export const REASONING_OPTIONS = ["xhigh", "high", "medium", "low"] as const;

0 commit comments

Comments
 (0)