diff --git a/packages/pi-ai/src/providers/google-shared.ts b/packages/pi-ai/src/providers/google-shared.ts index e942314f9..0ae58171b 100644 --- a/packages/pi-ai/src/providers/google-shared.ts +++ b/packages/pi-ai/src/providers/google-shared.ts @@ -204,7 +204,7 @@ export function convertMessages(model: Model, contex // Cloud Code Assist API requires all function responses to be in a single user turn. // Check if the last content is already a user turn with function responses and merge. const lastContent = contents[contents.length - 1]; - if (lastContent?.role === "user" && lastContent.parts?.some((p) => p.functionResponse)) { + if (lastContent?.role === "user" && lastContent.parts?.some((p: Part) => p.functionResponse)) { lastContent.parts.push(functionResponsePart); } else { contents.push({ @@ -226,6 +226,62 @@ export function convertMessages(model: Model, contex return contents; } +/** + * Sanitize a JSON Schema for Google's function declarations API. + * Google's API rejects `patternProperties` and `const` fields which are valid in JSON Schema. + * + * This function recursively: + * - Removes all `patternProperties` fields + * - Converts `const: "value"` to `enum: ["value"]` in anyOf/oneOf blocks + * + * This is needed for providers like `google-antigravity` when proxying Claude models, + * since Google Cloud Code Assist uses a restricted subset of JSON Schema. + */ +function sanitizeSchemaForGoogle(schema: unknown): unknown { + if (!schema || typeof schema !== "object") { + return schema; + } + + if (Array.isArray(schema)) { + return schema.map((item) => sanitizeSchemaForGoogle(item)); + } + + const obj = schema as Record; + const sanitized: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + // Skip patternProperties entirely + if (key === "patternProperties") { + continue; + } + + // Convert const to enum in anyOf/oneOf blocks + if (key === "const" && typeof value === "string") { + // Only convert if we're inside anyOf/oneOf; otherwise leave as-is + // This will be handled by the anyOf/oneOf case below + sanitized.enum = [value]; + continue; + } + + // Recursively sanitize nested objects and arrays + if (key === "properties" && typeof value === "object") { + sanitized[key] = sanitizeSchemaForGoogle(value); + } else if (key === "items" && typeof value === "object") { + sanitized[key] = sanitizeSchemaForGoogle(value); + } else if (key === "anyOf" || key === "oneOf" || key === "allOf") { + sanitized[key] = sanitizeSchemaForGoogle(value); + } else if (key === "additionalProperties" && typeof value === "object") { + sanitized[key] = sanitizeSchemaForGoogle(value); + } else if (typeof value === "object" && !Array.isArray(value)) { + sanitized[key] = sanitizeSchemaForGoogle(value); + } else { + sanitized[key] = value; + } + } + + return sanitized; +} + /** * Convert tools to Gemini function declarations format. * @@ -233,6 +289,9 @@ export function convertMessages(model: Model, contex * anyOf, oneOf, const, etc.). Set `useParameters` to true to use the legacy `parameters` * field instead (OpenAPI 3.03 Schema). This is needed for Cloud Code Assist with Claude * models, where the API translates `parameters` into Anthropic's `input_schema`. + * + * The schema is automatically sanitized to remove fields not supported by Google's + * function declarations API (patternProperties, const converted to enum, etc.). */ export function convertTools( tools: Tool[], @@ -244,7 +303,9 @@ export function convertTools( functionDeclarations: tools.map((tool) => ({ name: tool.name, description: tool.description, - ...(useParameters ? { parameters: tool.parameters } : { parametersJsonSchema: tool.parameters }), + ...(useParameters + ? { parameters: sanitizeSchemaForGoogle(tool.parameters) } + : { parametersJsonSchema: sanitizeSchemaForGoogle(tool.parameters) }), })), }, ]; @@ -291,10 +352,9 @@ export function mapStopReason(reason: FinishReason): StopReason { case FinishReason.UNEXPECTED_TOOL_CALL: case FinishReason.NO_IMAGE: return "error"; - default: { - const _exhaustive: never = reason; - throw new Error(`Unhandled stop reason: ${_exhaustive}`); - } + default: + // Fallback for new/unknown FinishReason values + return "error"; } }