@bitkyc08/opencodex 2.7.34 → 2.7.35

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.
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-BkmJJgg6.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-BunUANVE.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-Sg-7L_oZ.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.7.34",
3
+ "version": "2.7.35",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -36,7 +36,7 @@
36
36
  "dev:proxy": "bun run src/cli/index.ts start",
37
37
  "dev:gui": "cd gui && bun run dev",
38
38
  "start": "bun run src/cli/index.ts start",
39
- "test": "bun test --isolate ./tests/",
39
+ "test": "bun scripts/test.ts",
40
40
  "typecheck": "bun x tsc --noEmit",
41
41
  "privacy:scan": "bun scripts/privacy-scan.ts",
42
42
  "generate:jawcode-metadata": "bun scripts/generate-jawcode-metadata.ts",
@@ -63,6 +63,9 @@
63
63
  "@types/bun": "1.3.14",
64
64
  "typescript": "5.9.3"
65
65
  },
66
+ "overrides": {
67
+ "fast-uri": "^3.1.4"
68
+ },
66
69
  "keywords": [
67
70
  "codex",
68
71
  "openai",
@@ -21,6 +21,7 @@ import { normalizeAnthropicImages } from "./anthropic-image-normalize";
21
21
  import { neutralizeIdentity } from "./identity";
22
22
  import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
23
23
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
24
+ import { decodeServerSentEvents } from "../lib/sse-decoder";
24
25
 
25
26
  /** Map a user content part to an Anthropic content block (text or image source). */
26
27
  function toAnthropicContentPart(p: OcxContentPart): unknown {
@@ -719,9 +720,6 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
719
720
  return;
720
721
  }
721
722
 
722
- const reader = response.body.getReader();
723
- const decoder = new TextDecoder();
724
- let buffer = "";
725
723
  let currentBlockType = "";
726
724
  let currentToolCallId = "";
727
725
  let currentToolCallName = "";
@@ -739,34 +737,19 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
739
737
  };
740
738
  };
741
739
 
742
- try {
743
- while (true) {
744
- const { done, value } = await reader.read();
745
- if (done) break;
746
- buffer += decoder.decode(value, { stream: true });
740
+ for await (const record of decodeServerSentEvents(response.body)) {
741
+ const payload = record.data.trim();
742
+ if (!payload) continue;
747
743
 
748
- const lines = buffer.split("\n");
749
- buffer = lines.pop() ?? "";
750
-
751
- let currentEventType = "";
752
- for (const line of lines) {
753
- if (line.startsWith("event: ")) {
754
- currentEventType = line.slice(7).trim();
755
- continue;
756
- }
757
- if (!line.startsWith("data: ")) continue;
758
- const payload = line.slice(6).trim();
759
- if (!payload) continue;
760
-
761
- let data: Record<string, unknown>;
762
- try {
763
- data = JSON.parse(payload) as Record<string, unknown>;
764
- } catch {
765
- debugDroppedFrame("anthropic", payload);
766
- continue;
767
- }
744
+ let data: Record<string, unknown>;
745
+ try {
746
+ data = JSON.parse(payload) as Record<string, unknown>;
747
+ } catch {
748
+ debugDroppedFrame("anthropic", payload);
749
+ continue;
750
+ }
768
751
 
769
- switch (currentEventType || data.type) {
752
+ switch (record.event || data.type) {
770
753
  case "message_start": {
771
754
  const message = data.message as { usage?: Record<string, number> } | undefined;
772
755
  pendingUsage = mergeAnthropicUsage(pendingUsage, message?.usage);
@@ -794,7 +777,12 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
794
777
  yield { type: "text_delta", text: delta.text };
795
778
  } else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
796
779
  yield { type: "thinking_delta", thinking: delta.thinking };
797
- } else if (delta.type === "signature_delta" && typeof delta.signature === "string" && currentBlockType === "thinking") {
780
+ } else if (delta.type === "reasoning_delta" && typeof delta.reasoning === "string") {
781
+ // Some Anthropic-compatible reasoning models use `reasoning` names for the
782
+ // otherwise equivalent thinking block. Preserve it as raw reasoning and keep
783
+ // later text blocks independent.
784
+ yield { type: "thinking_delta", thinking: delta.reasoning };
785
+ } else if (delta.type === "signature_delta" && typeof delta.signature === "string" && (currentBlockType === "thinking" || currentBlockType === "reasoning")) {
798
786
  // Arrives once, just before the thinking block's content_block_stop; block-scoped
799
787
  // so a stray signature on a non-thinking block can never be captured.
800
788
  yield { type: "thinking_signature", signature: delta.signature };
@@ -827,20 +815,15 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
827
815
  yield { type: "error", message: err?.message ?? "Anthropic error" };
828
816
  return;
829
817
  }
830
- }
831
- currentEventType = "";
832
- }
833
818
  }
834
- if (pendingUsage && !emittedDone) yield* emitDone();
835
- } finally {
836
- reader.releaseLock();
837
819
  }
820
+ if (pendingUsage && !emittedDone) yield* emitDone();
838
821
  },
