opencode-antigravity-auth 1.2.2 → 1.2.4

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.
@@ -1,4 +1,495 @@
1
+ import { KEEP_THINKING_BLOCKS } from "../constants.js";
1
2
  const ANTIGRAVITY_PREVIEW_LINK = "https://goo.gle/enable-preview-features"; // TODO: Update to Antigravity link if available
3
+ // ============================================================================
4
+ // JSON SCHEMA CLEANING FOR ANTIGRAVITY API
5
+ // Ported from CLIProxyAPI's CleanJSONSchemaForAntigravity (gemini_schema.go)
6
+ // ============================================================================
7
+ /**
8
+ * Unsupported constraint keywords that should be moved to description hints.
9
+ * Claude/Gemini reject these in VALIDATED mode.
10
+ */
11
+ const UNSUPPORTED_CONSTRAINTS = [
12
+ "minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum",
13
+ "pattern", "minItems", "maxItems", "format",
14
+ "default", "examples",
15
+ ];
16
+ /**
17
+ * Keywords that should be removed after hint extraction.
18
+ */
19
+ const UNSUPPORTED_KEYWORDS = [
20
+ ...UNSUPPORTED_CONSTRAINTS,
21
+ "$schema", "$defs", "definitions", "const", "$ref", "additionalProperties",
22
+ "propertyNames", "title", "$id", "$comment",
23
+ ];
24
+ /**
25
+ * Appends a hint to a schema's description field.
26
+ */
27
+ function appendDescriptionHint(schema, hint) {
28
+ if (!schema || typeof schema !== "object") {
29
+ return schema;
30
+ }
31
+ const existing = typeof schema.description === "string" ? schema.description : "";
32
+ const newDescription = existing ? `${existing} (${hint})` : hint;
33
+ return { ...schema, description: newDescription };
34
+ }
35
+ /**
36
+ * Phase 1a: Converts $ref to description hints.
37
+ * $ref: "#/$defs/Foo" → { type: "object", description: "See: Foo" }
38
+ */
39
+ function convertRefsToHints(schema) {
40
+ if (!schema || typeof schema !== "object") {
41
+ return schema;
42
+ }
43
+ if (Array.isArray(schema)) {
44
+ return schema.map(item => convertRefsToHints(item));
45
+ }
46
+ // If this object has $ref, replace it with a hint
47
+ if (typeof schema.$ref === "string") {
48
+ const refVal = schema.$ref;
49
+ const defName = refVal.includes("/") ? refVal.split("/").pop() : refVal;
50
+ const hint = `See: ${defName}`;
51
+ const existingDesc = typeof schema.description === "string" ? schema.description : "";
52
+ const newDescription = existingDesc ? `${existingDesc} (${hint})` : hint;
53
+ return { type: "object", description: newDescription };
54
+ }
55
+ // Recursively process all properties
56
+ const result = {};
57
+ for (const [key, value] of Object.entries(schema)) {
58
+ result[key] = convertRefsToHints(value);
59
+ }
60
+ return result;
61
+ }
62
+ /**
63
+ * Phase 1b: Converts const to enum.
64
+ * { const: "foo" } → { enum: ["foo"] }
65
+ */
66
+ function convertConstToEnum(schema) {
67
+ if (!schema || typeof schema !== "object") {
68
+ return schema;
69
+ }
70
+ if (Array.isArray(schema)) {
71
+ return schema.map(item => convertConstToEnum(item));
72
+ }
73
+ const result = {};
74
+ for (const [key, value] of Object.entries(schema)) {
75
+ if (key === "const" && !schema.enum) {
76
+ result.enum = [value];
77
+ }
78
+ else {
79
+ result[key] = convertConstToEnum(value);
80
+ }
81
+ }
82
+ return result;
83
+ }
84
+ /**
85
+ * Phase 1c: Adds enum hints to description.
86
+ * { enum: ["a", "b", "c"] } → adds "(Allowed: a, b, c)" to description
87
+ */
88
+ function addEnumHints(schema) {
89
+ if (!schema || typeof schema !== "object") {
90
+ return schema;
91
+ }
92
+ if (Array.isArray(schema)) {
93
+ return schema.map(item => addEnumHints(item));
94
+ }
95
+ let result = { ...schema };
96
+ // Add enum hint if enum has 2-10 items
97
+ if (Array.isArray(result.enum) && result.enum.length > 1 && result.enum.length <= 10) {
98
+ const vals = result.enum.map((v) => String(v)).join(", ");
99
+ result = appendDescriptionHint(result, `Allowed: ${vals}`);
100
+ }
101
+ // Recursively process nested objects
102
+ for (const [key, value] of Object.entries(result)) {
103
+ if (key !== "enum" && typeof value === "object" && value !== null) {
104
+ result[key] = addEnumHints(value);
105
+ }
106
+ }
107
+ return result;
108
+ }
109
+ /**
110
+ * Phase 1d: Adds additionalProperties hints.
111
+ * { additionalProperties: false } → adds "(No extra properties allowed)" to description
112
+ */
113
+ function addAdditionalPropertiesHints(schema) {
114
+ if (!schema || typeof schema !== "object") {
115
+ return schema;
116
+ }
117
+ if (Array.isArray(schema)) {
118
+ return schema.map(item => addAdditionalPropertiesHints(item));
119
+ }
120
+ let result = { ...schema };
121
+ if (result.additionalProperties === false) {
122
+ result = appendDescriptionHint(result, "No extra properties allowed");
123
+ }
124
+ // Recursively process nested objects
125
+ for (const [key, value] of Object.entries(result)) {
126
+ if (key !== "additionalProperties" && typeof value === "object" && value !== null) {
127
+ result[key] = addAdditionalPropertiesHints(value);
128
+ }
129
+ }
130
+ return result;
131
+ }
132
+ /**
133
+ * Phase 1e: Moves unsupported constraints to description hints.
134
+ * { minLength: 1, maxLength: 100 } → adds "(minLength: 1) (maxLength: 100)" to description
135
+ */
136
+ function moveConstraintsToDescription(schema) {
137
+ if (!schema || typeof schema !== "object") {
138
+ return schema;
139
+ }
140
+ if (Array.isArray(schema)) {
141
+ return schema.map(item => moveConstraintsToDescription(item));
142
+ }
143
+ let result = { ...schema };
144
+ // Move constraint values to description
145
+ for (const constraint of UNSUPPORTED_CONSTRAINTS) {
146
+ if (result[constraint] !== undefined && typeof result[constraint] !== "object") {
147
+ result = appendDescriptionHint(result, `${constraint}: ${result[constraint]}`);
148
+ }
149
+ }
150
+ // Recursively process nested objects
151
+ for (const [key, value] of Object.entries(result)) {
152
+ if (typeof value === "object" && value !== null) {
153
+ result[key] = moveConstraintsToDescription(value);
154
+ }
155
+ }
156
+ return result;
157
+ }
158
+ /**
159
+ * Phase 2a: Merges allOf schemas into a single object.
160
+ * { allOf: [{ properties: { a: ... } }, { properties: { b: ... } }] }
161
+ * → { properties: { a: ..., b: ... } }
162
+ */
163
+ function mergeAllOf(schema) {
164
+ if (!schema || typeof schema !== "object") {
165
+ return schema;
166
+ }
167
+ if (Array.isArray(schema)) {
168
+ return schema.map(item => mergeAllOf(item));
169
+ }
170
+ let result = { ...schema };
171
+ // If this object has allOf, merge its contents
172
+ if (Array.isArray(result.allOf)) {
173
+ const merged = {};
174
+ const mergedRequired = [];
175
+ for (const item of result.allOf) {
176
+ if (!item || typeof item !== "object")
177
+ continue;
178
+ // Merge properties
179
+ if (item.properties && typeof item.properties === "object") {
180
+ merged.properties = { ...merged.properties, ...item.properties };
181
+ }
182
+ // Merge required arrays
183
+ if (Array.isArray(item.required)) {
184
+ for (const req of item.required) {
185
+ if (!mergedRequired.includes(req)) {
186
+ mergedRequired.push(req);
187
+ }
188
+ }
189
+ }
190
+ // Copy other fields from allOf items
191
+ for (const [key, value] of Object.entries(item)) {
192
+ if (key !== "properties" && key !== "required" && merged[key] === undefined) {
193
+ merged[key] = value;
194
+ }
195
+ }
196
+ }
197
+ // Apply merged content to result
198
+ if (merged.properties) {
199
+ result.properties = { ...result.properties, ...merged.properties };
200
+ }
201
+ if (mergedRequired.length > 0) {
202
+ const existingRequired = Array.isArray(result.required) ? result.required : [];
203
+ result.required = Array.from(new Set([...existingRequired, ...mergedRequired]));
204
+ }
205
+ // Copy other merged fields
206
+ for (const [key, value] of Object.entries(merged)) {
207
+ if (key !== "properties" && key !== "required" && result[key] === undefined) {
208
+ result[key] = value;
209
+ }
210
+ }
211
+ delete result.allOf;
212
+ }
213
+ // Recursively process nested objects
214
+ for (const [key, value] of Object.entries(result)) {
215
+ if (typeof value === "object" && value !== null) {
216
+ result[key] = mergeAllOf(value);
217
+ }
218
+ }
219
+ return result;
220
+ }
221
+ /**
222
+ * Scores a schema option for selection in anyOf/oneOf flattening.
223
+ * Higher score = more preferred.
224
+ */
225
+ function scoreSchemaOption(schema) {
226
+ if (!schema || typeof schema !== "object") {
227
+ return { score: 0, typeName: "unknown" };
228
+ }
229
+ const type = schema.type;
230
+ // Object or has properties = highest priority
231
+ if (type === "object" || schema.properties) {
232
+ return { score: 3, typeName: "object" };
233
+ }
234
+ // Array or has items = second priority
235
+ if (type === "array" || schema.items) {
236
+ return { score: 2, typeName: "array" };
237
+ }
238
+ // Any other non-null type
239
+ if (type && type !== "null") {
240
+ return { score: 1, typeName: type };
241
+ }
242
+ // Null or no type
243
+ return { score: 0, typeName: type || "null" };
244
+ }
245
+ /**
246
+ * Phase 2b: Flattens anyOf/oneOf to the best option with type hints.
247
+ * { anyOf: [{ type: "string" }, { type: "number" }] }
248
+ * → { type: "string", description: "(Accepts: string | number)" }
249
+ */
250
+ function flattenAnyOfOneOf(schema) {
251
+ if (!schema || typeof schema !== "object") {
252
+ return schema;
253
+ }
254
+ if (Array.isArray(schema)) {
255
+ return schema.map(item => flattenAnyOfOneOf(item));
256
+ }
257
+ let result = { ...schema };
258
+ // Process anyOf or oneOf
259
+ for (const unionKey of ["anyOf", "oneOf"]) {
260
+ if (Array.isArray(result[unionKey]) && result[unionKey].length > 0) {
261
+ const options = result[unionKey];
262
+ const parentDesc = typeof result.description === "string" ? result.description : "";
263
+ // Score each option and find the best
264
+ let bestIdx = 0;
265
+ let bestScore = -1;
266
+ const allTypes = [];
267
+ for (let i = 0; i < options.length; i++) {
268
+ const { score, typeName } = scoreSchemaOption(options[i]);
269
+ if (typeName) {
270
+ allTypes.push(typeName);
271
+ }
272
+ if (score > bestScore) {
273
+ bestScore = score;
274
+ bestIdx = i;
275
+ }
276
+ }
277
+ // Select the best option and flatten it recursively
278
+ let selected = flattenAnyOfOneOf(options[bestIdx]) || { type: "string" };
279
+ // Preserve parent description
280
+ if (parentDesc) {
281
+ const childDesc = typeof selected.description === "string" ? selected.description : "";
282
+ if (childDesc && childDesc !== parentDesc) {
283
+ selected = { ...selected, description: `${parentDesc} (${childDesc})` };
284
+ }
285
+ else if (!childDesc) {
286
+ selected = { ...selected, description: parentDesc };
287
+ }
288
+ }
289
+ if (allTypes.length > 1) {
290
+ const uniqueTypes = Array.from(new Set(allTypes));
291
+ const hint = `Accepts: ${uniqueTypes.join(" | ")}`;
292
+ selected = appendDescriptionHint(selected, hint);
293
+ }
294
+ // Replace result with selected schema, preserving other fields
295
+ const { [unionKey]: _, description: __, ...rest } = result;
296
+ result = { ...rest, ...selected };
297
+ }
298
+ }
299
+ // Recursively process nested objects
300
+ for (const [key, value] of Object.entries(result)) {
301
+ if (typeof value === "object" && value !== null) {
302
+ result[key] = flattenAnyOfOneOf(value);
303
+ }
304
+ }
305
+ return result;
306
+ }
307
+ /**
308
+ * Phase 2c: Flattens type arrays to single type with nullable hint.
309
+ * { type: ["string", "null"] } → { type: "string", description: "(nullable)" }
310
+ */
311
+ function flattenTypeArrays(schema, nullableFields, currentPath) {
312
+ if (!schema || typeof schema !== "object") {
313
+ return schema;
314
+ }
315
+ if (Array.isArray(schema)) {
316
+ return schema.map((item, idx) => flattenTypeArrays(item, nullableFields, `${currentPath || ""}[${idx}]`));
317
+ }
318
+ let result = { ...schema };
319
+ const localNullableFields = nullableFields || new Map();
320
+ // Handle type array
321
+ if (Array.isArray(result.type)) {
322
+ const types = result.type;
323
+ const hasNull = types.includes("null");
324
+ const nonNullTypes = types.filter(t => t !== "null" && t);
325
+ // Select first non-null type, or "string" as fallback
326
+ const firstType = nonNullTypes.length > 0 ? nonNullTypes[0] : "string";
327
+ result.type = firstType;
328
+ // Add hint for multiple types
329
+ if (nonNullTypes.length > 1) {
330
+ result = appendDescriptionHint(result, `Accepts: ${nonNullTypes.join(" | ")}`);
331
+ }
332
+ // Add nullable hint
333
+ if (hasNull) {
334
+ result = appendDescriptionHint(result, "nullable");
335
+ }
336
+ }
337
+ // Recursively process properties
338
+ if (result.properties && typeof result.properties === "object") {
339
+ const newProps = {};
340
+ for (const [propKey, propValue] of Object.entries(result.properties)) {
341
+ const propPath = currentPath ? `${currentPath}.properties.${propKey}` : `properties.${propKey}`;
342
+ const processed = flattenTypeArrays(propValue, localNullableFields, propPath);
343
+ newProps[propKey] = processed;
344
+ // Track nullable fields for required array cleanup
345
+ if (processed && typeof processed === "object" &&
346
+ typeof processed.description === "string" &&
347
+ processed.description.includes("nullable")) {
348
+ const objectPath = currentPath || "";
349
+ const existing = localNullableFields.get(objectPath) || [];
350
+ existing.push(propKey);
351
+ localNullableFields.set(objectPath, existing);
352
+ }
353
+ }
354
+ result.properties = newProps;
355
+ }
356
+ // Remove nullable fields from required array
357
+ if (Array.isArray(result.required) && !nullableFields) {
358
+ // Only at root level, filter out nullable fields
359
+ const nullableAtRoot = localNullableFields.get("") || [];
360
+ if (nullableAtRoot.length > 0) {
361
+ result.required = result.required.filter((r) => !nullableAtRoot.includes(r));
362
+ if (result.required.length === 0) {
363
+ delete result.required;
364
+ }
365
+ }
366
+ }
367
+ // Recursively process other nested objects
368
+ for (const [key, value] of Object.entries(result)) {
369
+ if (key !== "properties" && typeof value === "object" && value !== null) {
370
+ result[key] = flattenTypeArrays(value, localNullableFields, `${currentPath || ""}.${key}`);
371
+ }
372
+ }
373
+ return result;
374
+ }
375
+ /**
376
+ * Phase 3: Removes unsupported keywords after hints have been extracted.
377
+ */
378
+ function removeUnsupportedKeywords(schema) {
379
+ if (!schema || typeof schema !== "object") {
380
+ return schema;
381
+ }
382
+ if (Array.isArray(schema)) {
383
+ return schema.map(item => removeUnsupportedKeywords(item));
384
+ }
385
+ const result = {};
386
+ for (const [key, value] of Object.entries(schema)) {
387
+ // Skip unsupported keywords
388
+ if (UNSUPPORTED_KEYWORDS.includes(key)) {
389
+ continue;
390
+ }
391
+ // Recursively process nested objects
392
+ if (typeof value === "object" && value !== null) {
393
+ result[key] = removeUnsupportedKeywords(value);
394
+ }
395
+ else {
396
+ result[key] = value;
397
+ }
398
+ }
399
+ return result;
400
+ }
401
+ /**
402
+ * Phase 3b: Cleans up required fields - removes entries that don't exist in properties.
403
+ */
404
+ function cleanupRequiredFields(schema) {
405
+ if (!schema || typeof schema !== "object") {
406
+ return schema;
407
+ }
408
+ if (Array.isArray(schema)) {
409
+ return schema.map(item => cleanupRequiredFields(item));
410
+ }
411
+ let result = { ...schema };
412
+ // Clean up required array if properties exist
413
+ if (Array.isArray(result.required) && result.properties && typeof result.properties === "object") {
414
+ const validRequired = result.required.filter((req) => Object.prototype.hasOwnProperty.call(result.properties, req));
415
+ if (validRequired.length === 0) {
416
+ delete result.required;
417
+ }
418
+ else if (validRequired.length !== result.required.length) {
419
+ result.required = validRequired;
420
+ }
421
+ }
422
+ // Recursively process nested objects
423
+ for (const [key, value] of Object.entries(result)) {
424
+ if (typeof value === "object" && value !== null) {
425
+ result[key] = cleanupRequiredFields(value);
426
+ }
427
+ }
428
+ return result;
429
+ }
430
+ /**
431
+ * Phase 4: Adds placeholder property for empty object schemas.
432
+ * Claude VALIDATED mode requires at least one property.
433
+ */
434
+ function addEmptySchemaPlaceholder(schema) {
435
+ if (!schema || typeof schema !== "object") {
436
+ return schema;
437
+ }
438
+ if (Array.isArray(schema)) {
439
+ return schema.map(item => addEmptySchemaPlaceholder(item));
440
+ }
441
+ let result = { ...schema };
442
+ // Check if this is an empty object schema
443
+ if (result.type === "object") {
444
+ const hasProperties = result.properties &&
445
+ typeof result.properties === "object" &&
446
+ Object.keys(result.properties).length > 0;
447
+ if (!hasProperties) {
448
+ result.properties = {
449
+ reason: {
450
+ type: "string",
451
+ description: "Brief explanation of why you are calling this tool",
452
+ },
453
+ };
454
+ result.required = ["reason"];
455
+ }
456
+ }
457
+ // Recursively process nested objects
458
+ for (const [key, value] of Object.entries(result)) {
459
+ if (typeof value === "object" && value !== null) {
460
+ result[key] = addEmptySchemaPlaceholder(value);
461
+ }
462
+ }
463
+ return result;
464
+ }
465
+ /**
466
+ * Cleans a JSON schema for Antigravity API compatibility.
467
+ * Transforms unsupported features into description hints while preserving semantic information.
468
+ *
469
+ * Ported from CLIProxyAPI's CleanJSONSchemaForAntigravity (gemini_schema.go)
470
+ */
471
+ export function cleanJSONSchemaForAntigravity(schema) {
472
+ if (!schema || typeof schema !== "object") {
473
+ return schema;
474
+ }
475
+ let result = schema;
476
+ // Phase 1: Convert and add hints
477
+ result = convertRefsToHints(result);
478
+ result = convertConstToEnum(result);
479
+ result = addEnumHints(result);
480
+ result = addAdditionalPropertiesHints(result);
481
+ result = moveConstraintsToDescription(result);
482
+ // Phase 2: Flatten complex structures
483
+ result = mergeAllOf(result);
484
+ result = flattenAnyOfOneOf(result);
485
+ result = flattenTypeArrays(result);
486
+ // Phase 3: Cleanup
487
+ result = removeUnsupportedKeywords(result);
488
+ result = cleanupRequiredFields(result);
489
+ // Phase 4: Add placeholder for empty object schemas
490
+ result = addEmptySchemaPlaceholder(result);
491
+ return result;
492
+ }
2
493
  /**
3
494
  * Default token budget for thinking/reasoning. 16000 tokens provides sufficient
4
495
  * space for complex reasoning while staying within typical model limits.
@@ -60,18 +551,70 @@ export function resolveThinkingConfig(userConfig, isThinkingModel, _isClaudeMode
60
551
  */
