Skip to content

Commit a85e6be

Browse files
author
tctinh
committed
feat: enhance schema normalization and streaming response handling in Antigravity requests
1 parent f12ad6d commit a85e6be

1 file changed

Lines changed: 110 additions & 13 deletions

File tree

src/plugin/request.ts

Lines changed: 110 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,55 @@ export function prepareAntigravityRequest(
195195
const functionDeclarations: any[] = [];
196196
const passthroughTools: any[] = [];
197197

198-
const normalizeSchema = (schema: any) => {
199-
if (schema && typeof schema === "object") {
198+
// Sanitize schema - remove features not supported by JSON Schema draft 2020-12
199+
// Recursively strips anyOf/allOf/oneOf and converts to permissive types
200+
const sanitizeSchema = (schema: any): any => {
201+
if (!schema || typeof schema !== "object") {
200202
return schema;
201203
}
202-
toolDebugMissing += 1;
203-
return { type: "object", properties: {} };
204+
205+
const sanitized: any = {};
206+
207+
for (const key of Object.keys(schema)) {
208+
// Skip anyOf/allOf/oneOf - not well supported
209+
if (key === "anyOf" || key === "allOf" || key === "oneOf") {
210+
continue;
211+
}
212+
213+
const value = schema[key];
214+
215+
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 = {};
219+
} else {
220+
sanitized.items = sanitizeSchema(value);
221+
}
222+
} else if (key === "properties" && value && typeof value === "object") {
223+
// Recursively sanitize properties
224+
sanitized.properties = {};
225+
for (const propKey of Object.keys(value)) {
226+
sanitized.properties[propKey] = sanitizeSchema(value[propKey]);
227+
}
228+
} else if (key === "additionalProperties" && value && typeof value === "object") {
229+
sanitized.additionalProperties = sanitizeSchema(value);
230+
} else {
231+
sanitized[key] = value;
232+
}
233+
}
234+
235+
return sanitized;
236+
};
237+
238+
const normalizeSchema = (schema: any) => {
239+
if (!schema || typeof schema !== "object") {
240+
toolDebugMissing += 1;
241+
// Minimal fallback for tools without schemas
242+
return { type: "object" };
243+
}
244+
245+
// Sanitize and pass through
246+
return sanitizeSchema(schema);
204247
};
205248

206249
requestPayload.tools.forEach((tool: any, idx: number) => {
@@ -218,13 +261,16 @@ export function prepareAntigravityRequest(
218261
tool.custom?.parameters ||
219262
tool.custom?.input_schema;
220263

221-
const name =
264+
let name =
222265
decl?.name ||
223266
tool.name ||
224267
tool.function?.name ||
225268
tool.custom?.name ||
226269
`tool-${functionDeclarations.length}`;
227270

271+
// Sanitize tool name: must be alphanumeric with underscores, no special chars
272+
name = String(name).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
273+
228274
const description =
229275
decl?.description ||
230276
tool.description ||
@@ -234,7 +280,7 @@ export function prepareAntigravityRequest(
234280

235281
functionDeclarations.push({
236282
name,
237-
description,
283+
description: String(description || ""),
238284
parameters: normalizeSchema(schema),
239285
});
240286

@@ -302,18 +348,18 @@ export function prepareAntigravityRequest(
302348
newTool.custom = {
303349
name: newTool.function.name || nameCandidate,
304350
description: newTool.function.description,
305-
input_schema: schema ?? { type: "object", properties: {} },
351+
input_schema: schema ?? { type: "object", properties: {}, additionalProperties: false },
306352
};
307353
}
308354
if (!newTool.custom && !newTool.function) {
309355
newTool.custom = {
310356
name: nameCandidate,
311357
description: newTool.description,
312-
input_schema: schema ?? { type: "object", properties: {} },
358+
input_schema: schema ?? { type: "object", properties: {}, additionalProperties: false },
313359
};
314360
}
315361
if (newTool.custom && !newTool.custom.input_schema) {
316-
newTool.custom.input_schema = { type: "object", properties: {} };
362+
newTool.custom.input_schema = { type: "object", properties: {}, additionalProperties: false };
317363
toolDebugMissing += 1;
318364
}
319365

@@ -487,6 +533,58 @@ export async function transformAntigravityResponse(
487533
return response;
488534
}
489535

536+
// For successful streaming responses, use TransformStream to transform SSE events
537+
// while maintaining real-time streaming (no buffering of entire response)
538+
if (streaming && response.ok && isEventStreamResponse && response.body) {
539+
const headers = new Headers(response.headers);
540+
541+
// Buffer for partial SSE events that span chunks
542+
let buffer = "";
543+
const decoder = new TextDecoder();
544+
const encoder = new TextEncoder();
545+
546+
const transformStream = new TransformStream<Uint8Array, Uint8Array>({
547+
transform(chunk, controller) {
548+
// Decode chunk with stream: true to handle multi-byte characters
549+
buffer += decoder.decode(chunk, { stream: true });
550+
551+
// Split on double newline (SSE event delimiter)
552+
const events = buffer.split("\n\n");
553+
554+
// Keep last part in buffer (may be incomplete)
555+
buffer = events.pop() || "";
556+
557+
// Process and forward complete events immediately
558+
for (const event of events) {
559+
if (event.trim()) {
560+
const transformed = transformStreamingPayload(event);
561+
controller.enqueue(encoder.encode(transformed + "\n\n"));
562+
}
563+
}
564+
},
565+
flush(controller) {
566+
// Flush any remaining bytes from TextDecoder
567+
buffer += decoder.decode();
568+
569+
// Handle any remaining data at stream end
570+
if (buffer.trim()) {
571+
const transformed = transformStreamingPayload(buffer);
572+
controller.enqueue(encoder.encode(transformed));
573+
}
574+
}
575+
});
576+
577+
logAntigravityDebugResponse(debugContext, response, {
578+
note: "Streaming SSE response (transformed)",
579+
});
580+
581+
return new Response(response.body.pipeThrough(transformStream), {
582+
status: response.status,
583+
statusText: response.statusText,
584+
headers,
585+
});
586+
}
587+
490588
try {
491589
const text = await response.text();
492590
const headers = new Headers(response.headers);
@@ -558,13 +656,12 @@ export async function transformAntigravityResponse(
558656

559657
logAntigravityDebugResponse(debugContext, response, {
560658
body: text,
561-
note: streaming ? "Streaming SSE payload" : undefined,
659+
note: streaming ? "Streaming SSE payload (buffered fallback)" : undefined,
562660
headersOverride: headers,
563661
});
564662

565-
if (streaming && response.ok && isEventStreamResponse) {
566-
return new Response(transformStreamingPayload(text), init);
567-
}
663+
// Note: successful streaming responses are handled above via TransformStream.
664+
// This path only handles non-streaming responses or failed streaming responses.
568665

569666
if (!parsed) {
570667
return new Response(text, init);

0 commit comments

Comments
 (0)