@sahiljassal/opencode-anthropic-auth 2.4.1 → 2.6.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/README.md CHANGED
@@ -40,6 +40,7 @@ Additional behaviours:
40
40
 
41
41
  - **System tail coalescing** — plugin-added system blocks beyond the primary prompt are merged into one block before placing the system anchor, preventing cache busts when block layout changes between requests
42
42
  - **Trailing assistant strip** — assistant messages at the tail of the request are removed before forwarding (OAuth rejects assistant prefill)
43
+ - **Tool pair repair** — `/compact` and `/undo` can leave a `tool_use` and its `tool_result` matched by id but no longer adjacent. Orphaned `tool_result` blocks are dropped; orphaned `tool_use` blocks are never deleted (Anthropic rejects edits to `thinking`/`redacted_thinking` blocks in the latest assistant message) — instead a placeholder `tool_result` is synthesized to restore adjacency
43
44
  - **Thinking block guard** — `thinking` and `redacted_thinking` blocks are excluded from cache anchor placement; messages containing only thinking blocks receive no `cache_control` (avoids Anthropic 400)
44
45
  - **SSE retryable errors** — transient server errors (`api_error`, `overloaded_error`, `server_error`) emitted inside HTTP 200 streams are detected and thrown as connection-reset errors so OpenCode auto-retries
45
46
  - **Buffered stream rewriting** — tool name stripping buffers partial `"name"` tokens across chunk boundaries to avoid corruption
@@ -7,6 +7,11 @@ export declare const CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code
7
7
  export declare const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
8
8
  export declare const OAUTH_SCOPES: string[];
9
9
  export declare const TOOL_PREFIX = "mcp_";
10
+ /**
11
+ * Content for a synthesized tool_result whose real output was removed by a
12
+ * /compact or /undo summary insertion (see repairOrphanedToolPairs).
13
+ */
14
+ export declare const TOOL_RESULT_PLACEHOLDER = "Tool result unavailable (removed during context compaction).";
10
15
  /**
11
16
  * Anthropic's cache lookback window size. Each explicit breakpoint scans at
12
17
  * most this many content blocks backward (counting the breakpoint block as
@@ -18,7 +23,15 @@ export declare const TOOL_PREFIX = "mcp_";
18
23
  export declare const ANTHROPIC_CACHE_LOOKBACK_BLOCKS = 20;
19
24
  export declare const REQUIRED_BETAS: string[];
20
25
  export declare const OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
21
- export declare const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
26
+ export declare const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
27
+ /**
28
+ * Model families with adaptive thinking (thinking defaults on, display
29
+ * defaults to "omitted"). These models reject legacy manual thinking
30
+ * (type: enabled + budget_tokens) and non-default temperature/top_p/top_k.
31
+ */
32
+ export declare const ADAPTIVE_THINKING_MODEL_PATTERN: RegExp;
33
+ /** Proactive OAuth refresh margin — refresh this long before actual expiry. */
34
+ export declare const OAUTH_REFRESH_SKEW_MS: number;
22
35
  export declare const CCH_SALT = "59cf53e54c78";
23
36
  export declare const CCH_POSITIONS: number[];
24
37
  export declare const CLAUDE_CODE_VERSION = "2.1.177";
package/dist/constants.js CHANGED
@@ -14,6 +14,11 @@ export const OAUTH_SCOPES = [
14
14
  'user:file_upload',
15
15
  ];
16
16
  export const TOOL_PREFIX = 'mcp_';
17
+ /**
18
+ * Content for a synthesized tool_result whose real output was removed by a
19
+ * /compact or /undo summary insertion (see repairOrphanedToolPairs).
20
+ */
21
+ export const TOOL_RESULT_PLACEHOLDER = 'Tool result unavailable (removed during context compaction).';
17
22
  /**
18
23
  * Anthropic's cache lookback window size. Each explicit breakpoint scans at
19
24
  * most this many content blocks backward (counting the breakpoint block as
@@ -27,10 +32,17 @@ export const REQUIRED_BETAS = [
27
32
  'oauth-2025-04-20',
28
33
  'claude-code-20250219',
29
34
  'interleaved-thinking-2025-05-14',
30
- 'fine-grained-tool-streaming-2025-05-14',
31
35
  ];
32
36
  export const OPENCODE_IDENTITY_PREFIX = 'You are OpenCode';
33
- export const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
37
+ export const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
38
+ /**
39
+ * Model families with adaptive thinking (thinking defaults on, display
40
+ * defaults to "omitted"). These models reject legacy manual thinking
41
+ * (type: enabled + budget_tokens) and non-default temperature/top_p/top_k.
42
+ */
43
+ export const ADAPTIVE_THINKING_MODEL_PATTERN = /claude-(opus-5|opus-4-8|opus-4-7|sonnet-5|fable-5|mythos-5)/i;
44
+ /** Proactive OAuth refresh margin — refresh this long before actual expiry. */
45
+ export const OAUTH_REFRESH_SKEW_MS = 5 * 60_000;
34
46
  export const CCH_SALT = '59cf53e54c78';
