| author | |
| committer | |
| log | add5f34619b65c3283317d8c77e1feef6fc63267 |
| tree | 234dc14d4e31828d3e9d240f15f5e60fd91c0c90 |
| parent | 9403429a7af114a1b96a79c96ecda218f9e0c0b2 |
| signature | Signed by PGP key B5690EEEBB952194 |
19 files changed, 414 insertions(+), 139 deletions(-)
apps/server/src/git/Layers/ClaudeTextGeneration.ts+2-1| ... | @@ -11,6 +11,7 @@ import { Effect, Layer, Option, Schema, Stream } from "effect"; | ... | @@ -11,6 +11,7 @@ import { Effect, Layer, Option, Schema, Stream } from "effect"; |
| 11 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 11 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 12 | 12 | ||
| 13 | import { ClaudeModelSelection } from "@t3tools/contracts"; | 13 | import { ClaudeModelSelection } from "@t3tools/contracts"; |
| 14 | import { resolveApiModelId } from "@t3tools/shared/model"; | ||
| 14 | import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; | 15 | import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; |
| 15 | 16 | ||
| 16 | import { TextGenerationError } from "../Errors.ts"; | 17 | import { TextGenerationError } from "../Errors.ts"; |
| ... | @@ -103,7 +104,7 @@ const makeClaudeTextGeneration = Effect.gen(function* () { | ... | @@ -103,7 +104,7 @@ const makeClaudeTextGeneration = Effect.gen(function* () { |
| 103 | "--json-schema", | 104 | "--json-schema", |
| 104 | jsonSchemaStr, | 105 | jsonSchemaStr, |
| 105 | "--model", | 106 | "--model", |
| 106 | modelSelection.model, | 107 | resolveApiModelId(modelSelection), |
| 107 | ...(normalizedOptions?.effort ? ["--effort", normalizedOptions.effort] : []), | 108 | ...(normalizedOptions?.effort ? ["--effort", normalizedOptions.effort] : []), |
| 108 | ...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []), | 109 | ...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []), |
| 109 | "--dangerously-skip-permissions", | 110 | "--dangerously-skip-permissions", |
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+3-3| ... | @@ -347,7 +347,7 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -347,7 +347,7 @@ describe("ClaudeAdapterLive", () => { |
| 347 | ); | 347 | ); |
| 348 | }); | 348 | }); |
| 349 | 349 | ||
| 350 | it.effect("ignores unsupported max effort for Sonnet 4.6", () => { | 350 | it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => { |
| 351 | const harness = makeHarness(); | 351 | const harness = makeHarness(); |
| 352 | return Effect.gen(function* () { | 352 | return Effect.gen(function* () { |
| 353 | const adapter = yield* ClaudeAdapter; | 353 | const adapter = yield* ClaudeAdapter; |
| ... | @@ -365,7 +365,7 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -365,7 +365,7 @@ describe("ClaudeAdapterLive", () => { |
| 365 | }); | 365 | }); |
| 366 | 366 | ||
| 367 | const createInput = harness.getLastCreateQueryInput(); | 367 | const createInput = harness.getLastCreateQueryInput(); |
| 368 | assert.equal(createInput?.options.effort, undefined); | 368 | assert.equal(createInput?.options.effort, "high"); |
| 369 | }).pipe( | 369 | }).pipe( |
| 370 | Effect.provideService(Random.Random, makeDeterministicRandomService()), | 370 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 371 | Effect.provide(harness.layer), | 371 | Effect.provide(harness.layer), |
| ... | @@ -532,7 +532,7 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -532,7 +532,7 @@ describe("ClaudeAdapterLive", () => { |
| 532 | }); | 532 | }); |
| 533 | 533 | ||
| 534 | const createInput = harness.getLastCreateQueryInput(); | 534 | const createInput = harness.getLastCreateQueryInput(); |
| 535 | assert.equal(createInput?.options.effort, undefined); | 535 | assert.equal(createInput?.options.effort, "high"); |
| 536 | const promptText = yield* Effect.promise(() => readFirstPromptText(createInput)); | 536 | const promptText = yield* Effect.promise(() => readFirstPromptText(createInput)); |
| 537 | assert.equal(promptText, "Ultrathink:\nInvestigate the edge cases"); | 537 | assert.equal(promptText, "Ultrathink:\nInvestigate the edge cases"); |
| 538 | }).pipe( | 538 | }).pipe( |
apps/server/src/provider/Layers/ClaudeAdapter.ts+18-13| ... | @@ -40,7 +40,12 @@ import { | ... | @@ -40,7 +40,12 @@ import { |
| 40 | type UserInputQuestion, | 40 | type UserInputQuestion, |
| 41 | ClaudeCodeEffort, | 41 | ClaudeCodeEffort, |
| 42 | } from "@t3tools/contracts"; | 42 | } from "@t3tools/contracts"; |
| 43 | import { hasEffortLevel, applyClaudePromptEffortPrefix, trimOrNull } from "@t3tools/shared/model"; | 43 | import { |
| 44 | applyClaudePromptEffortPrefix, | ||
| 45 | resolveApiModelId, | ||
| 46 | resolveEffort, | ||
| 47 | trimOrNull, | ||
| 48 | } from "@t3tools/shared/model"; | ||
| 44 | import { | 49 | import { |
| 45 | Cause, | 50 | Cause, |
| 46 | DateTime, | 51 | DateTime, |
| ... | @@ -506,16 +511,15 @@ const CLAUDE_SETTING_SOURCES = [ | ... | @@ -506,16 +511,15 @@ const CLAUDE_SETTING_SOURCES = [ |
| 506 | function buildPromptText(input: ProviderSendTurnInput): string { | 511 | function buildPromptText(input: ProviderSendTurnInput): string { |
| 507 | const rawEffort = | 512 | const rawEffort = |
| 508 | input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.options?.effort : null; | 513 | input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.options?.effort : null; |
| 509 | const requestedEffort = trimOrNull(rawEffort); | ||
| 510 | const claudeModel = | 514 | const claudeModel = |
| 511 | input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.model : undefined; | 515 | input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.model : undefined; |
| 512 | const caps = getClaudeModelCapabilities(claudeModel); | 516 | const caps = getClaudeModelCapabilities(claudeModel); |
| 517 | |||
| 518 | // For prompt injection, we check if the raw effort is a prompt-injected level (e.g. "ultrathink"). | ||
| 519 | // resolveEffort strips prompt-injected values (returning the default instead), so we check the raw value directly. | ||
| 520 | const trimmedEffort = trimOrNull(rawEffort); | ||
| 513 | const promptEffort = | 521 | const promptEffort = |
| 514 | requestedEffort === "ultrathink" && caps.reasoningEffortLevels.length > 0 | 522 | trimmedEffort && caps.promptInjectedEffortLevels.includes(trimmedEffort) ? trimmedEffort : null; |
| 515 | ? "ultrathink" | ||
| 516 | : requestedEffort && hasEffortLevel(caps, requestedEffort) | ||
| 517 | ? requestedEffort | ||
| 518 | : null; | ||
| 519 | return applyClaudePromptEffortPrefix(input.input?.trim() ?? "", promptEffort); | 523 | return applyClaudePromptEffortPrefix(input.input?.trim() ?? "", promptEffort); |
| 520 | } | 524 | } |
| 521 | 525 | ||
| ... | @@ -2727,10 +2731,10 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { | ... | @@ -2727,10 +2731,10 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { |
| 2727 | const claudeBinaryPath = claudeSettings.binaryPath; | 2731 | const claudeBinaryPath = claudeSettings.binaryPath; |
| 2728 | const modelSelection = | 2732 | const modelSelection = |
| 2729 | input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined; | 2733 | input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined; |
| 2730 | const requestedEffort = trimOrNull(modelSelection?.options?.effort ?? null); | ||
| 2731 | const caps = getClaudeModelCapabilities(modelSelection?.model); | 2734 | const caps = getClaudeModelCapabilities(modelSelection?.model); |
| 2732 | const effort = | 2735 | const apiModelId = modelSelection ? resolveApiModelId(modelSelection) : undefined; |
| 2733 | requestedEffort && hasEffortLevel(caps, requestedEffort) ? requestedEffort : null; | 2736 | const effort = (resolveEffort(caps, modelSelection?.options?.effort) ?? |
| 2737 | null) as ClaudeCodeEffort | null; | ||
| 2734 | const fastMode = modelSelection?.options?.fastMode === true && caps.supportsFastMode; | 2738 | const fastMode = modelSelection?.options?.fastMode === true && caps.supportsFastMode; |
| 2735 | const thinking = | 2739 | const thinking = |
| 2736 | typeof modelSelection?.options?.thinking === "boolean" && caps.supportsThinkingToggle | 2740 | typeof modelSelection?.options?.thinking === "boolean" && caps.supportsThinkingToggle |
| ... | @@ -2746,7 +2750,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { | ... | @@ -2746,7 +2750,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { |
| 2746 | 2750 | ||
| 2747 | const queryOptions: ClaudeQueryOptions = { | 2751 | const queryOptions: ClaudeQueryOptions = { |
| 2748 | ...(input.cwd ? { cwd: input.cwd } : {}), | 2752 | ...(input.cwd ? { cwd: input.cwd } : {}), |
| 2749 | ...(modelSelection?.model ? { model: modelSelection.model } : {}), | 2753 | ...(apiModelId ? { model: apiModelId } : {}), |
| 2750 | pathToClaudeCodeExecutable: claudeBinaryPath, | 2754 | pathToClaudeCodeExecutable: claudeBinaryPath, |
| 2751 | settingSources: [...CLAUDE_SETTING_SOURCES], | 2755 | settingSources: [...CLAUDE_SETTING_SOURCES], |
| 2752 | ...(effectiveEffort ? { effort: effectiveEffort } : {}), | 2756 | ...(effectiveEffort ? { effort: effectiveEffort } : {}), |
| ... | @@ -2840,7 +2844,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { | ... | @@ -2840,7 +2844,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { |
| 2840 | threadId, | 2844 | threadId, |
| 2841 | payload: { | 2845 | payload: { |
| 2842 | config: { | 2846 | config: { |
| 2843 | ...(modelSelection?.model ? { model: modelSelection.model } : {}), | 2847 | ...(apiModelId ? { model: apiModelId } : {}), |
| 2844 | ...(input.cwd ? { cwd: input.cwd } : {}), | 2848 | ...(input.cwd ? { cwd: input.cwd } : {}), |
| 2845 | ...(effectiveEffort ? { effort: effectiveEffort } : {}), | 2849 | ...(effectiveEffort ? { effort: effectiveEffort } : {}), |
| 2846 | ...(permissionMode ? { permissionMode } : {}), | 2850 | ...(permissionMode ? { permissionMode } : {}), |
| ... | @@ -2893,8 +2897,9 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { | ... | @@ -2893,8 +2897,9 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { |
| 2893 | } | 2897 | } |
| 2894 | 2898 | ||
| 2895 | if (modelSelection?.model) { | 2899 | if (modelSelection?.model) { |
| 2900 | const apiModelId = resolveApiModelId(modelSelection); | ||
| 2896 | yield* Effect.tryPromise({ | 2901 | yield* Effect.tryPromise({ |
| 2897 | try: () => context.query.setModel(modelSelection.model), | 2902 | try: () => context.query.setModel(apiModelId), |
| 2898 | catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause), | 2903 | catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause), |
| 2899 | }); | 2904 | }); |
| 2900 | } | 2905 | } |
apps/server/src/provider/Layers/ClaudeProvider.ts+15-12| ... | @@ -9,7 +9,7 @@ import type { | ... | @@ -9,7 +9,7 @@ import type { |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import { Effect, Equal, Layer, Option, Result, Stream } from "effect"; | 10 | import { Effect, Equal, Layer, Option, Result, Stream } from "effect"; |
| 11 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 11 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 12 | import { getDefaultEffort, hasEffortLevel, trimOrNull } from "@t3tools/shared/model"; | 12 | import { resolveContextWindow, resolveEffort } from "@t3tools/shared/model"; |
| 13 | 13 | ||
| 14 | import { | 14 | import { |
| 15 | buildServerProvider, | 15 | buildServerProvider, |
| ... | @@ -42,6 +42,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -42,6 +42,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 42 | ], | 42 | ], |
| 43 | supportsFastMode: true, | 43 | supportsFastMode: true, |
| 44 | supportsThinkingToggle: false, | 44 | supportsThinkingToggle: false, |
| 45 | contextWindowOptions: [ | ||
| 46 | { value: "200k", label: "200k" }, | ||
| 47 | { value: "1m", label: "1M", isDefault: true }, | ||
| 48 | ], | ||
| 45 | promptInjectedEffortLevels: ["ultrathink"], | 49 | promptInjectedEffortLevels: ["ultrathink"], |
| 46 | } satisfies ModelCapabilities, | 50 | } satisfies ModelCapabilities, |
| 47 | }, | 51 | }, |
| ... | @@ -58,6 +62,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -58,6 +62,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 58 | ], | 62 | ], |
| 59 | supportsFastMode: false, | 63 | supportsFastMode: false, |
| 60 | supportsThinkingToggle: false, | 64 | supportsThinkingToggle: false, |
| 65 | contextWindowOptions: [ | ||
| 66 | { value: "200k", label: "200k" }, | ||
| 67 | { value: "1m", label: "1M", isDefault: true }, | ||
| 68 | ], | ||
| 61 | promptInjectedEffortLevels: ["ultrathink"], | 69 | promptInjectedEffortLevels: ["ultrathink"], |
| 62 | } satisfies ModelCapabilities, | 70 | } satisfies ModelCapabilities, |
| 63 | }, | 71 | }, |
| ... | @@ -69,6 +77,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -69,6 +77,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 69 | reasoningEffortLevels: [], | 77 | reasoningEffortLevels: [], |
| 70 | supportsFastMode: false, | 78 | supportsFastMode: false, |
| 71 | supportsThinkingToggle: true, | 79 | supportsThinkingToggle: true, |
| 80 | contextWindowOptions: [], | ||
| 72 | promptInjectedEffortLevels: [], | 81 | promptInjectedEffortLevels: [], |
| 73 | } satisfies ModelCapabilities, | 82 | } satisfies ModelCapabilities, |
| 74 | }, | 83 | }, |
| ... | @@ -81,6 +90,7 @@ export function getClaudeModelCapabilities(model: string | null | undefined): Mo | ... | @@ -81,6 +90,7 @@ export function getClaudeModelCapabilities(model: string | null | undefined): Mo |
| 81 | reasoningEffortLevels: [], | 90 | reasoningEffortLevels: [], |
| 82 | supportsFastMode: false, | 91 | supportsFastMode: false, |
| 83 | supportsThinkingToggle: false, | 92 | supportsThinkingToggle: false, |
| 93 | contextWindowOptions: [], | ||
| 84 | promptInjectedEffortLevels: [], | 94 | promptInjectedEffortLevels: [], |
| 85 | } | 95 | } |
| 86 | ); | 96 | ); |
| ... | @@ -91,23 +101,16 @@ export function normalizeClaudeModelOptions( | ... | @@ -91,23 +101,16 @@ export function normalizeClaudeModelOptions( |
| 91 | modelOptions: ClaudeModelOptions | null | undefined, | 101 | modelOptions: ClaudeModelOptions | null | undefined, |
| 92 | ): ClaudeModelOptions | undefined { | 102 | ): ClaudeModelOptions | undefined { |
| 93 | const caps = getClaudeModelCapabilities(model); | 103 | const caps = getClaudeModelCapabilities(model); |
| 94 | const defaultReasoningEffort = getDefaultEffort(caps); | 104 | const effort = resolveEffort(caps, modelOptions?.effort); |
| 95 | const resolvedEffort = trimOrNull(modelOptions?.effort); | ||
| 96 | const isPromptInjected = caps.promptInjectedEffortLevels.includes(resolvedEffort ?? ""); | ||
| 97 | const effort = | ||
| 98 | resolvedEffort && | ||
| 99 | !isPromptInjected && | ||
| 100 | hasEffortLevel(caps, resolvedEffort) && | ||
| 101 | resolvedEffort !== defaultReasoningEffort | ||
| 102 | ? resolvedEffort | ||
| 103 | : undefined; | ||
| 104 | const thinking = | 105 | const thinking = |
| 105 | caps.supportsThinkingToggle && modelOptions?.thinking === false ? false : undefined; | 106 | caps.supportsThinkingToggle && modelOptions?.thinking === false ? false : undefined; |
| 106 | const fastMode = caps.supportsFastMode && modelOptions?.fastMode === true ? true : undefined; | 107 | const fastMode = caps.supportsFastMode && modelOptions?.fastMode === true ? true : undefined; |
| 108 | const contextWindow = resolveContextWindow(caps, modelOptions?.contextWindow); | ||
| 107 | const nextOptions: ClaudeModelOptions = { | 109 | const nextOptions: ClaudeModelOptions = { |
| 108 | ...(thinking === false ? { thinking: false } : {}), | 110 | ...(thinking === false ? { thinking: false } : {}), |
| 109 | ...(effort ? { effort } : {}), | 111 | ...(effort ? { effort: effort as ClaudeModelOptions["effort"] } : {}), |
| 110 | ...(fastMode ? { fastMode: true } : {}), | 112 | ...(fastMode ? { fastMode: true } : {}), |
| 113 | ...(contextWindow ? { contextWindow } : {}), | ||
| 111 | }; | 114 | }; |
| 112 | return Object.keys(nextOptions).length > 0 ? nextOptions : undefined; | 115 | return Object.keys(nextOptions).length > 0 ? nextOptions : undefined; |
| 113 | } | 116 | } |
apps/server/src/provider/Layers/CodexProvider.ts+10-4| ... | @@ -10,7 +10,7 @@ import type { | ... | @@ -10,7 +10,7 @@ import type { |
| 10 | } from "@t3tools/contracts"; | 10 | } from "@t3tools/contracts"; |
| 11 | import { Effect, Equal, FileSystem, Layer, Option, Path, Result, Stream } from "effect"; | 11 | import { Effect, Equal, FileSystem, Layer, Option, Path, Result, Stream } from "effect"; |
| 12 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 12 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 13 | import { getDefaultEffort, trimOrNull } from "@t3tools/shared/model"; | 13 | import { resolveEffort } from "@t3tools/shared/model"; |
| 14 | 14 | ||
| 15 | import { | 15 | import { |
| 16 | buildServerProvider, | 16 | buildServerProvider, |
| ... | @@ -48,6 +48,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -48,6 +48,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 48 | ], | 48 | ], |
| 49 | supportsFastMode: true, | 49 | supportsFastMode: true, |
| 50 | supportsThinkingToggle: false, | 50 | supportsThinkingToggle: false, |
| 51 | contextWindowOptions: [], | ||
| 51 | promptInjectedEffortLevels: [], | 52 | promptInjectedEffortLevels: [], |
| 52 | }, | 53 | }, |
| 53 | }, | 54 | }, |
| ... | @@ -64,6 +65,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -64,6 +65,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 64 | ], | 65 | ], |
| 65 | supportsFastMode: true, | 66 | supportsFastMode: true, |
| 66 | supportsThinkingToggle: false, | 67 | supportsThinkingToggle: false, |
| 68 | contextWindowOptions: [], | ||
| 67 | promptInjectedEffortLevels: [], | 69 | promptInjectedEffortLevels: [], |
| 68 | }, | 70 | }, |
| 69 | }, | 71 | }, |
| ... | @@ -80,6 +82,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -80,6 +82,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 80 | ], | 82 | ], |
| 81 | supportsFastMode: true, | 83 | supportsFastMode: true, |
| 82 | supportsThinkingToggle: false, | 84 | supportsThinkingToggle: false, |
| 85 | contextWindowOptions: [], | ||
| 83 | promptInjectedEffortLevels: [], | 86 | promptInjectedEffortLevels: [], |
| 84 | }, | 87 | }, |
| 85 | }, | 88 | }, |
| ... | @@ -96,6 +99,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -96,6 +99,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 96 | ], | 99 | ], |
| 97 | supportsFastMode: true, | 100 | supportsFastMode: true, |
| 98 | supportsThinkingToggle: false, | 101 | supportsThinkingToggle: false, |
| 102 | contextWindowOptions: [], | ||
| 99 | promptInjectedEffortLevels: [], | 103 | promptInjectedEffortLevels: [], |
| 100 | }, | 104 | }, |
| 101 | }, | 105 | }, |
| ... | @@ -112,6 +116,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -112,6 +116,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 112 | ], | 116 | ], |
| 113 | supportsFastMode: true, | 117 | supportsFastMode: true, |
| 114 | supportsThinkingToggle: false, | 118 | supportsThinkingToggle: false, |
| 119 | contextWindowOptions: [], | ||
| 115 | promptInjectedEffortLevels: [], | 120 | promptInjectedEffortLevels: [], |
| 116 | }, | 121 | }, |
| 117 | }, | 122 | }, |
| ... | @@ -128,6 +133,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -128,6 +133,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 128 | ], | 133 | ], |
| 129 | supportsFastMode: true, | 134 | supportsFastMode: true, |
| 130 | supportsThinkingToggle: false, | 135 | supportsThinkingToggle: false, |
| 136 | contextWindowOptions: [], | ||
| 131 | promptInjectedEffortLevels: [], | 137 | promptInjectedEffortLevels: [], |
| 132 | }, | 138 | }, |
| 133 | }, | 139 | }, |
| ... | @@ -140,6 +146,7 @@ export function getCodexModelCapabilities(model: string | null | undefined): Mod | ... | @@ -140,6 +146,7 @@ export function getCodexModelCapabilities(model: string | null | undefined): Mod |
| 140 | reasoningEffortLevels: [], | 146 | reasoningEffortLevels: [], |
| 141 | supportsFastMode: false, | 147 | supportsFastMode: false, |
| 142 | supportsThinkingToggle: false, | 148 | supportsThinkingToggle: false, |
| 149 | contextWindowOptions: [], | ||
| 143 | promptInjectedEffortLevels: [], | 150 | promptInjectedEffortLevels: [], |
| 144 | } | 151 | } |
| 145 | ); | 152 | ); |
| ... | @@ -150,11 +157,10 @@ export function normalizeCodexModelOptions( | ... | @@ -150,11 +157,10 @@ export function normalizeCodexModelOptions( |
| 150 | modelOptions: CodexModelOptions | null | undefined, | 157 | modelOptions: CodexModelOptions | null | undefined, |
| 151 | ): CodexModelOptions | undefined { | 158 | ): CodexModelOptions | undefined { |
| 152 | const caps = getCodexModelCapabilities(model); | 159 | const caps = getCodexModelCapabilities(model); |
| 153 | const defaultReasoningEffort = getDefaultEffort(caps); | 160 | const reasoningEffort = resolveEffort(caps, modelOptions?.reasoningEffort); |
| 154 | const reasoningEffort = trimOrNull(modelOptions?.reasoningEffort) ?? defaultReasoningEffort; | ||
| 155 | const fastModeEnabled = modelOptions?.fastMode === true; | 161 | const fastModeEnabled = modelOptions?.fastMode === true; |
| 156 | const nextOptions: CodexModelOptions = { | 162 | const nextOptions: CodexModelOptions = { |
| 157 | ...(reasoningEffort && reasoningEffort !== defaultReasoningEffort | 163 | ...(reasoningEffort |
| 158 | ? { reasoningEffort: reasoningEffort as CodexModelOptions["reasoningEffort"] } | 164 | ? { reasoningEffort: reasoningEffort as CodexModelOptions["reasoningEffort"] } |
| 159 | : {}), | 165 | : {}), |
| 160 | ...(fastModeEnabled ? { fastMode: true } : {}), | 166 | ...(fastModeEnabled ? { fastMode: true } : {}), |
apps/web/src/components/ChatView.tsx+1-2| ... | @@ -5,7 +5,6 @@ import { | ... | @@ -5,7 +5,6 @@ import { |
| 5 | type MessageId, | 5 | type MessageId, |
| 6 | type ModelSelection, | 6 | type ModelSelection, |
| 7 | type ProjectScript, | 7 | type ProjectScript, |
| 8 | type ModelSlug, | ||
| 9 | type ProviderKind, | 8 | type ProviderKind, |
| 10 | type ProjectEntry, | 9 | type ProjectEntry, |
| 11 | type ProjectId, | 10 | type ProjectId, |
| ... | @@ -3105,7 +3104,7 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -3105,7 +3104,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3105 | ]); | 3104 | ]); |
| 3106 | 3105 | ||
| 3107 | const onProviderModelSelect = useCallback( | 3106 | const onProviderModelSelect = useCallback( |
| 3108 | (provider: ProviderKind, model: ModelSlug) => { | 3107 | (provider: ProviderKind, model: string) => { |
| 3109 | if (!activeThread) return; | 3108 | if (!activeThread) return; |
| 3110 | if (lockedProvider !== null && provider !== lockedProvider) { | 3109 | if (lockedProvider !== null && provider !== lockedProvider) { |
| 3111 | scheduleComposerFocus(); | 3110 | scheduleComposerFocus(); |
apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx+4| ... | @@ -60,6 +60,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str | ... | @@ -60,6 +60,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str |
| 60 | ], | 60 | ], |
| 61 | supportsFastMode: true, | 61 | supportsFastMode: true, |
| 62 | supportsThinkingToggle: false, | 62 | supportsThinkingToggle: false, |
| 63 | contextWindowOptions: [], | ||
| 63 | promptInjectedEffortLevels: ["ultrathink"], | 64 | promptInjectedEffortLevels: ["ultrathink"], |
| 64 | }, | 65 | }, |
| 65 | }, | 66 | }, |
| ... | @@ -71,6 +72,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str | ... | @@ -71,6 +72,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str |
| 71 | reasoningEffortLevels: [], | 72 | reasoningEffortLevels: [], |
| 72 | supportsFastMode: false, | 73 | supportsFastMode: false, |
| 73 | supportsThinkingToggle: true, | 74 | supportsThinkingToggle: true, |
| 75 | contextWindowOptions: [], | ||
| 74 | promptInjectedEffortLevels: [], | 76 | promptInjectedEffortLevels: [], |
| 75 | }, | 77 | }, |
| 76 | }, | 78 | }, |
| ... | @@ -87,6 +89,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str | ... | @@ -87,6 +89,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str |
| 87 | ], | 89 | ], |
| 88 | supportsFastMode: false, | 90 | supportsFastMode: false, |
| 89 | supportsThinkingToggle: false, | 91 | supportsThinkingToggle: false, |
| 92 | contextWindowOptions: [], | ||
| 90 | promptInjectedEffortLevels: ["ultrathink"], | 93 | promptInjectedEffortLevels: ["ultrathink"], |
| 91 | }, | 94 | }, |
| 92 | }, | 95 | }, |
| ... | @@ -103,6 +106,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str | ... | @@ -103,6 +106,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str |
| 103 | ], | 106 | ], |
| 104 | supportsFastMode: true, | 107 | supportsFastMode: true, |
| 105 | supportsThinkingToggle: false, | 108 | supportsThinkingToggle: false, |
| 109 | contextWindowOptions: [], | ||
| 106 | promptInjectedEffortLevels: [], | 110 | promptInjectedEffortLevels: [], |
| 107 | }, | 111 | }, |
| 108 | }, | 112 | }, |
apps/web/src/components/chat/ComposerCommandMenu.tsx+2-2| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { type ProjectEntry, type ModelSlug, type ProviderKind } from "@t3tools/contracts"; | 1 | import { type ProjectEntry, type ProviderKind } from "@t3tools/contracts"; |
| 2 | import { memo } from "react"; | 2 | import { memo } from "react"; |
| 3 | import { type ComposerSlashCommand, type ComposerTriggerKind } from "../../composer-logic"; | 3 | import { type ComposerSlashCommand, type ComposerTriggerKind } from "../../composer-logic"; |
| 4 | import { BotIcon } from "lucide-react"; | 4 | import { BotIcon } from "lucide-react"; |
| ... | @@ -27,7 +27,7 @@ export type ComposerCommandItem = | ... | @@ -27,7 +27,7 @@ export type ComposerCommandItem = |
| 27 | id: string; | 27 | id: string; |
| 28 | type: "model"; | 28 | type: "model"; |
| 29 | provider: ProviderKind; | 29 | provider: ProviderKind; |
| 30 | model: ModelSlug; | 30 | model: string; |
| 31 | label: string; | 31 | label: string; |
| 32 | description: string; | 32 | description: string; |
| 33 | }; | 33 | }; |
apps/web/src/components/chat/ProviderModelPicker.browser.tsx+7-2| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { type ModelSlug, type ProviderKind, type ServerProvider } from "@t3tools/contracts"; | 1 | import { type ProviderKind, type ServerProvider } from "@t3tools/contracts"; |
| 2 | import { page } from "vitest/browser"; | 2 | import { page } from "vitest/browser"; |
| 3 | import { afterEach, describe, expect, it, vi } from "vitest"; | 3 | import { afterEach, describe, expect, it, vi } from "vitest"; |
| 4 | import { render } from "vitest-browser-react"; | 4 | import { render } from "vitest-browser-react"; |
| ... | @@ -33,6 +33,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ | ... | @@ -33,6 +33,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ |
| 33 | reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")], | 33 | reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")], |
| 34 | supportsFastMode: true, | 34 | supportsFastMode: true, |
| 35 | supportsThinkingToggle: false, | 35 | supportsThinkingToggle: false, |
| 36 | contextWindowOptions: [], | ||
| 36 | promptInjectedEffortLevels: [], | 37 | promptInjectedEffortLevels: [], |
| 37 | }, | 38 | }, |
| 38 | }, | 39 | }, |
| ... | @@ -44,6 +45,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ | ... | @@ -44,6 +45,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ |
| 44 | reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")], | 45 | reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")], |
| 45 | supportsFastMode: true, | 46 | supportsFastMode: true, |
| 46 | supportsThinkingToggle: false, | 47 | supportsThinkingToggle: false, |
| 48 | contextWindowOptions: [], | ||
| 47 | promptInjectedEffortLevels: [], | 49 | promptInjectedEffortLevels: [], |
| 48 | }, | 50 | }, |
| 49 | }, | 51 | }, |
| ... | @@ -71,6 +73,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ | ... | @@ -71,6 +73,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ |
| 71 | ], | 73 | ], |
| 72 | supportsFastMode: false, | 74 | supportsFastMode: false, |
| 73 | supportsThinkingToggle: true, | 75 | supportsThinkingToggle: true, |
| 76 | contextWindowOptions: [], | ||
| 74 | promptInjectedEffortLevels: [], | 77 | promptInjectedEffortLevels: [], |
| 75 | }, | 78 | }, |
| 76 | }, | 79 | }, |
| ... | @@ -87,6 +90,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ | ... | @@ -87,6 +90,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ |
| 87 | ], | 90 | ], |
| 88 | supportsFastMode: false, | 91 | supportsFastMode: false, |
| 89 | supportsThinkingToggle: true, | 92 | supportsThinkingToggle: true, |
| 93 | contextWindowOptions: [], | ||
| 90 | promptInjectedEffortLevels: [], | 94 | promptInjectedEffortLevels: [], |
| 91 | }, | 95 | }, |
| 92 | }, | 96 | }, |
| ... | @@ -98,6 +102,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ | ... | @@ -98,6 +102,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ |
| 98 | reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")], | 102 | reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")], |
| 99 | supportsFastMode: false, | 103 | supportsFastMode: false, |
| 100 | supportsThinkingToggle: true, | 104 | supportsThinkingToggle: true, |
| 105 | contextWindowOptions: [], | ||
| 101 | promptInjectedEffortLevels: [], | 106 | promptInjectedEffortLevels: [], |
| 102 | }, | 107 | }, |
| 103 | }, | 108 | }, |
| ... | @@ -107,7 +112,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ | ... | @@ -107,7 +112,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ |
| 107 | 112 | ||
| 108 | async function mountPicker(props: { | 113 | async function mountPicker(props: { |
| 109 | provider: ProviderKind; | 114 | provider: ProviderKind; |
| 110 | model: ModelSlug; | 115 | model: string; |
| 111 | lockedProvider: ProviderKind | null; | 116 | lockedProvider: ProviderKind | null; |
| 112 | providers?: ReadonlyArray<ServerProvider>; | 117 | providers?: ReadonlyArray<ServerProvider>; |
| 113 | triggerVariant?: "ghost" | "outline"; | 118 | triggerVariant?: "ghost" | "outline"; |
apps/web/src/components/chat/ProviderModelPicker.tsx+3-3| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { type ModelSlug, type ProviderKind, type ServerProvider } from "@t3tools/contracts"; | 1 | import { type ProviderKind, type ServerProvider } from "@t3tools/contracts"; |
| 2 | import { resolveSelectableModel } from "@t3tools/shared/model"; | 2 | import { resolveSelectableModel } from "@t3tools/shared/model"; |
| 3 | import { memo, useState } from "react"; | 3 | import { memo, useState } from "react"; |
| 4 | import type { VariantProps } from "class-variance-authority"; | 4 | import type { VariantProps } from "class-variance-authority"; |
| ... | @@ -52,7 +52,7 @@ function providerIconClassName( | ... | @@ -52,7 +52,7 @@ function providerIconClassName( |
| 52 | 52 | ||
| 53 | export const ProviderModelPicker = memo(function ProviderModelPicker(props: { | 53 | export const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 54 | provider: ProviderKind; | 54 | provider: ProviderKind; |
| 55 | model: ModelSlug; | 55 | model: string; |
| 56 | lockedProvider: ProviderKind | null; | 56 | lockedProvider: ProviderKind | null; |
| 57 | providers?: ReadonlyArray<ServerProvider>; | 57 | providers?: ReadonlyArray<ServerProvider>; |
| 58 | modelOptionsByProvider: Record<ProviderKind, ReadonlyArray<{ slug: string; name: string }>>; | 58 | modelOptionsByProvider: Record<ProviderKind, ReadonlyArray<{ slug: string; name: string }>>; |
| ... | @@ -61,7 +61,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { | ... | @@ -61,7 +61,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 61 | disabled?: boolean; | 61 | disabled?: boolean; |
| 62 | triggerVariant?: VariantProps<typeof buttonVariants>["variant"]; | 62 | triggerVariant?: VariantProps<typeof buttonVariants>["variant"]; |
| 63 | triggerClassName?: string; | 63 | triggerClassName?: string; |
| 64 | onProviderModelChange: (provider: ProviderKind, model: ModelSlug) => void; | 64 | onProviderModelChange: (provider: ProviderKind, model: string) => void; |
| 65 | }) { | 65 | }) { |
| 66 | const [isMenuOpen, setIsMenuOpen] = useState(false); | 66 | const [isMenuOpen, setIsMenuOpen] = useState(false); |
| 67 | const activeProvider = props.lockedProvider ?? props.provider; | 67 | const activeProvider = props.lockedProvider ?? props.provider; |
apps/web/src/components/chat/TraitsPicker.browser.tsx+4| ... | @@ -49,6 +49,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ | ... | @@ -49,6 +49,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ |
| 49 | ], | 49 | ], |
| 50 | supportsFastMode: true, | 50 | supportsFastMode: true, |
| 51 | supportsThinkingToggle: false, | 51 | supportsThinkingToggle: false, |
| 52 | contextWindowOptions: [], | ||
| 52 | promptInjectedEffortLevels: [], | 53 | promptInjectedEffortLevels: [], |
| 53 | }, | 54 | }, |
| 54 | }, | 55 | }, |
| ... | @@ -77,6 +78,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ | ... | @@ -77,6 +78,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ |
| 77 | ], | 78 | ], |
| 78 | supportsFastMode: true, | 79 | supportsFastMode: true, |
| 79 | supportsThinkingToggle: false, | 80 | supportsThinkingToggle: false, |
| 81 | contextWindowOptions: [], | ||
| 80 | promptInjectedEffortLevels: ["ultrathink"], | 82 | promptInjectedEffortLevels: ["ultrathink"], |
| 81 | }, | 83 | }, |
| 82 | }, | 84 | }, |
| ... | @@ -93,6 +95,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ | ... | @@ -93,6 +95,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ |
| 93 | ], | 95 | ], |
| 94 | supportsFastMode: false, | 96 | supportsFastMode: false, |
| 95 | supportsThinkingToggle: false, | 97 | supportsThinkingToggle: false, |
| 98 | contextWindowOptions: [], | ||
| 96 | promptInjectedEffortLevels: ["ultrathink"], | 99 | promptInjectedEffortLevels: ["ultrathink"], |
| 97 | }, | 100 | }, |
| 98 | }, | 101 | }, |
| ... | @@ -104,6 +107,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ | ... | @@ -104,6 +107,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [ |
| 104 | reasoningEffortLevels: [], | 107 | reasoningEffortLevels: [], |
| 105 | supportsFastMode: false, | 108 | supportsFastMode: false, |
| 106 | supportsThinkingToggle: true, | 109 | supportsThinkingToggle: true, |
| 110 | contextWindowOptions: [], | ||
| 107 | promptInjectedEffortLevels: [], | 111 | promptInjectedEffortLevels: [], |
| 108 | }, | 112 | }, |
| 109 | }, | 113 | }, |
apps/web/src/components/chat/TraitsPicker.tsx+66-15| ... | @@ -11,7 +11,9 @@ import { | ... | @@ -11,7 +11,9 @@ import { |
| 11 | isClaudeUltrathinkPrompt, | 11 | isClaudeUltrathinkPrompt, |
| 12 | trimOrNull, | 12 | trimOrNull, |
| 13 | getDefaultEffort, | 13 | getDefaultEffort, |
| 14 | hasEffortLevel, | 14 | getDefaultContextWindow, |
| 15 | hasContextWindowOption, | ||
| 16 | resolveEffort, | ||
| 15 | } from "@t3tools/shared/model"; | 17 | } from "@t3tools/shared/model"; |
| 16 | import { memo, useCallback, useState } from "react"; | 18 | import { memo, useCallback, useState } from "react"; |
| 17 | import type { VariantProps } from "class-variance-authority"; | 19 | import type { VariantProps } from "class-variance-authority"; |
| ... | @@ -53,6 +55,16 @@ function getRawEffort( | ... | @@ -53,6 +55,16 @@ function getRawEffort( |
| 53 | return trimOrNull((modelOptions as ClaudeModelOptions | undefined)?.effort); | 55 | return trimOrNull((modelOptions as ClaudeModelOptions | undefined)?.effort); |
| 54 | } | 56 | } |
| 55 | 57 | ||
| 58 | function getRawContextWindow( | ||
| 59 | provider: ProviderKind, | ||
| 60 | modelOptions: ProviderOptions | null | undefined, | ||
| 61 | ): string | null { | ||
| 62 | if (provider === "claudeAgent") { | ||
| 63 | return trimOrNull((modelOptions as ClaudeModelOptions | undefined)?.contextWindow); | ||
| 64 | } | ||
| 65 | return null; | ||
| 66 | } | ||
| 67 | |||
| 56 | function buildNextOptions( | 68 | function buildNextOptions( |
| 57 | provider: ProviderKind, | 69 | provider: ProviderKind, |
| 58 | modelOptions: ProviderOptions | null | undefined, | 70 | modelOptions: ProviderOptions | null | undefined, |
| ... | @@ -78,21 +90,10 @@ function getSelectedTraits( | ... | @@ -78,21 +90,10 @@ function getSelectedTraits( |
| 78 | : caps.reasoningEffortLevels.filter( | 90 | : caps.reasoningEffortLevels.filter( |
| 79 | (option) => !caps.promptInjectedEffortLevels.includes(option.value), | 91 | (option) => !caps.promptInjectedEffortLevels.includes(option.value), |
| 80 | ); | 92 | ); |
| 81 | const defaultEffort = getDefaultEffort(caps); | ||
| 82 | 93 | ||
| 83 | // Resolve effort from options (provider-specific key) | 94 | // Resolve effort from options (provider-specific key) |
| 84 | const resolvedEffort = getRawEffort(provider, modelOptions); | 95 | const rawEffort = getRawEffort(provider, modelOptions); |
| 85 | 96 | const effort = resolveEffort(caps, rawEffort) ?? null; | |
| 86 | // Filter out prompt-injected efforts from the "current effort" display | ||
| 87 | const isPromptInjected = resolvedEffort | ||
| 88 | ? caps.promptInjectedEffortLevels.includes(resolvedEffort) | ||
| 89 | : false; | ||
| 90 | const effort = | ||
| 91 | resolvedEffort && !isPromptInjected && hasEffortLevel(caps, resolvedEffort) | ||
| 92 | ? resolvedEffort | ||
| 93 | : defaultEffort && hasEffortLevel(caps, defaultEffort) | ||
| 94 | ? defaultEffort | ||
| 95 | : null; | ||
| 96 | 97 | ||
| 97 | // Thinking toggle (only for models that support it) | 98 | // Thinking toggle (only for models that support it) |
| 98 | const thinkingEnabled = caps.supportsThinkingToggle | 99 | const thinkingEnabled = caps.supportsThinkingToggle |
| ... | @@ -104,6 +105,15 @@ function getSelectedTraits( | ... | @@ -104,6 +105,15 @@ function getSelectedTraits( |
| 104 | caps.supportsFastMode && | 105 | caps.supportsFastMode && |
| 105 | (modelOptions as { fastMode?: boolean } | undefined)?.fastMode === true; | 106 | (modelOptions as { fastMode?: boolean } | undefined)?.fastMode === true; |
| 106 | 107 | ||
| 108 | // Context window | ||
| 109 | const contextWindowOptions = caps.contextWindowOptions; | ||
| 110 | const rawContextWindow = getRawContextWindow(provider, modelOptions); | ||
| 111 | const defaultContextWindow = getDefaultContextWindow(caps); | ||
| 112 | const contextWindow = | ||
| 113 | rawContextWindow && hasContextWindowOption(caps, rawContextWindow) | ||
| 114 | ? rawContextWindow | ||
| 115 | : defaultContextWindow; | ||
| 116 | |||
| 107 | // Prompt-controlled effort (e.g. ultrathink in prompt text) | 117 | // Prompt-controlled effort (e.g. ultrathink in prompt text) |
| 108 | const ultrathinkPromptControlled = | 118 | const ultrathinkPromptControlled = |
| 109 | allowPromptInjectedEffort && | 119 | allowPromptInjectedEffort && |
| ... | @@ -116,6 +126,9 @@ function getSelectedTraits( | ... | @@ -116,6 +126,9 @@ function getSelectedTraits( |
| 116 | effortLevels, | 126 | effortLevels, |
| 117 | thinkingEnabled, | 127 | thinkingEnabled, |
| 118 | fastModeEnabled, | 128 | fastModeEnabled, |
| 129 | contextWindowOptions, | ||
| 130 | contextWindow, | ||
| 131 | defaultContextWindow, | ||
| 119 | ultrathinkPromptControlled, | 132 | ultrathinkPromptControlled, |
| 120 | }; | 133 | }; |
| 121 | } | 134 | } |
| ... | @@ -159,6 +172,9 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ | ... | @@ -159,6 +172,9 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ |
| 159 | effortLevels, | 172 | effortLevels, |
| 160 | thinkingEnabled, | 173 | thinkingEnabled, |
| 161 | fastModeEnabled, | 174 | fastModeEnabled, |
| 175 | contextWindowOptions, | ||
| 176 | contextWindow, | ||
| 177 | defaultContextWindow, | ||
| 162 | ultrathinkPromptControlled, | 178 | ultrathinkPromptControlled, |
| 163 | } = getSelectedTraits(provider, models, model, prompt, modelOptions, allowPromptInjectedEffort); | 179 | } = getSelectedTraits(provider, models, model, prompt, modelOptions, allowPromptInjectedEffort); |
| 164 | const defaultEffort = getDefaultEffort(caps); | 180 | const defaultEffort = getDefaultEffort(caps); |
| ... | @@ -194,7 +210,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ | ... | @@ -194,7 +210,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ |
| 194 | ], | 210 | ], |
| 195 | ); | 211 | ); |
| 196 | 212 | ||
| 197 | if (effort === null && thinkingEnabled === null) { | 213 | if (effort === null && thinkingEnabled === null && contextWindowOptions.length <= 1) { |
| 198 | return null; | 214 | return null; |
| 199 | } | 215 | } |
| 200 | 216 | ||
| ... | @@ -258,6 +274,33 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ | ... | @@ -258,6 +274,33 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ |
| 258 | </MenuGroup> | 274 | </MenuGroup> |
| 259 | </> | 275 | </> |
| 260 | ) : null} | 276 | ) : null} |
| 277 | {contextWindowOptions.length > 1 ? ( | ||
| 278 | <> | ||
| 279 | <MenuDivider /> | ||
| 280 | <MenuGroup> | ||
| 281 | <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs"> | ||
| 282 | Context Window | ||
| 283 | </div> | ||
| 284 | <MenuRadioGroup | ||
| 285 | value={contextWindow ?? defaultContextWindow ?? ""} | ||
| 286 | onValueChange={(value) => { | ||
| 287 | updateModelOptions( | ||
| 288 | buildNextOptions(provider, modelOptions, { | ||
| 289 | contextWindow: value, | ||
| 290 | }), | ||
| 291 | ); | ||
| 292 | }} | ||
| 293 | > | ||
| 294 | {contextWindowOptions.map((option) => ( | ||
| 295 | <MenuRadioItem key={option.value} value={option.value}> | ||
| 296 | {option.label} | ||
| 297 | {option.value === defaultContextWindow ? " (default)" : ""} | ||
| 298 | </MenuRadioItem> | ||
| 299 | ))} | ||
| 300 | </MenuRadioGroup> | ||
| 301 | </MenuGroup> | ||
| 302 | </> | ||
| 303 | ) : null} | ||
| 261 | </> | 304 | </> |
| 262 | ); | 305 | ); |
| 263 | }); | 306 | }); |
| ... | @@ -281,12 +324,19 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -281,12 +324,19 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 281 | effortLevels, | 324 | effortLevels, |
| 282 | thinkingEnabled, | 325 | thinkingEnabled, |
| 283 | fastModeEnabled, | 326 | fastModeEnabled, |
| 327 | contextWindowOptions, | ||
| 328 | contextWindow, | ||
| 329 | defaultContextWindow, | ||
| 284 | ultrathinkPromptControlled, | 330 | ultrathinkPromptControlled, |
| 285 | } = getSelectedTraits(provider, models, model, prompt, modelOptions, allowPromptInjectedEffort); | 331 | } = getSelectedTraits(provider, models, model, prompt, modelOptions, allowPromptInjectedEffort); |
| 286 | 332 | ||
| 287 | const effortLabel = effort | 333 | const effortLabel = effort |
| 288 | ? (effortLevels.find((l) => l.value === effort)?.label ?? effort) | 334 | ? (effortLevels.find((l) => l.value === effort)?.label ?? effort) |
| 289 | : null; | 335 | : null; |
| 336 | const contextWindowLabel = | ||
| 337 | contextWindowOptions.length > 1 && contextWindow !== defaultContextWindow | ||
| 338 | ? (contextWindowOptions.find((o) => o.value === contextWindow)?.label ?? null) | ||
| 339 | : null; | ||
| 290 | const triggerLabel = [ | 340 | const triggerLabel = [ |
| 291 | ultrathinkPromptControlled | 341 | ultrathinkPromptControlled |
| 292 | ? "Ultrathink" | 342 | ? "Ultrathink" |
| ... | @@ -296,6 +346,7 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -296,6 +346,7 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 296 | ? null | 346 | ? null |
| 297 | : `Thinking ${thinkingEnabled ? "On" : "Off"}`, | 347 | : `Thinking ${thinkingEnabled ? "On" : "Off"}`, |
| 298 | ...(caps.supportsFastMode && fastModeEnabled ? ["Fast"] : []), | 348 | ...(caps.supportsFastMode && fastModeEnabled ? ["Fast"] : []), |
| 349 | ...(contextWindowLabel ? [contextWindowLabel] : []), | ||
| 299 | ] | 350 | ] |
| 300 | .filter(Boolean) | 351 | .filter(Boolean) |
| 301 | .join(" · "); | 352 | .join(" · "); |
apps/web/src/components/chat/composerProviderRegistry.test.tsx+20-6| ... | @@ -16,6 +16,7 @@ const CODEX_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -16,6 +16,7 @@ const CODEX_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 16 | ], | 16 | ], |
| 17 | supportsFastMode: true, | 17 | supportsFastMode: true, |
| 18 | supportsThinkingToggle: false, | 18 | supportsThinkingToggle: false, |
| 19 | contextWindowOptions: [], | ||
| 19 | promptInjectedEffortLevels: [], | 20 | promptInjectedEffortLevels: [], |
| 20 | }, | 21 | }, |
| 21 | }, | 22 | }, |
| ... | @@ -35,6 +36,7 @@ const CLAUDE_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -35,6 +36,7 @@ const CLAUDE_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 35 | ], | 36 | ], |
| 36 | supportsFastMode: true, | 37 | supportsFastMode: true, |
| 37 | supportsThinkingToggle: false, | 38 | supportsThinkingToggle: false, |
| 39 | contextWindowOptions: [], | ||
| 38 | promptInjectedEffortLevels: ["ultrathink"], | 40 | promptInjectedEffortLevels: ["ultrathink"], |
| 39 | }, | 41 | }, |
| 40 | }, | 42 | }, |
| ... | @@ -51,6 +53,7 @@ const CLAUDE_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -51,6 +53,7 @@ const CLAUDE_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 51 | ], | 53 | ], |
| 52 | supportsFastMode: false, | 54 | supportsFastMode: false, |
| 53 | supportsThinkingToggle: false, | 55 | supportsThinkingToggle: false, |
| 56 | contextWindowOptions: [], | ||
| 54 | promptInjectedEffortLevels: ["ultrathink"], | 57 | promptInjectedEffortLevels: ["ultrathink"], |
| 55 | }, | 58 | }, |
| 56 | }, | 59 | }, |
| ... | @@ -62,6 +65,7 @@ const CLAUDE_MODELS: ReadonlyArray<ServerProviderModel> = [ | ... | @@ -62,6 +65,7 @@ const CLAUDE_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 62 | reasoningEffortLevels: [], | 65 | reasoningEffortLevels: [], |
| 63 | supportsFastMode: false, | 66 | supportsFastMode: false, |
| 64 | supportsThinkingToggle: true, | 67 | supportsThinkingToggle: true, |
| 68 | contextWindowOptions: [], | ||
| 65 | promptInjectedEffortLevels: [], | 69 | promptInjectedEffortLevels: [], |
| 66 | }, | 70 | }, |
| 67 | }, | 71 | }, |
| ... | @@ -80,7 +84,9 @@ describe("getComposerProviderState", () => { | ... | @@ -80,7 +84,9 @@ describe("getComposerProviderState", () => { |
| 80 | expect(state).toEqual({ | 84 | expect(state).toEqual({ |
| 81 | provider: "codex", | 85 | provider: "codex", |
| 82 | promptEffort: "high", | 86 | promptEffort: "high", |
| 83 | modelOptionsForDispatch: undefined, | 87 | modelOptionsForDispatch: { |
| 88 | reasoningEffort: "high", | ||
| 89 | }, | ||
| 84 | }); | 90 | }); |
| 85 | }); | 91 | }); |
| 86 | 92 | ||
| ... | @@ -125,12 +131,13 @@ describe("getComposerProviderState", () => { | ... | @@ -125,12 +131,13 @@ describe("getComposerProviderState", () => { |
| 125 | provider: "codex", | 131 | provider: "codex", |
| 126 | promptEffort: "high", | 132 | promptEffort: "high", |
| 127 | modelOptionsForDispatch: { | 133 | modelOptionsForDispatch: { |
| 134 | reasoningEffort: "high", | ||
| 128 | fastMode: true, | 135 | fastMode: true, |
| 129 | }, | 136 | }, |
| 130 | }); | 137 | }); |
| 131 | }); | 138 | }); |
| 132 | 139 | ||
| 133 | it("drops explicit codex default/off overrides from dispatch while keeping the selected effort label", () => { | 140 | it("preserves codex default effort explicitly in dispatch options", () => { |
| 134 | const state = getComposerProviderState({ | 141 | const state = getComposerProviderState({ |
| 135 | provider: "codex", | 142 | provider: "codex", |
| 136 | model: "gpt-5.4", | 143 | model: "gpt-5.4", |
| ... | @@ -147,7 +154,9 @@ describe("getComposerProviderState", () => { | ... | @@ -147,7 +154,9 @@ describe("getComposerProviderState", () => { |
| 147 | expect(state).toEqual({ | 154 | expect(state).toEqual({ |
| 148 | provider: "codex", | 155 | provider: "codex", |
| 149 | promptEffort: "high", | 156 | promptEffort: "high", |
| 150 | modelOptionsForDispatch: undefined, | 157 | modelOptionsForDispatch: { |
| 158 | reasoningEffort: "high", | ||
| 159 | }, | ||
| 151 | }); | 160 | }); |
| 152 | }); | 161 | }); |
| 153 | 162 | ||
| ... | @@ -163,7 +172,9 @@ describe("getComposerProviderState", () => { | ... | @@ -163,7 +172,9 @@ describe("getComposerProviderState", () => { |
| 163 | expect(state).toEqual({ | 172 | expect(state).toEqual({ |
| 164 | provider: "claudeAgent", | 173 | provider: "claudeAgent", |
| 165 | promptEffort: "high", | 174 | promptEffort: "high", |
| 166 | modelOptionsForDispatch: undefined, | 175 | modelOptionsForDispatch: { |
| 176 | effort: "high", | ||
| 177 | }, | ||
| 167 | }); | 178 | }); |
| 168 | }); | 179 | }); |
| 169 | 180 | ||
| ... | @@ -232,12 +243,13 @@ describe("getComposerProviderState", () => { | ... | @@ -232,12 +243,13 @@ describe("getComposerProviderState", () => { |
| 232 | provider: "claudeAgent", | 243 | provider: "claudeAgent", |
| 233 | promptEffort: "high", | 244 | promptEffort: "high", |
| 234 | modelOptionsForDispatch: { | 245 | modelOptionsForDispatch: { |
| 246 | effort: "high", | ||
| 235 | fastMode: true, | 247 | fastMode: true, |
| 236 | }, | 248 | }, |
| 237 | }); | 249 | }); |
| 238 | }); | 250 | }); |
| 239 | 251 | ||
| 240 | it("drops explicit Claude default/off overrides from dispatch while keeping the selected effort label", () => { | 252 | it("preserves Claude default effort explicitly in dispatch options", () => { |
| 241 | const state = getComposerProviderState({ | 253 | const state = getComposerProviderState({ |
| 242 | provider: "claudeAgent", | 254 | provider: "claudeAgent", |
| 243 | model: "claude-opus-4-6", | 255 | model: "claude-opus-4-6", |
| ... | @@ -254,7 +266,9 @@ describe("getComposerProviderState", () => { | ... | @@ -254,7 +266,9 @@ describe("getComposerProviderState", () => { |
| 254 | expect(state).toEqual({ | 266 | expect(state).toEqual({ |
| 255 | provider: "claudeAgent", | 267 | provider: "claudeAgent", |
| 256 | promptEffort: "high", | 268 | promptEffort: "high", |
| 257 | modelOptionsForDispatch: undefined, | 269 | modelOptionsForDispatch: { |
| 270 | effort: "high", | ||
| 271 | }, | ||
| 258 | }); | 272 | }); |
| 259 | }); | 273 | }); |
| 260 | }); | 274 | }); |
apps/web/src/components/chat/composerProviderRegistry.tsx+7-23| ... | @@ -1,16 +1,10 @@ | ... | @@ -1,16 +1,10 @@ |
| 1 | import { | 1 | import { |
| 2 | type ModelSlug, | ||
| 3 | type ProviderKind, | 2 | type ProviderKind, |
| 4 | type ProviderModelOptions, | 3 | type ProviderModelOptions, |
| 5 | type ServerProviderModel, | 4 | type ServerProviderModel, |
| 6 | type ThreadId, | 5 | type ThreadId, |
| 7 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| 8 | import { | 7 | import { isClaudeUltrathinkPrompt, resolveEffort } from "@t3tools/shared/model"; |
| 9 | isClaudeUltrathinkPrompt, | ||
| 10 | trimOrNull, | ||
| 11 | getDefaultEffort, | ||
| 12 | hasEffortLevel, | ||
| 13 | } from "@t3tools/shared/model"; | ||
| 14 | import type { ReactNode } from "react"; | 8 | import type { ReactNode } from "react"; |
| 15 | import { | 9 | import { |
| 16 | getProviderModelCapabilities, | 10 | getProviderModelCapabilities, |
| ... | @@ -21,7 +15,7 @@ import { TraitsMenuContent, TraitsPicker } from "./TraitsPicker"; | ... | @@ -21,7 +15,7 @@ import { TraitsMenuContent, TraitsPicker } from "./TraitsPicker"; |
| 21 | 15 | ||
| 22 | export type ComposerProviderStateInput = { | 16 | export type ComposerProviderStateInput = { |
| 23 | provider: ProviderKind; | 17 | provider: ProviderKind; |
| 24 | model: ModelSlug; | 18 | model: string; |
| 25 | models: ReadonlyArray<ServerProviderModel>; | 19 | models: ReadonlyArray<ServerProviderModel>; |
| 26 | prompt: string; | 20 | prompt: string; |
| 27 | modelOptions: ProviderModelOptions | null | undefined; | 21 | modelOptions: ProviderModelOptions | null | undefined; |
| ... | @@ -40,7 +34,7 @@ type ProviderRegistryEntry = { | ... | @@ -40,7 +34,7 @@ type ProviderRegistryEntry = { |
| 40 | getState: (input: ComposerProviderStateInput) => ComposerProviderState; | 34 | getState: (input: ComposerProviderStateInput) => ComposerProviderState; |
| 41 | renderTraitsMenuContent: (input: { | 35 | renderTraitsMenuContent: (input: { |
| 42 | threadId: ThreadId; | 36 | threadId: ThreadId; |
| 43 | model: ModelSlug; | 37 | model: string; |
| 44 | models: ReadonlyArray<ServerProviderModel>; | 38 | models: ReadonlyArray<ServerProviderModel>; |
| 45 | modelOptions: ProviderModelOptions[ProviderKind] | undefined; | 39 | modelOptions: ProviderModelOptions[ProviderKind] | undefined; |
| 46 | prompt: string; | 40 | prompt: string; |
| ... | @@ -48,7 +42,7 @@ type ProviderRegistryEntry = { | ... | @@ -48,7 +42,7 @@ type ProviderRegistryEntry = { |
| 48 | }) => ReactNode; | 42 | }) => ReactNode; |
| 49 | renderTraitsPicker: (input: { | 43 | renderTraitsPicker: (input: { |
| 50 | threadId: ThreadId; | 44 | threadId: ThreadId; |
| 51 | model: ModelSlug; | 45 | model: string; |
| 52 | models: ReadonlyArray<ServerProviderModel>; | 46 | models: ReadonlyArray<ServerProviderModel>; |
| 53 | modelOptions: ProviderModelOptions[ProviderKind] | undefined; | 47 | modelOptions: ProviderModelOptions[ProviderKind] | undefined; |
| 54 | prompt: string; | 48 | prompt: string; |
| ... | @@ -72,17 +66,7 @@ function getProviderStateFromCapabilities( | ... | @@ -72,17 +66,7 @@ function getProviderStateFromCapabilities( |
| 72 | : null | 66 | : null |
| 73 | : null; | 67 | : null; |
| 74 | 68 | ||
| 75 | const draftEffort = trimOrNull(rawEffort); | 69 | const promptEffort = resolveEffort(caps, rawEffort) ?? null; |
| 76 | const defaultEffort = getDefaultEffort(caps); | ||
| 77 | const isPromptInjected = draftEffort | ||
| 78 | ? caps.promptInjectedEffortLevels.includes(draftEffort) | ||
| 79 | : false; | ||
| 80 | const promptEffort = | ||
| 81 | draftEffort && !isPromptInjected && hasEffortLevel(caps, draftEffort) | ||
| 82 | ? draftEffort | ||
| 83 | : defaultEffort && hasEffortLevel(caps, defaultEffort) | ||
| 84 | ? defaultEffort | ||
| 85 | : null; | ||
| 86 | 70 | ||
| 87 | // Normalize options for dispatch | 71 | // Normalize options for dispatch |
| 88 | const normalizedOptions = | 72 | const normalizedOptions = |
| ... | @@ -180,7 +164,7 @@ export function getComposerProviderState(input: ComposerProviderStateInput): Com | ... | @@ -180,7 +164,7 @@ export function getComposerProviderState(input: ComposerProviderStateInput): Com |
| 180 | export function renderProviderTraitsMenuContent(input: { | 164 | export function renderProviderTraitsMenuContent(input: { |
| 181 | provider: ProviderKind; | 165 | provider: ProviderKind; |
| 182 | threadId: ThreadId; | 166 | threadId: ThreadId; |
| 183 | model: ModelSlug; | 167 | model: string; |
| 184 | models: ReadonlyArray<ServerProviderModel>; | 168 | models: ReadonlyArray<ServerProviderModel>; |
| 185 | modelOptions: ProviderModelOptions[ProviderKind] | undefined; | 169 | modelOptions: ProviderModelOptions[ProviderKind] | undefined; |
| 186 | prompt: string; | 170 | prompt: string; |
| ... | @@ -199,7 +183,7 @@ export function renderProviderTraitsMenuContent(input: { | ... | @@ -199,7 +183,7 @@ export function renderProviderTraitsMenuContent(input: { |
| 199 | export function renderProviderTraitsPicker(input: { | 183 | export function renderProviderTraitsPicker(input: { |
| 200 | provider: ProviderKind; | 184 | provider: ProviderKind; |
| 201 | threadId: ThreadId; | 185 | threadId: ThreadId; |
| 202 | model: ModelSlug; | 186 | model: string; |
| 203 | models: ReadonlyArray<ServerProviderModel>; | 187 | models: ReadonlyArray<ServerProviderModel>; |
| 204 | modelOptions: ProviderModelOptions[ProviderKind] | undefined; | 188 | modelOptions: ProviderModelOptions[ProviderKind] | undefined; |
| 205 | prompt: string; | 189 | prompt: string; |
apps/web/src/composerDraftStore.ts+16-8| ... | @@ -2,7 +2,7 @@ import { | ... | @@ -2,7 +2,7 @@ import { |
| 2 | CODEX_REASONING_EFFORT_OPTIONS, | 2 | CODEX_REASONING_EFFORT_OPTIONS, |
| 3 | type ClaudeCodeEffort, | 3 | type ClaudeCodeEffort, |
| 4 | type CodexReasoningEffort, | 4 | type CodexReasoningEffort, |
| 5 | type ModelSlug, | 5 | DEFAULT_MODEL_BY_PROVIDER, |
| 6 | ModelSelection, | 6 | ModelSelection, |
| 7 | ProjectId, | 7 | ProjectId, |
| 8 | ProviderInteractionMode, | 8 | ProviderInteractionMode, |
| ... | @@ -15,7 +15,7 @@ import { | ... | @@ -15,7 +15,7 @@ import { |
| 15 | import * as Schema from "effect/Schema"; | 15 | import * as Schema from "effect/Schema"; |
| 16 | import * as Equal from "effect/Equal"; | 16 | import * as Equal from "effect/Equal"; |
| 17 | import { DeepMutable } from "effect/Types"; | 17 | import { DeepMutable } from "effect/Types"; |
| 18 | import { getDefaultModel, normalizeModelSlug } from "@t3tools/shared/model"; | 18 | import { normalizeModelSlug } from "@t3tools/shared/model"; |
| 19 | import { useMemo } from "react"; | 19 | import { useMemo } from "react"; |
| 20 | import { getLocalStorageItem } from "./hooks/useLocalStorage"; | 20 | import { getLocalStorageItem } from "./hooks/useLocalStorage"; |
| 21 | import { resolveAppModelSelection } from "./modelSelection"; | 21 | import { resolveAppModelSelection } from "./modelSelection"; |
| ... | @@ -263,7 +263,7 @@ interface ComposerDraftStoreState { | ... | @@ -263,7 +263,7 @@ interface ComposerDraftStoreState { |
| 263 | } | 263 | } |
| 264 | 264 | ||
| 265 | export interface EffectiveComposerModelState { | 265 | export interface EffectiveComposerModelState { |
| 266 | selectedModel: ModelSlug; | 266 | selectedModel: string; |
| 267 | modelOptions: ProviderModelOptions | null; | 267 | modelOptions: ProviderModelOptions | null; |
| 268 | } | 268 | } |
| 269 | 269 | ||
| ... | @@ -475,12 +475,20 @@ function normalizeProviderModelOptions( | ... | @@ -475,12 +475,20 @@ function normalizeProviderModelOptions( |
| 475 | : claudeCandidate?.fastMode === false | 475 | : claudeCandidate?.fastMode === false |
| 476 | ? false | 476 | ? false |
| 477 | : undefined; | 477 | : undefined; |
| 478 | const claudeContextWindow = | ||
| 479 | typeof claudeCandidate?.contextWindow === "string" && claudeCandidate.contextWindow.length > 0 | ||
| 480 | ? claudeCandidate.contextWindow | ||
| 481 | : undefined; | ||
| 478 | const claude = | 482 | const claude = |
| 479 | claudeThinking !== undefined || claudeEffort !== undefined || claudeFastMode !== undefined | 483 | claudeThinking !== undefined || |
| 484 | claudeEffort !== undefined || | ||
| 485 | claudeFastMode !== undefined || | ||
| 486 | claudeContextWindow !== undefined | ||
| 480 | ? { | 487 | ? { |
| 481 | ...(claudeThinking !== undefined ? { thinking: claudeThinking } : {}), | 488 | ...(claudeThinking !== undefined ? { thinking: claudeThinking } : {}), |
| 482 | ...(claudeEffort !== undefined ? { effort: claudeEffort } : {}), | 489 | ...(claudeEffort !== undefined ? { effort: claudeEffort } : {}), |
| 483 | ...(claudeFastMode !== undefined ? { fastMode: claudeFastMode } : {}), | 490 | ...(claudeFastMode !== undefined ? { fastMode: claudeFastMode } : {}), |
| 491 | ...(claudeContextWindow !== undefined ? { contextWindow: claudeContextWindow } : {}), | ||
| 484 | } | 492 | } |
| 485 | : undefined; | 493 | : undefined; |
| 486 | 494 | ||
| ... | @@ -594,7 +602,7 @@ function legacyToModelSelectionByProvider( | ... | @@ -594,7 +602,7 @@ function legacyToModelSelectionByProvider( |
| 594 | model: | 602 | model: |
| 595 | modelSelection?.provider === provider | 603 | modelSelection?.provider === provider |
| 596 | ? modelSelection.model | 604 | ? modelSelection.model |
| 597 | : getDefaultModel(provider), | 605 | : DEFAULT_MODEL_BY_PROVIDER[provider], |
| 598 | options, | 606 | options, |
| 599 | }; | 607 | }; |
| 600 | } | 608 | } |
| ... | @@ -1676,7 +1684,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -1676,7 +1684,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 1676 | if (opts) { | 1684 | if (opts) { |
| 1677 | nextMap[provider] = { | 1685 | nextMap[provider] = { |
| 1678 | provider, | 1686 | provider, |
| 1679 | model: current?.model ?? getDefaultModel(provider), | 1687 | model: current?.model ?? DEFAULT_MODEL_BY_PROVIDER[provider], |
| 1680 | options: opts, | 1688 | options: opts, |
| 1681 | }; | 1689 | }; |
| 1682 | } else if (current?.options) { | 1690 | } else if (current?.options) { |
| ... | @@ -1726,7 +1734,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -1726,7 +1734,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 1726 | if (providerOpts) { | 1734 | if (providerOpts) { |
| 1727 | nextMap[normalizedProvider] = { | 1735 | nextMap[normalizedProvider] = { |
| 1728 | provider: normalizedProvider, | 1736 | provider: normalizedProvider, |
| 1729 | model: currentForProvider?.model ?? getDefaultModel(normalizedProvider), | 1737 | model: currentForProvider?.model ?? DEFAULT_MODEL_BY_PROVIDER[normalizedProvider], |
| 1730 | options: providerOpts, | 1738 | options: providerOpts, |
| 1731 | }; | 1739 | }; |
| 1732 | } else if (currentForProvider?.options) { | 1740 | } else if (currentForProvider?.options) { |
| ... | @@ -1744,7 +1752,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( | ... | @@ -1744,7 +1752,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()( |
| 1744 | base.modelSelectionByProvider[normalizedProvider] ?? | 1752 | base.modelSelectionByProvider[normalizedProvider] ?? |
| 1745 | ({ | 1753 | ({ |
| 1746 | provider: normalizedProvider, | 1754 | provider: normalizedProvider, |
| 1747 | model: getDefaultModel(normalizedProvider), | 1755 | model: DEFAULT_MODEL_BY_PROVIDER[normalizedProvider], |
| 1748 | } as ModelSelection); | 1756 | } as ModelSelection); |
| 1749 | if (providerOpts) { | 1757 | if (providerOpts) { |
| 1750 | nextStickyMap[normalizedProvider] = { | 1758 | nextStickyMap[normalizedProvider] = { |
apps/web/src/providerModels.ts+8-20| ... | @@ -7,17 +7,13 @@ import { | ... | @@ -7,17 +7,13 @@ import { |
| 7 | type ServerProvider, | 7 | type ServerProvider, |
| 8 | type ServerProviderModel, | 8 | type ServerProviderModel, |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import { | 10 | import { normalizeModelSlug, resolveContextWindow, resolveEffort } from "@t3tools/shared/model"; |
| 11 | getDefaultEffort, | ||
| 12 | hasEffortLevel, | ||
| 13 | normalizeModelSlug, | ||
| 14 | trimOrNull, | ||
| 15 | } from "@t3tools/shared/model"; | ||
| 16 | 11 | ||
| 17 | const EMPTY_CAPABILITIES: ModelCapabilities = { | 12 | const EMPTY_CAPABILITIES: ModelCapabilities = { |
| 18 | reasoningEffortLevels: [], | 13 | reasoningEffortLevels: [], |
| 19 | supportsFastMode: false, | 14 | supportsFastMode: false, |
| 20 | supportsThinkingToggle: false, | 15 | supportsThinkingToggle: false, |
| 16 | contextWindowOptions: [], | ||
| 21 | promptInjectedEffortLevels: [], | 17 | promptInjectedEffortLevels: [], |
| 22 | }; | 18 | }; |
| 23 | 19 | ||
| ... | @@ -78,11 +74,10 @@ export function normalizeCodexModelOptionsWithCapabilities( | ... | @@ -78,11 +74,10 @@ export function normalizeCodexModelOptionsWithCapabilities( |
| 78 | caps: ModelCapabilities, | 74 | caps: ModelCapabilities, |
| 79 | modelOptions: CodexModelOptions | null | undefined, | 75 | modelOptions: CodexModelOptions | null | undefined, |
| 80 | ): CodexModelOptions | undefined { | 76 | ): CodexModelOptions | undefined { |
| 81 | const defaultReasoningEffort = getDefaultEffort(caps); | 77 | const reasoningEffort = resolveEffort(caps, modelOptions?.reasoningEffort); |
| 82 | const reasoningEffort = trimOrNull(modelOptions?.reasoningEffort) ?? defaultReasoningEffort; | ||
| 83 | const fastModeEnabled = modelOptions?.fastMode === true; | 78 | const fastModeEnabled = modelOptions?.fastMode === true; |
| 84 | const nextOptions: CodexModelOptions = { | 79 | const nextOptions: CodexModelOptions = { |
| 85 | ...(reasoningEffort && reasoningEffort !== defaultReasoningEffort | 80 | ...(reasoningEffort |
| 86 | ? { reasoningEffort: reasoningEffort as CodexModelOptions["reasoningEffort"] } | 81 | ? { reasoningEffort: reasoningEffort as CodexModelOptions["reasoningEffort"] } |
| 87 | : {}), | 82 | : {}), |
| 88 | ...(fastModeEnabled ? { fastMode: true } : {}), | 83 | ...(fastModeEnabled ? { fastMode: true } : {}), |
| ... | @@ -94,23 +89,16 @@ export function normalizeClaudeModelOptionsWithCapabilities( | ... | @@ -94,23 +89,16 @@ export function normalizeClaudeModelOptionsWithCapabilities( |
| 94 | caps: ModelCapabilities, | 89 | caps: ModelCapabilities, |
| 95 | modelOptions: ClaudeModelOptions | null | undefined, | 90 | modelOptions: ClaudeModelOptions | null | undefined, |
| 96 | ): ClaudeModelOptions | undefined { | 91 | ): ClaudeModelOptions | undefined { |
| 97 | const defaultReasoningEffort = getDefaultEffort(caps); | 92 | const effort = resolveEffort(caps, modelOptions?.effort); |
| 98 | const resolvedEffort = trimOrNull(modelOptions?.effort); | ||
| 99 | const isPromptInjected = caps.promptInjectedEffortLevels.includes(resolvedEffort ?? ""); | ||
| 100 | const effort = | ||
| 101 | resolvedEffort && | ||
| 102 | !isPromptInjected && | ||
| 103 | hasEffortLevel(caps, resolvedEffort) && | ||
| 104 | resolvedEffort !== defaultReasoningEffort | ||
| 105 | ? resolvedEffort | ||
| 106 | : undefined; | ||
| 107 | const thinking = | 93 | const thinking = |
| 108 | caps.supportsThinkingToggle && modelOptions?.thinking === false ? false : undefined; | 94 | caps.supportsThinkingToggle && modelOptions?.thinking === false ? false : undefined; |
| 109 | const fastMode = caps.supportsFastMode && modelOptions?.fastMode === true ? true : undefined; | 95 | const fastMode = caps.supportsFastMode && modelOptions?.fastMode === true ? true : undefined; |
| 96 | const contextWindow = resolveContextWindow(caps, modelOptions?.contextWindow); | ||
| 110 | const nextOptions: ClaudeModelOptions = { | 97 | const nextOptions: ClaudeModelOptions = { |
| 111 | ...(thinking === false ? { thinking: false } : {}), | 98 | ...(thinking === false ? { thinking: false } : {}), |
| 112 | ...(effort ? { effort } : {}), | 99 | ...(effort ? { effort: effort as ClaudeModelOptions["effort"] } : {}), |
| 113 | ...(fastMode ? { fastMode: true } : {}), | 100 | ...(fastMode ? { fastMode: true } : {}), |
| 101 | ...(contextWindow ? { contextWindow } : {}), | ||
| 114 | }; | 102 | }; |
| 115 | return Object.keys(nextOptions).length > 0 ? nextOptions : undefined; | 103 | return Object.keys(nextOptions).length > 0 ? nextOptions : undefined; |
| 116 | } | 104 | } |
packages/contracts/src/model.ts+11-4| ... | @@ -18,6 +18,7 @@ export const ClaudeModelOptions = Schema.Struct({ | ... | @@ -18,6 +18,7 @@ export const ClaudeModelOptions = Schema.Struct({ |
| 18 | thinking: Schema.optional(Schema.Boolean), | 18 | thinking: Schema.optional(Schema.Boolean), |
| 19 | effort: Schema.optional(Schema.Literals(CLAUDE_CODE_EFFORT_OPTIONS)), | 19 | effort: Schema.optional(Schema.Literals(CLAUDE_CODE_EFFORT_OPTIONS)), |
| 20 | fastMode: Schema.optional(Schema.Boolean), | 20 | fastMode: Schema.optional(Schema.Boolean), |
| 21 | contextWindow: Schema.optional(Schema.String), | ||
| 21 | }); | 22 | }); |
| 22 | export type ClaudeModelOptions = typeof ClaudeModelOptions.Type; | 23 | export type ClaudeModelOptions = typeof ClaudeModelOptions.Type; |
| 23 | 24 | ||
| ... | @@ -34,17 +35,23 @@ export const EffortOption = Schema.Struct({ | ... | @@ -34,17 +35,23 @@ export const EffortOption = Schema.Struct({ |
| 34 | }); | 35 | }); |
| 35 | export type EffortOption = typeof EffortOption.Type; | 36 | export type EffortOption = typeof EffortOption.Type; |
| 36 | 37 | ||
| 38 | export const ContextWindowOption = Schema.Struct({ | ||
| 39 | value: TrimmedNonEmptyString, | ||
| 40 | label: TrimmedNonEmptyString, | ||
| 41 | isDefault: Schema.optional(Schema.Boolean), | ||
| 42 | }); | ||
| 43 | export type ContextWindowOption = typeof ContextWindowOption.Type; | ||
| 44 | |||
| 37 | export const ModelCapabilities = Schema.Struct({ | 45 | export const ModelCapabilities = Schema.Struct({ |
| 38 | reasoningEffortLevels: Schema.Array(EffortOption), | 46 | reasoningEffortLevels: Schema.Array(EffortOption), |
| 39 | supportsFastMode: Schema.Boolean, | 47 | supportsFastMode: Schema.Boolean, |
| 40 | supportsThinkingToggle: Schema.Boolean, | 48 | supportsThinkingToggle: Schema.Boolean, |
| 49 | contextWindowOptions: Schema.Array(ContextWindowOption), | ||
| 41 | promptInjectedEffortLevels: Schema.Array(TrimmedNonEmptyString), | 50 | promptInjectedEffortLevels: Schema.Array(TrimmedNonEmptyString), |
| 42 | }); | 51 | }); |
| 43 | export type ModelCapabilities = typeof ModelCapabilities.Type; | 52 | export type ModelCapabilities = typeof ModelCapabilities.Type; |
| 44 | 53 | ||
| 45 | export type ModelSlug = string & {}; | 54 | export const DEFAULT_MODEL_BY_PROVIDER: Record<ProviderKind, string> = { |
| 46 | |||
| 47 | export const DEFAULT_MODEL_BY_PROVIDER: Record<ProviderKind, ModelSlug> = { | ||
| 48 | codex: "gpt-5.4", | 55 | codex: "gpt-5.4", |
| 49 | claudeAgent: "claude-sonnet-4-6", | 56 | claudeAgent: "claude-sonnet-4-6", |
| 50 | }; | 57 | }; |
| ... | @@ -57,7 +64,7 @@ export const DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER: Record<ProviderKind, | ... | @@ -57,7 +64,7 @@ export const DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER: Record<ProviderKind, |
| 57 | claudeAgent: "claude-haiku-4-5", | 64 | claudeAgent: "claude-haiku-4-5", |
| 58 | }; | 65 | }; |
| 59 | 66 | ||
| 60 | export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string, ModelSlug>> = { | 67 | export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string, string>> = { |
| 61 | codex: { | 68 | codex: { |
| 62 | "5.4": "gpt-5.4", | 69 | "5.4": "gpt-5.4", |
| 63 | "5.3": "gpt-5.3-codex", | 70 | "5.3": "gpt-5.3-codex", |
packages/shared/src/model.test.ts+124-7| ... | @@ -1,16 +1,17 @@ | ... | @@ -1,16 +1,17 @@ |
| 1 | import { describe, expect, it } from "vitest"; | 1 | import { describe, expect, it } from "vitest"; |
| 2 | import { | 2 | import { DEFAULT_MODEL_BY_PROVIDER, type ModelCapabilities } from "@t3tools/contracts"; |
| 3 | DEFAULT_MODEL, | ||
| 4 | DEFAULT_MODEL_BY_PROVIDER, | ||
| 5 | type ModelCapabilities, | ||
| 6 | } from "@t3tools/contracts"; | ||
| 7 | 3 | ||
| 8 | import { | 4 | import { |
| 9 | applyClaudePromptEffortPrefix, | 5 | applyClaudePromptEffortPrefix, |
| 6 | getDefaultContextWindow, | ||
| 10 | getDefaultEffort, | 7 | getDefaultEffort, |
| 8 | hasContextWindowOption, | ||
| 11 | hasEffortLevel, | 9 | hasEffortLevel, |
| 12 | isClaudeUltrathinkPrompt, | 10 | isClaudeUltrathinkPrompt, |
| 13 | normalizeModelSlug, | 11 | normalizeModelSlug, |
| 12 | resolveApiModelId, | ||
| 13 | resolveContextWindow, | ||
| 14 | resolveEffort, | ||
| 14 | resolveModelSlug, | 15 | resolveModelSlug, |
| 15 | resolveModelSlugForProvider, | 16 | resolveModelSlugForProvider, |
| 16 | resolveSelectableModel, | 17 | resolveSelectableModel, |
| ... | @@ -24,6 +25,7 @@ const codexCaps: ModelCapabilities = { | ... | @@ -24,6 +25,7 @@ const codexCaps: ModelCapabilities = { |
| 24 | ], | 25 | ], |
| 25 | supportsFastMode: true, | 26 | supportsFastMode: true, |
| 26 | supportsThinkingToggle: false, | 27 | supportsThinkingToggle: false, |
| 28 | contextWindowOptions: [], | ||
| 27 | promptInjectedEffortLevels: [], | 29 | promptInjectedEffortLevels: [], |
| 28 | }; | 30 | }; |
| 29 | 31 | ||
| ... | @@ -35,6 +37,10 @@ const claudeCaps: ModelCapabilities = { | ... | @@ -35,6 +37,10 @@ const claudeCaps: ModelCapabilities = { |
| 35 | ], | 37 | ], |
| 36 | supportsFastMode: false, | 38 | supportsFastMode: false, |
| 37 | supportsThinkingToggle: false, | 39 | supportsThinkingToggle: false, |
| 40 | contextWindowOptions: [ | ||
| 41 | { value: "200k", label: "200k" }, | ||
| 42 | { value: "1m", label: "1M", isDefault: true }, | ||
| 43 | ], | ||
| 38 | promptInjectedEffortLevels: ["ultrathink"], | 44 | promptInjectedEffortLevels: ["ultrathink"], |
| 39 | }; | 45 | }; |
| 40 | 46 | ||
| ... | @@ -54,14 +60,15 @@ describe("normalizeModelSlug", () => { | ... | @@ -54,14 +60,15 @@ describe("normalizeModelSlug", () => { |
| 54 | 60 | ||
| 55 | describe("resolveModelSlug", () => { | 61 | describe("resolveModelSlug", () => { |
| 56 | it("returns defaults when the model is missing", () => { | 62 | it("returns defaults when the model is missing", () => { |
| 57 | expect(resolveModelSlug(undefined)).toBe(DEFAULT_MODEL); | 63 | expect(resolveModelSlug(undefined, "codex")).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); |
| 64 | |||
| 58 | expect(resolveModelSlugForProvider("claudeAgent", undefined)).toBe( | 65 | expect(resolveModelSlugForProvider("claudeAgent", undefined)).toBe( |
| 59 | DEFAULT_MODEL_BY_PROVIDER.claudeAgent, | 66 | DEFAULT_MODEL_BY_PROVIDER.claudeAgent, |
| 60 | ); | 67 | ); |
| 61 | }); | 68 | }); |
| 62 | 69 | ||
| 63 | it("preserves normalized unknown models", () => { | 70 | it("preserves normalized unknown models", () => { |
| 64 | expect(resolveModelSlug("custom/internal-model")).toBe("custom/internal-model"); | 71 | expect(resolveModelSlug("custom/internal-model", "codex")).toBe("custom/internal-model"); |
| 65 | }); | 72 | }); |
| 66 | }); | 73 | }); |
| 67 | 74 | ||
| ... | @@ -89,6 +96,42 @@ describe("capability helpers", () => { | ... | @@ -89,6 +96,42 @@ describe("capability helpers", () => { |
| 89 | }); | 96 | }); |
| 90 | }); | 97 | }); |
| 91 | 98 | ||
| 99 | describe("resolveEffort", () => { | ||
| 100 | it("returns the explicit value when supported and not prompt-injected", () => { | ||
| 101 | expect(resolveEffort(codexCaps, "xhigh")).toBe("xhigh"); | ||
| 102 | expect(resolveEffort(codexCaps, "high")).toBe("high"); | ||
| 103 | expect(resolveEffort(claudeCaps, "medium")).toBe("medium"); | ||
| 104 | }); | ||
| 105 | |||
| 106 | it("falls back to default when value is unsupported", () => { | ||
| 107 | expect(resolveEffort(codexCaps, "bogus")).toBe("high"); | ||
| 108 | expect(resolveEffort(claudeCaps, "bogus")).toBe("high"); | ||
| 109 | }); | ||
| 110 | |||
| 111 | it("returns the default when no value is provided", () => { | ||
| 112 | expect(resolveEffort(codexCaps, undefined)).toBe("high"); | ||
| 113 | expect(resolveEffort(codexCaps, null)).toBe("high"); | ||
| 114 | expect(resolveEffort(codexCaps, "")).toBe("high"); | ||
| 115 | expect(resolveEffort(codexCaps, " ")).toBe("high"); | ||
| 116 | }); | ||
| 117 | |||
| 118 | it("excludes prompt-injected efforts and falls back to default", () => { | ||
| 119 | expect(resolveEffort(claudeCaps, "ultrathink")).toBe("high"); | ||
| 120 | }); | ||
| 121 | |||
| 122 | it("returns undefined for models with no effort levels", () => { | ||
| 123 | const noCaps: ModelCapabilities = { | ||
| 124 | reasoningEffortLevels: [], | ||
| 125 | supportsFastMode: false, | ||
| 126 | supportsThinkingToggle: false, | ||
| 127 | contextWindowOptions: [], | ||
| 128 | promptInjectedEffortLevels: [], | ||
| 129 | }; | ||
| 130 | expect(resolveEffort(noCaps, undefined)).toBeUndefined(); | ||
| 131 | expect(resolveEffort(noCaps, "high")).toBeUndefined(); | ||
| 132 | }); | ||
| 133 | }); | ||
| 134 | |||
| 92 | describe("misc helpers", () => { | 135 | describe("misc helpers", () => { |
| 93 | it("detects ultrathink prompts", () => { | 136 | it("detects ultrathink prompts", () => { |
| 94 | expect(isClaudeUltrathinkPrompt("Ultrathink:\nInvestigate")).toBe(true); | 137 | expect(isClaudeUltrathinkPrompt("Ultrathink:\nInvestigate")).toBe(true); |
| ... | @@ -109,3 +152,77 @@ describe("misc helpers", () => { | ... | @@ -109,3 +152,77 @@ describe("misc helpers", () => { |
| 109 | expect(trimOrNull(" ")).toBeNull(); | 152 | expect(trimOrNull(" ")).toBeNull(); |
| 110 | }); | 153 | }); |
| 111 | }); | 154 | }); |
| 155 | |||
| 156 | describe("context window helpers", () => { | ||
| 157 | it("reads default context window", () => { | ||
| 158 | expect(getDefaultContextWindow(claudeCaps)).toBe("1m"); | ||
| 159 | }); | ||
| 160 | |||
| 161 | it("returns null for models without context window options", () => { | ||
| 162 | expect(getDefaultContextWindow(codexCaps)).toBeNull(); | ||
| 163 | }); | ||
| 164 | |||
| 165 | it("checks context window support", () => { | ||
| 166 | expect(hasContextWindowOption(claudeCaps, "1m")).toBe(true); | ||
| 167 | expect(hasContextWindowOption(claudeCaps, "200k")).toBe(true); | ||
| 168 | expect(hasContextWindowOption(claudeCaps, "bogus")).toBe(false); | ||
| 169 | expect(hasContextWindowOption(codexCaps, "1m")).toBe(false); | ||
| 170 | }); | ||
| 171 | }); | ||
| 172 | |||
| 173 | describe("resolveContextWindow", () => { | ||
| 174 | it("returns the explicit value when supported", () => { | ||
| 175 | expect(resolveContextWindow(claudeCaps, "200k")).toBe("200k"); | ||
| 176 | expect(resolveContextWindow(claudeCaps, "1m")).toBe("1m"); | ||
| 177 | }); | ||
| 178 | |||
| 179 | it("falls back to default when value is unsupported", () => { | ||
| 180 | expect(resolveContextWindow(claudeCaps, "bogus")).toBe("1m"); | ||
| 181 | }); | ||
| 182 | |||
| 183 | it("returns the default when no value is provided", () => { | ||
| 184 | expect(resolveContextWindow(claudeCaps, undefined)).toBe("1m"); | ||
| 185 | expect(resolveContextWindow(claudeCaps, null)).toBe("1m"); | ||
| 186 | expect(resolveContextWindow(claudeCaps, "")).toBe("1m"); | ||
| 187 | }); | ||
| 188 | |||
| 189 | it("returns undefined for models with no context window options", () => { | ||
| 190 | expect(resolveContextWindow(codexCaps, undefined)).toBeUndefined(); | ||
| 191 | expect(resolveContextWindow(codexCaps, "1m")).toBeUndefined(); | ||
| 192 | }); | ||
| 193 | }); | ||
| 194 | |||
| 195 | describe("resolveApiModelId", () => { | ||
| 196 | it("appends [1m] suffix for 1m context window", () => { | ||
| 197 | expect( | ||
| 198 | resolveApiModelId({ | ||
| 199 | provider: "claudeAgent", | ||
| 200 | model: "claude-opus-4-6", | ||
| 201 | options: { contextWindow: "1m" }, | ||
| 202 | }), | ||
| 203 | ).toBe("claude-opus-4-6[1m]"); | ||
| 204 | }); | ||
| 205 | |||
| 206 | it("returns the model as-is for 200k context window", () => { | ||
| 207 | expect( | ||
| 208 | resolveApiModelId({ | ||
| 209 | provider: "claudeAgent", | ||
| 210 | model: "claude-opus-4-6", | ||
| 211 | options: { contextWindow: "200k" }, | ||
| 212 | }), | ||
| 213 | ).toBe("claude-opus-4-6"); | ||
| 214 | }); | ||
| 215 | |||
| 216 | it("returns the model as-is when no context window is set", () => { | ||
| 217 | expect(resolveApiModelId({ provider: "claudeAgent", model: "claude-opus-4-6" })).toBe( | ||
| 218 | "claude-opus-4-6", | ||
| 219 | ); | ||
| 220 | expect( | ||
| 221 | resolveApiModelId({ provider: "claudeAgent", model: "claude-opus-4-6", options: {} }), | ||
| 222 | ).toBe("claude-opus-4-6"); | ||
| 223 | }); | ||
| 224 | |||
| 225 | it("returns the model as-is for Codex selections", () => { | ||
| 226 | expect(resolveApiModelId({ provider: "codex", model: "gpt-5.4" })).toBe("gpt-5.4"); | ||
| 227 | }); | ||
| 228 | }); |
packages/shared/src/model.ts+93-14| ... | @@ -3,7 +3,7 @@ import { | ... | @@ -3,7 +3,7 @@ import { |
| 3 | MODEL_SLUG_ALIASES_BY_PROVIDER, | 3 | MODEL_SLUG_ALIASES_BY_PROVIDER, |
| 4 | type ClaudeCodeEffort, | 4 | type ClaudeCodeEffort, |
| 5 | type ModelCapabilities, | 5 | type ModelCapabilities, |
| 6 | type ModelSlug, | 6 | type ModelSelection, |
| 7 | type ProviderKind, | 7 | type ProviderKind, |
| 8 | } from "@t3tools/contracts"; | 8 | } from "@t3tools/contracts"; |
| 9 | 9 | ||
| ... | @@ -12,10 +12,6 @@ export interface SelectableModelOption { | ... | @@ -12,10 +12,6 @@ export interface SelectableModelOption { |
| 12 | name: string; | 12 | name: string; |
| 13 | } | 13 | } |
| 14 | 14 | ||
| 15 | export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug { | ||
| 16 | return DEFAULT_MODEL_BY_PROVIDER[provider]; | ||
| 17 | } | ||
| 18 | |||
| 19 | // ── Effort helpers ──────────────────────────────────────────────────── | 15 | // ── Effort helpers ──────────────────────────────────────────────────── |
| 20 | 16 | ||
| 21 | /** Check whether a capabilities object includes a given effort value. */ | 17 | /** Check whether a capabilities object includes a given effort value. */ |
| ... | @@ -28,6 +24,65 @@ export function getDefaultEffort(caps: ModelCapabilities): string | null { | ... | @@ -28,6 +24,65 @@ export function getDefaultEffort(caps: ModelCapabilities): string | null { |
| 28 | return caps.reasoningEffortLevels.find((l) => l.isDefault)?.value ?? null; | 24 | return caps.reasoningEffortLevels.find((l) => l.isDefault)?.value ?? null; |
| 29 | } | 25 | } |
| 30 | 26 | ||
| 27 | /** | ||
| 28 | * Resolve a raw effort option against capabilities. | ||
| 29 | * | ||
| 30 | * Returns the effective effort value — the explicit value if supported and not | ||
| 31 | * prompt-injected, otherwise the model's default. Returns `undefined` only | ||
| 32 | * when the model has no effort levels at all. | ||
| 33 | * | ||
| 34 | * Prompt-injected efforts (e.g. "ultrathink") are excluded because they are | ||
| 35 | * applied via prompt text, not the effort API parameter. | ||
| 36 | */ | ||
| 37 | export function resolveEffort( | ||
| 38 | caps: ModelCapabilities, | ||
| 39 | raw: string | null | undefined, | ||
| 40 | ): string | undefined { | ||
| 41 | const defaultValue = getDefaultEffort(caps); | ||
| 42 | const trimmed = typeof raw === "string" ? raw.trim() : null; | ||
| 43 | if ( | ||
| 44 | trimmed && | ||
| 45 | !caps.promptInjectedEffortLevels.includes(trimmed) && | ||
| 46 | hasEffortLevel(caps, trimmed) | ||
| 47 | ) { | ||
| 48 | return trimmed; | ||
| 49 | } | ||
| 50 | return defaultValue ?? undefined; | ||
| 51 | } | ||
| 52 | |||
| 53 | // ── Context window helpers ─────────────────────────────────────────── | ||
| 54 | |||
| 55 | /** Check whether a capabilities object includes a given context window value. */ | ||
| 56 | export function hasContextWindowOption(caps: ModelCapabilities, value: string): boolean { | ||
| 57 | return caps.contextWindowOptions.some((o) => o.value === value); | ||
| 58 | } | ||
| 59 | |||
| 60 | /** Return the default context window value, or `null` if none is defined. */ | ||
| 61 | export function getDefaultContextWindow(caps: ModelCapabilities): string | null { | ||
| 62 | return caps.contextWindowOptions.find((o) => o.isDefault)?.value ?? null; | ||
| 63 | } | ||
| 64 | |||
| 65 | /** | ||
| 66 | * Resolve a raw `contextWindow` option against capabilities. | ||
| 67 | * | ||
| 68 | * Returns the effective context window value — the explicit value if supported, | ||
| 69 | * otherwise the model's default. Returns `undefined` only when the model has | ||
| 70 | * no context window options at all. | ||
| 71 | * | ||
| 72 | * Unlike effort levels (where the API has matching defaults), the context | ||
| 73 | * window requires an explicit API suffix (e.g. `[1m]`), so we always preserve | ||
| 74 | * the resolved value to avoid ambiguity between "user chose the default" and | ||
| 75 | * "not specified". | ||
| 76 | */ | ||
| 77 | export function resolveContextWindow( | ||
| 78 | caps: ModelCapabilities, | ||
| 79 | raw: string | null | undefined, | ||
| 80 | ): string | undefined { | ||
| 81 | const defaultValue = getDefaultContextWindow(caps); | ||
| 82 | if (!raw) return defaultValue ?? undefined; | ||
| 83 | return hasContextWindowOption(caps, raw) ? raw : (defaultValue ?? undefined); | ||
| 84 | } | ||
| 85 | |||
| 31 | export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean { | 86 | export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean { |
| 32 | return typeof text === "string" && /\bultrathink\b/i.test(text); | 87 | return typeof text === "string" && /\bultrathink\b/i.test(text); |
| 33 | } | 88 | } |
| ... | @@ -35,7 +90,7 @@ export function isClaudeUltrathinkPrompt(text: string | null | undefined): boole | ... | @@ -35,7 +90,7 @@ export function isClaudeUltrathinkPrompt(text: string | null | undefined): boole |
| 35 | export function normalizeModelSlug( | 90 | export function normalizeModelSlug( |
| 36 | model: string | null | undefined, | 91 | model: string | null | undefined, |
| 37 | provider: ProviderKind = "codex", | 92 | provider: ProviderKind = "codex", |
| 38 | ): ModelSlug | null { | 93 | ): string | null { |
| 39 | if (typeof model !== "string") { | 94 | if (typeof model !== "string") { |
| 40 | return null; | 95 | return null; |
| 41 | } | 96 | } |
| ... | @@ -45,18 +100,18 @@ export function normalizeModelSlug( | ... | @@ -45,18 +100,18 @@ export function normalizeModelSlug( |
| 45 | return null; | 100 | return null; |
| 46 | } | 101 | } |
| 47 | 102 | ||
| 48 | const aliases = MODEL_SLUG_ALIASES_BY_PROVIDER[provider] as Record<string, ModelSlug>; | 103 | const aliases = MODEL_SLUG_ALIASES_BY_PROVIDER[provider] as Record<string, string>; |
| 49 | const aliased = Object.prototype.hasOwnProperty.call(aliases, trimmed) | 104 | const aliased = Object.prototype.hasOwnProperty.call(aliases, trimmed) |
| 50 | ? aliases[trimmed] | 105 | ? aliases[trimmed] |
| 51 | : undefined; | 106 | : undefined; |
| 52 | return typeof aliased === "string" ? aliased : (trimmed as ModelSlug); | 107 | return typeof aliased === "string" ? aliased : trimmed; |
| 53 | } | 108 | } |
| 54 | 109 | ||
| 55 | export function resolveSelectableModel( | 110 | export function resolveSelectableModel( |
| 56 | provider: ProviderKind, | 111 | provider: ProviderKind, |
| 57 | value: string | null | undefined, | 112 | value: string | null | undefined, |
| 58 | options: ReadonlyArray<SelectableModelOption>, | 113 | options: ReadonlyArray<SelectableModelOption>, |
| 59 | ): ModelSlug | null { | 114 | ): string | null { |
| 60 | if (typeof value !== "string") { | 115 | if (typeof value !== "string") { |
| 61 | return null; | 116 | return null; |
| 62 | } | 117 | } |
| ... | @@ -85,10 +140,7 @@ export function resolveSelectableModel( | ... | @@ -85,10 +140,7 @@ export function resolveSelectableModel( |
| 85 | return resolved ? resolved.slug : null; | 140 | return resolved ? resolved.slug : null; |
| 86 | } | 141 | } |
| 87 | 142 | ||
| 88 | export function resolveModelSlug( | 143 | export function resolveModelSlug(model: string | null | undefined, provider: ProviderKind): string { |
| 89 | model: string | null | undefined, | ||
| 90 | provider: ProviderKind = "codex", | ||
| 91 | ): ModelSlug { | ||
| 92 | const normalized = normalizeModelSlug(model, provider); | 144 | const normalized = normalizeModelSlug(model, provider); |
| 93 | if (!normalized) { | 145 | if (!normalized) { |
| 94 | return DEFAULT_MODEL_BY_PROVIDER[provider]; | 146 | return DEFAULT_MODEL_BY_PROVIDER[provider]; |
| ... | @@ -99,7 +151,7 @@ export function resolveModelSlug( | ... | @@ -99,7 +151,7 @@ export function resolveModelSlug( |
| 99 | export function resolveModelSlugForProvider( | 151 | export function resolveModelSlugForProvider( |
| 100 | provider: ProviderKind, | 152 | provider: ProviderKind, |
| 101 | model: string | null | undefined, | 153 | model: string | null | undefined, |
| 102 | ): ModelSlug { | 154 | ): string { |
| 103 | return resolveModelSlug(model, provider); | 155 | return resolveModelSlug(model, provider); |
| 104 | } | 156 | } |
| 105 | 157 | ||
| ... | @@ -110,6 +162,33 @@ export function trimOrNull<T extends string>(value: T | null | undefined): T | n | ... | @@ -110,6 +162,33 @@ export function trimOrNull<T extends string>(value: T | null | undefined): T | n |
| 110 | return trimmed || null; | 162 | return trimmed || null; |
| 111 | } | 163 | } |
| 112 | 164 | ||
| 165 | /** | ||
| 166 | * Resolve the actual API model identifier from a model selection. | ||
| 167 | * | ||
| 168 | * Provider-aware: each provider can map `contextWindow` (or other options) | ||
| 169 | * to whatever the API requires — a model-id suffix, a separate parameter, etc. | ||
| 170 | * The canonical slug stored in the selection stays unchanged so the | ||
| 171 | * capabilities system keeps working. | ||
| 172 | * | ||
| 173 | * Expects `contextWindow` to already be resolved (via `resolveContextWindow`) | ||
| 174 | * to the effective value, not stripped to `undefined` for defaults. | ||
| 175 | */ | ||
| 176 | export function resolveApiModelId(modelSelection: ModelSelection): string { | ||
| 177 | switch (modelSelection.provider) { | ||
| 178 | case "claudeAgent": { | ||
| 179 | switch (modelSelection.options?.contextWindow) { | ||
| 180 | case "1m": | ||
| 181 | return `${modelSelection.model}[1m]`; | ||
| 182 | default: | ||
| 183 | return modelSelection.model; | ||
| 184 | } | ||
| 185 | } | ||
| 186 | default: { | ||
| 187 | return modelSelection.model; | ||
| 188 | } | ||
| 189 | } | ||
| 190 | } | ||
| 191 | |||
| 113 | export function applyClaudePromptEffortPrefix( | 192 | export function applyClaudePromptEffortPrefix( |
| 114 | text: string, | 193 | text: string, |
| 115 | effort: ClaudeCodeEffort | null | undefined, | 194 | effort: ClaudeCodeEffort | null | undefined, |