@oh-my-pi/pi-ai 18.1.2 → 18.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/types/providers/anthropic.d.ts +2 -2
- package/dist/types/providers/cursor.d.ts +1 -1
- package/package.json +5 -5
- package/src/auth-storage.ts +3 -3
- package/src/dialect/deepseek.ts +36 -2
- package/src/providers/anthropic.ts +181 -90
- package/src/providers/cursor.ts +10 -3
- package/src/providers/google-shared.ts +7 -0
- package/src/utils/tool-call-loop-guard.ts +19 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.1.3] - 2026-09-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Gemini 3 sessions on Antigravity/Cloud Code Assist and Vertex AI getting permanently stuck on `400 INVALID_ARGUMENT` after a turn with parallel tool calls ([#9638](https://github.com/can1357/oh-my-pi/issues/9638)).
|
|
10
|
+
- Preserved Anthropic thinking now survives side requests, tool-description drift, turn-scoped reminders, and recoverable prefix mismatches without corrupting the conversation prefix.
|
|
11
|
+
- Fixed Anthropic-compatible endpoints backed by Amazon Bedrock permanently rejecting a session once an unsigned thinking block entered its history. The transport now recognizes Bedrock's `ValidationException … thinking.signature: Field required` as the same unsigned-thinking rejection it already heals for other signing proxies, so it demotes the unsigned block to text, retries once, and remembers the endpoint for the rest of the session instead of failing every turn and walking the model fallback chain.
|
|
12
|
+
- Fixed the DeepSeek DSML markup healer leaking orphan `</|DSML|parameter>`/`</|DSML|invoke>` close tags into visible text, which poisoned long-session history and reinforced the model's XML-protocol mimicry until tool calls stopped dispatching ([#10556](https://github.com/can1357/oh-my-pi/issues/10556)).
|
|
13
|
+
- Fixed repeated parallel tool-call batches bypassing the configured loop guard.
|
|
14
|
+
- Fixed Cursor tool-schema composition failures by projecting unsupported keywords only for confirmed Fable models; Grok and other Cursor models retain canonical schemas.
|
|
15
|
+
- Fixed API-key account rotation to honor provider-reported quota reset windows, including overlapping exhausted windows ([#10325](https://github.com/can1357/oh-my-pi/pull/10325) by [@usr-bin-roygbiv](https://github.com/usr-bin-roygbiv)).
|
|
16
|
+
|
|
5
17
|
## [18.1.2] - 2026-09-01
|
|
6
18
|
|
|
7
19
|
### Added
|
|
@@ -215,8 +215,8 @@ export declare function applyAnthropicUsageExtras(usage: Usage, source: Anthropi
|
|
|
215
215
|
export declare function isThinkingPrefixBindingError(message: string): boolean;
|
|
216
216
|
export declare function isInvalidThinkingSignatureError(message: string): boolean;
|
|
217
217
|
/**
|
|
218
|
-
* Prepend a pointed remediation to
|
|
219
|
-
*
|
|
218
|
+
* Prepend a pointed remediation to a thinking-signature rejection 400 when the
|
|
219
|
+
* model looks like an unmarked custom signing proxy
|
|
220
220
|
* (opaque baseUrl, `spec.reasoning: true`, no explicit
|
|
221
221
|
* `compat.replayUnsignedThinking` override). The default is native replay for
|
|
222
222
|
* the 3p reasoning majority (#2005); this hint turns the misconfigured-proxy
|
|
@@ -223,7 +223,7 @@ export declare function processInteractionUpdate(update: any, output: AssistantM
|
|
|
223
223
|
* that reconstruction.
|
|
224
224
|
*/
|
|
225
225
|
export declare function buildCursorRequestContextRules(systemPrompt: readonly string[] | undefined): CursorRule[];
|
|
226
|
-
export declare function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[];
|
|
226
|
+
export declare function buildMcpToolDefinitions(tools: Tool[] | undefined, requiresCursorToolSchemaProjection?: boolean): McpToolDefinition[];
|
|
227
227
|
/**
|
|
228
228
|
* Build `ConversationStateStructure.rootPromptMessagesJson` blob IDs for the
|
|
229
229
|
* system prompt plus prior conversation history, as JSON blobs matching
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "18.1.
|
|
4
|
+
"version": "18.1.4",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/omptype": "18.1.
|
|
41
|
-
"@oh-my-pi/pi-catalog": "18.1.
|
|
42
|
-
"@oh-my-pi/pi-utils": "18.1.
|
|
43
|
-
"@oh-my-pi/pi-wire": "18.1.
|
|
40
|
+
"@oh-my-pi/omptype": "18.1.4",
|
|
41
|
+
"@oh-my-pi/pi-catalog": "18.1.4",
|
|
42
|
+
"@oh-my-pi/pi-utils": "18.1.4",
|
|
43
|
+
"@oh-my-pi/pi-wire": "18.1.4"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/bun": "^1.3.14"
|
package/src/auth-storage.ts
CHANGED
|
@@ -3873,7 +3873,7 @@ export class AuthStorage {
|
|
|
3873
3873
|
return limits.some(limit => this.#isUsageLimitExhausted(limit));
|
|
3874
3874
|
}
|
|
3875
3875
|
|
|
3876
|
-
/** Extracts
|
|
3876
|
+
/** Extracts when every currently exhausted window has reset (in ms). */
|
|
3877
3877
|
#getUsageResetAtMs(limits: UsageLimit[], nowMs: number): number | undefined {
|
|
3878
3878
|
const candidates: number[] = [];
|
|
3879
3879
|
for (const limit of limits) {
|
|
@@ -3884,7 +3884,7 @@ export class AuthStorage {
|
|
|
3884
3884
|
}
|
|
3885
3885
|
}
|
|
3886
3886
|
if (candidates.length === 0) return undefined;
|
|
3887
|
-
return Math.
|
|
3887
|
+
return Math.max(...candidates);
|
|
3888
3888
|
}
|
|
3889
3889
|
|
|
3890
3890
|
async #getUsageReport(
|
|
@@ -4572,7 +4572,7 @@ export class AuthStorage {
|
|
|
4572
4572
|
const now = Date.now();
|
|
4573
4573
|
let blockedUntil = now + (options?.retryAfterMs ?? AuthStorage.#defaultBackoffMs);
|
|
4574
4574
|
|
|
4575
|
-
if (
|
|
4575
|
+
if (target && routing.strategy) {
|
|
4576
4576
|
const report = await raceUsageWithSignal(
|
|
4577
4577
|
this.#getUsageReport(provider, target.credential, options),
|
|
4578
4578
|
options?.signal,
|
package/src/dialect/deepseek.ts
CHANGED
|
@@ -34,6 +34,10 @@ const DSML_TOOL_CALLS_OPEN_FULLWIDTH = "<|DSML|tool_calls>";
|
|
|
34
34
|
const DSML_TOOL_CALLS_CLOSE_FULLWIDTH = "</|DSML|tool_calls>";
|
|
35
35
|
const DSML_TOOL_CALLS_OPEN_ASCII = "<|DSML|tool_calls>";
|
|
36
36
|
const DSML_TOOL_CALLS_CLOSE_ASCII = "</|DSML|tool_calls>";
|
|
37
|
+
const DSML_INVOKE_CLOSE_FULLWIDTH = "</|DSML|invoke>";
|
|
38
|
+
const DSML_INVOKE_CLOSE_ASCII = "</|DSML|invoke>";
|
|
39
|
+
const DSML_PARAMETER_CLOSE_FULLWIDTH = "</|DSML|parameter>";
|
|
40
|
+
const DSML_PARAMETER_CLOSE_ASCII = "</|DSML|parameter>";
|
|
37
41
|
|
|
38
42
|
const CONTROL_TOKENS = [
|
|
39
43
|
DEEPSEEK_BOS,
|
|
@@ -53,6 +57,18 @@ const CONTROL_TOKENS = [
|
|
|
53
57
|
DEEPSEEK_TOOL_OUTPUT_END,
|
|
54
58
|
] as const;
|
|
55
59
|
|
|
60
|
+
// Bare DSML invoke/parameter close tags with no matching open — leaked into
|
|
61
|
+
// visible text once a session's history is poisoned (issue #10556). Stripped in
|
|
62
|
+
// the `outside` state so they never reach stored assistant content and reinforce
|
|
63
|
+
// the model's XML-protocol mimicry. Inside a well-formed envelope these closers
|
|
64
|
+
// are consumed by the `dsmlInvoke`/`dsmlParam` states and never reach `outside`.
|
|
65
|
+
const DSML_ORPHAN_CLOSE_TOKENS = [
|
|
66
|
+
DSML_INVOKE_CLOSE_FULLWIDTH,
|
|
67
|
+
DSML_INVOKE_CLOSE_ASCII,
|
|
68
|
+
DSML_PARAMETER_CLOSE_FULLWIDTH,
|
|
69
|
+
DSML_PARAMETER_CLOSE_ASCII,
|
|
70
|
+
] as const;
|
|
71
|
+
|
|
56
72
|
const OUTSIDE_TOKENS = [
|
|
57
73
|
DEEPSEEK_TOOL_CALLS_BEGIN,
|
|
58
74
|
DEEPSEEK_TOOL_CALLS_END,
|
|
@@ -63,6 +79,7 @@ const OUTSIDE_TOKENS = [
|
|
|
63
79
|
DSML_TOOL_CALLS_OPEN_ASCII,
|
|
64
80
|
DSML_TOOL_CALLS_CLOSE_FULLWIDTH,
|
|
65
81
|
DSML_TOOL_CALLS_CLOSE_ASCII,
|
|
82
|
+
...DSML_ORPHAN_CLOSE_TOKENS,
|
|
66
83
|
...CONTROL_TOKENS,
|
|
67
84
|
] as const;
|
|
68
85
|
|
|
@@ -73,8 +90,13 @@ const DSML_SECTION_TOKENS = [
|
|
|
73
90
|
"<|DSML|invoke",
|
|
74
91
|
"<|DSML|invoke",
|
|
75
92
|
] as const;
|
|
76
|
-
const DSML_INVOKE_TOKENS = [
|
|
77
|
-
|
|
93
|
+
const DSML_INVOKE_TOKENS = [
|
|
94
|
+
DSML_INVOKE_CLOSE_FULLWIDTH,
|
|
95
|
+
DSML_INVOKE_CLOSE_ASCII,
|
|
96
|
+
"<|DSML|parameter",
|
|
97
|
+
"<|DSML|parameter",
|
|
98
|
+
] as const;
|
|
99
|
+
const DSML_PARAMETER_CLOSE_TOKENS = [DSML_PARAMETER_CLOSE_FULLWIDTH, DSML_PARAMETER_CLOSE_ASCII] as const;
|
|
78
100
|
|
|
79
101
|
type State =
|
|
80
102
|
| "outside"
|
|
@@ -216,6 +238,11 @@ export class DeepSeekInbandScanner implements InbandScanner {
|
|
|
216
238
|
this.#state = "dsmlSection";
|
|
217
239
|
return;
|
|
218
240
|
}
|
|
241
|
+
const orphanClose = this.#matchingOrphanDsmlClose();
|
|
242
|
+
if (orphanClose) {
|
|
243
|
+
this.#buffer = this.#buffer.slice(orphanClose.length);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
219
246
|
const control = this.#matchingControlToken();
|
|
220
247
|
if (control) {
|
|
221
248
|
this.#buffer = this.#buffer.slice(control.length);
|
|
@@ -478,6 +505,13 @@ export class DeepSeekInbandScanner implements InbandScanner {
|
|
|
478
505
|
return "";
|
|
479
506
|
}
|
|
480
507
|
|
|
508
|
+
#matchingOrphanDsmlClose(): string | undefined {
|
|
509
|
+
for (const token of DSML_ORPHAN_CLOSE_TOKENS) {
|
|
510
|
+
if (this.#buffer.startsWith(token)) return token;
|
|
511
|
+
}
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
|
|
481
515
|
#matchingControlToken(): string | undefined {
|
|
482
516
|
if (this.#buffer.startsWith(DEEPSEEK_TOOL_CALLS_END)) return DEEPSEEK_TOOL_CALLS_END;
|
|
483
517
|
if (this.#buffer.startsWith(THINK_CLOSE)) return THINK_CLOSE;
|
|
@@ -199,7 +199,6 @@ function resolveAnthropicControlBetas(
|
|
|
199
199
|
): string[] {
|
|
200
200
|
const betas: string[] = [];
|
|
201
201
|
if (prefixMismatchBehavior) betas.push(THINKING_BINDING_CONTROLS_BETA);
|
|
202
|
-
if (model.compat.supportsMidConversationSystem) betas.push(midConversationSystemBeta);
|
|
203
202
|
if (model.compat.supportsTurnScopedSystem) betas.push(midConversationSystemClearAtBeta);
|
|
204
203
|
if (model.compat.supportsMidConversationToolChanges) betas.push(midConversationToolChangesBeta);
|
|
205
204
|
if (model.compat.supportsPerMessageEffort) betas.push(midConversationOutputConfigBeta);
|
|
@@ -420,22 +419,7 @@ type AnthropicControlTransition = {
|
|
|
420
419
|
effort?: AnthropicOutputEffort;
|
|
421
420
|
};
|
|
422
421
|
|
|
423
|
-
type
|
|
424
|
-
strictToolsDisabled: boolean;
|
|
425
|
-
fastModeDisabled: boolean;
|
|
426
|
-
/**
|
|
427
|
-
* Runtime-learned: this endpoint returned `400 Invalid signature in
|
|
428
|
-
* thinking block` for a replayed unsigned thinking block, so it must be
|
|
429
|
-
* treated as a signing proxy from now on. All subsequent requests demote
|
|
430
|
-
* unsigned thinking to text for this (baseUrl, modelId), same behavior as
|
|
431
|
-
* an explicit `compat.replayUnsignedThinking: false`. Cleared on session
|
|
432
|
-
* close.
|
|
433
|
-
*/
|
|
434
|
-
replayUnsignedThinkingDisabled: boolean;
|
|
435
|
-
/** Thinking blocks the API permanently dropped after a prefix mismatch. */
|
|
436
|
-
prefixDroppedThinkingBlocks: Set<string>;
|
|
437
|
-
/** Model the control baseline below was captured for; a switch re-baselines. */
|
|
438
|
-
controlModelId: string | undefined;
|
|
422
|
+
type AnthropicControlState = {
|
|
439
423
|
/** `tools` declared at baseline plus later `defer_loading` additions, in wire order. */
|
|
440
424
|
declaredTools: AnthropicWireTool[] | undefined;
|
|
441
425
|
activeToolNames: Set<string>;
|
|
@@ -447,13 +431,25 @@ type AnthropicProviderSessionState = ProviderSessionState & {
|
|
|
447
431
|
currentEffort: AnthropicOutputEffort | undefined;
|
|
448
432
|
};
|
|
449
433
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
434
|
+
type AnthropicProviderSessionState = ProviderSessionState & {
|
|
435
|
+
strictToolsDisabled: boolean;
|
|
436
|
+
fastModeDisabled: boolean;
|
|
437
|
+
/**
|
|
438
|
+
* Runtime-learned: this endpoint rejected a replayed unsigned thinking
|
|
439
|
+
* block, so it must be treated as a signing proxy from now on. All
|
|
440
|
+
* subsequent requests demote unsigned thinking to text for this (baseUrl,
|
|
441
|
+
* modelId), same behavior as an explicit
|
|
442
|
+
* `compat.replayUnsignedThinking: false`. Cleared on session close.
|
|
443
|
+
*/
|
|
444
|
+
replayUnsignedThinkingDisabled: boolean;
|
|
445
|
+
/** Thinking blocks the API permanently dropped after a prefix mismatch. */
|
|
446
|
+
prefixDroppedThinkingBlocks: Set<string>;
|
|
447
|
+
/** Conversation-scoped control baselines, isolated from side requests and advisors. */
|
|
448
|
+
controlStates: Map<string, AnthropicControlState>;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
function createAnthropicControlState(): AnthropicControlState {
|
|
452
|
+
return {
|
|
457
453
|
declaredTools: undefined,
|
|
458
454
|
activeToolNames: new Set(),
|
|
459
455
|
stableSystemBlocks: undefined,
|
|
@@ -462,13 +458,22 @@ function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
|
|
|
462
458
|
baseEffort: undefined,
|
|
463
459
|
baseEffortWire: undefined,
|
|
464
460
|
currentEffort: undefined,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
|
|
465
|
+
const state: AnthropicProviderSessionState = {
|
|
466
|
+
strictToolsDisabled: false,
|
|
467
|
+
fastModeDisabled: false,
|
|
468
|
+
replayUnsignedThinkingDisabled: false,
|
|
469
|
+
prefixDroppedThinkingBlocks: new Set(),
|
|
470
|
+
controlStates: new Map(),
|
|
465
471
|
close: () => {
|
|
466
472
|
state.strictToolsDisabled = false;
|
|
467
473
|
state.fastModeDisabled = false;
|
|
468
474
|
state.replayUnsignedThinkingDisabled = false;
|
|
469
475
|
state.prefixDroppedThinkingBlocks.clear();
|
|
470
|
-
state.
|
|
471
|
-
resetAnthropicControlState(state);
|
|
476
|
+
state.controlStates.clear();
|
|
472
477
|
},
|
|
473
478
|
};
|
|
474
479
|
return state;
|
|
@@ -496,8 +501,7 @@ function getAnthropicProviderSessionState(
|
|
|
496
501
|
const existing = providerSessionState.get(key) as AnthropicProviderSessionState | undefined;
|
|
497
502
|
if (existing) {
|
|
498
503
|
existing.prefixDroppedThinkingBlocks ??= new Set();
|
|
499
|
-
existing.
|
|
500
|
-
existing.controlTransitions ??= [];
|
|
504
|
+
existing.controlStates ??= new Map();
|
|
501
505
|
return existing;
|
|
502
506
|
}
|
|
503
507
|
const created = createAnthropicProviderSessionState();
|
|
@@ -1863,11 +1867,20 @@ function calculateFallbackTurnCost(
|
|
|
1863
1867
|
}
|
|
1864
1868
|
|
|
1865
1869
|
/**
|
|
1866
|
-
* Detects the
|
|
1867
|
-
*
|
|
1868
|
-
*
|
|
1870
|
+
* Detects the two shapes a signature-enforcing endpoint uses to reject a
|
|
1871
|
+
* replayed unsigned thinking block (sent as `signature: ""`):
|
|
1872
|
+
*
|
|
1873
|
+
* - Anthropic and Anthropic-fronting proxies: `400 Invalid `signature` in
|
|
1874
|
+
* `thinking` block`.
|
|
1875
|
+
* - Bedrock-backed proxies: the empty string fails schema validation before
|
|
1876
|
+
* signature checking, so it comes back as `ValidationException: The model
|
|
1877
|
+
* returned the following errors: messages.N.content.M.thinking.signature:
|
|
1878
|
+
* Field required`.
|
|
1879
|
+
*
|
|
1880
|
+
* Exported for the compat tests.
|
|
1869
1881
|
*/
|
|
1870
1882
|
const INVALID_THINKING_SIGNATURE_PATTERN = /invalid\s+`?signature`?\s+in\s+`?thinking`?(?:\s+block)?/i;
|
|
1883
|
+
const MISSING_THINKING_SIGNATURE_PATTERN = /thinking\.signature\b[^"\n]{0,32}\brequired\b/i;
|
|
1871
1884
|
const THINKING_PREFIX_BINDING_PATTERN =
|
|
1872
1885
|
/(?:bound to a different conversation|block_binding\.prefix_mismatch_behavior|prefix_mismatch_behavior)/i;
|
|
1873
1886
|
|
|
@@ -1877,10 +1890,11 @@ export function isThinkingPrefixBindingError(message: string): boolean {
|
|
|
1877
1890
|
}
|
|
1878
1891
|
|
|
1879
1892
|
export function isInvalidThinkingSignatureError(message: string): boolean {
|
|
1880
|
-
return INVALID_THINKING_SIGNATURE_PATTERN.test(message);
|
|
1893
|
+
return INVALID_THINKING_SIGNATURE_PATTERN.test(message) || MISSING_THINKING_SIGNATURE_PATTERN.test(message);
|
|
1881
1894
|
}
|
|
1882
1895
|
|
|
1883
1896
|
const INPUT_TRANSFORMATION_PATH_PATTERN = /^messages\.(\d+)\.content\.(\d+)$/;
|
|
1897
|
+
const PREFIX_BINDING_ERROR_PATH_PATTERN = /messages\.(\d+)\.content\.(\d+)/;
|
|
1884
1898
|
|
|
1885
1899
|
function thinkingReplayKey(block: ContentBlockParam): string | undefined {
|
|
1886
1900
|
if (block.type === "thinking") return block.signature ? `thinking:${block.signature}` : undefined;
|
|
@@ -1923,6 +1937,31 @@ function rememberPrefixDroppedThinking(
|
|
|
1923
1937
|
}
|
|
1924
1938
|
}
|
|
1925
1939
|
|
|
1940
|
+
function rememberPrefixBindingFailure(
|
|
1941
|
+
params: MessageCreateParamsStreaming,
|
|
1942
|
+
message: string,
|
|
1943
|
+
state: AnthropicProviderSessionState | undefined,
|
|
1944
|
+
): boolean {
|
|
1945
|
+
if (!state) return false;
|
|
1946
|
+
const match = PREFIX_BINDING_ERROR_PATH_PATTERN.exec(message);
|
|
1947
|
+
let path = match ? `messages.${match[1]}.content.${match[2]}` : undefined;
|
|
1948
|
+
if (!path) {
|
|
1949
|
+
for (let messageIndex = 0; messageIndex < params.messages.length && !path; messageIndex++) {
|
|
1950
|
+
const candidate = params.messages[messageIndex];
|
|
1951
|
+
if (!candidate || !Array.isArray(candidate.content)) continue;
|
|
1952
|
+
const blockIndex = candidate.content.findIndex(block => thinkingReplayKey(block) !== undefined);
|
|
1953
|
+
if (blockIndex >= 0) path = `messages.${messageIndex}.content.${blockIndex}`;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
if (!path) return false;
|
|
1957
|
+
rememberPrefixDroppedThinking(
|
|
1958
|
+
params,
|
|
1959
|
+
[{ type: "thinking_dropped", reason: "prefix_binding_mismatch", path }],
|
|
1960
|
+
state,
|
|
1961
|
+
);
|
|
1962
|
+
return true;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1926
1965
|
function applyReportedInputTransformations(
|
|
1927
1966
|
output: AssistantMessage,
|
|
1928
1967
|
params: MessageCreateParamsStreaming,
|
|
@@ -1956,8 +1995,8 @@ function applyReportedInputTransformations(
|
|
|
1956
1995
|
}
|
|
1957
1996
|
|
|
1958
1997
|
/**
|
|
1959
|
-
* Prepend a pointed remediation to
|
|
1960
|
-
*
|
|
1998
|
+
* Prepend a pointed remediation to a thinking-signature rejection 400 when the
|
|
1999
|
+
* model looks like an unmarked custom signing proxy
|
|
1961
2000
|
* (opaque baseUrl, `spec.reasoning: true`, no explicit
|
|
1962
2001
|
* `compat.replayUnsignedThinking` override). The default is native replay for
|
|
1963
2002
|
* the 3p reasoning majority (#2005); this hint turns the misconfigured-proxy
|
|
@@ -2028,6 +2067,7 @@ const streamAnthropicOnce = (
|
|
|
2028
2067
|
let dropFastMode = providerSessionState?.fastModeDisabled ?? false;
|
|
2029
2068
|
let forceDemoteUnsignedThinking = providerSessionState?.replayUnsignedThinkingDisabled ?? false;
|
|
2030
2069
|
let dropAllThinking = false;
|
|
2070
|
+
let prefixBindingRetryAttempted = false;
|
|
2031
2071
|
let prefixMismatchBehavior =
|
|
2032
2072
|
model.thinking?.prefixBinding && model.compat.supportsThinkingBindingControls
|
|
2033
2073
|
? (options?.anthropicPrefixMismatchBehavior ?? "drop_block")
|
|
@@ -2935,7 +2975,8 @@ const streamAnthropicOnce = (
|
|
|
2935
2975
|
const streamFailureMessage =
|
|
2936
2976
|
streamFailure instanceof Error ? streamFailure.message : String(streamFailure);
|
|
2937
2977
|
if (
|
|
2938
|
-
!
|
|
2978
|
+
!prefixBindingRetryAttempted &&
|
|
2979
|
+
options?.anthropicPrefixMismatchBehavior !== "error" &&
|
|
2939
2980
|
firstTokenTime === undefined &&
|
|
2940
2981
|
!streamedReplayUnsafeContent &&
|
|
2941
2982
|
isThinkingPrefixBindingError(streamFailureMessage)
|
|
@@ -2945,8 +2986,9 @@ const streamAnthropicOnce = (
|
|
|
2945
2986
|
model: model.id,
|
|
2946
2987
|
baseUrl,
|
|
2947
2988
|
});
|
|
2989
|
+
prefixBindingRetryAttempted = true;
|
|
2948
2990
|
prefixMismatchBehavior = undefined;
|
|
2949
|
-
dropAllThinking =
|
|
2991
|
+
dropAllThinking = !rememberPrefixBindingFailure(params, streamFailureMessage, providerSessionState);
|
|
2950
2992
|
params = await prepareParams();
|
|
2951
2993
|
providerRetryAttempt = 0;
|
|
2952
2994
|
output.content.length = 0;
|
|
@@ -2968,7 +3010,7 @@ const streamAnthropicOnce = (
|
|
|
2968
3010
|
isInvalidThinkingSignatureError(streamFailureMessage)
|
|
2969
3011
|
) {
|
|
2970
3012
|
logger.warn(
|
|
2971
|
-
"anthropic: signing proxy detected (
|
|
3013
|
+
"anthropic: signing proxy detected (thinking signature rejected), demoting unsigned thinking and retrying",
|
|
2972
3014
|
{
|
|
2973
3015
|
provider: model.provider,
|
|
2974
3016
|
model: model.id,
|
|
@@ -3460,10 +3502,11 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
3460
3502
|
trailingMessage.content === "Continue." &&
|
|
3461
3503
|
params.messages[trailingIndex - 1]?.role === "assistant";
|
|
3462
3504
|
const messageEnd = hasTrailingAssistantPad ? trailingIndex - 1 : trailingIndex;
|
|
3463
|
-
|
|
3464
|
-
for (let index = messageEnd; index >=
|
|
3505
|
+
let eligibleMessages = 0;
|
|
3506
|
+
for (let index = messageEnd; index >= 0 && eligibleMessages < 2; index--) {
|
|
3465
3507
|
const message = params.messages[index];
|
|
3466
|
-
if (!message) continue;
|
|
3508
|
+
if (!message || message.clear_at === "next_user_message") continue;
|
|
3509
|
+
eligibleMessages++;
|
|
3467
3510
|
if (typeof message.content === "string") {
|
|
3468
3511
|
message.content = [
|
|
3469
3512
|
{ type: "text", text: message.content, cache_control: cloneAnthropicCacheControl(cacheControl) },
|
|
@@ -3525,7 +3568,9 @@ function extractClaudeCodeFirstUserMessageText(messages: readonly Message[]): st
|
|
|
3525
3568
|
return "";
|
|
3526
3569
|
}
|
|
3527
3570
|
|
|
3528
|
-
|
|
3571
|
+
const MAX_ANTHROPIC_CONTROL_STATES = 16;
|
|
3572
|
+
|
|
3573
|
+
function resetAnthropicControlState(state: AnthropicControlState): void {
|
|
3529
3574
|
state.declaredTools = undefined;
|
|
3530
3575
|
state.activeToolNames.clear();
|
|
3531
3576
|
state.stableSystemBlocks = undefined;
|
|
@@ -3536,10 +3581,51 @@ function resetAnthropicControlState(state: AnthropicProviderSessionState): void
|
|
|
3536
3581
|
state.currentEffort = undefined;
|
|
3537
3582
|
}
|
|
3538
3583
|
|
|
3584
|
+
function anthropicControlMessageProjection(message: MessageParam): MessageParam {
|
|
3585
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) return message;
|
|
3586
|
+
return {
|
|
3587
|
+
...message,
|
|
3588
|
+
content: message.content.filter(block => block.type !== "thinking" && block.type !== "redacted_thinking"),
|
|
3589
|
+
};
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
function getAnthropicControlState(
|
|
3593
|
+
state: AnthropicProviderSessionState | undefined,
|
|
3594
|
+
sessionId: string | undefined,
|
|
3595
|
+
system: readonly AnthropicSystemBlock[] | undefined,
|
|
3596
|
+
messages: readonly MessageParam[],
|
|
3597
|
+
): AnthropicControlState | undefined {
|
|
3598
|
+
if (!state) return undefined;
|
|
3599
|
+
const root = messages[0];
|
|
3600
|
+
const fingerprint = String(
|
|
3601
|
+
Bun.hash(
|
|
3602
|
+
JSON.stringify([
|
|
3603
|
+
sessionId ?? "",
|
|
3604
|
+
system?.map(block => block.text) ?? null,
|
|
3605
|
+
root ? anthropicControlMessageProjection(root) : null,
|
|
3606
|
+
]),
|
|
3607
|
+
),
|
|
3608
|
+
);
|
|
3609
|
+
const existing = state.controlStates.get(fingerprint);
|
|
3610
|
+
if (existing) {
|
|
3611
|
+
state.controlStates.delete(fingerprint);
|
|
3612
|
+
state.controlStates.set(fingerprint, existing);
|
|
3613
|
+
return existing;
|
|
3614
|
+
}
|
|
3615
|
+
const created = createAnthropicControlState();
|
|
3616
|
+
state.controlStates.set(fingerprint, created);
|
|
3617
|
+
if (state.controlStates.size > MAX_ANTHROPIC_CONTROL_STATES) {
|
|
3618
|
+
const oldest = state.controlStates.keys().next().value;
|
|
3619
|
+
if (oldest !== undefined) state.controlStates.delete(oldest);
|
|
3620
|
+
}
|
|
3621
|
+
return created;
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3539
3624
|
/** Fingerprint of the wire message a control transition is attached after. */
|
|
3540
3625
|
function anthropicControlAnchor(messages: readonly MessageParam[], messageCount: number): string {
|
|
3541
3626
|
if (messageCount === 0) return "";
|
|
3542
|
-
|
|
3627
|
+
const message = messages[messageCount - 1];
|
|
3628
|
+
return message ? String(Bun.hash(JSON.stringify(anthropicControlMessageProjection(message)))) : "";
|
|
3543
3629
|
}
|
|
3544
3630
|
|
|
3545
3631
|
/**
|
|
@@ -3548,16 +3634,7 @@ function anthropicControlAnchor(messages: readonly MessageParam[], messageCount:
|
|
|
3548
3634
|
* shrank or was rewritten under a recorded transition (compaction, branch
|
|
3549
3635
|
* switch, `/clear`). The next request re-baselines from its own payload.
|
|
3550
3636
|
*/
|
|
3551
|
-
function syncAnthropicControlState(
|
|
3552
|
-
state: AnthropicProviderSessionState,
|
|
3553
|
-
model: Model<"anthropic-messages">,
|
|
3554
|
-
messages: readonly MessageParam[],
|
|
3555
|
-
): void {
|
|
3556
|
-
if (state.controlModelId !== model.id) {
|
|
3557
|
-
state.controlModelId = model.id;
|
|
3558
|
-
resetAnthropicControlState(state);
|
|
3559
|
-
return;
|
|
3560
|
-
}
|
|
3637
|
+
function syncAnthropicControlState(state: AnthropicControlState, messages: readonly MessageParam[]): void {
|
|
3561
3638
|
for (const transition of state.controlTransitions) {
|
|
3562
3639
|
if (
|
|
3563
3640
|
transition.messageCount > messages.length ||
|
|
@@ -3581,7 +3658,7 @@ function syncAnthropicControlState(
|
|
|
3581
3658
|
*/
|
|
3582
3659
|
function planStableAnthropicSystem(
|
|
3583
3660
|
current: AnthropicSystemBlock[] | undefined,
|
|
3584
|
-
state:
|
|
3661
|
+
state: AnthropicControlState | undefined,
|
|
3585
3662
|
enabled: boolean,
|
|
3586
3663
|
): AnthropicSystemBlock[] | undefined {
|
|
3587
3664
|
if (!state || !enabled) return current;
|
|
@@ -3600,11 +3677,16 @@ function planStableAnthropicSystem(
|
|
|
3600
3677
|
function anthropicToolDefinitionKey(tool: AnthropicWireTool): string {
|
|
3601
3678
|
const stable = { ...tool };
|
|
3602
3679
|
delete stable.defer_loading;
|
|
3680
|
+
delete stable.description;
|
|
3603
3681
|
return JSON.stringify(stable);
|
|
3604
3682
|
}
|
|
3605
3683
|
|
|
3684
|
+
function cloneAnthropicTools(tools: readonly AnthropicWireTool[]): AnthropicWireTool[] {
|
|
3685
|
+
return tools.map(tool => ({ ...tool }));
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3606
3688
|
function recordAnthropicControlTransition(
|
|
3607
|
-
state:
|
|
3689
|
+
state: AnthropicControlState,
|
|
3608
3690
|
messages: readonly MessageParam[],
|
|
3609
3691
|
messageCount: number,
|
|
3610
3692
|
content: ContentBlockParam[],
|
|
@@ -3634,14 +3716,14 @@ function recordAnthropicControlTransition(
|
|
|
3634
3716
|
function planStableAnthropicTools(
|
|
3635
3717
|
current: AnthropicWireTool[] | undefined,
|
|
3636
3718
|
messages: readonly MessageParam[],
|
|
3637
|
-
state:
|
|
3719
|
+
state: AnthropicControlState | undefined,
|
|
3638
3720
|
enabled: boolean,
|
|
3639
3721
|
): AnthropicWireTool[] | undefined {
|
|
3640
3722
|
if (!state || !enabled || !current) return current;
|
|
3641
3723
|
if (!state.declaredTools) {
|
|
3642
|
-
state.declaredTools = current
|
|
3724
|
+
state.declaredTools = cloneAnthropicTools(current);
|
|
3643
3725
|
state.activeToolNames = new Set(current.map(tool => tool.name));
|
|
3644
|
-
return state.declaredTools;
|
|
3726
|
+
return cloneAnthropicTools(state.declaredTools);
|
|
3645
3727
|
}
|
|
3646
3728
|
|
|
3647
3729
|
const declaredByName = new Map(state.declaredTools.map(tool => [tool.name, tool]));
|
|
@@ -3649,9 +3731,9 @@ function planStableAnthropicTools(
|
|
|
3649
3731
|
const declared = declaredByName.get(tool.name);
|
|
3650
3732
|
if (declared && anthropicToolDefinitionKey(declared) !== anthropicToolDefinitionKey(tool)) {
|
|
3651
3733
|
resetAnthropicControlState(state);
|
|
3652
|
-
state.declaredTools = current
|
|
3734
|
+
state.declaredTools = cloneAnthropicTools(current);
|
|
3653
3735
|
state.activeToolNames = new Set(current.map(candidate => candidate.name));
|
|
3654
|
-
return state.declaredTools;
|
|
3736
|
+
return cloneAnthropicTools(state.declaredTools);
|
|
3655
3737
|
}
|
|
3656
3738
|
}
|
|
3657
3739
|
|
|
@@ -3678,7 +3760,7 @@ function planStableAnthropicTools(
|
|
|
3678
3760
|
}
|
|
3679
3761
|
if (changes.length > 0) recordAnthropicControlTransition(state, messages, messages.length, changes);
|
|
3680
3762
|
state.activeToolNames = nextActive;
|
|
3681
|
-
return state.declaredTools;
|
|
3763
|
+
return cloneAnthropicTools(state.declaredTools);
|
|
3682
3764
|
}
|
|
3683
3765
|
|
|
3684
3766
|
/**
|
|
@@ -3690,7 +3772,7 @@ function planStableAnthropicTools(
|
|
|
3690
3772
|
function planStableAnthropicEffort(
|
|
3691
3773
|
current: AnthropicOutputEffort | undefined,
|
|
3692
3774
|
messages: readonly MessageParam[],
|
|
3693
|
-
state:
|
|
3775
|
+
state: AnthropicControlState | undefined,
|
|
3694
3776
|
enabled: boolean,
|
|
3695
3777
|
): AnthropicOutputEffort | undefined {
|
|
3696
3778
|
if (!state || !enabled) return current;
|
|
@@ -3712,7 +3794,7 @@ function planStableAnthropicEffort(
|
|
|
3712
3794
|
|
|
3713
3795
|
function materializeAnthropicControlTransitions(
|
|
3714
3796
|
messages: MessageParam[],
|
|
3715
|
-
state:
|
|
3797
|
+
state: AnthropicControlState | undefined,
|
|
3716
3798
|
): MessageParam[] {
|
|
3717
3799
|
if (!state || state.controlTransitions.length === 0) return messages;
|
|
3718
3800
|
const result = messages.slice();
|
|
@@ -3736,7 +3818,7 @@ function materializeAnthropicControlTransitions(
|
|
|
3736
3818
|
}
|
|
3737
3819
|
result.splice(index, 0, {
|
|
3738
3820
|
role: "system",
|
|
3739
|
-
content: transition.content,
|
|
3821
|
+
content: transition.content.map(block => ({ ...block })),
|
|
3740
3822
|
...(transition.effort === undefined ? {} : { output_config: { effort: transition.effort } }),
|
|
3741
3823
|
});
|
|
3742
3824
|
offset++;
|
|
@@ -3906,25 +3988,17 @@ function buildParams(
|
|
|
3906
3988
|
dropAllThinking,
|
|
3907
3989
|
droppedThinkingBlocks,
|
|
3908
3990
|
});
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
model.compat.supportsMidConversationSystem,
|
|
3914
|
-
);
|
|
3915
|
-
tools = planStableAnthropicTools(
|
|
3916
|
-
tools,
|
|
3917
|
-
wireMessages,
|
|
3918
|
-
providerSessionState,
|
|
3919
|
-
model.compat.supportsMidConversationToolChanges,
|
|
3920
|
-
);
|
|
3991
|
+
const controlState = getAnthropicControlState(providerSessionState, options?.sessionId, systemBlocks, wireMessages);
|
|
3992
|
+
if (controlState) syncAnthropicControlState(controlState, wireMessages);
|
|
3993
|
+
systemBlocks = planStableAnthropicSystem(systemBlocks, controlState, model.compat.supportsMidConversationSystem);
|
|
3994
|
+
tools = planStableAnthropicTools(tools, wireMessages, controlState, model.compat.supportsMidConversationToolChanges);
|
|
3921
3995
|
const topLevelEffort = planStableAnthropicEffort(
|
|
3922
3996
|
outputConfigEffort,
|
|
3923
3997
|
wireMessages,
|
|
3924
|
-
|
|
3998
|
+
controlState,
|
|
3925
3999
|
model.compat.supportsPerMessageEffort,
|
|
3926
4000
|
);
|
|
3927
|
-
wireMessages = materializeAnthropicControlTransitions(wireMessages,
|
|
4001
|
+
wireMessages = materializeAnthropicControlTransitions(wireMessages, controlState);
|
|
3928
4002
|
|
|
3929
4003
|
const outputConfigEntries: AnthropicOutputConfig = {};
|
|
3930
4004
|
if (topLevelEffort && model.compat.supportsOutputEffort) outputConfigEntries.effort = topLevelEffort;
|
|
@@ -4303,7 +4377,7 @@ export function convertAnthropicMessages(
|
|
|
4303
4377
|
for (const block of blocks) {
|
|
4304
4378
|
if (block.type === "tool_use") {
|
|
4305
4379
|
sawToolUse = true;
|
|
4306
|
-
} else if (sawToolUse) {
|
|
4380
|
+
} else if (sawToolUse && block.type !== "thinking" && block.type !== "redacted_thinking") {
|
|
4307
4381
|
needsPartition = true;
|
|
4308
4382
|
break;
|
|
4309
4383
|
}
|
|
@@ -4366,7 +4440,7 @@ export function convertAnthropicMessages(
|
|
|
4366
4440
|
// covers both the "followed by assistant / last" and "no consecutive system"
|
|
4367
4441
|
// constraints. Anything that does not qualify stays a `user` message.
|
|
4368
4442
|
if (developerParams.length > 0 && model.compat.supportsMidConversationSystem) {
|
|
4369
|
-
for (const developer of developerParams) {
|
|
4443
|
+
for (const developer of developerParams.toReversed()) {
|
|
4370
4444
|
const idx = developer.index;
|
|
4371
4445
|
const followsUser = idx > 0 && params[idx - 1]?.role === "user";
|
|
4372
4446
|
const next = params[idx + 1];
|
|
@@ -4378,18 +4452,35 @@ export function convertAnthropicMessages(
|
|
|
4378
4452
|
block => block.type === "text" || block.type === "tool_addition" || block.type === "tool_removal",
|
|
4379
4453
|
);
|
|
4380
4454
|
const effortOnly = developer.payload?.effort !== undefined && Array.isArray(content) && content.length === 0;
|
|
4381
|
-
if ((followsUser && lastOrBeforeAssistant && systemCompatible) || effortOnly)
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4455
|
+
if (!((followsUser && lastOrBeforeAssistant && systemCompatible) || effortOnly)) continue;
|
|
4456
|
+
|
|
4457
|
+
const turnScoped = developer.payload?.clearAt === "next_user_message" && model.compat.supportsTurnScopedSystem;
|
|
4458
|
+
const hasEffort = developer.payload?.effort !== undefined && model.compat.supportsPerMessageEffort;
|
|
4459
|
+
const hasToolChanges = (developer.payload?.toolChanges?.length ?? 0) > 0;
|
|
4460
|
+
if (turnScoped && (hasEffort || hasToolChanges) && Array.isArray(content)) {
|
|
4461
|
+
const scopedContent = content.filter(block => block.type === "text");
|
|
4462
|
+
const controlContent = content.filter(block => block.type !== "text");
|
|
4463
|
+
if (scopedContent.length > 0) {
|
|
4464
|
+
params[idx] = {
|
|
4465
|
+
role: "system",
|
|
4466
|
+
content: scopedContent,
|
|
4467
|
+
clear_at: "next_user_message",
|
|
4468
|
+
};
|
|
4469
|
+
params.splice(idx + 1, 0, {
|
|
4470
|
+
role: "system",
|
|
4471
|
+
content: controlContent,
|
|
4472
|
+
...(hasEffort ? { output_config: { effort: developer.payload?.effort } } : {}),
|
|
4473
|
+
});
|
|
4474
|
+
continue;
|
|
4475
|
+
}
|
|
4392
4476
|
}
|
|
4477
|
+
|
|
4478
|
+
params[idx] = {
|
|
4479
|
+
role: "system",
|
|
4480
|
+
content,
|
|
4481
|
+
...(turnScoped && !hasEffort && !hasToolChanges ? { clear_at: "next_user_message" } : {}),
|
|
4482
|
+
...(hasEffort ? { output_config: { effort: developer.payload?.effort } } : {}),
|
|
4483
|
+
};
|
|
4393
4484
|
}
|
|
4394
4485
|
}
|
|
4395
4486
|
// Dropped empty user/developer turns can leave two assistant params adjacent;
|
package/src/providers/cursor.ts
CHANGED
|
@@ -702,7 +702,10 @@ function streamCursorWithWireMode(
|
|
|
702
702
|
const { requestBytes, conversationState } = builtRequest;
|
|
703
703
|
serializedFallbackWireModelId = builtRequest.fallbackWireModelId;
|
|
704
704
|
conversationStateCache.set(conversationId, conversationState);
|
|
705
|
-
const requestContextTools = buildMcpToolDefinitions(
|
|
705
|
+
const requestContextTools = buildMcpToolDefinitions(
|
|
706
|
+
context.tools,
|
|
707
|
+
model.requiresCursorToolSchemaProjection === true,
|
|
708
|
+
);
|
|
706
709
|
const requestContextRules = buildCursorRequestContextRules(context.systemPrompt);
|
|
707
710
|
|
|
708
711
|
const baseUrl = model.baseUrl || CURSOR_API_URL;
|
|
@@ -4612,7 +4615,10 @@ function isJsonValue(value: unknown): value is JsonValue {
|
|
|
4612
4615
|
return true;
|
|
4613
4616
|
}
|
|
4614
4617
|
|
|
4615
|
-
export function buildMcpToolDefinitions(
|
|
4618
|
+
export function buildMcpToolDefinitions(
|
|
4619
|
+
tools: Tool[] | undefined,
|
|
4620
|
+
requiresCursorToolSchemaProjection = false,
|
|
4621
|
+
): McpToolDefinition[] {
|
|
4616
4622
|
if (!tools || tools.length === 0) {
|
|
4617
4623
|
return [];
|
|
4618
4624
|
}
|
|
@@ -4632,7 +4638,8 @@ export function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefin
|
|
|
4632
4638
|
const forwarded = writeTool ? [...advertisedTools, writeTool] : advertisedTools;
|
|
4633
4639
|
|
|
4634
4640
|
return forwarded.map(tool => {
|
|
4635
|
-
const
|
|
4641
|
+
const wireSchema = toolWireSchema(tool);
|
|
4642
|
+
const jsonSchema = requiresCursorToolSchemaProjection ? sanitizeSchemaForCursor(wireSchema) : wireSchema;
|
|
4636
4643
|
const schemaValue: JsonValue =
|
|
4637
4644
|
jsonSchema !== null && !Array.isArray(jsonSchema) && isJsonValue(jsonSchema)
|
|
4638
4645
|
? jsonSchema
|
|
@@ -244,6 +244,13 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
244
244
|
}
|
|
245
245
|
} else if (block.type === "toolCall") {
|
|
246
246
|
emittedToolCallNames.set(block.id, block.name);
|
|
247
|
+
// Gemini 3 requires a thought signature on function calls it makes. For
|
|
248
|
+
// calls we cannot sign (cross-model replay, secret-redacted args, or the
|
|
249
|
+
// secondary calls of a parallel turn), the public Gemini API accepts the
|
|
250
|
+
// `skip_thought_signature_validator` sentinel as a bypass. Cloud Code
|
|
251
|
+
// Assist / Antigravity and Vertex AI reject that sentinel with 400
|
|
252
|
+
// INVALID_ARGUMENT — permanently wedging any session whose history
|
|
253
|
+
// contains one — so for those transports we omit the field instead. (#9638)
|
|
247
254
|
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature);
|
|
248
255
|
const effectiveSignature =
|
|
249
256
|
thoughtSignature || (model.compat.requiresSkipThoughtSignature ? SKIP_THOUGHT_SIGNATURE : undefined);
|
|
@@ -79,29 +79,39 @@ export class ToolCallLoopGuard {
|
|
|
79
79
|
/** Records one completed turn and returns the threshold hit, if any. */
|
|
80
80
|
recordTurn(turn: ToolCallLoopTurn): RepeatedToolCallDetection | null {
|
|
81
81
|
const toolCalls = turn.message.content.filter((part): part is ToolCall => part.type === "toolCall");
|
|
82
|
-
if (toolCalls.length
|
|
82
|
+
if (toolCalls.length === 0) {
|
|
83
|
+
this.#lastHash = undefined;
|
|
84
|
+
this.#count = 0;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (toolCalls.every(tc => this.#exemptTools.has(tc.name))) {
|
|
83
88
|
this.#lastHash = undefined;
|
|
84
89
|
this.#count = 0;
|
|
85
90
|
return null;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
const canonicalCalls = toolCalls
|
|
94
|
+
.map(tc => JSON.stringify([tc.name, canonicalizeToolCallValue(tc.arguments)]))
|
|
95
|
+
.sort();
|
|
96
|
+
const turnHash = JSON.stringify(canonicalCalls);
|
|
97
|
+
if (turnHash === this.#lastHash) {
|
|
92
98
|
this.#count++;
|
|
93
99
|
} else {
|
|
94
|
-
this.#lastHash =
|
|
100
|
+
this.#lastHash = turnHash;
|
|
95
101
|
this.#count = 1;
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
if (this.#count !== this.#threshold) return null;
|
|
105
|
+
const reportCall = toolCalls.find(tc => !this.#exemptTools.has(tc.name)) ?? toolCalls[0]!;
|
|
99
106
|
return {
|
|
100
107
|
kind: "repeated_tool_call",
|
|
101
|
-
toolName:
|
|
108
|
+
toolName: reportCall.name,
|
|
102
109
|
count: this.#count,
|
|
103
|
-
resultSummary: summarizeToolResult(turn.toolResults,
|
|
104
|
-
argumentsSummary: summarizeText(
|
|
110
|
+
resultSummary: summarizeToolResult(turn.toolResults, reportCall.id),
|
|
111
|
+
argumentsSummary: summarizeText(
|
|
112
|
+
JSON.stringify(canonicalizeToolCallValue(reportCall.arguments)),
|
|
113
|
+
ARGUMENT_SUMMARY_LIMIT,
|
|
114
|
+
),
|
|
105
115
|
};
|
|
106
116
|
}
|
|
107
117
|
}
|