839
822
 
840
823
  async parseResponse(response: Response): Promise<AdapterEvent[]> {
841
824
  const json = await response.json() as Record<string, unknown>;
842
825
  const events: AdapterEvent[] = [];
843
- const content = json.content as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; signature?: string; data?: string }[] | undefined;
826
+ const content = json.content as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; reasoning?: string; signature?: string; data?: string }[] | undefined;
844
827
  if (content) {
845
828
  for (const block of content) {
846
829
  if (block.type === "text" && block.text) {
@@ -850,6 +833,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
850
833
  if (typeof block.signature === "string" && block.signature) {
851
834
  events.push({ type: "thinking_signature", signature: block.signature });
852
835
  }
836
+ } else if (block.type === "reasoning" && typeof block.reasoning === "string") {
837
+ events.push({ type: "thinking_delta", thinking: block.reasoning });
853
838
  } else if (block.type === "redacted_thinking" && typeof block.data === "string") {
854
839
  events.push({ type: "redacted_thinking", data: block.data });
855
840
  } else if (block.type === "tool_use") {
@@ -36,6 +36,17 @@ export function isCursorBenignCancelError(value: unknown): boolean {
36
36
  return false;
37
37
  }
38
38
 
39
+ /**
40
+ * True when Cursor Connect rejected the turn with invalid_argument.
41
+ * Seen after stepCompleted on brittle external-model continuations.
42
+ */
43
+ export function isCursorInvalidArgumentError(value: unknown): boolean {
44
+ const code = errorCode(value).toLowerCase();
45
+ if (code === "invalid_argument") return true;
46
+ const message = errorMessage(value).toLowerCase();
47
+ return message.includes("invalid_argument");
48
+ }
49
+
39
50
  /**
40
51
  * Classify a Cursor transport/Connect/gRPC error message into an actionable category.
41
52
  * The returned prefix string is recognized by `src/lib/errors.ts` `classifyError` keywords,
@@ -120,6 +120,31 @@ export function cursorCodexToWireModelId(modelId: string): string {
120
120
  return cursorWireModelSelection(modelId).modelId;
121
121
  }
122
122
 
123
+ /**
124
+ * Cursor-native wire models keep server-side conversation state reliably.
125
+ * External models (gpt/claude/gemini/grok families and similar) are more brittle on resumeAction.
126
+ */
127
+ export function isCursorNativeWireModel(modelId: string): boolean {
128
+ const wire = cursorCodexToWireModelId(modelId).trim().toLowerCase();
129
+ const bare = stripCursorEffortSuffix(wire);
130
+ if (bare === CURSOR_AUTO_WIRE_MODEL_ID || bare === CURSOR_AUTO_MODEL_ID) return true;
131
+ return bare.startsWith("composer-");
132
+ }
133
+
134
+ /** Inverse of {@link isCursorNativeWireModel}. */
135
+ export function isCursorExternalWireModel(modelId: string): boolean {
136
+ return !isCursorNativeWireModel(modelId);
137
+ }
138
+
139
+ function stripCursorEffortSuffix(wireModelId: string): string {
140
+ const suffixes = [...CANONICAL_EFFORT_SUFFIXES].sort((a, b) => b.length - a.length);
141
+ for (const suffix of suffixes) {
142
+ const marker = `-${suffix}`;
143
+ if (wireModelId.endsWith(marker)) return wireModelId.slice(0, -marker.length);
144
+ }
145
+ return wireModelId;
146
+ }
147
+
123
148
  export function isCursorRouterModelId(modelId: string): boolean {
124
149
  return (CURSOR_ROUTER_MODEL_IDS as readonly string[]).includes(modelId);
125
150
  }
@@ -70,6 +70,11 @@ const GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS = 1_800;
70
70
  const GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS = 125;
71
71
  const cursorContextUsageTracker = createCursorContextUsageTracker();
72
72
 
73
+ /** Carry context-usage totals across conversation-id rotation for external-model replay. */
74
+ export function rekeyCursorContextUsage(fromConversationId: string, toConversationId: string): void {
75
+ cursorContextUsageTracker.rekey(fromConversationId, toConversationId);
76
+ }
77
+
73
78
  export class CursorMissingCredentialError extends Error {
74
79
  readonly code = "cursor_missing_credential";
75
80
 
@@ -22,6 +22,8 @@ export interface CursorContextUsageTracker {
22
22
  controlsForConversation(conversationId: string, options?: { clearPrior?: boolean; storeCheckpoints?: boolean }): CursorContextUsageControls;
23
23
  get(conversationId: string): number | undefined;
24
24
  record(conversationId: string, tokens: number): void;
25
+ /** Copy numeric carry-forward totals when a conversation id is rotated for replay. */
26
+ rekey(fromConversationId: string, toConversationId: string): void;
25
27
  clear(conversationId: string): void;
26
28
  clearAll(): void;
27
29
  }
@@ -91,6 +93,18 @@ export function createCursorContextUsageTracker(options: { maxEntries?: number;
91
93
  },
92
94
  get,
93
95
  record,
96
+ rekey(fromConversationId, toConversationId) {
97
+ if (!fromConversationId || !toConversationId || fromConversationId === toConversationId) return;
98
+ prune();
99
+ const from = entries.get(fromConversationId);
100
+ if (!from) return;
101
+ const to = entries.get(toConversationId);
102
+ const tokens = Math.max(from.tokens, to?.tokens ?? 0);
103
+ entries.delete(fromConversationId);
104
+ entries.delete(toConversationId);
105
+ entries.set(toConversationId, { tokens, updatedAt: now() });
106
+ prune();
107
+ },
94
108
  clear(conversationId) {
95
109
  entries.delete(conversationId);
96
110
  },
@@ -4,6 +4,8 @@ import { ValueSchema } from "@bufbuild/protobuf/wkt";
4
4
  import type { OcxAssistantContentPart, OcxMessage, OcxToolResultMessage } from "../../types";
5
5
  import { namespacedToolName } from "../../types";
6
6
  import type { CursorRunRequest } from "./types";
7
+ import { isCursorExternalWireModel } from "./discovery";
8
+ import { debugProviderDiagnostic } from "../../lib/debug";
7
9
  import { storeCursorBlob } from "./native-exec";
8
10
  import {
9
11
  AgentClientMessageSchema,
@@ -326,8 +328,9 @@ export function encodeCursorRunRequest(request: CursorRunRequest): Uint8Array {
326
328
  // would pollute the model input and double-deliver the result. Use ResumeAction so Cursor picks up
327
329
  // from the history we provided.
328
330
  const lastRawIsToolResult = request.rawMessages?.at(-1)?.role === "toolResult";
331
+ const actionCase = !lastRawIsToolResult && text.trim().length > 0 ? "userMessageAction" : "resumeAction";
329
332
  const action = create(ConversationActionSchema, {
330
- action: !lastRawIsToolResult && text.trim().length > 0
333
+ action: actionCase === "userMessageAction"
331
334
  ? {
332
335
  case: "userMessageAction",
333
336
  value: create(UserMessageActionSchema, {
@@ -345,6 +348,13 @@ export function encodeCursorRunRequest(request: CursorRunRequest): Uint8Array {
345
348
  }),
346
349
  },
347
350
  });
351
+ debugProviderDiagnostic("cursor", "run-request", {
352
+ wireModel: request.modelId,
353
+ action: actionCase,
354
+ conversationId: request.conversationId,
355
+ turnType: lastRawIsToolResult ? "tool-continuation" : "initial",
356
+ externalModel: isCursorExternalWireModel(request.modelId),
357
+ });
348
358
 
349
359
  const runRequest = create(AgentRunRequestSchema, {
350
360
  conversationId: request.conversationId,
@@ -8,7 +8,7 @@ import type {
8
8
  } from "../../types";
9
9
  import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
10
10
  import type { CursorRequestMessage, CursorRunRequest } from "./types";
11
- import { cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
11
+ import { cursorWireModelSelection, isCursorExternalWireModel, type CursorRoutingLevel } from "./discovery";
12
12
  import { cursorEffortSuffix } from "./effort-map";
13
13
  import {
14
14
  cursorMcpToolEncodedSize,
@@ -159,7 +159,15 @@ export function generatedCursorConversationId(): string {
159
159
  return `cursor_${crypto.randomUUID().replace(/-/g, "")}`;
160
160
  }
161
161
 
162
- export function createCursorRequest(parsed: OcxParsedRequest): CursorRunRequest {
162
+ export interface CreateCursorRequestOptions {
163
+ /** Force a brand-new Cursor conversation id even when remembered state exists. */
164
+ forceFreshConversation?: boolean;
165
+ }
166
+
167
+ export function createCursorRequest(
168
+ parsed: OcxParsedRequest,
169
+ options: CreateCursorRequestOptions = {},
170
+ ): CursorRunRequest {
163
171
  const messages = parsed.context.messages
164
172
  .map(requestMessage)
165
173
  .filter((message): message is CursorRequestMessage => !!message && message.content.length > 0);
@@ -168,6 +176,13 @@ export function createCursorRequest(parsed: OcxParsedRequest): CursorRunRequest
168
176
  const budget = applyCursorToolBudget(visibleTools, parsed.options.toolChoice);
169
177
  const limitNote = catalogLimitNote(budget.tools, budget.omitted);
170
178
  const model = normalizeCursorModelId(parsed.modelId, parsed.options.reasoning);
179
+ const lastRaw = parsed.context.messages.at(-1);
180
+ // External Cursor models (e.g. gpt-5.6-sol) can corrupt server-side conversation state across
181
+ // tool-result continuations when ResumeAction reuses the same conversationId. Force a fresh id
182
+ // so the full history is replayed without depending on that state.
183
+ const forceFreshConversation =
184
+ options.forceFreshConversation === true
185
+ || (lastRaw?.role === "toolResult" && isCursorExternalWireModel(model.modelId));
171
186
  return {
172
187
  modelId: model.modelId,
173
188
  ...(model.routingLevel ? { routingLevel: model.routingLevel } : {}),
@@ -175,7 +190,9 @@ export function createCursorRequest(parsed: OcxParsedRequest): CursorRunRequest
175
190
  // back to the OpenAI Responses previous_response_id (resp_*): that is a Responses-chain id in a
176
191
  // different namespace and would start an unrelated Cursor conversation, breaking tool-result
177
192
  // continuation. If we have no remembered Cursor conversation, start a fresh one.
178
- conversationId: parsed._cursorConversationId ?? generatedCursorConversationId(),
193
+ conversationId: forceFreshConversation
194
+ ? generatedCursorConversationId()
195
+ : (parsed._cursorConversationId ?? generatedCursorConversationId()),
179
196
  system: [...(parsed.context.systemPrompt ?? []), ...(limitNote ? [limitNote] : [])],
180
197
  messages,
181
198
  rawMessages: parsed.context.messages,
@@ -1,11 +1,16 @@
1
1
  import type { AdapterEvent, OcxProviderConfig } from "../types";
2
2
  import type { ProviderAdapter } from "./base";
3
3
  import { cursorExecDeniedMessage, cursorRequestDeclaresFullAccess } from "./cursor/exec-policy";
4
- import { isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor/cursor-errors";
4
+ import { isCursorBenignCancelError, isCursorInvalidArgumentError, safeCursorErrorMessage } from "./cursor/cursor-errors";
5
+ import { isCursorExternalWireModel } from "./cursor/discovery";
5
6
  import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
6
7
  import { mapCursorServerMessage } from "./cursor/message-mapper";
7
8
  import { createCursorRequest, generatedCursorConversationId } from "./cursor/request-builder";
8
- import { createLiveCursorTransport, CursorMissingCredentialError } from "./cursor/live-transport";
9
+ import {
10
+ createLiveCursorTransport,
11
+ CursorMissingCredentialError,
12
+ rekeyCursorContextUsage,
13
+ } from "./cursor/live-transport";
9
14
  import { runCursorTurnWithRetry } from "./cursor/transport-retry";
10
15
  import {
11
16
  createDisabledCursorTransport,
@@ -70,26 +75,68 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
70
75
  const makeTransport = deps.createTransport ?? createLiveCursorTransport;
71
76
  const kv = deps.kv ?? createCursorKvStore();
72
77
  _parsed._cursorConversationId ??= generatedCursorConversationId();
73
- const request = createCursorRequest(_parsed);
74
- await runCursorTurnWithRetry(
75
- makeTransport,
76
- { provider, headers: incoming.headers, requestDeclaresFullAccess: cursorRequestDeclaresFullAccess(request) },
77
- request,
78
- incoming.abortSignal,
79
- (message, activeTransport) => {
80
- if (incoming.abortSignal?.aborted) {
81
- emit({ type: "error", message: "Cursor turn was aborted." });
82
- return;
83
- }
84
- const events = mapCursorServerMessage(message, {
85
- kv,
86
- writeClient: clientMessage => {
87
- void activeTransport.writeClient(clientMessage);
88
- },
89
- });
90
- for (const event of events) emit(event);
91
- },
92
- );
78
+ const previousConversationId = _parsed._cursorConversationId;
79
+ let request = createCursorRequest(_parsed);
80
+ // Keep remembered conversation id in sync when the request builder mints a fresh id
81
+ // for external-model tool-result continuations (stateless replay).
82
+ if (request.conversationId !== previousConversationId) {
83
+ rekeyCursorContextUsage(previousConversationId, request.conversationId);
84
+ }
85
+ _parsed._cursorConversationId = request.conversationId;
86
+ let emittedOutput = false;
87
+ const lastRawIsToolResult = _parsed.context.messages.at(-1)?.role === "toolResult";
88
+
89
+ const runOnce = async (activeRequest: ReturnType<typeof createCursorRequest>) => {
90
+ await runCursorTurnWithRetry(
91
+ makeTransport,
92
+ {
93
+ provider,
94
+ headers: incoming.headers,
95
+ requestDeclaresFullAccess: cursorRequestDeclaresFullAccess(activeRequest),
96
+ },
97
+ activeRequest,
98
+ incoming.abortSignal,
99
+ (message, activeTransport) => {
100
+ if (incoming.abortSignal?.aborted) {
101
+ emit({ type: "error", message: "Cursor turn was aborted." });
102
+ return;
103
+ }
104
+ const events = mapCursorServerMessage(message, {
105
+ kv,
106
+ writeClient: clientMessage => {
107
+ void activeTransport.writeClient(clientMessage);
108
+ },
109
+ });
110
+ for (const event of events) {
111
+ if (event.type !== "heartbeat") emittedOutput = true;
112
+ emit(event);
113
+ }
114
+ },
115
+ );
116
+ };
117
+
118
+ try {
119
+ await runOnce(request);
120
+ } catch (err) {
121
+ // One-shot fallback: only for external-model tool-result continuations that fail
122
+ // with Connect invalid_argument before any non-heartbeat output was forwarded.
123
+ // Replaying after text/tool events would duplicate output.
124
+ if (
125
+ !isCursorInvalidArgumentError(err)
126
+ || !isCursorExternalWireModel(request.modelId)
127
+ || !lastRawIsToolResult
128
+ || emittedOutput
129
+ || incoming.abortSignal?.aborted
130
+ ) {
131
+ throw err;
132
+ }
133
+ const failedConversationId = request.conversationId;
134
+ _parsed._cursorConversationId = undefined;
135
+ request = createCursorRequest(_parsed, { forceFreshConversation: true });
136
+ rekeyCursorContextUsage(failedConversationId, request.conversationId);
137
+ _parsed._cursorConversationId = request.conversationId;
138
+ await runOnce(request);
139
+ }
93
140
  } catch (err) {
94
141
  if (isCursorBenignCancelError(err)) return;
95
142
  const partialUsage = (err as { partialUsage?: import("../types").OcxUsage }).partialUsage;
@@ -1,5 +1,6 @@
1
1
  import type { AdapterFetchContext, AdapterRequest } from "./base";
2
2
  import { isQuotaExhaustedBody, retryableGoogleStatus, safeGoogleHttpErrorMessage } from "./google-errors";
3
+ import { repairGoogleInvalidRequestBody } from "./google-wire-compiler";
3
4
  import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error";
4
5
  import {
5
6
  abortError,
@@ -29,14 +30,32 @@ async function normalizeFinalGoogleError(label: string, res: Response, signal?:
29
30
  export async function fetchGoogleWithRetry(label: string, request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
30
31
  const timeoutMs = ctx.timeoutMs ?? 200_000;
31
32
  let lastError: unknown;
33
+ let activeRequest = request;
34
+ let compatibilityReplayUsed = false;
32
35
  for (let attempt = 0; attempt < GOOGLE_RETRY_ATTEMPTS; attempt++) {
33
36
  if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
34
37
  try {
35
- const res = await fetchWithAttemptDeadline(request.url, {
36
- method: request.method,
37
- headers: request.headers,
38
- body: request.body,
38
+ const res = await fetchWithAttemptDeadline(activeRequest.url, {
39
+ method: activeRequest.method,
40
+ headers: activeRequest.headers,
41
+ body: activeRequest.body,
39
42
  }, timeoutMs, ctx.abortSignal, ctx.stream);
43
+ if (res.status === 400 && !compatibilityReplayUsed) {
44
+ let payloadText = "";
45
+ try {
46
+ payloadText = await readDisplaySafeErrorPayloadText(res.clone(), ctx.abortSignal);
47
+ } catch (error) {
48
+ if (ctx.abortSignal?.aborted) throw error;
49
+ }
50
+ const repairedBody = repairGoogleInvalidRequestBody(activeRequest.body, payloadText);
51
+ if (repairedBody !== undefined) {
52
+ compatibilityReplayUsed = true;
53
+ activeRequest = { ...activeRequest, body: repairedBody };
54
+ cancelResponseBodyBestEffort(res);
55
+ attempt--; // The changed-request replay is separate from transient retry accounting.
56
+ continue;
57
+ }
58
+ }
40
59
  if (!retryableGoogleStatus(res.status) || attempt === GOOGLE_RETRY_ATTEMPTS - 1) {
41
60
  return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
42
61
  }