authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-05 21:16:30-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
loge972fad30075aeff9831f38052ef05badabd236f
tree08b832a973c333feff735bd29eda1456d272913a
parent1bd4d6d953a01232a049485612d40faa1a48afa8

model: move Claude catalogs out of core

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 40 insertions(+), 75 deletions(-)

packages/contracts/src/model.ts+12-31
......@@ -1,5 +1,4 @@
11import { Schema } from "effect";
2import type { ProviderKind } from "./orchestration";
32
43export const CURSOR_REASONING_OPTIONS = ["low", "normal", "high", "xhigh"] as const;
54export type CursorReasoningOption = (typeof CURSOR_REASONING_OPTIONS)[number];
......@@ -58,11 +57,6 @@ export const MODEL_OPTIONS_BY_PROVIDER = {
5857 { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
5958 { slug: "gpt-5.2", name: "GPT-5.2" },
6059 ],
61 claudeCode: [
62 { slug: "claude-opus-4-6", name: "Claude Opus 4.6" },
63 { slug: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
64 { slug: "claude-haiku-4-5", name: "Claude Haiku 4.5" },
65 ],
6660 cursor: [
6761 { slug: "auto", name: "Auto" },
6862 { slug: "composer-1.5", name: "Composer 1.5" },
......@@ -84,23 +78,23 @@ export const MODEL_OPTIONS_BY_PROVIDER = {
8478 { slug: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" },
8579 { slug: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
8680 ],
87} as const satisfies Record<ProviderKind, readonly ModelOption[]>;
81} as const satisfies Record<"codex" | "cursor", readonly ModelOption[]>;
8882
89type BuiltInModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)[ProviderKind][number]["slug"];
83type BuiltInModelSlug =
84 (typeof MODEL_OPTIONS_BY_PROVIDER)[keyof typeof MODEL_OPTIONS_BY_PROVIDER][number]["slug"];
9085export type ModelSlug = BuiltInModelSlug | (string & {});
9186export type CursorModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)["cursor"][number]["slug"];
9287
93export const DEFAULT_MODEL_BY_PROVIDER: Record<ProviderKind, ModelSlug> = {
88export const DEFAULT_MODEL_BY_PROVIDER = {
9489 codex: "gpt-5.3-codex",
95 claudeCode: "claude-sonnet-4-6",
9690 cursor: "opus-4.6-thinking",
97};
91} as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, ModelSlug>;
9892
9993// Backward compatibility for existing Codex-only call sites.
10094export const MODEL_OPTIONS = MODEL_OPTIONS_BY_PROVIDER.codex;
10195export const DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex;
10296
103export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string, ModelSlug>> = {
97export const MODEL_SLUG_ALIASES_BY_PROVIDER = {
10498 codex: {
10599 "5.4": "gpt-5.4",
106100 "5.3": "gpt-5.3-codex",
......@@ -108,20 +102,6 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string,
108102 "5.3-spark": "gpt-5.3-codex-spark",
109103 "gpt-5.3-spark": "gpt-5.3-codex-spark",
110104 },
111 claudeCode: {
112 opus: "claude-opus-4-6",
113 "opus-4.6": "claude-opus-4-6",
114 "claude-opus-4.6": "claude-opus-4-6",
115 "claude-opus-4-6-20251117": "claude-opus-4-6",
116 sonnet: "claude-sonnet-4-6",
117 "sonnet-4.6": "claude-sonnet-4-6",
118 "claude-sonnet-4.6": "claude-sonnet-4-6",
119 "claude-sonnet-4-6-20251117": "claude-sonnet-4-6",
120 haiku: "claude-haiku-4-5",
121 "haiku-4.5": "claude-haiku-4-5",
122 "claude-haiku-4.5": "claude-haiku-4-5",
123 "claude-haiku-4-5-20251001": "claude-haiku-4-5",
124 },
125105 cursor: {
126106 composer: "composer-1.5",
127107 "composer-1.5": "composer-1.5",
......@@ -136,16 +116,17 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string,
136116 "opus-4.6-thinking": "opus-4.6-thinking",
137117 "opus-4.5-thinking": "opus-4.5-thinking",
138118 },
139};
119} as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, Record<string, ModelSlug>>;
140120
141121export const REASONING_EFFORT_OPTIONS_BY_PROVIDER = {
142122 codex: CODEX_REASONING_EFFORT_OPTIONS,
143 claudeCode: [],
144123 cursor: [],
145} as const satisfies Record<ProviderKind, readonly CodexReasoningEffort[]>;
124} as const satisfies Record<
125 keyof typeof MODEL_OPTIONS_BY_PROVIDER,
126 readonly CodexReasoningEffort[]
127>;
146128
147129export const DEFAULT_REASONING_EFFORT_BY_PROVIDER = {
148130 codex: "high",
149 claudeCode: null,
150131 cursor: null,
151} as const satisfies Record<ProviderKind, CodexReasoningEffort | null>;
132} as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, CodexReasoningEffort | null>;
packages/shared/src/model.test.ts+4-31
......@@ -4,10 +4,8 @@ import {
44 CURSOR_REASONING_OPTIONS,
55 DEFAULT_MODEL,
66 DEFAULT_MODEL_BY_PROVIDER,
7 DEFAULT_REASONING_EFFORT_BY_PROVIDER,
87 MODEL_OPTIONS,
98 MODEL_OPTIONS_BY_PROVIDER,
10 REASONING_EFFORT_OPTIONS_BY_PROVIDER,
119} from "../../contracts/src";
1210
1311import {
......@@ -42,11 +40,6 @@ describe("normalizeModelSlug", () => {
4240 });
4341
4442 it("uses provider-specific aliases", () => {
45 expect(normalizeModelSlug("sonnet", "claudeCode")).toBe("claude-sonnet-4-6");
46 expect(normalizeModelSlug("opus-4.6", "claudeCode")).toBe("claude-opus-4-6");
47 expect(normalizeModelSlug("claude-haiku-4-5-20251001", "claudeCode")).toBe(
48 "claude-haiku-4-5",
49 );
5043 expect(normalizeModelSlug("composer", "cursor")).toBe("composer-1.5");
5144 expect(normalizeModelSlug("gpt-5.3-codex-spark", "cursor")).toBe(
5245 "gpt-5.3-codex-spark-preview",
......@@ -76,13 +69,6 @@ describe("resolveModelSlug", () => {
7669 });
7770
7871 it("supports provider-aware resolution", () => {
79 expect(resolveModelSlugForProvider("claudeCode", undefined)).toBe(
80 DEFAULT_MODEL_BY_PROVIDER.claudeCode,
81 );
82 expect(resolveModelSlugForProvider("claudeCode", "sonnet")).toBe("claude-sonnet-4-6");
83 expect(resolveModelSlugForProvider("claudeCode", "gpt-5.3-codex")).toBe(
84 DEFAULT_MODEL_BY_PROVIDER.claudeCode,
85 );
8672 expect(resolveModelSlugForProvider("cursor", undefined)).toBe(
8773 DEFAULT_MODEL_BY_PROVIDER.cursor,
8874 );
......@@ -98,7 +84,7 @@ describe("resolveModelSlug", () => {
9884 it("keeps codex defaults for backward compatibility", () => {
9985 expect(getDefaultModel()).toBe(DEFAULT_MODEL);
10086 expect(getModelOptions()).toEqual(MODEL_OPTIONS);
101 expect(getModelOptions("claudeCode")).toEqual(MODEL_OPTIONS_BY_PROVIDER.claudeCode);
87 expect(getModelOptions("claudeCode")).toEqual([]);
10288 expect(getModelOptions("cursor")).toEqual(MODEL_OPTIONS_BY_PROVIDER.cursor);
10389 expect(getCursorModelFamilyOptions()).toEqual(CURSOR_MODEL_FAMILY_OPTIONS);
10490 });
......@@ -151,13 +137,7 @@ describe("cursor model selection", () => {
151137
152138describe("getReasoningEffortOptions", () => {
153139 it("returns codex reasoning options for codex", () => {
154 expect(getReasoningEffortOptions("codex")).toEqual(
155 REASONING_EFFORT_OPTIONS_BY_PROVIDER.codex,
156 );
157 });
158
159 it("returns no reasoning options for claudeCode", () => {
160 expect(getReasoningEffortOptions("claudeCode")).toEqual([]);
140 expect(getReasoningEffortOptions("codex")).toEqual(["xhigh", "high", "medium", "low"]);
161141 });
162142
163143 it("returns no reasoning options for cursor", () => {
......@@ -167,14 +147,7 @@ describe("getReasoningEffortOptions", () => {
167147
168148describe("getDefaultReasoningEffort", () => {
169149 it("returns provider-scoped defaults", () => {
170 expect(getDefaultReasoningEffort("codex")).toBe(
171 DEFAULT_REASONING_EFFORT_BY_PROVIDER.codex,
172 );
173 expect(getDefaultReasoningEffort("claudeCode")).toBe(
174 DEFAULT_REASONING_EFFORT_BY_PROVIDER.claudeCode,
175 );
176 expect(getDefaultReasoningEffort("cursor")).toBe(
177 DEFAULT_REASONING_EFFORT_BY_PROVIDER.cursor,
178 );
150 expect(getDefaultReasoningEffort("codex")).toBe("high");
151 expect(getDefaultReasoningEffort("cursor")).toBeNull();
179152 });
180153});
packages/shared/src/model.ts+24-13
......@@ -2,11 +2,10 @@ import {
22 CODEX_REASONING_EFFORT_OPTIONS,
33 CURSOR_MODEL_FAMILY_OPTIONS,
44 CURSOR_REASONING_OPTIONS,
5 DEFAULT_MODEL,
56 DEFAULT_MODEL_BY_PROVIDER,
6 DEFAULT_REASONING_EFFORT_BY_PROVIDER,
77 MODEL_OPTIONS_BY_PROVIDER,
88 MODEL_SLUG_ALIASES_BY_PROVIDER,
9 REASONING_EFFORT_OPTIONS_BY_PROVIDER,
109 type CodexReasoningEffort,
1110 type CursorModelFamily,
1211 type CursorModelSlug,
......@@ -15,6 +14,8 @@ import {
1514 type ProviderKind,
1615} from "../../contracts/src";
1716
17type CatalogProvider = keyof typeof MODEL_OPTIONS_BY_PROVIDER;
18
1819type CursorModelCapability = {
1920 readonly supportsReasoning: boolean;
2021 readonly supportsFast: boolean;
......@@ -89,8 +90,11 @@ const CURSOR_MODEL_CAPABILITY_BY_FAMILY: Record<CursorModelFamily, CursorModelCa
8990 },
9091};
9192
92const MODEL_SLUG_SET_BY_PROVIDER: Record<ProviderKind, ReadonlySet<ModelSlug>> = {
93 claudeCode: new Set(MODEL_OPTIONS_BY_PROVIDER.claudeCode.map((option) => option.slug)),
93function hasModelCatalog(provider: ProviderKind): provider is CatalogProvider {
94 return Object.hasOwn(MODEL_OPTIONS_BY_PROVIDER, provider);
95}
96
97const MODEL_SLUG_SET_BY_PROVIDER: Record<CatalogProvider, ReadonlySet<ModelSlug>> = {
9498 codex: new Set(MODEL_OPTIONS_BY_PROVIDER.codex.map((option) => option.slug)),
9599 cursor: new Set(MODEL_OPTIONS_BY_PROVIDER.cursor.map((option) => option.slug)),
96100};
......@@ -107,7 +111,7 @@ export interface CursorModelSelection {
107111}
108112
109113export function getModelOptions(provider: ProviderKind = "codex") {
110 return MODEL_OPTIONS_BY_PROVIDER[provider];
114 return hasModelCatalog(provider) ? MODEL_OPTIONS_BY_PROVIDER[provider] : [];
111115}
112116
113117export function getCursorModelFamilyOptions() {
......@@ -223,7 +227,7 @@ export function resolveCursorModelFromSelection(input: {
223227}
224228
225229export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug {
226 return DEFAULT_MODEL_BY_PROVIDER[provider];
230 return hasModelCatalog(provider) ? DEFAULT_MODEL_BY_PROVIDER[provider] : DEFAULT_MODEL;
227231}
228232
229233export function normalizeModelSlug(
......@@ -239,7 +243,12 @@ export function normalizeModelSlug(
239243 return null;
240244 }
241245
242 return MODEL_SLUG_ALIASES_BY_PROVIDER[provider][trimmed] ?? (trimmed as ModelSlug);
246 if (!hasModelCatalog(provider)) {
247 return trimmed as ModelSlug;
248 }
249
250 const aliases = MODEL_SLUG_ALIASES_BY_PROVIDER[provider] as Record<string, ModelSlug>;
251 return aliases[trimmed] ?? (trimmed as ModelSlug);
243252}
244253
245254export function resolveModelSlug(
......@@ -248,12 +257,14 @@ export function resolveModelSlug(
248257): ModelSlug {
249258 const normalized = normalizeModelSlug(model, provider);
250259 if (!normalized) {
251 return DEFAULT_MODEL_BY_PROVIDER[provider];
260 return getDefaultModel(provider);
261 }
262
263 if (!hasModelCatalog(provider)) {
264 return getDefaultModel(provider);
252265 }
253266
254 return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized)
255 ? normalized
256 : DEFAULT_MODEL_BY_PROVIDER[provider];
267 return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized) ? normalized : getDefaultModel(provider);
257268}
258269
259270export function resolveModelSlugForProvider(
......@@ -266,7 +277,7 @@ export function resolveModelSlugForProvider(
266277export function getReasoningEffortOptions(
267278 provider: ProviderKind = "codex",
268279): ReadonlyArray<CodexReasoningEffort> {
269 return REASONING_EFFORT_OPTIONS_BY_PROVIDER[provider];
280 return provider === "codex" ? CODEX_REASONING_EFFORT_OPTIONS : [];
270281}
271282
272283export function getDefaultReasoningEffort(provider: "codex"): CodexReasoningEffort;
......@@ -274,7 +285,7 @@ export function getDefaultReasoningEffort(provider: ProviderKind): CodexReasonin
274285export function getDefaultReasoningEffort(
275286 provider: ProviderKind = "codex",
276287): CodexReasoningEffort | null {
277 return DEFAULT_REASONING_EFFORT_BY_PROVIDER[provider];
288 return provider === "codex" ? "high" : null;
278289}
279290
280291export { CODEX_REASONING_EFFORT_OPTIONS };