opencode-antigravity-auth 1.1.4 → 1.2.1

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 (60) hide show
  1. package/README.md +223 -99
  2. package/dist/src/hooks/auto-update-checker/cache.d.ts +3 -0
  3. package/dist/src/hooks/auto-update-checker/cache.d.ts.map +1 -0
  4. package/dist/src/hooks/auto-update-checker/cache.js +71 -0
  5. package/dist/src/hooks/auto-update-checker/cache.js.map +1 -0
  6. package/dist/src/hooks/auto-update-checker/checker.d.ts +16 -0
  7. package/dist/src/hooks/auto-update-checker/checker.d.ts.map +1 -0
  8. package/dist/src/hooks/auto-update-checker/checker.js +237 -0
  9. package/dist/src/hooks/auto-update-checker/checker.js.map +1 -0
  10. package/dist/src/hooks/auto-update-checker/constants.d.ts +9 -0
  11. package/dist/src/hooks/auto-update-checker/constants.d.ts.map +1 -0
  12. package/dist/src/hooks/auto-update-checker/constants.js +23 -0
  13. package/dist/src/hooks/auto-update-checker/constants.js.map +1 -0
  14. package/dist/src/hooks/auto-update-checker/index.d.ts +34 -0
  15. package/dist/src/hooks/auto-update-checker/index.d.ts.map +1 -0
  16. package/dist/src/hooks/auto-update-checker/index.js +121 -0
  17. package/dist/src/hooks/auto-update-checker/index.js.map +1 -0
  18. package/dist/src/hooks/auto-update-checker/types.d.ts +25 -0
  19. package/dist/src/hooks/auto-update-checker/types.d.ts.map +1 -0
  20. package/dist/src/hooks/auto-update-checker/types.js +1 -0
  21. package/dist/src/hooks/auto-update-checker/types.js.map +1 -0
  22. package/dist/src/plugin/accounts.d.ts +21 -10
  23. package/dist/src/plugin/accounts.d.ts.map +1 -1
  24. package/dist/src/plugin/accounts.js +101 -55
  25. package/dist/src/plugin/accounts.js.map +1 -1
  26. package/dist/src/plugin/cache.d.ts +14 -0
  27. package/dist/src/plugin/cache.d.ts.map +1 -1
  28. package/dist/src/plugin/cache.js +82 -0
  29. package/dist/src/plugin/cache.js.map +1 -1
  30. package/dist/src/plugin/debug.d.ts +32 -0
  31. package/dist/src/plugin/debug.d.ts.map +1 -1
  32. package/dist/src/plugin/debug.js +140 -12
  33. package/dist/src/plugin/debug.js.map +1 -1
  34. package/dist/src/plugin/request-helpers.d.ts +13 -4
  35. package/dist/src/plugin/request-helpers.d.ts.map +1 -1
  36. package/dist/src/plugin/request-helpers.js +171 -18
  37. package/dist/src/plugin/request-helpers.js.map +1 -1
  38. package/dist/src/plugin/request.d.ts +10 -2
  39. package/dist/src/plugin/request.d.ts.map +1 -1
  40. package/dist/src/plugin/request.js +614 -67
  41. package/dist/src/plugin/request.js.map +1 -1
  42. package/dist/src/plugin/storage.d.ts +23 -7
  43. package/dist/src/plugin/storage.d.ts.map +1 -1
  44. package/dist/src/plugin/storage.js +54 -10
  45. package/dist/src/plugin/storage.js.map +1 -1
  46. package/dist/src/plugin/types.d.ts +13 -1
  47. package/dist/src/plugin/types.d.ts.map +1 -1
  48. package/dist/src/plugin.d.ts +3 -3
  49. package/dist/src/plugin.d.ts.map +1 -1
  50. package/dist/src/plugin.js +780 -474
  51. package/dist/src/plugin.js.map +1 -1
  52. package/package.json +1 -1
  53. package/dist/src/plugin/accounts.test.d.ts +0 -2
  54. package/dist/src/plugin/accounts.test.d.ts.map +0 -1
  55. package/dist/src/plugin/accounts.test.js +0 -139
  56. package/dist/src/plugin/accounts.test.js.map +0 -1
  57. package/dist/src/plugin/token.test.d.ts +0 -2
  58. package/dist/src/plugin/token.test.d.ts.map +0 -1
  59. package/dist/src/plugin/token.test.js +0 -64
  60. package/dist/src/plugin/token.test.js.map +0 -1
@@ -1,7 +1,273 @@
1
1
  import crypto from "node:crypto";
2
2
  import { ANTIGRAVITY_HEADERS, ANTIGRAVITY_ENDPOINT, } from "../constants";
