Skip to content

Commit 90d8261

Browse files
committed
fix: normalize xhigh effort for Claude CLI and extract shared prompt-injected descriptor lookup
- Add normalizeClaudeEffortForCli() in ClaudeProvider.ts to map xhigh->max and filter ultrathink before passing to CLI/SDK - Apply normalization in ClaudeTextGeneration.ts (was missing, producing invalid --effort xhigh CLI flag for Opus 4.7) - Refactor getEffectiveClaudeAgentEffort in ClaudeAdapter.ts to delegate to the new shared normalizer - Extract findPromptInjectedDescriptor() into @t3tools/shared/model to deduplicate the .find() logic between ClaudeAdapter and ChatView
1 parent 29261cf commit 90d8261

5 files changed

Lines changed: 44 additions & 41 deletions

File tree

apps/server/src/git/Layers/ClaudeTextGeneration.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
import { getProviderOptionCurrentValue, getProviderOptionDescriptors } from "@t3tools/shared/model";
3232
import {
3333
getClaudeModelCapabilities,
34+
normalizeClaudeEffortForCli,
3435
resolveClaudeApiModelId,
3536
resolveClaudeEffort,
3637
} from "../../provider/Layers/ClaudeProvider.ts";
@@ -95,7 +96,7 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
9596
const findDescriptor = (id: string) => descriptors.find((descriptor) => descriptor.id === id);
9697
const rawEffortValue = getProviderOptionCurrentValue(findDescriptor("effort"));
9798
const rawEffort = typeof rawEffortValue === "string" ? rawEffortValue : undefined;
98-
const resolvedEffort = resolveClaudeEffort(caps, rawEffort);
99+
const resolvedEffort = normalizeClaudeEffortForCli(resolveClaudeEffort(caps, rawEffort));
99100
const thinkingDescriptor = findDescriptor("thinking");
100101
const fastModeDescriptor = findDescriptor("fastMode");
101102
const thinking =

apps/server/src/provider/Layers/ClaudeAdapter.ts

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
} from "@t3tools/contracts";
4444
import {
4545
applyClaudePromptEffortPrefix,
46+
findPromptInjectedDescriptor,
4647
getProviderOptionDescriptors,
4748
getModelSelectionOptionValue,
4849
trimOrNull,
@@ -67,6 +68,7 @@ import { ServerConfig } from "../../config.ts";
6768
import { ServerSettingsService } from "../../serverSettings.ts";
6869
import {
6970
getClaudeModelCapabilities,
71+
normalizeClaudeEffortForCli,
7072
resolveClaudeApiModelId,
7173
resolveClaudeEffort,
7274
} from "./ClaudeProvider.ts";
@@ -220,16 +222,8 @@ function normalizeClaudeStreamMessages(cause: Cause.Cause<Error>): ReadonlyArray
220222
}
221223

222224
function getEffectiveClaudeAgentEffort(effort: string | null | undefined): ClaudeSdkEffort | null {
223-
if (!effort) {
224-
return null;
225-
}
226-
if (effort === "ultrathink") {
227-
return null;
228-
}
229-
if (effort === "xhigh") {
230-
return "max";
231-
}
232-
return effort as ClaudeSdkEffort;
225+
const normalized = normalizeClaudeEffortForCli(effort);
226+
return normalized ? (normalized as ClaudeSdkEffort) : null;
233227
}
234228

