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 @@...@@ -1,5 +1,4 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import type { ProviderKind } from "./orchestration";
32
4export const CURSOR_REASONING_OPTIONS = ["low", "normal", "high", "xhigh"] as const;3export const CURSOR_REASONING_OPTIONS = ["low", "normal", "high", "xhigh"] as const;
5export type CursorReasoningOption = (typeof CURSOR_REASONING_OPTIONS)[number];4export type CursorReasoningOption = (typeof CURSOR_REASONING_OPTIONS)[number];
...@@ -58,11 +57,6 @@ export const MODEL_OPTIONS_BY_PROVIDER = {...@@ -58,11 +57,6 @@ export const MODEL_OPTIONS_BY_PROVIDER = {
58 { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" },57 { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
59 { slug: "gpt-5.2", name: "GPT-5.2" },58 { slug: "gpt-5.2", name: "GPT-5.2" },
60 ],59 ],
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 ],
66 cursor: [60 cursor: [
67 { slug: "auto", name: "Auto" },61 { slug: "auto", name: "Auto" },
68 { slug: "composer-1.5", name: "Composer 1.5" },62 { slug: "composer-1.5", name: "Composer 1.5" },
...@@ -84,23 +78,23 @@ export const MODEL_OPTIONS_BY_PROVIDER = {...@@ -84,23 +78,23 @@ export const MODEL_OPTIONS_BY_PROVIDER = {
84 { slug: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" },78 { slug: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" },
85 { slug: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },79 { slug: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
86 ],80 ],
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"];
90export type ModelSlug = BuiltInModelSlug | (string & {});85export type ModelSlug = BuiltInModelSlug | (string & {});
91export type CursorModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)["cursor"][number]["slug"];86export 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 = {
94 codex: "gpt-5.3-codex",89 codex: "gpt-5.3-codex",
95 claudeCode: "claude-sonnet-4-6",
96 cursor: "opus-4.6-thinking",90 cursor: "opus-4.6-thinking",
97};91} as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, ModelSlug>;
9892
99// Backward compatibility for existing Codex-only call sites.93// Backward compatibility for existing Codex-only call sites.
100export const MODEL_OPTIONS = MODEL_OPTIONS_BY_PROVIDER.codex;94export const MODEL_OPTIONS = MODEL_OPTIONS_BY_PROVIDER.codex;
101export const DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex;95export 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 = {
104 codex: {98 codex: {
105 "5.4": "gpt-5.4",99 "5.4": "gpt-5.4",
106 "5.3": "gpt-5.3-codex",100 "5.3": "gpt-5.3-codex",
...@@ -108,20 +102,6 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string,...@@ -108,20 +102,6 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string,
108 "5.3-spark": "gpt-5.3-codex-spark",102 "5.3-spark": "gpt-5.3-codex-spark",
109 "gpt-5.3-spark": "gpt-5.3-codex-spark",103 "gpt-5.3-spark": "gpt-5.3-codex-spark",
110 },104 },
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 },
125 cursor: {105 cursor: {
126 composer: "composer-1.5",106 composer: "composer-1.5",
127 "composer-1.5": "composer-1.5",107 "composer-1.5": "composer-1.5",
...@@ -136,16 +116,17 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string,...@@ -136,16 +116,17 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string,
136 "opus-4.6-thinking": "opus-4.6-thinking",116 "opus-4.6-thinking": "opus-4.6-thinking",
137 "opus-4.5-thinking": "opus-4.5-thinking",117 "opus-4.5-thinking": "opus-4.5-thinking",
138 },118 },
139};119} as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, Record<string, ModelSlug>>;
140120
141export const REASONING_EFFORT_OPTIONS_BY_PROVIDER = {121export const REASONING_EFFORT_OPTIONS_BY_PROVIDER = {
142 codex: CODEX_REASONING_EFFORT_OPTIONS,122 codex: CODEX_REASONING_EFFORT_OPTIONS,
143 claudeCode: [],
144 cursor: [],123 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
147export const DEFAULT_REASONING_EFFORT_BY_PROVIDER = {129export const DEFAULT_REASONING_EFFORT_BY_PROVIDER = {
148 codex: "high",130 codex: "high",
149 claudeCode: null,
150 cursor: null,131 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 {...@@ -4,10 +4,8 @@ import {
4 CURSOR_REASONING_OPTIONS,4 CURSOR_REASONING_OPTIONS,
5 DEFAULT_MODEL,5 DEFAULT_MODEL,
6 DEFAULT_MODEL_BY_PROVIDER,6 DEFAULT_MODEL_BY_PROVIDER,
7 DEFAULT_REASONING_EFFORT_BY_PROVIDER,
8 MODEL_OPTIONS,7 MODEL_OPTIONS,
9 MODEL_OPTIONS_BY_PROVIDER,8 MODEL_OPTIONS_BY_PROVIDER,
10 REASONING_EFFORT_OPTIONS_BY_PROVIDER,
11} from "../../contracts/src";9} from "../../contracts/src";
1210
13import {11import {
...@@ -42,11 +40,6 @@ describe("normalizeModelSlug", () => {...@@ -42,11 +40,6 @@ describe("normalizeModelSlug", () => {
42 });40 });
4341
44 it("uses provider-specific aliases", () => {42 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 );
50 expect(normalizeModelSlug("composer", "cursor")).toBe("composer-1.5");43 expect(normalizeModelSlug("composer", "cursor")).toBe("composer-1.5");
51 expect(normalizeModelSlug("gpt-5.3-codex-spark", "cursor")).toBe(44 expect(normalizeModelSlug("gpt-5.3-codex-spark", "cursor")).toBe(
52 "gpt-5.3-codex-spark-preview",45 "gpt-5.3-codex-spark-preview",
...@@ -76,13 +69,6 @@ describe("resolveModelSlug", () => {...@@ -76,13 +69,6 @@ describe("resolveModelSlug", () => {
76 });69 });
7770
78 it("supports provider-aware resolution", () => {71 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 );
86 expect(resolveModelSlugForProvider("cursor", undefined)).toBe(72 expect(resolveModelSlugForProvider("cursor", undefined)).toBe(
87 DEFAULT_MODEL_BY_PROVIDER.cursor,73 DEFAULT_MODEL_BY_PROVIDER.cursor,
88 );74 );
...@@ -98,7 +84,7 @@ describe("resolveModelSlug", () => {...@@ -98,7 +84,7 @@ describe("resolveModelSlug", () => {
98 it("keeps codex defaults for backward compatibility", () => {84 it("keeps codex defaults for backward compatibility", () => {
99 expect(getDefaultModel()).toBe(DEFAULT_MODEL);85 expect(getDefaultModel()).toBe(DEFAULT_MODEL);
100 expect(getModelOptions()).toEqual(MODEL_OPTIONS);86 expect(getModelOptions()).toEqual(MODEL_OPTIONS);
101 expect(getModelOptions("claudeCode")).toEqual(MODEL_OPTIONS_BY_PROVIDER.claudeCode);87 expect(getModelOptions("claudeCode")).toEqual([]);
102 expect(getModelOptions("cursor")).toEqual(MODEL_OPTIONS_BY_PROVIDER.cursor);88 expect(getModelOptions("cursor")).toEqual(MODEL_OPTIONS_BY_PROVIDER.cursor);
103 expect(getCursorModelFamilyOptions()).toEqual(CURSOR_MODEL_FAMILY_OPTIONS);89 expect(getCursorModelFamilyOptions()).toEqual(CURSOR_MODEL_FAMILY_OPTIONS);
104 });90 });
...@@ -151,13 +137,7 @@ describe("cursor model selection", () => {...@@ -151,13 +137,7 @@ describe("cursor model selection", () => {
151137
152describe("getReasoningEffortOptions", () => {138describe("getReasoningEffortOptions", () => {
153 it("returns codex reasoning options for codex", () => {139 it("returns codex reasoning options for codex", () => {
154 expect(getReasoningEffortOptions("codex")).toEqual(140 expect(getReasoningEffortOptions("codex")).toEqual(["xhigh", "high", "medium", "low"]);
155 REASONING_EFFORT_OPTIONS_BY_PROVIDER.codex,
156 );
157 });
158
159 it("returns no reasoning options for claudeCode", () => {
160 expect(getReasoningEffortOptions("claudeCode")).toEqual([]);
161 });141 });
162142
163 it("returns no reasoning options for cursor", () => {143 it("returns no reasoning options for cursor", () => {
...@@ -167,14 +147,7 @@ describe("getReasoningEffortOptions", () => {...@@ -167,14 +147,7 @@ describe("getReasoningEffortOptions", () => {
167147
168describe("getDefaultReasoningEffort", () => {148describe("getDefaultReasoningEffort", () => {
169 it("returns provider-scoped defaults", () => {149 it("returns provider-scoped defaults", () => {
170 expect(getDefaultReasoningEffort("codex")).toBe(150 expect(getDefaultReasoningEffort("codex")).toBe("high");
171 DEFAULT_REASONING_EFFORT_BY_PROVIDER.codex,151 expect(getDefaultReasoningEffort("cursor")).toBeNull();
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 );
179 });152 });
180});153});
packages/shared/src/model.ts+24-13
...@@ -2,11 +2,10 @@ import {...@@ -2,11 +2,10 @@ import {
2 CODEX_REASONING_EFFORT_OPTIONS,2 CODEX_REASONING_EFFORT_OPTIONS,
3 CURSOR_MODEL_FAMILY_OPTIONS,3 CURSOR_MODEL_FAMILY_OPTIONS,
4 CURSOR_REASONING_OPTIONS,4 CURSOR_REASONING_OPTIONS,
5 DEFAULT_MODEL,
5 DEFAULT_MODEL_BY_PROVIDER,6 DEFAULT_MODEL_BY_PROVIDER,
6 DEFAULT_REASONING_EFFORT_BY_PROVIDER,
7 MODEL_OPTIONS_BY_PROVIDER,7 MODEL_OPTIONS_BY_PROVIDER,
8 MODEL_SLUG_ALIASES_BY_PROVIDER,8 MODEL_SLUG_ALIASES_BY_PROVIDER,
9 REASONING_EFFORT_OPTIONS_BY_PROVIDER,
10 type CodexReasoningEffort,9 type CodexReasoningEffort,
11 type CursorModelFamily,10 type CursorModelFamily,
12 type CursorModelSlug,11 type CursorModelSlug,
...@@ -15,6 +14,8 @@ import {...@@ -15,6 +14,8 @@ import {
15 type ProviderKind,14 type ProviderKind,
16} from "../../contracts/src";15} from "../../contracts/src";
1716
17type CatalogProvider = keyof typeof MODEL_OPTIONS_BY_PROVIDER;
18
18type CursorModelCapability = {19type CursorModelCapability = {
19 readonly supportsReasoning: boolean;20 readonly supportsReasoning: boolean;
20 readonly supportsFast: boolean;21 readonly supportsFast: boolean;
...@@ -89,8 +90,11 @@ const CURSOR_MODEL_CAPABILITY_BY_FAMILY: Record<CursorModelFamily, CursorModelCa...@@ -89,8 +90,11 @@ const CURSOR_MODEL_CAPABILITY_BY_FAMILY: Record<CursorModelFamily, CursorModelCa
89 },90 },
90};91};
9192
92const MODEL_SLUG_SET_BY_PROVIDER: Record<ProviderKind, ReadonlySet<ModelSlug>> = {93function hasModelCatalog(provider: ProviderKind): provider is CatalogProvider {
93 claudeCode: new Set(MODEL_OPTIONS_BY_PROVIDER.claudeCode.map((option) => option.slug)),94 return Object.hasOwn(MODEL_OPTIONS_BY_PROVIDER, provider);
95}
96
97const MODEL_SLUG_SET_BY_PROVIDER: Record<CatalogProvider, ReadonlySet<ModelSlug>> = {
94 codex: new Set(MODEL_OPTIONS_BY_PROVIDER.codex.map((option) => option.slug)),98 codex: new Set(MODEL_OPTIONS_BY_PROVIDER.codex.map((option) => option.slug)),
95 cursor: new Set(MODEL_OPTIONS_BY_PROVIDER.cursor.map((option) => option.slug)),99 cursor: new Set(MODEL_OPTIONS_BY_PROVIDER.cursor.map((option) => option.slug)),
96};100};
...@@ -107,7 +111,7 @@ export interface CursorModelSelection {...@@ -107,7 +111,7 @@ export interface CursorModelSelection {
107}111}
108112
109export function getModelOptions(provider: ProviderKind = "codex") {113export function getModelOptions(provider: ProviderKind = "codex") {
110 return MODEL_OPTIONS_BY_PROVIDER[provider];114 return hasModelCatalog(provider) ? MODEL_OPTIONS_BY_PROVIDER[provider] : [];
111}115}
112116
113export function getCursorModelFamilyOptions() {117export function getCursorModelFamilyOptions() {
...@@ -223,7 +227,7 @@ export function resolveCursorModelFromSelection(input: {...@@ -223,7 +227,7 @@ export function resolveCursorModelFromSelection(input: {
223}227}
224228
225export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug {229export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug {
226 return DEFAULT_MODEL_BY_PROVIDER[provider];230 return hasModelCatalog(provider) ? DEFAULT_MODEL_BY_PROVIDER[provider] : DEFAULT_MODEL;
227}231}
228232
229export function normalizeModelSlug(233export function normalizeModelSlug(
...@@ -239,7 +243,12 @@ export function normalizeModelSlug(...@@ -239,7 +243,12 @@ export function normalizeModelSlug(
239 return null;243 return null;
240 }244 }
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);
243}252}
244253
245export function resolveModelSlug(254export function resolveModelSlug(
...@@ -248,12 +257,14 @@ export function resolveModelSlug(...@@ -248,12 +257,14 @@ export function resolveModelSlug(
248): ModelSlug {257): ModelSlug {
249 const normalized = normalizeModelSlug(model, provider);258 const normalized = normalizeModelSlug(model, provider);
250 if (!normalized) {259 if (!normalized) {
251 return DEFAULT_MODEL_BY_PROVIDER[provider];260 return getDefaultModel(provider);
261 }
262
263 if (!hasModelCatalog(provider)) {
264 return getDefaultModel(provider);
252 }265 }
253266
254 return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized)267 return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized) ? normalized : getDefaultModel(provider);
255 ? normalized
256 : DEFAULT_MODEL_BY_PROVIDER[provider];
257}268}
258269
259export function resolveModelSlugForProvider(270export function resolveModelSlugForProvider(
...@@ -266,7 +277,7 @@ export function resolveModelSlugForProvider(...@@ -266,7 +277,7 @@ export function resolveModelSlugForProvider(
266export function getReasoningEffortOptions(277export function getReasoningEffortOptions(
267 provider: ProviderKind = "codex",278 provider: ProviderKind = "codex",
268): ReadonlyArray<CodexReasoningEffort> {279): ReadonlyArray<CodexReasoningEffort> {
269 return REASONING_EFFORT_OPTIONS_BY_PROVIDER[provider];280 return provider === "codex" ? CODEX_REASONING_EFFORT_OPTIONS : [];
270}281}
271282
272export function getDefaultReasoningEffort(provider: "codex"): CodexReasoningEffort;283export function getDefaultReasoningEffort(provider: "codex"): CodexReasoningEffort;
...@@ -274,7 +285,7 @@ export function getDefaultReasoningEffort(provider: ProviderKind): CodexReasonin...@@ -274,7 +285,7 @@ export function getDefaultReasoningEffort(provider: ProviderKind): CodexReasonin
274export function getDefaultReasoningEffort(285export function getDefaultReasoningEffort(
275 provider: ProviderKind = "codex",286 provider: ProviderKind = "codex",
276): CodexReasoningEffort | null {287): CodexReasoningEffort | null {
277 return DEFAULT_REASONING_EFFORT_BY_PROVIDER[provider];288 return provider === "codex" ? "high" : null;
278}289}
279290
280export { CODEX_REASONING_EFFORT_OPTIONS };291export { CODEX_REASONING_EFFORT_OPTIONS };