3
- import { logAntigravityDebugResponse } from "./debug";
4
- import { extractThinkingConfig, extractUsageFromSsePayload, extractUsageMetadata, filterUnsignedThinkingBlocks, isThinkingCapableModel, normalizeThinkingConfig, parseAntigravityApiBody, resolveThinkingConfig, rewriteAntigravityPreviewAccessError, transformThinkingParts, } from "./request-helpers";
3
+ import { cacheSignature, getCachedSignature } from "./cache";
4
+ import { DEBUG_MESSAGE_PREFIX, isDebugEnabled, logAntigravityDebugResponse, } from "./debug";
5
+ import { DEFAULT_THINKING_BUDGET, extractThinkingConfig, extractUsageFromSsePayload, extractUsageMetadata, filterUnsignedThinkingBlocks, filterMessagesThinkingBlocks, isThinkingCapableModel, normalizeThinkingConfig, parseAntigravityApiBody, resolveThinkingConfig, rewriteAntigravityPreviewAccessError, transformThinkingParts, } from "./request-helpers";
6
+ /**
7
+ * Stable session ID for the plugin's lifetime.
8
+ * This is used for caching thinking signatures across multi-turn conversations.
9
+ * Generated once at plugin load time and reused for all requests.
10
+ */
11
+ const PLUGIN_SESSION_ID = `-${crypto.randomUUID()}`;
12
+ // Claude thinking models need a sufficiently large max output token limit when thinking is enabled.
13
+ const CLAUDE_THINKING_MAX_OUTPUT_TOKENS = 64_000;
14
+ const MIN_SIGNATURE_LENGTH = 50;
15
+ const lastSignedThinkingBySessionId = new Map();
16
+ function formatDebugLinesForThinking(lines) {
17
+ const cleaned = lines
18
+ .map((line) => line.trim())
19
+ .filter((line) => line.length > 0)
20
+ .slice(-50);
21
+ return `${DEBUG_MESSAGE_PREFIX}\n${cleaned.map((line) => `- ${line}`).join("\n")}`;
22
+ }
23
+ function injectDebugThinking(response, debugText) {
24
+ if (!response || typeof response !== "object") {
25
+ return response;
26
+ }
27
+ const resp = response;
28
+ if (Array.isArray(resp.candidates) && resp.candidates.length > 0) {
29
+ const candidates = resp.candidates.slice();
30
+ const first = candidates[0];
31
+ if (first &&
32
+ typeof first === "object" &&
33
+ first.content &&
34
+ typeof first.content === "object" &&
35
+ Array.isArray(first.content.parts)) {
36
+ const parts = [{ thought: true, text: debugText }, ...first.content.parts];
37
+ candidates[0] = { ...first, content: { ...first.content, parts } };
38
+ return { ...resp, candidates };
39
+ }
40
+ return resp;
41
+ }
42
+ if (Array.isArray(resp.content)) {
43
+ const content = [{ type: "thinking", thinking: debugText }, ...resp.content];
44
+ return { ...resp, content };
45
+ }
46
+ if (!resp.reasoning_content) {
47
+ return { ...resp, reasoning_content: debugText };
48
+ }
49
+ return resp;
50
+ }
51
+ function stripInjectedDebugFromParts(parts) {
52
+ if (!Array.isArray(parts)) {
53
+ return parts;
54
+ }
55
+ return parts.filter((part) => {
56
+ if (!part || typeof part !== "object") {
57
+ return true;
58
+ }
59
+ const record = part;
60
+ const text = typeof record.text === "string"
61
+ ? record.text
62
+ : typeof record.thinking === "string"
63
+ ? record.thinking
64
+ : undefined;
65
+ if (text && text.startsWith(DEBUG_MESSAGE_PREFIX)) {
66
+ return false;
67
+ }
68
+ return true;
69
+ });
70
+ }
71
+ function stripInjectedDebugFromRequestPayload(payload) {
72
+ const anyPayload = payload;
73
+ if (Array.isArray(anyPayload.contents)) {
74
+ anyPayload.contents = anyPayload.contents.map((content) => {
75
+ if (!content || typeof content !== "object") {
76
+ return content;
77
+ }
78
+ if (Array.isArray(content.parts)) {
79
+ return { ...content, parts: stripInjectedDebugFromParts(content.parts) };
80
+ }
81
+ if (Array.isArray(content.content)) {
82
+ return { ...content, content: stripInjectedDebugFromParts(content.content) };
83
+ }
84
+ return content;
85
+ });
86
+ }
87
+ if (Array.isArray(anyPayload.messages)) {
88
+ anyPayload.messages = anyPayload.messages.map((message) => {
89
+ if (!message || typeof message !== "object") {
90
+ return message;
91
+ }
92
+ if (Array.isArray(message.content)) {
93
+ return { ...message, content: stripInjectedDebugFromParts(message.content) };
94
+ }
95
+ return message;
96
+ });
97
+ }
98
+ }
99
+ function isGeminiToolUsePart(part) {
100
+ return !!(part && typeof part === "object" && (part.functionCall || part.tool_use || part.toolUse));
101
+ }
102
+ function isGeminiThinkingPart(part) {
103
+ return !!(part &&
104
+ typeof part === "object" &&
105
+ (part.thought === true || part.type === "thinking" || part.type === "reasoning"));
106
+ }
107
+ function ensureThoughtSignature(part, sessionId) {
108
+ if (!part || typeof part !== "object") {
109
+ return part;
110
+ }
111
+ const text = typeof part.text === "string" ? part.text : typeof part.thinking === "string" ? part.thinking : "";
112
+ if (!text) {
113
+ return part;
114
+ }
115
+ if (part.thought === true) {
116
+ if (!part.thoughtSignature) {
117
+ const cached = getCachedSignature(sessionId, text);
118
+ if (cached) {
119
+ return { ...part, thoughtSignature: cached };
120
+ }
121
+ }
122
+ return part;
123
+ }
124
+ if ((part.type === "thinking" || part.type === "reasoning") && !part.signature) {
125
+ const cached = getCachedSignature(sessionId, text);
126
+ if (cached) {
127
+ return { ...part, signature: cached };
128
+ }
129
+ }
130
+ return part;
131
+ }
132
+ function hasSignedThinkingPart(part) {
133
+ if (!part || typeof part !== "object") {
134
+ return false;
135
+ }
136
+ if (part.thought === true) {
137
+ return typeof part.thoughtSignature === "string" && part.thoughtSignature.length >= MIN_SIGNATURE_LENGTH;
138
+ }
139
+ if (part.type === "thinking" || part.type === "reasoning") {
140
+ return typeof part.signature === "string" && part.signature.length >= MIN_SIGNATURE_LENGTH;
141
+ }
142
+ return false;
143
+ }
144
+ function ensureThinkingBeforeToolUseInContents(contents, sessionId) {
145
+ return contents.map((content) => {
146
+ if (!content || typeof content !== "object" || !Array.isArray(content.parts)) {
147
+ return content;
148
+ }
149
+ const role = content.role;
150
+ if (role !== "model" && role !== "assistant") {
151
+ return content;
152
+ }
153
+ const parts = content.parts;
154
+ const hasToolUse = parts.some(isGeminiToolUsePart);
155
+ if (!hasToolUse) {
156
+ return content;
157
+ }
158
+ const thinkingParts = parts.filter(isGeminiThinkingPart).map((p) => ensureThoughtSignature(p, sessionId));
159
+ const otherParts = parts.filter((p) => !isGeminiThinkingPart(p));
160
+ const hasSignedThinking = thinkingParts.some(hasSignedThinkingPart);
161
+ if (hasSignedThinking) {
162
+ return { ...content, parts: [...thinkingParts, ...otherParts] };
163
+ }
164
+ const lastThinking = lastSignedThinkingBySessionId.get(sessionId);
165
+ if (!lastThinking) {
166
+ return content;
167
+ }
168
+ const injected = {
169
+ thought: true,
170
+ text: lastThinking.text,
171
+ thoughtSignature: lastThinking.signature,
172
+ };
173
+ return { ...content, parts: [injected, ...otherParts] };
174
+ });
175
+ }
176
+ function ensureMessageThinkingSignature(block, sessionId) {
177
+ if (!block || typeof block !== "object") {
178
+ return block;
179
+ }
180
+ if (block.type !== "thinking" && block.type !== "redacted_thinking") {
181
+ return block;
182
+ }
183
+ if (typeof block.signature === "string" && block.signature.length >= MIN_SIGNATURE_LENGTH) {
184
+ return block;
185
+ }
186
+ const text = typeof block.thinking === "string" ? block.thinking : typeof block.text === "string" ? block.text : "";
187
+ if (!text) {
188
+ return block;
189
+ }
190
+ const cached = getCachedSignature(sessionId, text);
191
+ if (cached) {
192
+ return { ...block, signature: cached };
193
+ }
194
+ return block;
195
+ }
196
+ function hasToolUseInContents(contents) {
197
+ return contents.some((content) => {
198
+ if (!content || typeof content !== "object" || !Array.isArray(content.parts)) {
199
+ return false;
200
+ }
201
+ return content.parts.some(isGeminiToolUsePart);
202
+ });
203
+ }
204
+ function hasSignedThinkingInContents(contents) {
205
+ return contents.some((content) => {
206
+ if (!content || typeof content !== "object" || !Array.isArray(content.parts)) {
207
+ return false;
208
+ }
209
+ return content.parts.some(hasSignedThinkingPart);
210
+ });
211
+ }
212
+ function hasToolUseInMessages(messages) {
213
+ return messages.some((message) => {
214
+ if (!message || typeof message !== "object" || !Array.isArray(message.content)) {
215
+ return false;
216
+ }
217
+ return message.content.some((block) => block && typeof block === "object" && (block.type === "tool_use" || block.type === "tool_result"));
218
+ });
219
+ }
220
+ function hasSignedThinkingInMessages(messages) {
221
+ return messages.some((message) => {
222
+ if (!message || typeof message !== "object" || !Array.isArray(message.content)) {
223
+ return false;
224
+ }
225
+ return message.content.some((block) => block &&
226
+ typeof block === "object" &&
227
+ (block.type === "thinking" || block.type === "redacted_thinking") &&
228
+ typeof block.signature === "string" &&
229
+ block.signature.length >= MIN_SIGNATURE_LENGTH);
230
+ });
231
+ }
232
+ function ensureThinkingBeforeToolUseInMessages(messages, sessionId) {
233
+ return messages.map((message) => {
234
+ if (!message || typeof message !== "object" || !Array.isArray(message.content)) {
235
+ return message;
236
+ }
237
+ if (message.role !== "assistant") {
238
+ return message;
239
+ }
240
+ const blocks = message.content;
241
+ const hasToolUse = blocks.some((b) => b && typeof b === "object" && (b.type === "tool_use" || b.type === "tool_result"));
242
+ if (!hasToolUse) {
243
+ return message;
244
+ }
245
+ const thinkingBlocks = blocks
246
+ .filter((b) => b && typeof b === "object" && (b.type === "thinking" || b.type === "redacted_thinking"))
247
+ .map((b) => ensureMessageThinkingSignature(b, sessionId));
248
+ const otherBlocks = blocks.filter((b) => !(b && typeof b === "object" && (b.type === "thinking" || b.type === "redacted_thinking")));
249
+ const hasSignedThinking = thinkingBlocks.some((b) => typeof b.signature === "string" && b.signature.length >= MIN_SIGNATURE_LENGTH);
250
+ if (hasSignedThinking) {
251
+ return { ...message, content: [...thinkingBlocks, ...otherBlocks] };
252
+ }
253
+ const lastThinking = lastSignedThinkingBySessionId.get(sessionId);
254
+ if (!lastThinking) {
255
+ return message;
256
+ }
257
+ const injected = {
258
+ type: "thinking",
259
+ thinking: lastThinking.text,
260
+ signature: lastThinking.signature,
261
+ };
262
+ return { ...message, content: [injected, ...otherBlocks] };
263
+ });
264
+ }
265
+ /**
266
+ * Gets the stable session ID for this plugin instance.
267
+ */
268
+ export function getPluginSessionId() {
269
+ return PLUGIN_SESSION_ID;
270
+ }
5
271
  function generateSyntheticProjectId() {
6
272
  const adjectives = ["useful", "bright", "swift", "calm", "bold"];
7
273
  const nouns = ["fuze", "wave", "spark", "flow", "core"];
@@ -46,28 +312,36 @@ function transformStreamingPayload(payload) {
46
312
  }
47
313
  /**
48
314
  * Creates a TransformStream that processes SSE chunks incrementally,
49
- * transforming each line as it arrives for true streaming support.
315
+ * transforming each line as it arrives for true real-time streaming support.
316
+ * Optionally caches thinking signatures for Claude multi-turn conversations.
50
317
  */
51
- function createStreamingTransformer() {
318
+ function createStreamingTransformer(sessionId, debugText) {
52
319
  const decoder = new TextDecoder();
53
320
  const encoder = new TextEncoder();
54
321
  let buffer = "";
322
+ // Buffer for accumulating thinking text per candidate index (for signature caching)
323
+ const thoughtBuffer = new Map();
324
+ const debugState = { injected: false };
55
325
  return new TransformStream({
56
326
  transform(chunk, controller) {
327
+ // Decode chunk with stream: true to handle multi-byte characters correctly
57
328
  buffer += decoder.decode(chunk, { stream: true });
58
- // Process complete lines
329
+ // Process complete lines immediately for real-time streaming
59
330
  const lines = buffer.split("\n");
60
331
  // Keep the last incomplete line in buffer
61
332
  buffer = lines.pop() || "";
62
333
  for (const line of lines) {
63
- const transformedLine = transformSseLine(line);
334
+ // Transform and forward each line immediately
335
+ const transformedLine = transformSseLine(line, sessionId, thoughtBuffer, debugText, debugState);
64
336
  controller.enqueue(encoder.encode(transformedLine + "\n"));
65
337
  }
66
338
  },
67
339
  flush(controller) {
340
+ // Flush any remaining bytes from TextDecoder
341
+ buffer += decoder.decode();
68
342
  // Process any remaining data in buffer
69
343
  if (buffer) {
70
- const transformedLine = transformSseLine(buffer);
344
+ const transformedLine = transformSseLine(buffer, sessionId, thoughtBuffer, debugText, debugState);
71
345
  controller.enqueue(encoder.encode(transformedLine));
72
346
  }
73
347
  },
@@ -75,8 +349,9 @@ function createStreamingTransformer() {
75
349
  }
76
350
  /**
77
351
  * Transforms a single SSE line, extracting and transforming the inner response.
352
+ * Optionally caches thinking signatures for Claude multi-turn support.
78
353
  */
79
- function transformSseLine(line) {
354
+ function transformSseLine(line, sessionId, thoughtBuffer, debugText, debugState) {
80
355
  if (!line.startsWith("data:")) {
81
356
  return line;
82
357
  }
@@ -87,13 +362,67 @@ function transformSseLine(line) {
87
362
  try {
88
363
  const parsed = JSON.parse(json);
89
364
  if (parsed.response !== undefined) {
90
- const transformed = transformThinkingParts(parsed.response);
365
+ if (sessionId && thoughtBuffer) {
366
+ cacheThinkingSignatures(parsed.response, sessionId, thoughtBuffer);
367
+ }
368
+ let response = parsed.response;
369
+ if (debugText && debugState && !debugState.injected) {
370
+ response = injectDebugThinking(response, debugText);
371
+ debugState.injected = true;
372
+ }
373
+ const transformed = transformThinkingParts(response);
91
374
  return `data: ${JSON.stringify(transformed)}`;
92
375
  }
93
376
  }
94
377
  catch (_) { }
95
378
  return line;
96
379
  }
380
+ /**
381
+ * Extracts and caches thinking signatures from a response for Claude multi-turn support.
382
+ */
383
+ function cacheThinkingSignatures(response, sessionId, thoughtBuffer) {
384
+ if (!response || typeof response !== "object")
385
+ return;
386
+ const resp = response;
387
+ // Handle Gemini-style candidates array (Claude through Antigravity uses this format)
388
+ if (Array.isArray(resp.candidates)) {
389
+ resp.candidates.forEach((candidate, index) => {
390
+ if (!candidate?.content?.parts)
391
+ return;
392
+ candidate.content.parts.forEach((part) => {
393
+ // Collect thinking text
394
+ if (part.thought === true || part.type === "thinking") {
395
+ const text = part.text || part.thinking || "";
396
+ if (text) {
397
+ const current = thoughtBuffer.get(index) ?? "";
398
+ thoughtBuffer.set(index, current + text);
399
+ }
400
+ }
401
+ // Cache signature when we receive it
402
+ if (part.thoughtSignature) {
403
+ const fullText = thoughtBuffer.get(index) ?? "";
404
+ if (fullText && sessionId) {
405
+ cacheSignature(sessionId, fullText, part.thoughtSignature);
406
+ lastSignedThinkingBySessionId.set(sessionId, { text: fullText, signature: part.thoughtSignature });
407
+ }
408
+ }
409
+ });
410
+ });
411
+ }
412
+ // Handle Anthropic-style content array
413
+ if (Array.isArray(resp.content)) {
414
+ let thinkingText = "";
415
+ resp.content.forEach((block) => {
416
+ if (block?.type === "thinking") {
417
+ thinkingText += block.thinking || block.text || "";
418
+ }
419
+ if (block?.signature && thinkingText && sessionId) {
420
+ cacheSignature(sessionId, thinkingText, block.signature);
421
+ lastSignedThinkingBySessionId.set(sessionId, { text: thinkingText, signature: block.signature });
422
+ }
423
+ });
424
+ }
425
+ }
97
426
  /**
98
427
  * Rewrites OpenAI-style requests into Antigravity shape, normalizing model, headers,
99
428
  * optional cached_content, and thinking config. Also toggles streaming mode for SSE actions.
@@ -105,6 +434,8 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
105
434
  let toolDebugMissing = 0;
106
435
  const toolDebugSummaries = [];
107
436
  let toolDebugPayload;
437
+ let sessionId;
438
+ let needsSignedThinkingWarmup = false;
108
439
  if (!isGenerativeLanguageRequest(input)) {
109
440
  return {
110
441
  request: input,
@@ -123,12 +454,17 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
123
454
  };
124
455
  }
125
456
  const [, rawModel = "", rawAction = ""] = match;
126
- const effectiveModel = rawModel;
127
- const upstreamModel = rawModel;
457
+ const requestedModel = rawModel;
458
+ let upstreamModel = rawModel;
459
+ if (upstreamModel === "gemini-2.5-flash-image") {
460
+ upstreamModel = "gemini-2.5-flash";
461
+ }
462
+ const effectiveModel = upstreamModel;
128
463
  const streaming = rawAction === STREAM_ACTION;
129
464
  const baseEndpoint = endpointOverride ?? ANTIGRAVITY_ENDPOINT;
130
465
  const transformedUrl = `${baseEndpoint}/v1internal:${rawAction}${streaming ? "?alt=sse" : ""}`;
131
466
  const isClaudeModel = upstreamModel.toLowerCase().includes("claude");
467
+ const isClaudeThinkingModel = isClaudeModel && upstreamModel.toLowerCase().includes("thinking");
132
468
  let body = baseInit.body;
133
469
  if (typeof baseInit.body === "string" && baseInit.body) {
134
470
  try {
@@ -139,12 +475,68 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
139
475
  ...parsedBody,
140
476
  model: effectiveModel,
141
477
  };
478
+ // Some callers may already send an Antigravity-wrapped body.
479
+ // We still need to sanitize Claude thinking blocks (remove cache_control)
480
+ // and attach a stable sessionId so multi-turn signature caching works.
481
+ const requestRoot = wrappedBody.request;
482
+ const requestObjects = [];
483
+ if (requestRoot && typeof requestRoot === "object") {
484
+ requestObjects.push(requestRoot);
485
+ const nested = requestRoot.request;
486
+ if (nested && typeof nested === "object") {
487
+ requestObjects.push(nested);
488
+ }
489
+ }
490
+ if (requestObjects.length > 0) {
491
+ sessionId = PLUGIN_SESSION_ID;
492
+ }
493
+ for (const req of requestObjects) {
494
+ // Use stable session ID for signature caching across multi-turn conversations
495
+ req.sessionId = PLUGIN_SESSION_ID;
496
+ stripInjectedDebugFromRequestPayload(req);
497
+ if (isClaudeModel) {
498
+ if (isClaudeThinkingModel && Array.isArray(req.contents)) {
499
+ req.contents = ensureThinkingBeforeToolUseInContents(req.contents, PLUGIN_SESSION_ID);
500
+ }
501
+ if (isClaudeThinkingModel && Array.isArray(req.messages)) {
502
+ req.messages = ensureThinkingBeforeToolUseInMessages(req.messages, PLUGIN_SESSION_ID);
503
+ }
504
+ if (Array.isArray(req.contents)) {
505
+ req.contents = filterUnsignedThinkingBlocks(req.contents, PLUGIN_SESSION_ID, getCachedSignature);
506
+ }
507
+ if (Array.isArray(req.messages)) {
508
+ req.messages = filterMessagesThinkingBlocks(req.messages, PLUGIN_SESSION_ID, getCachedSignature);
509
+ }
510
+ }
511
+ }
512
+ if (isClaudeThinkingModel && sessionId) {
513
+ const hasToolUse = requestObjects.some((req) => (Array.isArray(req.contents) && hasToolUseInContents(req.contents)) ||
514
+ (Array.isArray(req.messages) && hasToolUseInMessages(req.messages)));
515
+ const hasSignedThinking = requestObjects.some((req) => (Array.isArray(req.contents) && hasSignedThinkingInContents(req.contents)) ||
516
+ (Array.isArray(req.messages) && hasSignedThinkingInMessages(req.messages)));
517
+ const hasCachedThinking = lastSignedThinkingBySessionId.has(sessionId);
518
+ needsSignedThinkingWarmup = hasToolUse && !hasSignedThinking && !hasCachedThinking;
519
+ }
142
520
  body = JSON.stringify(wrappedBody);
143
521
  }
144
522
  else {
145
523
  const requestPayload = { ...parsedBody };
146
524
  const rawGenerationConfig = requestPayload.generationConfig;
147
525
  const extraBody = requestPayload.extra_body;
526
+ if (isClaudeModel) {
527
+ if (!requestPayload.toolConfig) {
528
+ requestPayload.toolConfig = {};
529
+ }
530
+ if (typeof requestPayload.toolConfig === "object" && requestPayload.toolConfig !== null) {
531
+ const toolConfig = requestPayload.toolConfig;
532
+ if (!toolConfig.functionCallingConfig) {
533
+ toolConfig.functionCallingConfig = {};
534
+ }
535
+ if (typeof toolConfig.functionCallingConfig === "object" && toolConfig.functionCallingConfig !== null) {
536
+ toolConfig.functionCallingConfig.mode = "VALIDATED";
537
+ }
538
+ }
539
+ }
148
540
  // Resolve thinking configuration based on user settings and model capabilities
149
541
  const userThinkingConfig = extractThinkingConfig(requestPayload, rawGenerationConfig, extraBody);
150
542
  const hasAssistantHistory = Array.isArray(requestPayload.contents) &&
@@ -152,12 +544,37 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
152
544
  const finalThinkingConfig = resolveThinkingConfig(userThinkingConfig, isThinkingCapableModel(upstreamModel), isClaudeModel, hasAssistantHistory);
153
545
  const normalizedThinking = normalizeThinkingConfig(finalThinkingConfig);
154
546
  if (normalizedThinking) {
547
+ const thinkingBudget = normalizedThinking.thinkingBudget;
548
+ const thinkingConfig = isClaudeThinkingModel
549
+ ? {
550
+ include_thoughts: normalizedThinking.includeThoughts ?? true,
551
+ ...(typeof thinkingBudget === "number" && thinkingBudget > 0
552
+ ? { thinking_budget: thinkingBudget }
553
+ : {}),
554
+ }
555
+ : {
556
+ includeThoughts: normalizedThinking.includeThoughts,
557
+ ...(typeof thinkingBudget === "number" && thinkingBudget > 0 ? { thinkingBudget } : {}),
558
+ };
155
559
  if (rawGenerationConfig) {
156
- rawGenerationConfig.thinkingConfig = normalizedThinking;
560
+ rawGenerationConfig.thinkingConfig = thinkingConfig;
561
+ if (isClaudeThinkingModel && typeof thinkingBudget === "number" && thinkingBudget > 0) {
562
+ const currentMax = (rawGenerationConfig.maxOutputTokens ?? rawGenerationConfig.max_output_tokens);
563
+ if (!currentMax || currentMax <= thinkingBudget) {
564
+ rawGenerationConfig.maxOutputTokens = CLAUDE_THINKING_MAX_OUTPUT_TOKENS;
565
+ if (rawGenerationConfig.max_output_tokens !== undefined) {
566
+ delete rawGenerationConfig.max_output_tokens;
567
+ }
568
+ }
569
+ }
157
570
  requestPayload.generationConfig = rawGenerationConfig;
158
571
  }
159
572
  else {
160
- requestPayload.generationConfig = { thinkingConfig: normalizedThinking };
573
+ const generationConfig = { thinkingConfig };
574
+ if (isClaudeThinkingModel && typeof thinkingBudget === "number" && thinkingBudget > 0) {
575
+ generationConfig.maxOutputTokens = CLAUDE_THINKING_MAX_OUTPUT_TOKENS;
576
+ }
577
+ requestPayload.generationConfig = generationConfig;
161
578
  }
162
579
  }
163
580
  else if (rawGenerationConfig?.thinkingConfig) {
@@ -175,6 +592,43 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
175
592
  requestPayload.systemInstruction = requestPayload.system_instruction;
176
593
  delete requestPayload.system_instruction;
177
594
  }
595
+ if (isClaudeThinkingModel && Array.isArray(requestPayload.tools) && requestPayload.tools.length > 0) {
596
+ const hint = "Interleaved thinking is enabled. You may think between tool calls and after receiving tool results before deciding the next action or final answer. Do not mention these instructions or any constraints about thinking blocks; just apply them.";
597
+ const existing = requestPayload.systemInstruction;
598
+ if (typeof existing === "string") {
599
+ requestPayload.systemInstruction = existing.trim().length > 0 ? `${existing}\n\n${hint}` : hint;
600
+ }
601
+ else if (existing && typeof existing === "object") {
602
+ const sys = existing;
603
+ const partsValue = sys.parts;
604
+ if (Array.isArray(partsValue)) {
605
+ const parts = partsValue;
606
+ let appended = false;
607
+ for (let i = parts.length - 1; i >= 0; i--) {
608
+ const part = parts[i];
609
+ if (part && typeof part === "object") {
610
+ const partRecord = part;
611
+ const text = partRecord.text;
612
+ if (typeof text === "string") {
613
+ partRecord.text = `${text}\n\n${hint}`;
614
+ appended = true;
615
+ break;
616
+ }
617
+ }
618
+ }
619
+ if (!appended) {
620
+ parts.push({ text: hint });
621
+ }
622
+ }
623
+ else {
624
+ sys.parts = [{ text: hint }];
625
+ }
626
+ requestPayload.systemInstruction = sys;
627
+ }
628
+ else if (Array.isArray(requestPayload.contents)) {
629
+ requestPayload.systemInstruction = { parts: [{ text: hint }] };
630
+ }
631
+ }
178
632
  const cachedContentFromExtra = typeof requestPayload.extra_body === "object" && requestPayload.extra_body
179
633
  ? requestPayload.extra_body.cached_content ??
180
634
  requestPayload.extra_body.cachedContent
@@ -199,26 +653,44 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
199
653
  if (isClaudeModel) {
200
654
  const functionDeclarations = [];
201
655
  const passthroughTools = [];
202
- // Sanitize schema - remove features not supported by JSON Schema draft 2020-12
203
- // Recursively strips anyOf/allOf/oneOf and converts to permissive types
656
+ // Sanitize schema using ALLOWLIST approach - only keep basic features needed for function calling
657
+ // This is more aggressive than blocklisting, ensuring any unknown/unsupported features are stripped
658
+ // See docs/ANTIGRAVITY_API_SPEC.md for full list of unsupported features
204
659
  const sanitizeSchema = (schema) => {
205
660
  if (!schema || typeof schema !== "object") {
206
661
  return schema;
207
662
  }
663
+ // Only keep these basic schema features (allowlist approach)
664
+ // Everything else gets stripped automatically
665
+ const ALLOWED_KEYS = new Set([
666
+ "type",
667
+ "properties",
668
+ "required",
669
+ "description",
670
+ "enum",
671
+ "items",
672
+ "additionalProperties",
673
+ ]);
208
674
  const sanitized = {};
209
675
  for (const key of Object.keys(schema)) {
210
- // Skip anyOf/allOf/oneOf - not well supported
211
- if (key === "anyOf" || key === "allOf" || key === "oneOf") {
676
+ // Convert "const" to "enum: [value]" (const is not supported but enum is)
677
+ if (key === "const") {
678
+ sanitized.enum = [schema[key]];
679
+ continue;
680
+ }
681
+ // Skip keys not in allowlist
682
+ if (!ALLOWED_KEYS.has(key)) {
212
683
  continue;
213
684
  }
214
685
  const value = schema[key];
215
686
  if (key === "items" && value && typeof value === "object") {
216
- // Handle array items - if it has anyOf, replace with permissive type
217
- if (value.anyOf || value.allOf || value.oneOf) {
218
- sanitized.items = {};
687
+ const sanitizedItems = sanitizeSchema(value);
688
+ // Empty items schema {} is invalid - convert to permissive string type
689
+ if (Object.keys(sanitizedItems).length === 0) {
690
+ sanitized.items = { type: "string" };
219
691
  }
220
692
  else {
221
- sanitized.items = sanitizeSchema(value);
693
+ sanitized.items = sanitizedItems;
222
694
  }
223
695
  }
224
696
  else if (key === "properties" && value && typeof value === "object") {
@@ -238,13 +710,32 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
238
710
  return sanitized;
239
711
  };
240
712
  const normalizeSchema = (schema) => {
713
+ // Helper to create a placeholder schema for empty parameter tools
714
+ // Antigravity API in VALIDATED mode cannot handle truly empty schemas
715
+ // The placeholder must be REQUIRED so the model sends a non-empty args object
716
+ const createPlaceholderSchema = (base = {}) => ({
717
+ ...base,
718
+ type: "object",
719
+ properties: {
720
+ reason: {
721
+ type: "string",
722
+ description: "Brief explanation of why you are calling this tool",
723
+ },
724
+ },
725
+ required: ["reason"],
726
+ });
241
727
  if (!schema || typeof schema !== "object") {
242
728
  toolDebugMissing += 1;
243
- // Minimal fallback for tools without schemas
244
- return { type: "object" };
729
+ // Fallback for tools without schemas - add dummy property for Antigravity API
730
+ return createPlaceholderSchema();
731
+ }
732
+ const sanitized = sanitizeSchema(schema);
733
+ // Check if schema is effectively empty (type: object with no properties)
734
+ if (sanitized.type === "object" &&
735
+ (!sanitized.properties || Object.keys(sanitized.properties).length === 0)) {
736
+ return createPlaceholderSchema(sanitized);
245
737
  }
246
- // Sanitize and pass through
247
- return sanitizeSchema(schema);
738
+ return sanitized;
248
739
  };
249
740
  requestPayload.tools.forEach((tool, idx) => {
250
741
  const pushDeclaration = (decl, source) => {
@@ -359,8 +850,30 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
359
850
  }
360
851
  }
361
852
  // For Claude models, filter out unsigned thinking blocks (required by Claude API)
362
- if (isClaudeModel && Array.isArray(requestPayload.contents)) {
363
- requestPayload.contents = filterUnsignedThinkingBlocks(requestPayload.contents);
853
+ // Attempts to restore signatures from cache for multi-turn conversations
854
+ // Handle both Gemini-style contents[] and Anthropic-style messages[] payloads.
855
+ if (isClaudeModel) {
856
+ if (isClaudeThinkingModel && Array.isArray(requestPayload.contents)) {
857
+ requestPayload.contents = ensureThinkingBeforeToolUseInContents(requestPayload.contents, PLUGIN_SESSION_ID);
858
+ }
859
+ if (isClaudeThinkingModel && Array.isArray(requestPayload.messages)) {
860
+ requestPayload.messages = ensureThinkingBeforeToolUseInMessages(requestPayload.messages, PLUGIN_SESSION_ID);
861
+ }
862
+ if (isClaudeThinkingModel) {
863
+ const sessionKey = PLUGIN_SESSION_ID;
864
+ const hasToolUse = (Array.isArray(requestPayload.contents) && hasToolUseInContents(requestPayload.contents)) ||
865
+ (Array.isArray(requestPayload.messages) && hasToolUseInMessages(requestPayload.messages));
866
+ const hasSignedThinking = (Array.isArray(requestPayload.contents) && hasSignedThinkingInContents(requestPayload.contents)) ||
867
+ (Array.isArray(requestPayload.messages) && hasSignedThinkingInMessages(requestPayload.messages));
868
+ const hasCachedThinking = lastSignedThinkingBySessionId.has(sessionKey);
869
+ needsSignedThinkingWarmup = hasToolUse && !hasSignedThinking && !hasCachedThinking;
870
+ }
871
+ if (Array.isArray(requestPayload.contents)) {
872
+ requestPayload.contents = filterUnsignedThinkingBlocks(requestPayload.contents, PLUGIN_SESSION_ID, getCachedSignature);
873
+ }
874
+ if (Array.isArray(requestPayload.messages)) {
875
+ requestPayload.messages = filterMessagesThinkingBlocks(requestPayload.messages, PLUGIN_SESSION_ID, getCachedSignature);
876
+ }
364
877
  }
365
878
  // For Claude models, ensure functionCall/tool use parts carry IDs (required by Anthropic).
366
879
  // We use a two-pass approach: first collect all functionCalls and assign IDs,
@@ -417,6 +930,7 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
417
930
  if ("model" in requestPayload) {
418
931
  delete requestPayload.model;
419
932
  }
933
+ stripInjectedDebugFromRequestPayload(requestPayload);
420
934
  const effectiveProjectId = projectId?.trim() || generateSyntheticProjectId();
421
935
  resolvedProjectId = effectiveProjectId;
422
936
  const wrappedBody = {
@@ -430,7 +944,9 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
430
944
  requestId: "agent-" + crypto.randomUUID(),
431
945
  });
432
946
  if (wrappedBody.request && typeof wrappedBody.request === 'object') {
433
- wrappedBody.request.sessionId = "-" + Math.floor(Math.random() * 9000000000000000000).toString();
947
+ // Use stable session ID for signature caching across multi-turn conversations
948
+ sessionId = PLUGIN_SESSION_ID;
949
+ wrappedBody.request.sessionId = sessionId;
434
950
  }
435
951
  body = JSON.stringify(wrappedBody);
436
952
  }
@@ -442,6 +958,20 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
442
958
  if (streaming) {
443
959
  headers.set("Accept", "text/event-stream");
444
960
  }
961
+ // Add interleaved thinking header for Claude thinking models
962
+ // This enables real-time streaming of thinking tokens
963
+ if (isClaudeThinkingModel) {
964
+ const existing = headers.get("anthropic-beta");
965
+ const interleavedHeader = "interleaved-thinking-2025-05-14";
966
+ if (existing) {
967
+ if (!existing.includes(interleavedHeader)) {
968
+ headers.set("anthropic-beta", `${existing},${interleavedHeader}`);
969
+ }
970
+ }
971
+ else {
972
+ headers.set("anthropic-beta", interleavedHeader);
973
+ }
974
+ }
445
975
  headers.set("User-Agent", ANTIGRAVITY_HEADERS["User-Agent"]);
446
976
  headers.set("X-Goog-Api-Client", ANTIGRAVITY_HEADERS["X-Goog-Api-Client"]);
447
977
  headers.set("Client-Metadata", ANTIGRAVITY_HEADERS["Client-Metadata"]);
@@ -457,25 +987,67 @@ export function prepareAntigravityRequest(input, init, accessToken, projectId, e
457
987
  body,
458
988
  },
459
989
  streaming,
460
- requestedModel: rawModel,
990
+ requestedModel,
461
991
  effectiveModel: upstreamModel,
462
992
  projectId: resolvedProjectId,
463
993
  endpoint: transformedUrl,
994
+ sessionId,
464
995
  toolDebugMissing,
465
996
  toolDebugSummary: toolDebugSummaries.slice(0, 20).join(" | "),
466
997
  toolDebugPayload,
998
+ needsSignedThinkingWarmup,
467
999
  };
468
1000
  }
1001
+ export function buildThinkingWarmupBody(bodyText, isClaudeThinkingModel) {
1002
+ if (!bodyText || !isClaudeThinkingModel) {
1003
+ return null;
1004
+ }
1005
+ let parsed;
1006
+ try {
1007
+ parsed = JSON.parse(bodyText);
1008
+ }
1009
+ catch {
1010
+ return null;
1011
+ }
1012
+ const warmupPrompt = "Warmup request for thinking signature.";
1013
+ const updateRequest = (req) => {
1014
+ req.contents = [{ role: "user", parts: [{ text: warmupPrompt }] }];
1015
+ delete req.tools;
1016
+ delete req.toolConfig;
1017
+ const generationConfig = (req.generationConfig ?? {});
1018
+ generationConfig.thinkingConfig = {
1019
+ include_thoughts: true,
1020
+ thinking_budget: DEFAULT_THINKING_BUDGET,
1021
+ };
1022
+ generationConfig.maxOutputTokens = CLAUDE_THINKING_MAX_OUTPUT_TOKENS;
1023
+ req.generationConfig = generationConfig;
1024
+ };
1025
+ if (parsed.request && typeof parsed.request === "object") {
1026
+ updateRequest(parsed.request);
1027
+ const nested = parsed.request.request;
1028
+ if (nested && typeof nested === "object") {
1029
+ updateRequest(nested);
1030
+ }
1031
+ }
1032
+ else {
1033
+ updateRequest(parsed);
1034
+ }
1035
+ return JSON.stringify(parsed);
1036
+ }
469
1037
  /**
470
1038
  * Normalizes Antigravity responses: applies retry headers, extracts cache usage into headers,
471
1039
  * rewrites preview errors, flattens streaming payloads, and logs debug metadata.
472
1040
  *
473
- * For streaming SSE responses, uses TransformStream for true incremental streaming.
1041
+ * For streaming SSE responses, uses TransformStream for true real-time incremental streaming.
1042
+ * Thinking/reasoning tokens are transformed and forwarded immediately as they arrive.
474
1043
  */
475
- export async function transformAntigravityResponse(response, streaming, debugContext, requestedModel, projectId, endpoint, effectiveModel, toolDebugMissing, toolDebugSummary, toolDebugPayload) {
1044
+ export async function transformAntigravityResponse(response, streaming, debugContext, requestedModel, projectId, endpoint, effectiveModel, sessionId, toolDebugMissing, toolDebugSummary, toolDebugPayload, debugLines) {
476
1045
  const contentType = response.headers.get("content-type") ?? "";
477
1046
  const isJsonResponse = contentType.includes("application/json");
478
1047
  const isEventStreamResponse = contentType.includes("text/event-stream");
1048
+ const debugText = isDebugEnabled() && Array.isArray(debugLines) && debugLines.length > 0
1049
+ ? formatDebugLinesForThinking(debugLines)
1050
+ : undefined;
479
1051
  if (!isJsonResponse && !isEventStreamResponse) {
480
1052
  logAntigravityDebugResponse(debugContext, response, {
481
1053
  note: "Non-JSON response (body omitted)",
@@ -483,43 +1055,16 @@ export async function transformAntigravityResponse(response, streaming, debugCon
483
1055
  return response;
484
1056
  }
485
1057
  // For successful streaming responses, use TransformStream to transform SSE events
486
- // while maintaining real-time streaming (no buffering of entire response)
1058
+ // while maintaining real-time streaming (no buffering of entire response).
1059
+ // This enables thinking tokens to be displayed as they arrive, like the Codex plugin.
487
1060
  if (streaming && response.ok && isEventStreamResponse && response.body) {
488
1061
  const headers = new Headers(response.headers);
489
- // Buffer for partial SSE events that span chunks
490
- let buffer = "";
491
- const decoder = new TextDecoder();
492
- const encoder = new TextEncoder();
493
- const transformStream = new TransformStream({
494
- transform(chunk, controller) {
495
- // Decode chunk with stream: true to handle multi-byte characters
496
- buffer += decoder.decode(chunk, { stream: true });
497
- // Split on double newline (SSE event delimiter)
498
- const events = buffer.split("\n\n");
499
- // Keep last part in buffer (may be incomplete)
500
- buffer = events.pop() || "";
501
- // Process and forward complete events immediately
502
- for (const event of events) {
503
- if (event.trim()) {
504
- const transformed = transformStreamingPayload(event);
505
- controller.enqueue(encoder.encode(transformed + "\n\n"));
506
- }
507
- }
508
- },
509
- flush(controller) {
510
- // Flush any remaining bytes from TextDecoder
511
- buffer += decoder.decode();
512
- // Handle any remaining data at stream end
513
- if (buffer.trim()) {
514
- const transformed = transformStreamingPayload(buffer);
515
- controller.enqueue(encoder.encode(transformed));
516
- }
517
- }
518
- });
519
1062
  logAntigravityDebugResponse(debugContext, response, {
520
- note: "Streaming SSE response (transformed)",
1063
+ note: "Streaming SSE response (real-time transform)",
521
1064
  });
522
- return new Response(response.body.pipeThrough(transformStream), {
1065
+ // Use the optimized line-by-line transformer for immediate forwarding
1066
+ // This ensures thinking/reasoning content streams in real-time
1067
+ return new Response(response.body.pipeThrough(createStreamingTransformer(sessionId, debugText)), {
523
1068
  status: response.status,
524
1069
  statusText: response.statusText,
525
1070
  headers,
@@ -538,8 +1083,9 @@ export async function transformAntigravityResponse(response, streaming, debugCon
538
1083
  }
539
1084
  // Inject Debug Info
540
1085
  if (errorBody?.error) {
541
- const debugInfo = `\n\n[Debug Info]\nRequested Model: ${requestedModel || "Unknown"}\nEffective Model: ${effectiveModel || "Unknown"}\nProject: ${projectId || "Unknown"}\nEndpoint: ${endpoint || "Unknown"}\nStatus: ${response.status}\nRequest ID: ${headers.get('x-request-id') || "N/A"}${toolDebugMissing !== undefined ? `\nTool Debug Missing: ${toolDebugMissing}` : ""}${toolDebugSummary ? `\nTool Debug Summary: ${toolDebugSummary}` : ""}${toolDebugPayload ? `\nTool Debug Payload: ${toolDebugPayload}` : ""}`;
542
- errorBody.error.message = (errorBody.error.message || "Unknown error") + debugInfo;
1086
+ const debugInfo = `\n\n[Debug Info]\nRequested Model: ${requestedModel || "Unknown"}\nEffective Model: ${effectiveModel || "Unknown"}\nProject: ${projectId || "Unknown"}\nEndpoint: ${endpoint || "Unknown"}\nStatus: ${response.status}\nRequest ID: ${headers.get("x-request-id") || "N/A"}${toolDebugMissing !== undefined ? `\nTool Debug Missing: ${toolDebugMissing}` : ""}${toolDebugSummary ? `\nTool Debug Summary: ${toolDebugSummary}` : ""}${toolDebugPayload ? `\nTool Debug Payload: ${toolDebugPayload}` : ""}`;
1087
+ const injectedDebug = debugText ? `\n\n${debugText}` : "";
1088
+ errorBody.error.message = (errorBody.error.message || "Unknown error") + debugInfo + injectedDebug;
543
1089
  return new Response(JSON.stringify(errorBody), {
544
1090
  status: response.status,
545
1091
  statusText: response.statusText,
@@ -595,7 +1141,8 @@ export async function transformAntigravityResponse(response, streaming, debugCon
595
1141
  return new Response(text, init);
596
1142
  }
597
1143
  if (effectiveBody?.response !== undefined) {
598
- const transformed = transformThinkingParts(effectiveBody.response);
1144
+ const responseBody = debugText ? injectDebugThinking(effectiveBody.response, debugText) : effectiveBody.response;
1145
+ const transformed = transformThinkingParts(responseBody);
599
1146
  return new Response(JSON.stringify(transformed), init);
600
1147
  }
601
1148
  if (patched) {