61
552
  function isThinkingPart(part) {
62
553
  return part.type === "thinking"
554
+ || part.type === "redacted_thinking"
63
555
  || part.type === "reasoning"
64
556
  || part.thinking !== undefined
65
557
  || part.thought === true;
66
558
  }
559
+ /**
560
+ * Checks if a part has a signature field (thinking block signature).
561
+ * Used to detect foreign thinking blocks that might have unknown type values.
562
+ */
563
+ function hasSignatureField(part) {
564
+ return part.signature !== undefined || part.thoughtSignature !== undefined;
565
+ }
566
+ /**
567
+ * Checks if a part is a tool block (tool_use or tool_result).
568
+ * Tool blocks must never be filtered - they're required for tool call/result pairing.
569
+ * Handles multiple formats:
570
+ * - Anthropic: { type: "tool_use" }, { type: "tool_result", tool_use_id }
571
+ * - Nested: { tool_result: { tool_use_id } }, { tool_use: { id } }
572
+ * - Gemini: { functionCall }, { functionResponse }
573
+ */
574
+ function isToolBlock(part) {
575
+ return part.type === "tool_use"
576
+ || part.type === "tool_result"
577
+ || part.tool_use_id !== undefined
578
+ || part.tool_call_id !== undefined
579
+ || part.tool_result !== undefined
580
+ || part.tool_use !== undefined
581
+ || part.toolUse !== undefined
582
+ || part.functionCall !== undefined
583
+ || part.functionResponse !== undefined;
584
+ }
585
+ /**
586
+ * Unconditionally strips ALL thinking/reasoning blocks from a content array.
587
+ * Used for Claude models to avoid signature validation errors entirely.
588
+ * Claude will generate fresh thinking for each turn.
589
+ */
590
+ function stripAllThinkingBlocks(contentArray) {
591
+ return contentArray.filter(item => {
592
+ if (!item || typeof item !== "object")
593
+ return true;
594
+ if (isToolBlock(item))
595
+ return true;
596
+ if (isThinkingPart(item))
597
+ return false;
598
+ if (hasSignatureField(item))
599
+ return false;
600
+ return true;
601
+ });
602
+ }
67
603
  /**
68
604
  * Removes trailing thinking blocks from a content array.
69
605
  * Claude API requires that assistant messages don't end with thinking blocks.
70
606
  * Only removes unsigned thinking blocks; preserves those with valid signatures.
71
607
  */
