@sahiljassal/opencode-anthropic-auth 2.4.1 → 2.5.0
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/dist/constants.d.ts +9 -1
- package/dist/constants.js +9 -2
- package/dist/index.js +104 -74
- package/dist/transform.d.ts +2 -2
- package/dist/transform.js +92 -23
- package/package.json +1 -1
package/dist/constants.d.ts
CHANGED
|
@@ -18,7 +18,15 @@ export declare const TOOL_PREFIX = "mcp_";
|
|
|
18
18
|
export declare const ANTHROPIC_CACHE_LOOKBACK_BLOCKS = 20;
|
|
19
19
|
export declare const REQUIRED_BETAS: string[];
|
|
20
20
|
export declare const OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
|
|
21
|
-
export declare const CLAUDE_CODE_IDENTITY = "You are
|
|
21
|
+
export declare const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
22
|
+
/**
|
|
23
|
+
* Model families with adaptive thinking (thinking defaults on, display
|
|
24
|
+
* defaults to "omitted"). These models reject legacy manual thinking
|
|
25
|
+
* (type: enabled + budget_tokens) and non-default temperature/top_p/top_k.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ADAPTIVE_THINKING_MODEL_PATTERN: RegExp;
|
|
28
|
+
/** Proactive OAuth refresh margin — refresh this long before actual expiry. */
|
|
29
|
+
export declare const OAUTH_REFRESH_SKEW_MS: number;
|
|
22
30
|
export declare const CCH_SALT = "59cf53e54c78";
|
|
23
31
|
export declare const CCH_POSITIONS: number[];
|
|
24
32
|
export declare const CLAUDE_CODE_VERSION = "2.1.177";
|
package/dist/constants.js
CHANGED
|
@@ -27,10 +27,17 @@ export const REQUIRED_BETAS = [
|
|
|
27
27
|
'oauth-2025-04-20',
|
|
28
28
|
'claude-code-20250219',
|
|
29
29
|
'interleaved-thinking-2025-05-14',
|
|
30
|
-
'fine-grained-tool-streaming-2025-05-14',
|
|
31
30
|
];
|
|
32
31
|
export const OPENCODE_IDENTITY_PREFIX = 'You are OpenCode';
|
|
33
|
-
export const CLAUDE_CODE_IDENTITY = "You are
|
|
32
|
+
export const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
33
|
+
/**
|
|
34
|
+
* Model families with adaptive thinking (thinking defaults on, display
|
|
35
|
+
* defaults to "omitted"). These models reject legacy manual thinking
|
|
36
|
+
* (type: enabled + budget_tokens) and non-default temperature/top_p/top_k.
|
|
37
|
+
*/
|
|
38
|
+
export const ADAPTIVE_THINKING_MODEL_PATTERN = /claude-(opus-5|opus-4-8|opus-4-7|sonnet-5|fable-5|mythos-5)/i;
|
|
39
|
+
/** Proactive OAuth refresh margin — refresh this long before actual expiry. */
|
|
40
|
+
export const OAUTH_REFRESH_SKEW_MS = 5 * 60_000;
|
|
34
41
|
export const CCH_SALT = '59cf53e54c78';
|
|
35
42
|
export const CCH_POSITIONS = [4, 7, 20];
|
|
36
43
|
export const CLAUDE_CODE_VERSION = '2.1.177';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { authorize, exchange } from "./auth.js";
|
|
2
|
-
import { CLIENT_ID, TOKEN_URL } from "./constants.js";
|
|
2
|
+
import { CLIENT_ID, OAUTH_REFRESH_SKEW_MS, TOKEN_URL } from "./constants.js";
|
|
3
3
|
import { computeRetryAfterDelayMs, createStrippedStream, extractModelId, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
4
4
|
const MAX_429_RETRIES = 3;
|
|
5
5
|
export const AnthropicAuthPlugin = async ({ client }) => {
|
|
@@ -23,85 +23,98 @@ export const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
23
23
|
// Shared inflight refresh promise — prevents concurrent token refreshes
|
|
24
24
|
// from racing against each other (and causing 401 cascades with token rotation)
|
|
25
25
|
let refreshPromise = null;
|
|
26
|
+
function triggerRefresh() {
|
|
27
|
+
if (!refreshPromise) {
|
|
28
|
+
refreshPromise = (async () => {
|
|
29
|
+
const maxRetries = 2;
|
|
30
|
+
const baseDelayMs = 500;
|
|
31
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
32
|
+
try {
|
|
33
|
+
if (attempt > 0) {
|
|
34
|
+
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
36
|
+
}
|
|
37
|
+
// Re-read auth to get the latest refresh token.
|
|
38
|
+
// The outer `auth` snapshot may be stale if tokens
|
|
39
|
+
// were rotated since the fetch() call was made.
|
|
40
|
+
const freshAuth = await getAuth();
|
|
41
|
+
const response = await fetch(TOKEN_URL, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
Accept: 'application/json, text/plain, */*',
|
|
46
|
+
'User-Agent': 'axios/1.13.6',
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
grant_type: 'refresh_token',
|
|
50
|
+
refresh_token: freshAuth.refresh,
|
|
51
|
+
client_id: CLIENT_ID,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
if (response.status === 429 && attempt < maxRetries) {
|
|
56
|
+
// Honor the token endpoint's own backoff hint instead
|
|
57
|
+
// of the generic exponential delay below.
|
|
58
|
+
const retryAfterDelay = computeRetryAfterDelayMs(response.headers.get('retry-after'), attempt);
|
|
59
|
+
await response.body?.cancel();
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfterDelay));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
64
|
+
await response.body?.cancel();
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const body = await response.text().catch(() => '');
|
|
68
|
+
throw new Error(`Token refresh failed: ${response.status} — ${body}`);
|
|
69
|
+
}
|
|
70
|
+
const json = (await response.json());
|
|
71
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
|
|
72
|
+
await client.auth.set({
|
|
73
|
+
path: {
|
|
74
|
+
id: 'anthropic',
|
|
75
|
+
},
|
|
76
|
+
body: {
|
|
77
|
+
type: 'oauth',
|
|
78
|
+
refresh: json.refresh_token,
|
|
79
|
+
access: json.access_token,
|
|
80
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return json.access_token;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const isNetworkError = error instanceof Error &&
|
|
87
|
+
(error.message.includes('fetch failed') ||
|
|
88
|
+
('code' in error &&
|
|
89
|
+
(error.code === 'ECONNRESET' ||
|
|
90
|
+
error.code === 'ECONNREFUSED' ||
|
|
91
|
+
error.code === 'ETIMEDOUT' ||
|
|
92
|
+
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
93
|
+
if (attempt < maxRetries && isNetworkError) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Unreachable — each iteration either returns or throws.
|
|
100
|
+
// Kept as a TypeScript exhaustiveness guard.
|
|
101
|
+
throw new Error('Token refresh exhausted all retries');
|
|
102
|
+
})().finally(() => {
|
|
103
|
+
refreshPromise = null;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return refreshPromise;
|
|
107
|
+
}
|
|
26
108
|
return {
|
|
27
109
|
apiKey: '',
|
|
28
110
|
async fetch(input, init) {
|
|
29
111
|
const auth = await getAuth();
|
|
30
112
|
if (auth.type !== 'oauth')
|
|
31
113
|
return fetch(input, init);
|
|
32
|
-
if (!auth.access ||
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const baseDelayMs = 500;
|
|
37
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
38
|
-
try {
|
|
39
|
-
if (attempt > 0) {
|
|
40
|
-
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
41
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
42
|
-
}
|
|
43
|
-
// Re-read auth to get the latest refresh token.
|
|
44
|
-
// The outer `auth` snapshot may be stale if tokens
|
|
45
|
-
// were rotated since the fetch() call was made.
|
|
46
|
-
const freshAuth = await getAuth();
|
|
47
|
-
const response = await fetch(TOKEN_URL, {
|
|
48
|
-
method: 'POST',
|
|
49
|
-
headers: {
|
|
50
|
-
'Content-Type': 'application/json',
|
|
51
|
-
Accept: 'application/json, text/plain, */*',
|
|
52
|
-
'User-Agent': 'axios/1.13.6',
|
|
53
|
-
},
|
|
54
|
-
body: JSON.stringify({
|
|
55
|
-
grant_type: 'refresh_token',
|
|
56
|
-
refresh_token: freshAuth.refresh,
|
|
57
|
-
client_id: CLIENT_ID,
|
|
58
|
-
}),
|
|
59
|
-
});
|
|
60
|
-
if (!response.ok) {
|
|
61
|
-
if (response.status >= 500 && attempt < maxRetries) {
|
|
62
|
-
await response.body?.cancel();
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const body = await response.text().catch(() => '');
|
|
66
|
-
throw new Error(`Token refresh failed: ${response.status} — ${body}`);
|
|
67
|
-
}
|
|
68
|
-
const json = (await response.json());
|
|
69
|
-
// biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
|
|
70
|
-
await client.auth.set({
|
|
71
|
-
path: {
|
|
72
|
-
id: 'anthropic',
|
|
73
|
-
},
|
|
74
|
-
body: {
|
|
75
|
-
type: 'oauth',
|
|
76
|
-
refresh: json.refresh_token,
|
|
77
|
-
access: json.access_token,
|
|
78
|
-
expires: Date.now() + json.expires_in * 1000,
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
return json.access_token;
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
const isNetworkError = error instanceof Error &&
|
|
85
|
-
(error.message.includes('fetch failed') ||
|
|
86
|
-
('code' in error &&
|
|
87
|
-
(error.code === 'ECONNRESET' ||
|
|
88
|
-
error.code === 'ECONNREFUSED' ||
|
|
89
|
-
error.code === 'ETIMEDOUT' ||
|
|
90
|
-
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
91
|
-
if (attempt < maxRetries && isNetworkError) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
// Unreachable — each iteration either returns or throws.
|
|
98
|
-
// Kept as a TypeScript exhaustiveness guard.
|
|
99
|
-
throw new Error('Token refresh exhausted all retries');
|
|
100
|
-
})().finally(() => {
|
|
101
|
-
refreshPromise = null;
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
auth.access = await refreshPromise;
|
|
114
|
+
if (!auth.access ||
|
|
115
|
+
!auth.expires ||
|
|
116
|
+
auth.expires < Date.now() + OAUTH_REFRESH_SKEW_MS) {
|
|
117
|
+
auth.access = await triggerRefresh();
|
|
105
118
|
}
|
|
106
119
|
const requestHeaders = mergeHeaders(input, init);
|
|
107
120
|
const rawBody = init?.body;
|
|
@@ -115,6 +128,8 @@ export const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
115
128
|
body = rewriteRequestBody(body);
|
|
116
129
|
}
|
|
117
130
|
const rewritten = rewriteUrl(input);
|
|
131
|
+
let accessToken = auth.access;
|
|
132
|
+
let forcedRefreshAttempted = false;
|
|
118
133
|
let response;
|
|
119
134
|
for (let attempt = 0;; attempt++) {
|
|
120
135
|
response = await fetch(rewritten.input, {
|
|
@@ -123,6 +138,21 @@ export const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
123
138
|
headers: requestHeaders,
|
|
124
139
|
...(isInsecure() && { tls: { rejectUnauthorized: false } }),
|
|
125
140
|
});
|
|
141
|
+
if (response.status === 401 && !forcedRefreshAttempted) {
|
|
142
|
+
forcedRefreshAttempted = true;
|
|
143
|
+
await response.body?.cancel();
|
|
144
|
+
// Force a refresh regardless of the cached expiry — the
|
|
145
|
+
// token may have been rejected before local expiry (e.g.
|
|
146
|
+
// revoked, or clock skew). Only retry if the refreshed
|
|
147
|
+
// token actually changed, to avoid looping forever
|
|
148
|
+
// against a permanently-rejected grant.
|
|
149
|
+
const refreshed = await triggerRefresh();
|
|
150
|
+
if (refreshed === accessToken)
|
|
151
|
+
break;
|
|
152
|
+
accessToken = refreshed;
|
|
153
|
+
setOAuthHeaders(requestHeaders, refreshed, modelId);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
126
156
|
if (response.status !== 429 || attempt >= MAX_429_RETRIES) {
|
|
127
157
|
break;
|
|
128
158
|
}
|
package/dist/transform.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ export type FetchInput = string | URL | Request;
|
|
|
6
6
|
export declare function mergeHeaders(input: FetchInput, init?: RequestInit): Headers;
|
|
7
7
|
/**
|
|
8
8
|
* Merge incoming beta headers with the required OAuth betas, deduplicating.
|
|
9
|
-
* Excludes betas
|
|
10
|
-
* on Haiku models).
|
|
9
|
+
* Excludes betas that are no-ops on the target model (e.g. interleaved-thinking
|
|
10
|
+
* on Haiku models, which don't support it).
|
|
11
11
|
*/
|
|
12
12
|
export declare function mergeBetaHeaders(headers: Headers, modelId?: string): string;
|
|
13
13
|
/**
|
package/dist/transform.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ANTHROPIC_CACHE_LOOKBACK_BLOCKS, CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY_PREFIX, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, USER_AGENT, } from "./constants.js";
|
|
1
|
+
import { ADAPTIVE_THINKING_MODEL_PATTERN, ANTHROPIC_CACHE_LOOKBACK_BLOCKS, CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY_PREFIX, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, USER_AGENT, } from "./constants.js";
|
|
2
2
|
function prefixName(name) {
|
|
3
3
|
return `${TOOL_PREFIX}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
4
4
|
}
|
|
@@ -45,16 +45,16 @@ export function mergeHeaders(input, init) {
|
|
|
45
45
|
return headers;
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* Haiku models don't support interleaved thinking — checks whether a model
|
|
49
|
+
* id refers to a Haiku model.
|
|
50
50
|
*/
|
|
51
51
|
function isHaikuModel(modelId) {
|
|
52
52
|
return /haiku/i.test(modelId ?? '');
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* Merge incoming beta headers with the required OAuth betas, deduplicating.
|
|
56
|
-
* Excludes betas
|
|
57
|
-
* on Haiku models).
|
|
56
|
+
* Excludes betas that are no-ops on the target model (e.g. interleaved-thinking
|
|
57
|
+
* on Haiku models, which don't support it).
|
|
58
58
|
*/
|
|
59
59
|
export function mergeBetaHeaders(headers, modelId) {
|
|
60
60
|
const incomingBeta = headers.get('anthropic-beta') || '';
|
|
@@ -461,45 +461,92 @@ function stripUnsupportedEffortForHaiku(parsed) {
|
|
|
461
461
|
delete parsed.thinking;
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
|
+
const ADAPTIVE_THINKING_DEFAULT = { type: 'adaptive', display: 'summarized' };
|
|
464
465
|
/**
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
466
|
+
* Adaptive-thinking models (Opus 5, Sonnet 5, …) default to hidden thinking
|
|
467
|
+
* and reject legacy manual `thinking.type: "enabled"` + budget_tokens (400
|
|
468
|
+
* on 4.7+). Normalizes to adaptive+summarized, canonicalizes disabled
|
|
469
|
+
* thinking to a bare object (extra fields 400), and demotes xhigh/max
|
|
470
|
+
* effort to high when thinking is disabled (that combination always 400s).
|
|
471
|
+
*/
|
|
472
|
+
function normalizeAdaptiveThinking(parsed) {
|
|
473
|
+
if (!ADAPTIVE_THINKING_MODEL_PATTERN.test(String(parsed.model ?? '')))
|
|
474
|
+
return;
|
|
475
|
+
const thinking = parsed.thinking;
|
|
476
|
+
if (isRecord(thinking) && thinking.type === 'disabled') {
|
|
477
|
+
parsed.thinking = { type: 'disabled' };
|
|
478
|
+
const outputConfig = parsed.output_config;
|
|
479
|
+
if (isRecord(outputConfig) &&
|
|
480
|
+
(outputConfig.effort === 'xhigh' || outputConfig.effort === 'max')) {
|
|
481
|
+
outputConfig.effort = 'high';
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
parsed.thinking = { ...ADAPTIVE_THINKING_DEFAULT };
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Adaptive-thinking models reject non-default temperature/top_p/top_k with
|
|
489
|
+
* a 400, regardless of whether thinking is enabled. Anthropic's default
|
|
490
|
+
* temperature is 1 — anything else is non-default and must be stripped.
|
|
491
|
+
* top_p/top_k have no usable default on these models, so always stripped.
|
|
492
|
+
*/
|
|
493
|
+
function stripRestrictedSamplingParams(parsed) {
|
|
494
|
+
if (!ADAPTIVE_THINKING_MODEL_PATTERN.test(String(parsed.model ?? '')))
|
|
495
|
+
return;
|
|
496
|
+
if (parsed.temperature !== undefined && parsed.temperature !== 1) {
|
|
497
|
+
delete parsed.temperature;
|
|
498
|
+
}
|
|
499
|
+
delete parsed.top_p;
|
|
500
|
+
delete parsed.top_k;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Remove tool_use/tool_result blocks that are not adjacent pairs. Anthropic
|
|
504
|
+
* requires a tool_result to be the first content in the message immediately
|
|
505
|
+
* following its tool_use — a summary inserted by /undo or /compact can leave
|
|
506
|
+
* the ids matched but no longer adjacent, which the API rejects with a 400.
|
|
469
507
|
* Messages left with no content blocks are dropped entirely.
|
|
470
508
|
*/
|
|
471
509
|
function repairOrphanedToolPairs(parsed) {
|
|
472
510
|
if (!Array.isArray(parsed.messages))
|
|
473
511
|
return;
|
|
474
|
-
const
|
|
475
|
-
const
|
|
476
|
-
|
|
512
|
+
const messages = parsed.messages;
|
|
513
|
+
const useMsgIndex = new Map();
|
|
514
|
+
const resultMsgIndex = new Map();
|
|
515
|
+
messages.forEach((msg, index) => {
|
|
477
516
|
if (!isRecord(msg) || !Array.isArray(msg.content))
|
|
478
|
-
|
|
517
|
+
return;
|
|
479
518
|
for (const block of msg.content) {
|
|
480
519
|
if (!isRecord(block))
|
|
481
520
|
continue;
|
|
482
|
-
if (block.type === 'tool_use' &&
|
|
483
|
-
|
|
521
|
+
if (block.type === 'tool_use' &&
|
|
522
|
+
typeof block.id === 'string' &&
|
|
523
|
+
!useMsgIndex.has(block.id)) {
|
|
524
|
+
useMsgIndex.set(block.id, index);
|
|
484
525
|
}
|
|
485
526
|
else if (block.type === 'tool_result' &&
|
|
486
|
-
typeof block.tool_use_id === 'string'
|
|
487
|
-
|
|
527
|
+
typeof block.tool_use_id === 'string' &&
|
|
528
|
+
!resultMsgIndex.has(block.tool_use_id)) {
|
|
529
|
+
resultMsgIndex.set(block.tool_use_id, index);
|
|
488
530
|
}
|
|
489
531
|
}
|
|
490
|
-
}
|
|
491
|
-
|
|
532
|
+
});
|
|
533
|
+
const isAdjacentPair = (id) => {
|
|
534
|
+
const useIndex = useMsgIndex.get(id);
|
|
535
|
+
return useIndex !== undefined && resultMsgIndex.get(id) === useIndex + 1;
|
|
536
|
+
};
|
|
537
|
+
parsed.messages = messages.filter((msg, index) => {
|
|
492
538
|
if (!isRecord(msg) || !Array.isArray(msg.content))
|
|
493
539
|
return true;
|
|
494
540
|
const filteredContent = msg.content.filter((block) => {
|
|
495
541
|
if (!isRecord(block))
|
|
496
542
|
return true;
|
|
497
543
|
if (block.type === 'tool_use' && typeof block.id === 'string') {
|
|
498
|
-
return
|
|
544
|
+
return isAdjacentPair(block.id) && useMsgIndex.get(block.id) === index;
|
|
499
545
|
}
|
|
500
546
|
if (block.type === 'tool_result' &&
|
|
501
547
|
typeof block.tool_use_id === 'string') {
|
|
502
|
-
return
|
|
548
|
+
return (isAdjacentPair(block.tool_use_id) &&
|
|
549
|
+
resultMsgIndex.get(block.tool_use_id) === index);
|
|
503
550
|
}
|
|
504
551
|
return true;
|
|
505
552
|
});
|
|
@@ -508,8 +555,27 @@ function repairOrphanedToolPairs(parsed) {
|
|
|
508
555
|
});
|
|
509
556
|
}
|
|
510
557
|
/**
|
|
511
|
-
*
|
|
512
|
-
*
|
|
558
|
+
* Anthropic requires tool_result blocks to precede any text in a message
|
|
559
|
+
* (text-before-tool_result is a 400). Reorder in place when both are present.
|
|
560
|
+
*/
|
|
561
|
+
function reorderToolResultBlocks(parsed) {
|
|
562
|
+
if (!Array.isArray(parsed.messages))
|
|
563
|
+
return;
|
|
564
|
+
for (const msg of parsed.messages) {
|
|
565
|
+
if (!isRecord(msg) || !Array.isArray(msg.content))
|
|
566
|
+
continue;
|
|
567
|
+
const hasToolResult = msg.content.some((block) => isRecord(block) && block.type === 'tool_result');
|
|
568
|
+
if (!hasToolResult)
|
|
569
|
+
continue;
|
|
570
|
+
const results = msg.content.filter((block) => isRecord(block) && block.type === 'tool_result');
|
|
571
|
+
const rest = msg.content.filter((block) => !(isRecord(block) && block.type === 'tool_result'));
|
|
572
|
+
msg.content = [...results, ...rest];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Remove trailing assistant-role messages. Claude 4.6+ models reject
|
|
577
|
+
* requests that end with an assistant turn (assistant prefill is not
|
|
578
|
+
* supported on these model generations).
|
|
513
579
|
*/
|
|
514
580
|
function stripTrailingAssistantMessages(parsed) {
|
|
515
581
|
if (!Array.isArray(parsed.messages))
|
|
@@ -625,6 +691,9 @@ export function rewriteRequestBody(body) {
|
|
|
625
691
|
const parsed = JSON.parse(body);
|
|
626
692
|
parsed.system = prependClaudeCodeIdentity(parsed.system);
|
|
627
693
|
repairOrphanedToolPairs(parsed);
|
|
694
|
+
reorderToolResultBlocks(parsed);
|
|
695
|
+
normalizeAdaptiveThinking(parsed);
|
|
696
|
+
stripRestrictedSamplingParams(parsed);
|
|
628
697
|
stripUnsupportedEffortForHaiku(parsed);
|
|
629
698
|
stripTrailingAssistantMessages(parsed);
|
|
630
699
|
applyHybridCache1h(parsed);
|