35
47
  export const CCH_POSITIONS = [4, 7, 20];
36
48
  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 || !auth.expires || auth.expires < Date.now()) {
33
- if (!refreshPromise) {
34
- refreshPromise = (async () => {
35
- const maxRetries = 2;
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
  }
@@ -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 unsupported by the target model (e.g. interleaved-thinking
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, TOOL_RESULT_PLACEHOLDER, 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
- * Anthropic rejects interleaved-thinking on Haiku models this checks
49
- * whether a model id refers to a Haiku model.
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 unsupported by the target model (e.g. interleaved-thinking
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,55 +461,165 @@ function stripUnsupportedEffortForHaiku(parsed) {
461
461
  delete parsed.thinking;
462
462
  }
463
463
  }
464
+ const ADAPTIVE_THINKING_DEFAULT = { type: 'adaptive', display: 'summarized' };
464
465
  /**
465
- * Remove tool_use blocks with no matching tool_result, and tool_result
466
- * blocks that reference a non-existent tool_use. Anthropic rejects requests
467
- * with unpaired tool_use/tool_result blocks this can happen when OpenCode
468
- * truncates or reconstructs conversation history after an interruption.
469
- * Messages left with no content blocks are dropped entirely.
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
+ function toolUseIdOf(block) {
503
+ return isRecord(block) &&
504
+ block.type === 'tool_use' &&
505
+ typeof block.id === 'string'
506
+ ? block.id
507
+ : undefined;
508
+ }
509
+ function toolResultIdOf(block) {
510
+ return isRecord(block) &&
511
+ block.type === 'tool_result' &&
512
+ typeof block.tool_use_id === 'string'
513
+ ? block.tool_use_id
514
+ : undefined;
515
+ }
516
+ /**
517
+ * Reconcile tool_use/tool_result adjacency broken by a /compact or /undo
518
+ * summary insertion. Anthropic requires a tool_result to be the first
519
+ * content in the message immediately following its tool_use, and rejects
520
+ * partial edits to an assistant message that holds thinking/redacted_thinking
521
+ * blocks ("thinking blocks in the latest assistant message cannot be
522
+ * modified") — so orphaned tool_use blocks can't simply be deleted. Two
523
+ * passes:
524
+ *
525
+ * 1. Remove tool_result blocks with no adjacent preceding tool_use (these
526
+ * only live in user turns, so no thinking block is affected).
527
+ * 2. Synthesize a placeholder tool_result, adjacent, for every tool_use
528
+ * that still lacks one — assistant message content is never rewritten.
470
529
  */
471
530
  function repairOrphanedToolPairs(parsed) {
472
531
  if (!Array.isArray(parsed.messages))
473
532
  return;
474
- const toolUseIds = new Set();
475
- const toolResultIds = new Set();
476
- for (const msg of parsed.messages) {
533
+ const messages = parsed.messages;
534
+ const hasAdjacentUse = (index, id) => {
535
+ const prev = messages[index - 1];
536
+ return (isRecord(prev) &&
537
+ Array.isArray(prev.content) &&
538
+ prev.content.some((block) => toolUseIdOf(block) === id));
539
+ };
540
+ const pass1 = messages.flatMap((msg, index) => {
541
+ if (!isRecord(msg) || !Array.isArray(msg.content))
542
+ return [msg];
543
+ const filtered = msg.content.filter((block) => {
544
+ const resultId = toolResultIdOf(block);
545
+ return resultId === undefined || hasAdjacentUse(index, resultId);
546
+ });
547
+ if (filtered.length === 0 && msg.content.length > 0)
548
+ return [];
549
+ return [
550
+ filtered.length === msg.content.length
551
+ ? msg
552
+ : { ...msg, content: filtered },
553
+ ];
554
+ });
555
+ const out = [];
556
+ for (let i = 0; i < pass1.length; i++) {
557
+ const msg = pass1[i];
558
+ out.push(msg);
477
559
  if (!isRecord(msg) || !Array.isArray(msg.content))
478
560
  continue;
479
- for (const block of msg.content) {
480
- if (!isRecord(block))
481
- continue;
482
- if (block.type === 'tool_use' && typeof block.id === 'string') {
483
- toolUseIds.add(block.id);
484
- }
485
- else if (block.type === 'tool_result' &&
486
- typeof block.tool_use_id === 'string') {
487
- toolResultIds.add(block.tool_use_id);
488
- }
561
+ const useIds = msg.content
562
+ .map(toolUseIdOf)
563
+ .filter((id) => id !== undefined);
564
+ if (useIds.length === 0)
565
+ continue;
566
+ const next = pass1[i + 1];
567
+ const presentIds = new Set(isRecord(next) && Array.isArray(next.content)
568
+ ? next.content
569
+ .map(toolResultIdOf)
570
+ .filter((id) => id !== undefined)
571
+ : []);
572
+ const missing = useIds.filter((id) => !presentIds.has(id));
573
+ if (missing.length === 0)
574
+ continue;
575
+ const synthetic = missing.map((id) => ({
576
+ type: 'tool_result',
577
+ tool_use_id: id,
578
+ content: TOOL_RESULT_PLACEHOLDER,
579
+ is_error: true,
580
+ }));
581
+ if (isRecord(next) && next.role === 'user' && Array.isArray(next.content)) {
582
+ out.push({ ...next, content: [...synthetic, ...next.content] });
583
+ i++;
584
+ }
585
+ else if (isRecord(next) &&
586
+ next.role === 'user' &&
587
+ typeof next.content === 'string') {
588
+ const text = next.content;
589
+ out.push({
590
+ ...next,
591
+ content: text.length > 0 ? [...synthetic, { type: 'text', text }] : synthetic,
592
+ });
593
+ i++;
594
+ }
595
+ else {
596
+ out.push({ role: 'user', content: synthetic });
489
597
  }
490
598
  }
491
- parsed.messages = parsed.messages.filter((msg) => {
599
+ parsed.messages = out;
600
+ }
601
+ /**
602
+ * Anthropic requires tool_result blocks to precede any text in a message
603
+ * (text-before-tool_result is a 400). Reorder in place when both are present.
604
+ */
605
+ function reorderToolResultBlocks(parsed) {
606
+ if (!Array.isArray(parsed.messages))
607
+ return;
608
+ for (const msg of parsed.messages) {
492
609
  if (!isRecord(msg) || !Array.isArray(msg.content))
493
- return true;
494
- const filteredContent = msg.content.filter((block) => {
495
- if (!isRecord(block))
496
- return true;
497
- if (block.type === 'tool_use' && typeof block.id === 'string') {
498
- return toolResultIds.has(block.id);
499
- }
500
- if (block.type === 'tool_result' &&
501
- typeof block.tool_use_id === 'string') {
502
- return toolUseIds.has(block.tool_use_id);
503
- }
504
- return true;
505
- });
506
- msg.content = filteredContent;
507
- return filteredContent.length > 0;
508
- });
610
+ continue;
611
+ const hasToolResult = msg.content.some((block) => isRecord(block) && block.type === 'tool_result');
612
+ if (!hasToolResult)
613
+ continue;
614
+ const results = msg.content.filter((block) => isRecord(block) && block.type === 'tool_result');
615
+ const rest = msg.content.filter((block) => !(isRecord(block) && block.type === 'tool_result'));
616
+ msg.content = [...results, ...rest];
617
+ }
509
618
  }
510
619
  /**
511
- * Remove trailing assistant-role messages. OAuth endpoints reject requests
512
- * that end with an assistant turn (assistant prefill is not supported).
620
+ * Remove trailing assistant-role messages. Claude 4.6+ models reject
621
+ * requests that end with an assistant turn (assistant prefill is not
622
+ * supported on these model generations).
513
623
  */
514
624
  function stripTrailingAssistantMessages(parsed) {
515
625
  if (!Array.isArray(parsed.messages))
@@ -625,6 +735,9 @@ export function rewriteRequestBody(body) {
625
735
  const parsed = JSON.parse(body);
626
736
  parsed.system = prependClaudeCodeIdentity(parsed.system);
627
737
  repairOrphanedToolPairs(parsed);
738
+ reorderToolResultBlocks(parsed);
739
+ normalizeAdaptiveThinking(parsed);
740
+ stripRestrictedSamplingParams(parsed);
628
741
  stripUnsupportedEffortForHaiku(parsed);
629
742
  stripTrailingAssistantMessages(parsed);
630
743
  applyHybridCache1h(parsed);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sahiljassal/opencode-anthropic-auth",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/shljsl75891/opencode-anthropic-auth.git"