72
- function removeTrailingThinkingBlocks(contentArray) {
608
+ function removeTrailingThinkingBlocks(contentArray, sessionId, getCachedSignatureFn) {
73
609
  const result = [...contentArray];
74
- while (result.length > 0 && isThinkingPart(result[result.length - 1]) && !hasValidSignature(result[result.length - 1])) {
610
+ while (result.length > 0 && isThinkingPart(result[result.length - 1])) {
611
+ const part = result[result.length - 1];
612
+ const isValid = sessionId && getCachedSignatureFn
613
+ ? isOurCachedSignature(part, sessionId, getCachedSignatureFn)
614
+ : hasValidSignature(part);
615
+ if (isValid) {
616
+ break;
617
+ }
75
618
  result.pop();
76
619
  }
77
620
  return result;
@@ -84,6 +627,33 @@ function hasValidSignature(part) {
84
627
  const signature = part.thought === true ? part.thoughtSignature : part.signature;
85
628
  return typeof signature === "string" && signature.length >= 50;
86
629
  }
630
+ /**
631
+ * Gets the signature from a thinking part, if present.
632
+ */
633
+ function getSignature(part) {
634
+ const signature = part.thought === true ? part.thoughtSignature : part.signature;
635
+ return typeof signature === "string" ? signature : undefined;
636
+ }
637
+ /**
638
+ * Checks if a thinking part's signature was generated by our plugin (exists in our cache).
639
+ * This prevents accepting signatures from other providers (e.g., direct Anthropic API, OpenAI)
640
+ * which would cause "Invalid signature" errors when sent to Antigravity Claude.
641
+ */
642
+ function isOurCachedSignature(part, sessionId, getCachedSignatureFn) {
643
+ if (!sessionId || !getCachedSignatureFn) {
644
+ return false;
645
+ }
646
+ const text = getThinkingText(part);
647
+ if (!text) {
648
+ return false;
649
+ }
650
+ const partSignature = getSignature(part);
651
+ if (!partSignature) {
652
+ return false;
653
+ }
654
+ const cachedSignature = getCachedSignatureFn(sessionId, text);
655
+ return cachedSignature === partSignature;
656
+ }
87
657
  /**
88
658
  * Gets the text content from a thinking part.
89
659
  */
@@ -92,7 +662,11 @@ function getThinkingText(part) {
92
662
  return part.text;
93
663
  if (typeof part.thinking === "string")
94
664
  return part.thinking;
95
- // Some SDKs wrap thinking in an object like { text: "...", cache_control: {...} }
665
+ if (part.text && typeof part.text === "object") {
666
+ const maybeText = part.text.text;
667
+ if (typeof maybeText === "string")
668
+ return maybeText;
669
+ }
96
670
  if (part.thinking && typeof part.thinking === "object") {
97
671
  const maybeText = part.thinking.text ?? part.thinking.thinking;
98
672
  if (typeof maybeText === "string")
@@ -128,7 +702,6 @@ function sanitizeThinkingPart(part) {
128
702
  if (part.thought === true) {
129
703
  const sanitized = { thought: true };
130
704
  if (part.text !== undefined) {
131
- // If text is wrapped, extract the inner string.
132
705
  if (typeof part.text === "object" && part.text !== null) {
133
706
  const maybeText = part.text.text;
134
707
  sanitized.text = typeof maybeText === "string" ? maybeText : part.text;
@@ -141,9 +714,9 @@ function sanitizeThinkingPart(part) {
141
714
  sanitized.thoughtSignature = part.thoughtSignature;
142
715
  return sanitized;
143
716
  }
144
- // Anthropic-style thinking blocks: { type: "thinking", thinking, signature }
145
- if (part.type === "thinking" || part.thinking !== undefined) {
146
- const sanitized = { type: "thinking" };
717
+ // Anthropic-style thinking/redacted_thinking blocks: { type: "thinking"|"redacted_thinking", thinking, signature }
718
+ if (part.type === "thinking" || part.type === "redacted_thinking" || part.thinking !== undefined) {
719
+ const sanitized = { type: part.type === "redacted_thinking" ? "redacted_thinking" : "thinking" };
147
720
  let thinkingContent = part.thinking ?? part.text;
148
721
  if (thinkingContent !== undefined && typeof thinkingContent === "object" && thinkingContent !== null) {
149
722
  const maybeText = thinkingContent.text ?? thinkingContent.thinking;
@@ -155,21 +728,48 @@ function sanitizeThinkingPart(part) {
155
728
  sanitized.signature = part.signature;
156
729
  return sanitized;
157
730
  }
731
+ // Reasoning blocks (OpenCode format): { type: "reasoning", text, signature }
732
+ if (part.type === "reasoning") {
733
+ const sanitized = { type: "reasoning" };
734
+ if (part.text !== undefined) {
735
+ if (typeof part.text === "object" && part.text !== null) {
736
+ const maybeText = part.text.text;
737
+ sanitized.text = typeof maybeText === "string" ? maybeText : part.text;
738
+ }
739
+ else {
740
+ sanitized.text = part.text;
741
+ }
742
+ }
743
+ if (part.signature !== undefined)
744
+ sanitized.signature = part.signature;
745
+ return sanitized;
746
+ }
158
747
  // Fallback: strip cache_control recursively.
159
748
  return stripCacheControlRecursively(part);
160
749
  }
161
- function filterContentArray(contentArray, sessionId, getCachedSignatureFn) {
750
+ function filterContentArray(contentArray, sessionId, getCachedSignatureFn, isClaudeModel) {
751
+ // For Claude models, strip thinking blocks by default for reliability
752
+ // User can opt-in to keep thinking via OPENCODE_ANTIGRAVITY_KEEP_THINKING=1
753
+ if (isClaudeModel && !KEEP_THINKING_BLOCKS) {
754
+ return stripAllThinkingBlocks(contentArray);
755
+ }
162
756
  const filtered = [];
163
757
  for (const item of contentArray) {
164
758
  if (!item || typeof item !== "object") {
165
759
  filtered.push(item);
166
760
  continue;
167
761
  }
168
- if (!isThinkingPart(item)) {
762
+ if (isToolBlock(item)) {
169
763
  filtered.push(item);
170
764
  continue;
171
765
  }
172
- if (hasValidSignature(item)) {
766
+ const isThinking = isThinkingPart(item);
767
+ const hasSignature = hasSignatureField(item);
768
+ if (!isThinking && !hasSignature) {
769
+ filtered.push(item);
770
+ continue;
771
+ }
772
+ if (isOurCachedSignature(item, sessionId, getCachedSignatureFn)) {
173
773
  filtered.push(sanitizeThinkingPart(item));
174
774
  continue;
175
775
  }
@@ -190,63 +790,85 @@ function filterContentArray(contentArray, sessionId, getCachedSignatureFn) {
190
790
  }
191
791
  }
192
792
  }
193
- // Drop unsigned/invalid thinking blocks.
194
793
  }
195
794
  return filtered;
196
795
  }
197
796
  /**
198
- * Filters out unsigned thinking blocks from contents (required by Claude API).
199
- * Attempts to restore signatures from cache for thinking blocks that lack valid signatures.
797
+ * Filters thinking blocks from contents unless the signature matches our cache.
798
+ * Attempts to restore signatures from cache for thinking blocks that lack signatures.
200
799
  *
201
800
  * @param contents - The contents array from the request
202
801
  * @param sessionId - Optional session ID for signature cache lookup
203
802
  * @param getCachedSignatureFn - Optional function to retrieve cached signatures
204
803
  */
205
- export function filterUnsignedThinkingBlocks(contents, sessionId, getCachedSignatureFn) {
804
+ export function filterUnsignedThinkingBlocks(contents, sessionId, getCachedSignatureFn, isClaudeModel) {
206
805
  return contents.map((content) => {
207
806
  if (!content || typeof content !== "object") {
208
807
  return content;
209
808
  }
210
- // Gemini format: contents[].parts[]
211
809
  if (Array.isArray(content.parts)) {
212
- let filteredParts = filterContentArray(content.parts, sessionId, getCachedSignatureFn);
213
- // Remove trailing thinking blocks for model role (assistant equivalent in Gemini)
214
- if (content.role === "model") {
215
- filteredParts = removeTrailingThinkingBlocks(filteredParts);
216
- }
217
- return { ...content, parts: filteredParts };
810
+ const filteredParts = filterContentArray(content.parts, sessionId, getCachedSignatureFn, isClaudeModel);
811
+ const trimmedParts = content.role === "model" && !isClaudeModel
812
+ ? removeTrailingThinkingBlocks(filteredParts, sessionId, getCachedSignatureFn)
813
+ : filteredParts;
814
+ return { ...content, parts: trimmedParts };
218
815
  }
219
- // Some Anthropic-style payloads may appear here as contents[].content[]
220
816
  if (Array.isArray(content.content)) {
221
- let filteredContent = filterContentArray(content.content, sessionId, getCachedSignatureFn);
222
- // Claude API requires assistant messages don't end with thinking blocks
223
- if (content.role === "assistant") {
224
- filteredContent = removeTrailingThinkingBlocks(filteredContent);
225
- }
226
- return { ...content, content: filteredContent };
817
+ const isAssistantRole = content.role === "assistant";
818
+ const filteredContent = filterContentArray(content.content, sessionId, getCachedSignatureFn, isClaudeModel);
819
+ const trimmedContent = isAssistantRole && !isClaudeModel
820
+ ? removeTrailingThinkingBlocks(filteredContent, sessionId, getCachedSignatureFn)
821
+ : filteredContent;
822
+ return { ...content, content: trimmedContent };
227
823
  }
228
824
  return content;
229
825
  });
230
826
  }
231
827
  /**
232
- * Filters thinking blocks from Anthropic-style messages[] payloads.
828
+ * Filters thinking blocks from Anthropic-style messages[] payloads using cached signatures.
233
829
  */
234
- export function filterMessagesThinkingBlocks(messages, sessionId, getCachedSignatureFn) {
830
+ export function filterMessagesThinkingBlocks(messages, sessionId, getCachedSignatureFn, isClaudeModel) {
235
831
  return messages.map((message) => {
236
832
  if (!message || typeof message !== "object") {
237
833
  return message;
238
834
  }
239
835
  if (Array.isArray(message.content)) {
240
- let filteredContent = filterContentArray(message.content, sessionId, getCachedSignatureFn);
241
- // Claude API requires assistant messages don't end with thinking blocks
242
- if (message.role === "assistant") {
243
- filteredContent = removeTrailingThinkingBlocks(filteredContent);
244
- }
245
- return { ...message, content: filteredContent };
836
+ const isAssistantRole = message.role === "assistant";
837
+ const filteredContent = filterContentArray(message.content, sessionId, getCachedSignatureFn, isClaudeModel);
838
+ const trimmedContent = isAssistantRole && !isClaudeModel
839
+ ? removeTrailingThinkingBlocks(filteredContent, sessionId, getCachedSignatureFn)
840
+ : filteredContent;
841
+ return { ...message, content: trimmedContent };
246
842
  }
247
843
  return message;
248
844
  });
249
845
  }
846
+ export function deepFilterThinkingBlocks(payload, sessionId, getCachedSignatureFn, isClaudeModel) {
847
+ const visited = new WeakSet();
848
+ const walk = (value) => {
849
+ if (!value || typeof value !== "object") {
850
+ return;
851
+ }
852
+ if (visited.has(value)) {
853
+ return;
854
+ }
855
+ visited.add(value);
856
+ if (Array.isArray(value)) {
857
+ value.forEach((item) => walk(item));
858
+ return;
859
+ }
860
+ const obj = value;
861
+ if (Array.isArray(obj.contents)) {
862
+ obj.contents = filterUnsignedThinkingBlocks(obj.contents, sessionId, getCachedSignatureFn, isClaudeModel);
863
+ }
864
+ if (Array.isArray(obj.messages)) {
865
+ obj.messages = filterMessagesThinkingBlocks(obj.messages, sessionId, getCachedSignatureFn, isClaudeModel);
866
+ }
867
+ Object.keys(obj).forEach((key) => walk(obj[key]));
868
+ };
869
+ walk(payload);
870
+ return payload;
871
+ }
250
872
  /**
251
873
  * Transforms Gemini-style thought parts (thought: true) and Anthropic-style
252
874
  * thinking parts (type: "thinking") to reasoning format.