235229
function isClaudeInterruptedMessage(message: string): boolean {
@@ -569,23 +563,10 @@ function buildPromptText(input: ProviderSendTurnInput): string {
569563
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.model : undefined;
570564
const caps = getClaudeModelCapabilities(claudeModel);
571565

572-
// For prompt injection, we check if the raw effort is a prompt-injected level (e.g. "ultrathink").
573-
// Normal Claude effort resolution strips prompt-injected values back to the model default,
574-
// so prompt formatting must look at the raw selection value directly.
575566
const trimmedEffort = trimOrNull(rawEffort);
576-
const promptInjectedDescriptor = getProviderOptionDescriptors({ caps }).find(
577-
(descriptor) =>
578-
descriptor.type === "select" &&
579-
(descriptor.id === "effort" ||
580-
descriptor.id === "reasoningEffort" ||
581-
descriptor.id === "reasoning" ||
582-
descriptor.id === "variant") &&
583-
(descriptor.promptInjectedValues?.length ?? 0) > 0,
584-
);
567+
const promptInjectedDescriptor = findPromptInjectedDescriptor(caps);
585568
const promptEffort =
586-
trimmedEffort &&
587-
promptInjectedDescriptor?.type === "select" &&
588-
promptInjectedDescriptor.promptInjectedValues?.includes(trimmedEffort)
569+
trimmedEffort && promptInjectedDescriptor?.promptInjectedValues?.includes(trimmedEffort)
589570
? trimmedEffort
590571
: null;
591572
return applyClaudePromptEffortPrefix(input.input?.trim() ?? "", promptEffort);

apps/server/src/provider/Layers/ClaudeProvider.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,20 @@ export function resolveClaudeEffort(
221221
return typeof value === "string" ? value : undefined;
222222
}
223223

224+
/**
225+
* Maps a T3-internal effort value to a Claude CLI/SDK-compatible effort value.
226+
* - `"xhigh"` (T3-internal level between high and max) maps to `"max"`.
227+
* - `"ultrathink"` is prompt-injected only; returns `undefined` so the CLI
228+
* flag is omitted entirely.
229+
* - All other values pass through unchanged.
230+
*/
231+
export function normalizeClaudeEffortForCli(effort: string | null | undefined): string | undefined {
232+
if (!effort) return undefined;
233+
if (effort === "ultrathink") return undefined;
234+
if (effort === "xhigh") return "max";
235+
return effort;
236+
}
237+
224238
export function resolveClaudeApiModelId(modelSelection: ClaudeModelSelection): string {
225239
switch (getModelSelectionOptionValue(modelSelection, "contextWindow")) {
226240
case "1m":

apps/web/src/components/ChatView.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
import {
2929
applyClaudePromptEffortPrefix,
3030
createModelSelection,
31-
getProviderOptionDescriptors,
31+
findPromptInjectedDescriptor,
3232
} from "@t3tools/shared/model";
3333
import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";
3434
import { truncate } from "@t3tools/shared/String";
@@ -309,20 +309,8 @@ function formatOutgoingPrompt(params: {
309309
text: string;
310310
}): string {
311311
const caps = getProviderModelCapabilities(params.models, params.model, params.provider);
312-
const promptInjectedDescriptor = getProviderOptionDescriptors({ caps }).find(
313-
(descriptor) =>
314-
descriptor.type === "select" &&
315-
(descriptor.id === "reasoningEffort" ||
316-
descriptor.id === "effort" ||
317-
descriptor.id === "reasoning" ||
318-
descriptor.id === "variant") &&
319-
(descriptor.promptInjectedValues?.length ?? 0) > 0,
320-
);
321-
if (
322-
params.effort &&
323-
promptInjectedDescriptor?.type === "select" &&
324-
promptInjectedDescriptor.promptInjectedValues?.includes(params.effort)
325-
) {
312+
const promptInjectedDescriptor = findPromptInjectedDescriptor(caps);
313+
if (params.effort && promptInjectedDescriptor?.promptInjectedValues?.includes(params.effort)) {
326314
return applyClaudePromptEffortPrefix(params.text, params.effort);
327315
}
328316
return params.text;

packages/shared/src/model.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,25 @@ export function createModelSelection(
292292
} as ModelSelection;
293293
}
294294

295+
const PROMPT_INJECTED_DESCRIPTOR_IDS = new Set([
296+
"effort",
297+
"reasoningEffort",
298+
"reasoning",
299+
"variant",
300+
]);
301+
302+
export function findPromptInjectedDescriptor(
303+
caps: ModelCapabilities,
304+
): Extract<ProviderOptionDescriptor, { type: "select" }> | undefined {
305+
const descriptor = getProviderOptionDescriptors({ caps }).find(
306+
(d) =>
307+
d.type === "select" &&
308+
PROMPT_INJECTED_DESCRIPTOR_IDS.has(d.id) &&
309+
(d.promptInjectedValues?.length ?? 0) > 0,
310+
);
311+
return descriptor?.type === "select" ? descriptor : undefined;
312+
}
313+
295314
export function applyClaudePromptEffortPrefix(
296315
text: string,
297316
effort: string | null | undefined,

0 commit comments

Comments
 (0)