authorgravatar for alex.vostres@gmail.comAlexander Vostres <alex.vostres@gmail.com> 2026-03-30 04:13:16+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-30 01:13:16+00:00
log792ad4b5c1ed226be33d1623de8cbaf828f9053d
tree76a57a5ed2343e91901a36df7618e1daae5e28dc
parentd41781987cea30ad3ff7d344292ecdfbf9325900
signature Signed by PGP key B5690EEEBB952194

Fix model settings getting stuck (#1538)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

12 files changed, 249 insertions(+), 93 deletions(-)

apps/server/src/git/Layers/ClaudeTextGeneration.ts+4-3
...@@ -29,8 +29,9 @@ import {...@@ -29,8 +29,9 @@ import {
29 sanitizeThreadTitle,29 sanitizeThreadTitle,
30 toJsonSchemaObject,30 toJsonSchemaObject,
31} from "../Utils.ts";31} from "../Utils.ts";
32import { normalizeClaudeModelOptions } from "../../provider/Layers/ClaudeProvider.ts";32import { normalizeClaudeModelOptionsWithCapabilities } from "@t3tools/shared/model";
33import { ServerSettingsService } from "../../serverSettings.ts";33import { ServerSettingsService } from "../../serverSettings.ts";
34import { getClaudeModelCapabilities } from "../../provider/Layers/ClaudeProvider.ts";
3435
35const CLAUDE_TIMEOUT_MS = 180_000;36const CLAUDE_TIMEOUT_MS = 180_000;
3637
...@@ -84,8 +85,8 @@ const makeClaudeTextGeneration = Effect.gen(function* () {...@@ -84,8 +85,8 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
84 }): Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]> =>85 }): Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]> =>
85 Effect.gen(function* () {86 Effect.gen(function* () {
86 const jsonSchemaStr = JSON.stringify(toJsonSchemaObject(outputSchemaJson));87 const jsonSchemaStr = JSON.stringify(toJsonSchemaObject(outputSchemaJson));
87 const normalizedOptions = normalizeClaudeModelOptions(88 const normalizedOptions = normalizeClaudeModelOptionsWithCapabilities(
88 modelSelection.model,89 getClaudeModelCapabilities(modelSelection.model),
89 modelSelection.options,90 modelSelection.options,
90 );91 );
91 const settings = {92 const settings = {
apps/server/src/git/Layers/CodexTextGeneration.ts+4-3
...@@ -28,8 +28,9 @@ import {...@@ -28,8 +28,9 @@ import {
28 sanitizeThreadTitle,28 sanitizeThreadTitle,
29 toJsonSchemaObject,29 toJsonSchemaObject,
30} from "../Utils.ts";30} from "../Utils.ts";
31import { normalizeCodexModelOptions } from "../../provider/Layers/CodexProvider.ts";31import { getCodexModelCapabilities } from "../../provider/Layers/CodexProvider.ts";
32import { ServerSettingsService } from "../../serverSettings.ts";32import { ServerSettingsService } from "../../serverSettings.ts";
33import { normalizeCodexModelOptionsWithCapabilities } from "@t3tools/shared/model";
3334
34const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low";35const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low";
35const CODEX_TIMEOUT_MS = 180_000;36const CODEX_TIMEOUT_MS = 180_000;
...@@ -156,8 +157,8 @@ const makeCodexTextGeneration = Effect.gen(function* () {...@@ -156,8 +157,8 @@ const makeCodexTextGeneration = Effect.gen(function* () {
156 ).pipe(Effect.catch(() => Effect.undefined));157 ).pipe(Effect.catch(() => Effect.undefined));
157158
158 const runCodexCommand = Effect.gen(function* () {159 const runCodexCommand = Effect.gen(function* () {
159 const normalizedOptions = normalizeCodexModelOptions(160 const normalizedOptions = normalizeCodexModelOptionsWithCapabilities(
160 modelSelection.model,161 getCodexModelCapabilities(modelSelection.model),
161 modelSelection.options,162 modelSelection.options,
162 );163 );
163 const reasoningEffort =164 const reasoningEffort =
apps/server/src/provider/Layers/ClaudeProvider.ts-21
...@@ -1,6 +1,5 @@...@@ -1,6 +1,5 @@
1import type {1import type {
2 ClaudeSettings,2 ClaudeSettings,
3 ClaudeModelOptions,
4 ModelCapabilities,3 ModelCapabilities,
5 ServerProvider,4 ServerProvider,
6 ServerProviderModel,5 ServerProviderModel,
...@@ -9,7 +8,6 @@ import type {...@@ -9,7 +8,6 @@ import type {
9} from "@t3tools/contracts";8} from "@t3tools/contracts";
10import { Cache, Duration, Effect, Equal, Layer, Option, Result, Schema, Stream } from "effect";9import { Cache, Duration, Effect, Equal, Layer, Option, Result, Schema, Stream } from "effect";
11import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";10import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
12import { resolveContextWindow, resolveEffort } from "@t3tools/shared/model";
13import { decodeJsonResult } from "@t3tools/shared/schemaJson";11import { decodeJsonResult } from "@t3tools/shared/schemaJson";
14import { query as claudeQuery } from "@anthropic-ai/claude-agent-sdk";12import { query as claudeQuery } from "@anthropic-ai/claude-agent-sdk";
1513
...@@ -98,25 +96,6 @@ export function getClaudeModelCapabilities(model: string | null | undefined): Mo...@@ -98,25 +96,6 @@ export function getClaudeModelCapabilities(model: string | null | undefined): Mo
98 );96 );
99}97}
10098
101export function normalizeClaudeModelOptions(
102 model: string | null | undefined,
103 modelOptions: ClaudeModelOptions | null | undefined,
104): ClaudeModelOptions | undefined {
105 const caps = getClaudeModelCapabilities(model);
106 const effort = resolveEffort(caps, modelOptions?.effort);
107 const thinking =
108 caps.supportsThinkingToggle && modelOptions?.thinking === false ? false : undefined;
109 const fastMode = caps.supportsFastMode && modelOptions?.fastMode === true ? true : undefined;
110 const contextWindow = resolveContextWindow(caps, modelOptions?.contextWindow);
111 const nextOptions: ClaudeModelOptions = {
112 ...(thinking === false ? { thinking: false } : {}),
113 ...(effort ? { effort: effort as ClaudeModelOptions["effort"] } : {}),
114 ...(fastMode ? { fastMode: true } : {}),
115 ...(contextWindow ? { contextWindow } : {}),
116 };
117 return Object.keys(nextOptions).length > 0 ? nextOptions : undefined;
118}
119
120export function parseClaudeAuthStatusFromOutput(result: CommandResult): {99export function parseClaudeAuthStatusFromOutput(result: CommandResult): {
121 readonly status: Exclude<ServerProviderState, "disabled">;100 readonly status: Exclude<ServerProviderState, "disabled">;
122 readonly auth: Pick<ServerProviderAuth, "status">;101 readonly auth: Pick<ServerProviderAuth, "status">;
apps/server/src/provider/Layers/CodexProvider.ts-18
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import * as OS from "node:os";1import * as OS from "node:os";
2import type {2import type {
3 ModelCapabilities,3 ModelCapabilities,
4 CodexModelOptions,
5 CodexSettings,4 CodexSettings,
6 ServerProvider,5 ServerProvider,
7 ServerProviderModel,6 ServerProviderModel,
...@@ -21,7 +20,6 @@ import {...@@ -21,7 +20,6 @@ import {
21 Stream,20 Stream,
22} from "effect";21} from "effect";
23import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";22import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
24import { resolveEffort } from "@t3tools/shared/model";
2523
26import {24import {
27 buildServerProvider,25 buildServerProvider,
...@@ -170,22 +168,6 @@ export function getCodexModelCapabilities(model: string | null | undefined): Mod...@@ -170,22 +168,6 @@ export function getCodexModelCapabilities(model: string | null | undefined): Mod
170 );168 );
171}169}
172170
173export function normalizeCodexModelOptions(
174 model: string | null | undefined,
175 modelOptions: CodexModelOptions | null | undefined,
176): CodexModelOptions | undefined {
177 const caps = getCodexModelCapabilities(model);
178 const reasoningEffort = resolveEffort(caps, modelOptions?.reasoningEffort);
179 const fastModeEnabled = modelOptions?.fastMode === true;
180 const nextOptions: CodexModelOptions = {
181 ...(reasoningEffort
182 ? { reasoningEffort: reasoningEffort as CodexModelOptions["reasoningEffort"] }
183 : {}),
184 ...(fastModeEnabled ? { fastMode: true } : {}),
185 };
186 return Object.keys(nextOptions).length > 0 ? nextOptions : undefined;
187}
188
189export function parseAuthStatusFromOutput(result: CommandResult): {171export function parseAuthStatusFromOutput(result: CommandResult): {
190 readonly status: Exclude<ServerProviderState, "disabled">;172 readonly status: Exclude<ServerProviderState, "disabled">;
191 readonly auth: Pick<ServerProviderAuth, "status">;173 readonly auth: Pick<ServerProviderAuth, "status">;
apps/server/src/serverSettings.ts+1-1
...@@ -136,7 +136,7 @@ const ATOMIC_SETTINGS_KEYS: ReadonlySet<string> = new Set(["textGenerationModelS...@@ -136,7 +136,7 @@ const ATOMIC_SETTINGS_KEYS: ReadonlySet<string> = new Set(["textGenerationModelS
136136
137function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined {137function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined {
138 if (Array.isArray(current) || Array.isArray(defaults)) {138 if (Array.isArray(current) || Array.isArray(defaults)) {
139 return JSON.stringify(current) === JSON.stringify(defaults) ? undefined : current;139 return Equal.equals(current, defaults) ? undefined : current;
140 }140 }
141141
142 if (142 if (
apps/web/src/components/chat/composerProviderRegistry.test.tsx+145
...@@ -71,6 +71,41 @@ const CLAUDE_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -71,6 +71,41 @@ const CLAUDE_MODELS: ReadonlyArray<ServerProviderModel> = [
71 },71 },
72];72];
7373
74const CLAUDE_MODELS_WITH_CONTEXT_WINDOW: ReadonlyArray<ServerProviderModel> = [
75 {
76 slug: "claude-opus-4-6",
77 name: "Claude Opus 4.6",
78 isCustom: false,
79 capabilities: {
80 reasoningEffortLevels: [
81 { value: "medium", label: "Medium" },
82 { value: "high", label: "High", isDefault: true },
83 { value: "max", label: "Max" },
84 { value: "ultrathink", label: "Ultrathink" },
85 ],
86 supportsFastMode: true,
87 supportsThinkingToggle: false,
88 contextWindowOptions: [
89 { value: "200k", label: "200k", isDefault: true },
90 { value: "1m", label: "1M" },
91 ],
92 promptInjectedEffortLevels: ["ultrathink"],
93 },
94 },
95 {
96 slug: "claude-haiku-4-5",
97 name: "Claude Haiku 4.5",
98 isCustom: false,
99 capabilities: {
100 reasoningEffortLevels: [],
101 supportsFastMode: false,
102 supportsThinkingToggle: true,
103 contextWindowOptions: [],
104 promptInjectedEffortLevels: [],
105 },
106 },
107];
108
74describe("getComposerProviderState", () => {109describe("getComposerProviderState", () => {
75 it("returns codex defaults when no codex draft options exist", () => {110 it("returns codex defaults when no codex draft options exist", () => {
76 const state = getComposerProviderState({111 const state = getComposerProviderState({
...@@ -156,6 +191,7 @@ describe("getComposerProviderState", () => {...@@ -156,6 +191,7 @@ describe("getComposerProviderState", () => {
156 promptEffort: "high",191 promptEffort: "high",
157 modelOptionsForDispatch: {192 modelOptionsForDispatch: {
158 reasoningEffort: "high",193 reasoningEffort: "high",
194 fastMode: false,
159 },195 },
160 });196 });
161 });197 });
...@@ -268,7 +304,116 @@ describe("getComposerProviderState", () => {...@@ -268,7 +304,116 @@ describe("getComposerProviderState", () => {
268 promptEffort: "high",304 promptEffort: "high",
269 modelOptionsForDispatch: {305 modelOptionsForDispatch: {
270 effort: "high",306 effort: "high",
307 fastMode: false,
308 },
309 });
310 });
311
312 it("preserves explicit fastMode: false so deepMerge can overwrite a prior true", () => {
313 // Regression: normalizeClaudeModelOptionsWithCapabilities used to strip
314 // fastMode: false, which meant deepMerge could never clear a previous true.
315 const state = getComposerProviderState({
316 provider: "claudeAgent",
317 model: "claude-opus-4-6",
318 models: CLAUDE_MODELS,
319 prompt: "",
320 modelOptions: {
321 claudeAgent: {
322 effort: "high",
323 fastMode: false,
324 },
325 },
326 });
327
328 expect(state.modelOptionsForDispatch).toHaveProperty("fastMode", false);
329 });
330
331 it("preserves explicit thinking: true so deepMerge can overwrite a prior false", () => {
332 // Regression: thinking: true (the default) used to be stripped, which
333 // meant deepMerge could never clear a previous thinking: false.
334 const state = getComposerProviderState({
335 provider: "claudeAgent",
336 model: "claude-haiku-4-5",
337 models: CLAUDE_MODELS,
338 prompt: "",
339 modelOptions: {
340 claudeAgent: {
341 thinking: true,
342 },
343 },
344 });
345
346 expect(state.modelOptionsForDispatch).toHaveProperty("thinking", true);
347 });
348
349 it("preserves Claude default context window explicitly in dispatch options", () => {
350 const state = getComposerProviderState({
351 provider: "claudeAgent",
352 model: "claude-opus-4-6",
353 models: CLAUDE_MODELS_WITH_CONTEXT_WINDOW,
354 prompt: "",
355 modelOptions: {
356 claudeAgent: {
357 effort: "high",
358 contextWindow: "200k",
359 },
360 },
361 });
362
363 expect(state.modelOptionsForDispatch).toMatchObject({
364 effort: "high",
365 contextWindow: "200k",
366 });
367 });
368
369 it("preserves explicit contextWindow default so deepMerge can overwrite a prior 1m", () => {
370 // Regression: the default contextWindow must survive normalization so
371 // deepMerge can clear an older non-default 1m selection.
372 const state = getComposerProviderState({
373 provider: "claudeAgent",
374 model: "claude-opus-4-6",
375 models: CLAUDE_MODELS_WITH_CONTEXT_WINDOW,
376 prompt: "",
377 modelOptions: {
378 claudeAgent: {
379 contextWindow: "200k",
380 },
271 },381 },
272 });382 });
383
384 expect(state.modelOptionsForDispatch).toHaveProperty("contextWindow", "200k");
385 });
386
387 it("omits contextWindow when the model does not support it", () => {
388 const state = getComposerProviderState({
389 provider: "claudeAgent",
390 model: "claude-haiku-4-5",
391 models: CLAUDE_MODELS_WITH_CONTEXT_WINDOW,
392 prompt: "",
393 modelOptions: {
394 claudeAgent: {
395 contextWindow: "1m",
396 },
397 },
398 });
399
400 expect(state.modelOptionsForDispatch).toBeUndefined();
401 });
402
403 it("omits fastMode when the model does not support it", () => {
404 const state = getComposerProviderState({
405 provider: "claudeAgent",
406 model: "claude-sonnet-4-6",
407 models: CLAUDE_MODELS,
408 prompt: "",
409 modelOptions: {
410 claudeAgent: {
411 effort: "high",
412 fastMode: true,
413 },
414 },
415 });
416
417 expect(state.modelOptionsForDispatch).not.toHaveProperty("fastMode");
273 });418 });
274});419});
apps/web/src/components/chat/composerProviderRegistry.tsx+3-3
...@@ -6,12 +6,12 @@ import {...@@ -6,12 +6,12 @@ import {
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { isClaudeUltrathinkPrompt, resolveEffort } from "@t3tools/shared/model";7import { isClaudeUltrathinkPrompt, resolveEffort } from "@t3tools/shared/model";
8import type { ReactNode } from "react";8import type { ReactNode } from "react";
9import { getProviderModelCapabilities } from "../../providerModels";
10import { TraitsMenuContent, TraitsPicker } from "./TraitsPicker";
9import {11import {
10 getProviderModelCapabilities,
11 normalizeClaudeModelOptionsWithCapabilities,12 normalizeClaudeModelOptionsWithCapabilities,
12 normalizeCodexModelOptionsWithCapabilities,13 normalizeCodexModelOptionsWithCapabilities,
13} from "../../providerModels";14} from "@t3tools/shared/model";
14import { TraitsMenuContent, TraitsPicker } from "./TraitsPicker";
1515
16export type ComposerProviderStateInput = {16export type ComposerProviderStateInput = {
17 provider: ProviderKind;17 provider: ProviderKind;
apps/web/src/providerModels.ts+1-36
...@@ -1,13 +1,11 @@...@@ -1,13 +1,11 @@
1import {1import {
2 DEFAULT_MODEL_BY_PROVIDER,2 DEFAULT_MODEL_BY_PROVIDER,
3 type ClaudeModelOptions,
4 type CodexModelOptions,
5 type ModelCapabilities,3 type ModelCapabilities,
6 type ProviderKind,4 type ProviderKind,
7 type ServerProvider,5 type ServerProvider,
8 type ServerProviderModel,6 type ServerProviderModel,
9} from "@t3tools/contracts";7} from "@t3tools/contracts";
10import { normalizeModelSlug, resolveContextWindow, resolveEffort } from "@t3tools/shared/model";8import { normalizeModelSlug } from "@t3tools/shared/model";
119
12const EMPTY_CAPABILITIES: ModelCapabilities = {10const EMPTY_CAPABILITIES: ModelCapabilities = {
13 reasoningEffortLevels: [],11 reasoningEffortLevels: [],
...@@ -69,36 +67,3 @@ export function getDefaultServerModel(...@@ -69,36 +67,3 @@ export function getDefaultServerModel(
69 DEFAULT_MODEL_BY_PROVIDER[provider]67 DEFAULT_MODEL_BY_PROVIDER[provider]
70 );68 );
71}69}
72
73export function normalizeCodexModelOptionsWithCapabilities(
74 caps: ModelCapabilities,
75 modelOptions: CodexModelOptions | null | undefined,
76): CodexModelOptions | undefined {
77 const reasoningEffort = resolveEffort(caps, modelOptions?.reasoningEffort);
78 const fastModeEnabled = modelOptions?.fastMode === true;
79 const nextOptions: CodexModelOptions = {
80 ...(reasoningEffort
81 ? { reasoningEffort: reasoningEffort as CodexModelOptions["reasoningEffort"] }
82 : {}),
83 ...(fastModeEnabled ? { fastMode: true } : {}),
84 };
85 return Object.keys(nextOptions).length > 0 ? nextOptions : undefined;
86}
87
88export function normalizeClaudeModelOptionsWithCapabilities(
89 caps: ModelCapabilities,
90 modelOptions: ClaudeModelOptions | null | undefined,
91): ClaudeModelOptions | undefined {
92 const effort = resolveEffort(caps, modelOptions?.effort);
93 const thinking =
94 caps.supportsThinkingToggle && modelOptions?.thinking === false ? false : undefined;
95 const fastMode = caps.supportsFastMode && modelOptions?.fastMode === true ? true : undefined;
96 const contextWindow = resolveContextWindow(caps, modelOptions?.contextWindow);
97 const nextOptions: ClaudeModelOptions = {
98 ...(thinking === false ? { thinking: false } : {}),
99 ...(effort ? { effort: effort as ClaudeModelOptions["effort"] } : {}),
100 ...(fastMode ? { fastMode: true } : {}),
101 ...(contextWindow ? { contextWindow } : {}),
102 };
103 return Object.keys(nextOptions).length > 0 ? nextOptions : undefined;
104}
packages/contracts/src/settings.test.ts deleted-8
...@@ -1,8 +0,0 @@
1import { describe, expect, it } from "vitest";
2import { DEFAULT_CLIENT_SETTINGS } from "./settings";
3
4describe("DEFAULT_CLIENT_SETTINGS", () => {
5 it("includes archive confirmation with a false default", () => {
6 expect(DEFAULT_CLIENT_SETTINGS.confirmThreadArchive).toBe(false);
7 });
8});
packages/contracts/src/settings.ts+1
...@@ -112,6 +112,7 @@ const ClaudeModelOptionsPatch = Schema.Struct({...@@ -112,6 +112,7 @@ const ClaudeModelOptionsPatch = Schema.Struct({
112 thinking: Schema.optionalKey(ClaudeModelOptions.fields.thinking),112 thinking: Schema.optionalKey(ClaudeModelOptions.fields.thinking),
113 effort: Schema.optionalKey(ClaudeModelOptions.fields.effort),113 effort: Schema.optionalKey(ClaudeModelOptions.fields.effort),
114 fastMode: Schema.optionalKey(ClaudeModelOptions.fields.fastMode),114 fastMode: Schema.optionalKey(ClaudeModelOptions.fields.fastMode),
115 contextWindow: Schema.optionalKey(ClaudeModelOptions.fields.contextWindow),
115});116});
116117
117const ModelSelectionPatch = Schema.Union([118const ModelSelectionPatch = Schema.Union([
packages/shared/src/model.test.ts+56
...@@ -8,6 +8,8 @@ import {...@@ -8,6 +8,8 @@ import {
8 hasContextWindowOption,8 hasContextWindowOption,
9 hasEffortLevel,9 hasEffortLevel,
10 isClaudeUltrathinkPrompt,10 isClaudeUltrathinkPrompt,
11 normalizeClaudeModelOptionsWithCapabilities,
12 normalizeCodexModelOptionsWithCapabilities,
11 normalizeModelSlug,13 normalizeModelSlug,
12 resolveApiModelId,14 resolveApiModelId,
13 resolveContextWindow,15 resolveContextWindow,
...@@ -226,3 +228,57 @@ describe("resolveApiModelId", () => {...@@ -226,3 +228,57 @@ describe("resolveApiModelId", () => {
226 expect(resolveApiModelId({ provider: "codex", model: "gpt-5.4" })).toBe("gpt-5.4");228 expect(resolveApiModelId({ provider: "codex", model: "gpt-5.4" })).toBe("gpt-5.4");
227 });229 });
228});230});
231
232describe("normalize*ModelOptionsWithCapabilities", () => {
233 it("preserves explicit false codex fast mode", () => {
234 expect(
235 normalizeCodexModelOptionsWithCapabilities(codexCaps, {
236 reasoningEffort: "high",
237 fastMode: false,
238 }),
239 ).toEqual({
240 reasoningEffort: "high",
241 fastMode: false,
242 });
243 });
244
245 it("preserves the default Claude context window explicitly", () => {
246 expect(
247 normalizeClaudeModelOptionsWithCapabilities(
248 {
249 ...claudeCaps,
250 contextWindowOptions: [
251 { value: "200k", label: "200k", isDefault: true },
252 { value: "1m", label: "1M" },
253 ],
254 },
255 {
256 effort: "high",
257 contextWindow: "200k",
258 },
259 ),
260 ).toEqual({
261 effort: "high",
262 contextWindow: "200k",
263 });
264 });
265
266 it("omits unsupported Claude context window options", () => {
267 expect(
268 normalizeClaudeModelOptionsWithCapabilities(
269 {
270 ...claudeCaps,
271 reasoningEffortLevels: [],
272 supportsThinkingToggle: true,
273 contextWindowOptions: [],
274 },
275 {
276 thinking: true,
277 contextWindow: "1m",
278 },
279 ),
280 ).toEqual({
281 thinking: true,
282 });
283 });
284});
packages/shared/src/model.ts+34
...@@ -2,6 +2,8 @@ import {...@@ -2,6 +2,8 @@ import {
2 DEFAULT_MODEL_BY_PROVIDER,2 DEFAULT_MODEL_BY_PROVIDER,
3 MODEL_SLUG_ALIASES_BY_PROVIDER,3 MODEL_SLUG_ALIASES_BY_PROVIDER,
4 type ClaudeCodeEffort,4 type ClaudeCodeEffort,
5 type ClaudeModelOptions,
6 type CodexModelOptions,
5 type ModelCapabilities,7 type ModelCapabilities,
6 type ModelSelection,8 type ModelSelection,
7 type ProviderKind,9 type ProviderKind,
...@@ -83,6 +85,38 @@ export function resolveContextWindow(...@@ -83,6 +85,38 @@ export function resolveContextWindow(
83 return hasContextWindowOption(caps, raw) ? raw : (defaultValue ?? undefined);85 return hasContextWindowOption(caps, raw) ? raw : (defaultValue ?? undefined);
84}86}
8587
88export function normalizeCodexModelOptionsWithCapabilities(
89 caps: ModelCapabilities,
90 modelOptions: CodexModelOptions | null | undefined,
91): CodexModelOptions | undefined {
92 const reasoningEffort = resolveEffort(caps, modelOptions?.reasoningEffort);
93 const fastMode = caps.supportsFastMode ? modelOptions?.fastMode : undefined;
94 const nextOptions: CodexModelOptions = {
95 ...(reasoningEffort
96 ? { reasoningEffort: reasoningEffort as CodexModelOptions["reasoningEffort"] }
97 : {}),
98 ...(fastMode !== undefined ? { fastMode } : {}),
99 };
100 return Object.keys(nextOptions).length > 0 ? nextOptions : undefined;
101}
102
103export function normalizeClaudeModelOptionsWithCapabilities(
104 caps: ModelCapabilities,
105 modelOptions: ClaudeModelOptions | null | undefined,
106): ClaudeModelOptions | undefined {
107 const effort = resolveEffort(caps, modelOptions?.effort);
108 const thinking = caps.supportsThinkingToggle ? modelOptions?.thinking : undefined;
109 const fastMode = caps.supportsFastMode ? modelOptions?.fastMode : undefined;
110 const contextWindow = resolveContextWindow(caps, modelOptions?.contextWindow);
111 const nextOptions: ClaudeModelOptions = {
112 ...(thinking !== undefined ? { thinking } : {}),
113 ...(effort ? { effort: effort as ClaudeModelOptions["effort"] } : {}),
114 ...(fastMode !== undefined ? { fastMode } : {}),
115 ...(contextWindow !== undefined ? { contextWindow } : {}),
116 };
117 return Object.keys(nextOptions).length > 0 ? nextOptions : undefined;
118}
119
86export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean {120export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean {
87 return typeof text === "string" && /\bultrathink\b/i.test(text);121 return typeof text === "string" && /\bultrathink\b/i.test(text);
88}122}