opencode-anthropic-fix 0.0.42 → 0.0.43

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.
Files changed (2) hide show
  1. package/index.mjs +100 -69
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -2354,28 +2354,35 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2354
2354
  // pin the first attempt to that account so files are accessible.
2355
2355
  // Without this, round-robin could route to an account that doesn't
2356
2356
  // have the referenced files, causing file_not_found errors.
2357
- let pinnedAccount = null;
2358
- if (typeof requestInit.body === "string" && fileAccountMap.size > 0) {
2357
+ // Parse body ONCE before the retry loop and reuse for all downstream logic.
2358
+ // This eliminates 4-5 redundant JSON.parse calls per turn (file pinning,
2359
+ // parseRequestBodyMetadata, adaptive context, toast, microcompact).
2360
+ let _parsedBodyOnce = null;
2361
+ if (typeof requestInit.body === "string") {
2359
2362
  try {
2360
- const bodyObj = JSON.parse(requestInit.body);
2361
- const fileIds = extractFileIds(bodyObj);
2362
- for (const fid of fileIds) {
2363
- const pinnedIndex = fileAccountMap.get(fid);
2364
- if (pinnedIndex !== undefined) {
2365
- const candidates = accountManager.getEnabledAccounts();
2366
- pinnedAccount = candidates.find((a) => a.index === pinnedIndex) ?? null;
2367
- if (pinnedAccount) {
2368
- debugLog("file-id pinning: routing to account", {
2369
- fileId: fid,
2370
- accountIndex: pinnedIndex,
2371
- email: pinnedAccount.email,
2372
- });
2373
- break;
2374
- }
2363
+ _parsedBodyOnce = JSON.parse(requestInit.body);
2364
+ } catch {
2365
+ // Non-JSON body — downstream code handles gracefully
2366
+ }
2367
+ }
2368
+
2369
+ let pinnedAccount = null;
2370
+ if (_parsedBodyOnce && fileAccountMap.size > 0) {
2371
+ const fileIds = extractFileIds(_parsedBodyOnce);
2372
+ for (const fid of fileIds) {
2373
+ const pinnedIndex = fileAccountMap.get(fid);
2374
+ if (pinnedIndex !== undefined) {
2375
+ const candidates = accountManager.getEnabledAccounts();
2376
+ pinnedAccount = candidates.find((a) => a.index === pinnedIndex) ?? null;
2377
+ if (pinnedAccount) {
2378
+ debugLog("file-id pinning: routing to account", {
2379
+ fileId: fid,
2380
+ accountIndex: pinnedIndex,
2381
+ email: pinnedAccount.email,
2382
+ });
2383
+ break;
2375
2384
  }
2376
2385
  }
2377
- } catch {
2378
- // Non-JSON body or parse error — skip pinning
2379
2386
  }
2380
2387
  }
2381
2388
 
@@ -2538,8 +2545,10 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2538
2545
 
2539
2546
  // Pre-compute the beta header so it can be injected into both the
2540
2547
  // request body (betas field) and the anthropic-beta header.
2548
+ // Reuse _parsedBodyOnce to avoid redundant JSON.parse.
2541
2549
  const { model: _reqModel, hasFileReferences: _reqHasFileRefs } = parseRequestBodyMetadata(
2542
2550
  requestInit.body,
2551
+ _parsedBodyOnce,
2543
2552
  );
2544
2553
  const _reqProvider = detectProvider(requestUrl);
2545
2554
 
@@ -2555,12 +2564,15 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2555
2564
  escalation_threshold: 150_000,
2556
2565
  deescalation_threshold: 100_000,
2557
2566
  },
2567
+ _parsedBodyOnce,
2558
2568
  );
2559
2569
  // Emit visual cue on state transitions (only when adaptive mode is on)
2560
2570
  if (config.adaptive_context?.enabled && _prevAdaptiveState !== adaptiveContextState.active) {
2561
2571
  const label = adaptiveContextState.active ? "1M context ON" : "1M context OFF";
2562
2572
  const variant = adaptiveContextState.active ? "info" : "success";
2563
- const est = estimatePromptTokens(requestInit.body);
2573
+ const est = _parsedBodyOnce
2574
+ ? estimatePromptTokensFromParsed(_parsedBodyOnce)
2575
+ : estimatePromptTokens(requestInit.body);
2564
2576
  toast(`⬡ ${label} (est. ${Math.round(est / 1000)}K tokens)`, variant, {
2565
2577
  debounceKey: "adaptive-ctx",
2566
2578
  }).catch(() => {});
@@ -2578,7 +2590,9 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2578
2590
  // Microcompact: inject clear betas at high context utilization
2579
2591
  let _microcompactBetas = null;
2580
2592
  if (requestInit.body) {
2581
- const estimatedTokens = estimatePromptTokens(requestInit.body);
2593
+ const estimatedTokens = _parsedBodyOnce
2594
+ ? estimatePromptTokensFromParsed(_parsedBodyOnce)
2595
+ : estimatePromptTokens(requestInit.body);
2582
2596
  if (shouldMicrocompact(estimatedTokens, config)) {
2583
2597
  _microcompactBetas = buildMicrocompactBetas();
2584
2598
  if (!microcompactState.active) {
@@ -3144,7 +3158,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3144
3158
  consecutive529Count = 0;
3145
3159
  }
3146
3160
 
3147
- const baseDelay = Math.min(0.5 * Math.pow(2, serviceWideRetryCount), 8);
3161
+ const baseDelay = Math.min(0.5 * Math.pow(2, serviceWideRetryCount), 3);
3148
3162
  const jitter = 1 - Math.random() * 0.25;
3149
3163
  const sleepMs = Math.round(baseDelay * jitter * 1000);
3150
3164
  const retryLabel = response.status === 529 ? "overloaded" : "unavailable";
@@ -4120,48 +4134,59 @@ function estimatePromptTokens(bodyString) {
4120
4134
  if (!bodyString || typeof bodyString !== "string") return 0;
4121
4135
  try {
4122
4136
  const parsed = JSON.parse(bodyString);
4123
- let charCount = 0;
4137
+ return estimatePromptTokensFromParsed(parsed);
4138
+ } catch {
4139
+ // Fallback: raw body length / 4 if JSON parsing fails
4140
+ return Math.ceil(bodyString.length / 4);
4141
+ }
4142
+ }
4124
4143
 
4125
- // Count system prompt text
4126
- if (Array.isArray(parsed.system)) {
4127
- for (const block of parsed.system) {
4128
- if (block.type === "text" && typeof block.text === "string") {
4129
- charCount += block.text.length;
4130
- }
4144
+ /**
4145
+ * Estimate prompt tokens from an already-parsed request body object.
4146
+ * Avoids redundant JSON.parse when the caller already has the parsed object.
4147
+ * @param {object} parsed - The parsed request body
4148
+ * @returns {number} Estimated token count
4149
+ */
4150
+ function estimatePromptTokensFromParsed(parsed) {
4151
+ if (!parsed || typeof parsed !== "object") return 0;
4152
+ let charCount = 0;
4153
+
4154
+ // Count system prompt text
4155
+ if (Array.isArray(parsed.system)) {
4156
+ for (const block of parsed.system) {
4157
+ if (block.type === "text" && typeof block.text === "string") {
4158
+ charCount += block.text.length;
4131
4159
  }
4132
- } else if (typeof parsed.system === "string") {
4133
- charCount += parsed.system.length;
4134
4160
  }
4161
+ } else if (typeof parsed.system === "string") {
4162
+ charCount += parsed.system.length;
4163
+ }
4135
4164
 
4136
- // Count messages content (text blocks, tool results) — skip tool definitions
4137
- if (Array.isArray(parsed.messages)) {
4138
- for (const msg of parsed.messages) {
4139
- if (typeof msg.content === "string") {
4140
- charCount += msg.content.length;
4141
- } else if (Array.isArray(msg.content)) {
4142
- for (const block of msg.content) {
4143
- if (block.type === "text" && typeof block.text === "string") {
4144
- charCount += block.text.length;
4145
- } else if (block.type === "tool_result" && typeof block.content === "string") {
4146
- charCount += block.content.length;
4147
- } else if (block.type === "tool_use") {
4148
- // Count serialized input as tokens
4149
- charCount += JSON.stringify(block.input || {}).length;
4150
- } else if (block.type === "image" || block.type === "image_url") {
4151
- // Images: ~2000 tokens per image (Anthropic tile-based counting)
4152
- charCount += 8000; // 2000 tokens * 4 chars/token
4153
- }
4165
+ // Count messages content (text blocks, tool results) — skip tool definitions
4166
+ if (Array.isArray(parsed.messages)) {
4167
+ for (const msg of parsed.messages) {
4168
+ if (typeof msg.content === "string") {
4169
+ charCount += msg.content.length;
4170
+ } else if (Array.isArray(msg.content)) {
4171
+ for (const block of msg.content) {
4172
+ if (block.type === "text" && typeof block.text === "string") {
4173
+ charCount += block.text.length;
4174
+ } else if (block.type === "tool_result" && typeof block.content === "string") {
4175
+ charCount += block.content.length;
4176
+ } else if (block.type === "tool_use") {
4177
+ // Count serialized input as tokens
4178
+ charCount += JSON.stringify(block.input || {}).length;
4179
+ } else if (block.type === "image" || block.type === "image_url") {
4180
+ // Images: ~2000 tokens per image (Anthropic tile-based counting)
4181
+ charCount += 8000; // 2000 tokens * 4 chars/token
4154
4182
  }
4155
4183
  }
4156
4184
  }
4157
4185
  }
4158
-
4159
- // 4 chars/token heuristic for text content (reasonable for English + code + JSON)
4160
- return Math.ceil(charCount / 4);
4161
- } catch {
4162
- // Fallback: raw body length / 4 if JSON parsing fails
4163
- return Math.ceil(bodyString.length / 4);
4164
4186
  }
4187
+
4188
+ // 4 chars/token heuristic for text content (reasonable for English + code + JSON)
4189
+ return Math.ceil(charCount / 4);
4165
4190
  }
4166
4191
 
4167
4192
  /**
@@ -4284,7 +4309,7 @@ function analyzeRequestContext(bodyStr) {
4284
4309
  * @param {import('./lib/config.mjs').AdaptiveContextConfig} adaptiveConfig
4285
4310
  * @returns {boolean}
4286
4311
  */
4287
- function resolveAdaptiveContext(bodyString, model, adaptiveConfig) {
4312
+ function resolveAdaptiveContext(bodyString, model, adaptiveConfig, parsedBody) {
4288
4313
  // Non-adaptive: use static check (only explicit "1m" models)
4289
4314
  if (!adaptiveConfig.enabled) {
4290
4315
  return hasOneMillionContext(model);
@@ -4300,7 +4325,7 @@ function resolveAdaptiveContext(bodyString, model, adaptiveConfig) {
4300
4325
  return false;
4301
4326
  }
4302
4327
 
4303
- const estimatedTokens = estimatePromptTokens(bodyString);
4328
+ const estimatedTokens = parsedBody ? estimatePromptTokensFromParsed(parsedBody) : estimatePromptTokens(bodyString);
4304
4329
  const turnsSinceTransition = sessionMetrics.turns - adaptiveContextState.lastTransitionTurn;
4305
4330
 
4306
4331
  if (adaptiveContextState.active) {
@@ -5231,21 +5256,27 @@ function detectProvider(requestUrl) {
5231
5256
  * @param {any} body
5232
5257
  * @returns {{model: string, tools: any[], messages: any[], hasFileReferences: boolean}}
5233
5258
  */
5234
- function parseRequestBodyMetadata(body) {
5235
- if (!body || typeof body !== "string") {
5259
+ function parseRequestBodyMetadata(body, parsedBody) {
5260
+ const parsed =
5261
+ parsedBody ||
5262
+ (typeof body === "string"
5263
+ ? (() => {
5264
+ try {
5265
+ return JSON.parse(body);
5266
+ } catch {
5267
+ return null;
5268
+ }
5269
+ })()
5270
+ : null);
5271
+ if (!parsed) {
5236
5272
  return { model: "", tools: [], messages: [], hasFileReferences: false };
5237
5273
  }
5238
5274
 
5239
- try {
5240
- const parsed = JSON.parse(body);
5241
- const model = typeof parsed?.model === "string" ? parsed.model : "";
5242
- const tools = Array.isArray(parsed?.tools) ? parsed.tools : [];
5243
- const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
5244
- const hasFileReferences = extractFileIds(parsed).length > 0;
5245
- return { model, tools, messages, hasFileReferences };
5246
- } catch {
5247
- return { model: "", tools: [], messages: [], hasFileReferences: false };
5248
- }
5275
+ const model = typeof parsed?.model === "string" ? parsed.model : "";
5276
+ const tools = Array.isArray(parsed?.tools) ? parsed.tools : [];
5277
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
5278
+ const hasFileReferences = extractFileIds(parsed).length > 0;
5279
+ return { model, tools, messages, hasFileReferences };
5249
5280
  }
5250
5281
 
5251
5282
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "main": "./index.mjs",
6
6
  "files": [