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.
- package/README.md +18 -12
- package/dist/src/constants.d.ts +12 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +13 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/plugin/request-helpers.d.ts +13 -5
- package/dist/src/plugin/request-helpers.d.ts.map +1 -1
- package/dist/src/plugin/request-helpers.js +658 -36
- package/dist/src/plugin/request-helpers.js.map +1 -1
- package/dist/src/plugin/request.d.ts.map +1 -1
- package/dist/src/plugin/request.js +164 -111
- package/dist/src/plugin/request.js.map +1 -1
- package/dist/src/plugin.d.ts.map +1 -1
- package/dist/src/plugin.js +26 -6
- package/dist/src/plugin.js.map +1 -1
- package/package.json +57 -57
|
@@ -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])
|
|
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
|
-
|
|
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 (
|
|
762
|
+
if (isToolBlock(item)) {
|
|
169
763
|
filtered.push(item);
|
|
170
764
|
continue;
|
|
171
765
|
}
|
|
172
|
-
|
|
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
|
|
199
|
-
* Attempts to restore signatures from cache for thinking blocks that lack
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return { ...content, content:
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return { ...message, content:
|
|
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.
|