| author | |
| committer | |
| log | 792ad4b5c1ed226be33d1623de8cbaf828f9053d |
| tree | 76a57a5ed2343e91901a36df7618e1daae5e28dc |
| parent | d41781987cea30ad3ff7d344292ecdfbf9325900 |
| signature | Signed by PGP key B5690EEEBB952194 |
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"; |
| 32 | import { normalizeClaudeModelOptions } from "../../provider/Layers/ClaudeProvider.ts"; | 32 | import { normalizeClaudeModelOptionsWithCapabilities } from "@t3tools/shared/model"; |
| 33 | import { ServerSettingsService } from "../../serverSettings.ts"; | 33 | import { ServerSettingsService } from "../../serverSettings.ts"; |
| 34 | import { getClaudeModelCapabilities } from "../../provider/Layers/ClaudeProvider.ts"; | ||
| 34 | 35 | ||
| 35 | const CLAUDE_TIMEOUT_MS = 180_000; | 36 | const CLAUDE_TIMEOUT_MS = 180_000; |
| 36 | 37 | ||
| ... | @@ -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"; |
| 31 | import { normalizeCodexModelOptions } from "../../provider/Layers/CodexProvider.ts"; | 31 | import { getCodexModelCapabilities } from "../../provider/Layers/CodexProvider.ts"; |
| 32 | import { ServerSettingsService } from "../../serverSettings.ts"; | 32 | import { ServerSettingsService } from "../../serverSettings.ts"; |
| 33 | import { normalizeCodexModelOptionsWithCapabilities } from "@t3tools/shared/model"; | ||
| 33 | 34 | ||
| 34 | const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low"; | 35 | const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low"; |
| 35 | const CODEX_TIMEOUT_MS = 180_000; | 36 | const 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)); |
| 157 | 158 | ||
| 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 @@ |
| 1 | import type { | 1 | import 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"; |
| 10 | import { Cache, Duration, Effect, Equal, Layer, Option, Result, Schema, Stream } from "effect"; | 9 | import { Cache, Duration, Effect, Equal, Layer, Option, Result, Schema, Stream } from "effect"; |
| 11 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 10 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 12 | import { resolveContextWindow, resolveEffort } from "@t3tools/shared/model"; | ||
| 13 | import { decodeJsonResult } from "@t3tools/shared/schemaJson"; | 11 | import { decodeJsonResult } from "@t3tools/shared/schemaJson"; |
| 14 | import { query as claudeQuery } from "@anthropic-ai/claude-agent-sdk"; | 12 | import { query as claudeQuery } from "@anthropic-ai/claude-agent-sdk"; |
| 15 | 13 | ||
| ... | @@ -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 | } |
| 100 | 98 | ||
| 101 | export 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 | |||
| 120 | export function parseClaudeAuthStatusFromOutput(result: CommandResult): { | 99 | export 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 @@ |
| 1 | import * as OS from "node:os"; | 1 | import * as OS from "node:os"; |
| 2 | import type { | 2 | import 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"; |
| 23 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 22 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 24 | import { resolveEffort } from "@t3tools/shared/model"; | ||
| 25 | 23 | ||
| 26 | import { | 24 | import { |
| 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 | } |
| 172 | 170 | ||
| 173 | export 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 | |||
| 189 | export function parseAuthStatusFromOutput(result: CommandResult): { | 171 | export 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 |
| 136 | 136 | ||
| 137 | function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined { | 137 | function 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 | } |
| 141 | 141 | ||
| 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 | ]; |
| 73 | 73 | ||
| 74 | const 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 | |||
| 74 | describe("getComposerProviderState", () => { | 109 | describe("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"; |
| 7 | import { isClaudeUltrathinkPrompt, resolveEffort } from "@t3tools/shared/model"; | 7 | import { isClaudeUltrathinkPrompt, resolveEffort } from "@t3tools/shared/model"; |
| 8 | import type { ReactNode } from "react"; | 8 | import type { ReactNode } from "react"; |
| 9 | import { getProviderModelCapabilities } from "../../providerModels"; | ||
| 10 | import { TraitsMenuContent, TraitsPicker } from "./TraitsPicker"; | ||
| 9 | import { | 11 | import { |
| 10 | getProviderModelCapabilities, | ||
| 11 | normalizeClaudeModelOptionsWithCapabilities, | 12 | normalizeClaudeModelOptionsWithCapabilities, |
| 12 | normalizeCodexModelOptionsWithCapabilities, | 13 | normalizeCodexModelOptionsWithCapabilities, |
| 13 | } from "../../providerModels"; | 14 | } from "@t3tools/shared/model"; |
| 14 | import { TraitsMenuContent, TraitsPicker } from "./TraitsPicker"; | ||
| 15 | 15 | ||
| 16 | export type ComposerProviderStateInput = { | 16 | export type ComposerProviderStateInput = { |
| 17 | provider: ProviderKind; | 17 | provider: ProviderKind; |
apps/web/src/providerModels.ts+1-36| ... | @@ -1,13 +1,11 @@ | ... | @@ -1,13 +1,11 @@ |
| 1 | import { | 1 | import { |
| 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"; |
| 10 | import { normalizeModelSlug, resolveContextWindow, resolveEffort } from "@t3tools/shared/model"; | 8 | import { normalizeModelSlug } from "@t3tools/shared/model"; |
| 11 | 9 | ||
| 12 | const EMPTY_CAPABILITIES: ModelCapabilities = { | 10 | const 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 | |||
| 73 | export 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 | |||
| 88 | export 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 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | import { DEFAULT_CLIENT_SETTINGS } from "./settings"; | ||
| 3 | |||
| 4 | describe("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 | }); |
| 116 | 117 | ||
| 117 | const ModelSelectionPatch = Schema.Union([ | 118 | const 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 | |||
| 232 | describe("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 | } |
| 85 | 87 | ||
| 88 | export 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 | |||
| 103 | export 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 | |||
| 86 | export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean { | 120 | export 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 | } |