290 lines
13 KiB
Diff
290 lines
13 KiB
Diff
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js
|
|
index b1d6a340e1817b6f5404c2a23efa49139249f754..9dd692688fd73d378802af9600e459abbce6a17e 100644
|
|
--- a/dist/providers/google-gemini-cli.js
|
|
+++ b/dist/providers/google-gemini-cli.js
|
|
@@ -168,7 +168,12 @@ export const streamGoogleGeminiCli = (model, context, options) => {
|
|
break; // Success, exit retry loop
|
|
}
|
|
const errorText = await response.text();
|
|
- // Check if retryable
|
|
+ // PATCH: Fail immediately on 429 to let caller rotate accounts
|
|
+ if (response.status === 429) {
|
|
+ console.log(`[pi-ai] 429 rate limit - failing fast to rotate account`);
|
|
+ throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`);
|
|
+ }
|
|
+ // Check if retryable (non-429 errors)
|
|
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
|
// Use server-provided delay or exponential backoff
|
|
const serverDelay = extractRetryDelay(errorText);
|
|
@@ -183,6 +188,10 @@ export const streamGoogleGeminiCli = (model, context, options) => {
|
|
if (error instanceof Error && error.message === "Request was aborted") {
|
|
throw error;
|
|
}
|
|
+ // PATCH: Don't retry 429 errors - let caller rotate accounts
|
|
+ if (error instanceof Error && error.message.includes("429")) {
|
|
+ throw error;
|
|
+ }
|
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
// Network errors are retryable
|
|
if (attempt < MAX_RETRIES) {
|
|
diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
|
|
index dbb9c0e263919c9184a5f1c7dfde47d1c3a37ff4..f1866f423f30a4dfbe812d052679abd1f011769f 100644
|
|
--- a/dist/providers/google-shared.js
|
|
+++ b/dist/providers/google-shared.js
|
|
@@ -41,13 +41,27 @@ export function retainThoughtSignature(existing, incoming) {
|
|
export function convertMessages(model, context) {
|
|
const contents = [];
|
|
const transformedMessages = transformMessages(context.messages, model);
|
|
+ /**
|
|
+ * Helper to add content while merging consecutive messages of the same role.
|
|
+ * Gemini/Cloud Code Assist requires strict role alternation (user/model/user/model).
|
|
+ * Consecutive messages of the same role cause "function call turn" errors.
|
|
+ */
|
|
+ function addContent(role, parts) {
|
|
+ if (parts.length === 0)
|
|
+ return;
|
|
+ const lastContent = contents[contents.length - 1];
|
|
+ if (lastContent?.role === role) {
|
|
+ // Merge into existing message of same role
|
|
+ lastContent.parts.push(...parts);
|
|
+ }
|
|
+ else {
|
|
+ contents.push({ role, parts });
|
|
+ }
|
|
+ }
|
|
for (const msg of transformedMessages) {
|
|
if (msg.role === "user") {
|
|
if (typeof msg.content === "string") {
|
|
- contents.push({
|
|
- role: "user",
|
|
- parts: [{ text: sanitizeSurrogates(msg.content) }],
|
|
- });
|
|
+ addContent("user", [{ text: sanitizeSurrogates(msg.content) }]);
|
|
}
|
|
else {
|
|
const parts = msg.content.map((item) => {
|
|
@@ -66,10 +80,7 @@ export function convertMessages(model, context) {
|
|
const filteredParts = !model.input.includes("image") ? parts.filter((p) => p.text !== undefined) : parts;
|
|
if (filteredParts.length === 0)
|
|
continue;
|
|
- contents.push({
|
|
- role: "user",
|
|
- parts: filteredParts,
|
|
- });
|
|
+ addContent("user", filteredParts);
|
|
}
|
|
}
|
|
else if (msg.role === "assistant") {
|
|
@@ -82,9 +93,19 @@ export function convertMessages(model, context) {
|
|
parts.push({ text: sanitizeSurrogates(block.text) });
|
|
}
|
|
else if (block.type === "thinking") {
|
|
- // Thinking blocks require signatures for Claude via Antigravity.
|
|
- // If signature is missing (e.g. from GPT-OSS), convert to regular text with delimiters.
|
|
- if (block.thinkingSignature) {
|
|
+ // Thinking blocks handling varies by model:
|
|
+ // - Claude via Antigravity: requires thinkingSignature
|
|
+ // - Gemini: skip entirely (doesn't understand thoughtSignature, and mimics <thinking> tags)
|
|
+ // - Other models: convert to text with delimiters
|
|
+ const isGemini = model.id.toLowerCase().includes("gemini");
|
|
+ const isClaude = model.id.toLowerCase().includes("claude");
|
|
+ if (isGemini) {
|
|
+ // Skip thinking blocks entirely for Gemini - it doesn't support them
|
|
+ // and will mimic <thinking> tags if we convert to text
|
|
+ continue;
|
|
+ }
|
|
+ if (block.thinkingSignature && isClaude) {
|
|
+ // Claude via Antigravity requires the signature
|
|
parts.push({
|
|
thought: true,
|
|
text: sanitizeSurrogates(block.thinking),
|
|
@@ -92,6 +113,7 @@ export function convertMessages(model, context) {
|
|
});
|
|
}
|
|
else {
|
|
+ // Other models: convert to text with delimiters
|
|
parts.push({
|
|
text: `<thinking>\n${sanitizeSurrogates(block.thinking)}\n</thinking>`,
|
|
});
|
|
@@ -116,10 +138,7 @@ export function convertMessages(model, context) {
|
|
}
|
|
if (parts.length === 0)
|
|
continue;
|
|
- contents.push({
|
|
- role: "model",
|
|
- parts,
|
|
- });
|
|
+ addContent("model", parts);
|
|
}
|
|
else if (msg.role === "toolResult") {
|
|
// Extract text and image content
|
|
@@ -156,27 +175,97 @@ export function convertMessages(model, context) {
|
|
}
|
|
// 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.
|
|
+ // Use addContent for proper role alternation handling.
|
|
const lastContent = contents[contents.length - 1];
|
|
if (lastContent?.role === "user" && lastContent.parts?.some((p) => p.functionResponse)) {
|
|
lastContent.parts.push(functionResponsePart);
|
|
}
|
|
else {
|
|
- contents.push({
|
|
- role: "user",
|
|
- parts: [functionResponsePart],
|
|
- });
|
|
+ addContent("user", [functionResponsePart]);
|
|
}
|
|
// For older models, add images in a separate user message
|
|
+ // Note: This may create consecutive user messages, but addContent will merge them
|
|
if (hasImages && !supportsMultimodalFunctionResponse) {
|
|
- contents.push({
|
|
- role: "user",
|
|
- parts: [{ text: "Tool result image:" }, ...imageParts],
|
|
- });
|
|
+ addContent("user", [{ text: "Tool result image:" }, ...imageParts]);
|
|
}
|
|
}
|
|
}
|
|
return contents;
|
|
}
|
|
+/**
|
|
+ * Sanitize JSON Schema for Google Cloud Code Assist API.
|
|
+ * Removes unsupported keywords like patternProperties, const, anyOf, etc.
|
|
+ * and converts to a format compatible with Google's function declarations.
|
|
+ */
|
|
+function sanitizeSchemaForGoogle(schema) {
|
|
+ if (!schema || typeof schema !== "object") {
|
|
+ return schema;
|
|
+ }
|
|
+ // If it's an array, sanitize each element
|
|
+ if (Array.isArray(schema)) {
|
|
+ return schema.map((item) => sanitizeSchemaForGoogle(item));
|
|
+ }
|
|
+ const sanitized = {};
|
|
+ // List of unsupported JSON Schema keywords that Google's API doesn't understand
|
|
+ const unsupportedKeywords = [
|
|
+ "patternProperties",
|
|
+ "const",
|
|
+ "anyOf",
|
|
+ "oneOf",
|
|
+ "allOf",
|
|
+ "not",
|
|
+ "$schema",
|
|
+ "$id",
|
|
+ "$ref",
|
|
+ "$defs",
|
|
+ "definitions",
|
|
+ "if",
|
|
+ "then",
|
|
+ "else",
|
|
+ "dependentSchemas",
|
|
+ "dependentRequired",
|
|
+ "unevaluatedProperties",
|
|
+ "unevaluatedItems",
|
|
+ "contentEncoding",
|
|
+ "contentMediaType",
|
|
+ "contentSchema",
|
|
+ "deprecated",
|
|
+ "readOnly",
|
|
+ "writeOnly",
|
|
+ "examples",
|
|
+ "$comment",
|
|
+ "additionalProperties",
|
|
+ ];
|
|
+ // TODO(steipete): lossy schema scrub; revisit when Google supports these keywords.
|
|
+ for (const [key, value] of Object.entries(schema)) {
|
|
+ // Skip unsupported keywords
|
|
+ if (unsupportedKeywords.includes(key)) {
|
|
+ continue;
|
|
+ }
|
|
+ // Recursively sanitize nested objects
|
|
+ if (key === "properties" && typeof value === "object" && value !== null) {
|
|
+ sanitized[key] = {};
|
|
+ for (const [propKey, propValue] of Object.entries(value)) {
|
|
+ sanitized[key][propKey] = sanitizeSchemaForGoogle(propValue);
|
|
+ }
|
|
+ }
|
|
+ else if (key === "items" && typeof value === "object") {
|
|
+ sanitized[key] = sanitizeSchemaForGoogle(value);
|
|
+ }
|
|
+ else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
+ sanitized[key] = sanitizeSchemaForGoogle(value);
|
|
+ }
|
|
+ else {
|
|
+ sanitized[key] = value;
|
|
+ }
|
|
+ }
|
|
+ // Ensure type: "object" is present when properties or required exist
|
|
+ // Google API requires type to be set when these fields are present
|
|
+ if (("properties" in sanitized || "required" in sanitized) && !("type" in sanitized)) {
|
|
+ sanitized.type = "object";
|
|
+ }
|
|
+ return sanitized;
|
|
+}
|
|
/**
|
|
* Convert tools to Gemini function declarations format.
|
|
*/
|
|
@@ -188,7 +277,7 @@ export function convertTools(tools) {
|
|
functionDeclarations: tools.map((tool) => ({
|
|
name: tool.name,
|
|
description: tool.description,
|
|
- parameters: tool.parameters,
|
|
+ parameters: sanitizeSchemaForGoogle(tool.parameters),
|
|
})),
|
|
},
|
|
];
|
|
diff --git a/dist/providers/openai-completions.d.ts b/dist/providers/openai-completions.d.ts
|
|
index 723addf341696b5d69c079202e571e9917685ce4..a1d0584a70a7d1fad1332026e301e56ef4f700a8 100644
|
|
--- a/dist/providers/openai-completions.d.ts
|
|
+++ b/dist/providers/openai-completions.d.ts
|
|
@@ -7,6 +7,8 @@ export interface OpenAICompletionsOptions extends StreamOptions {
|
|
};
|
|
};
|
|
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
+ /** Extra params to pass directly to the API (e.g., Z.AI GLM thinking mode params) */
|
|
+ extraParams?: Record<string, unknown>;
|
|
}
|
|
export declare const streamOpenAICompletions: StreamFunction<"openai-completions">;
|
|
//# sourceMappingURL=openai-completions.d.ts.map
|
|
diff --git a/dist/providers/openai-completions.js b/dist/providers/openai-completions.js
|
|
index 2590381cc5544c4e73c24c1c9a5853202f31361b..b76e1087dd31ccf099e02b1214b9e12d371b9b2d 100644
|
|
--- a/dist/providers/openai-completions.js
|
|
+++ b/dist/providers/openai-completions.js
|
|
@@ -335,6 +335,11 @@ function buildParams(model, context, options) {
|
|
if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
|
|
params.reasoning_effort = options.reasoningEffort;
|
|
}
|
|
+ // PATCH: Support arbitrary extra params for provider-specific features
|
|
+ // (e.g., Z.AI GLM-4.7 thinking: { type: "enabled", clear_thinking: boolean })
|
|
+ if (options?.extraParams && typeof options.extraParams === "object") {
|
|
+ Object.assign(params, options.extraParams);
|
|
+ }
|
|
return params;
|
|
}
|
|
function convertMessages(model, context, compat) {
|
|
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js
|
|
index 20fb0a22aaa28f7ff7c2f44a8b628fa1d9d7d936..c2bc63f483f3285b00755901ba97db810221cea6 100644
|
|
--- a/dist/providers/openai-responses.js
|
|
+++ b/dist/providers/openai-responses.js
|
|
@@ -486,7 +486,6 @@ function convertTools(tools) {
|
|
name: tool.name,
|
|
description: tool.description,
|
|
parameters: tool.parameters, // TypeBox already generates JSON Schema
|
|
- strict: null,
|
|
}));
|
|
}
|
|
function mapStopReason(status) {
|
|
diff --git a/dist/stream.js b/dist/stream.js
|
|
index da54f4270e9b8d9e9cf1f902af976cc239601d4c..7ed71597c3369f8e3c1a3da0eb870a68215b714d 100644
|
|
--- a/dist/stream.js
|
|
+++ b/dist/stream.js
|
|
@@ -108,6 +108,8 @@ function mapOptionsForApi(model, options, apiKey) {
|
|
signal: options?.signal,
|
|
apiKey: apiKey || options?.apiKey,
|
|
sessionId: options?.sessionId,
|
|
+ // PATCH: Pass extraParams through to provider-specific API handlers
|
|
+ extraParams: options?.extraParams,
|
|
};
|
|
// Helper to clamp xhigh to high for providers that don't support it
|
|
const clampReasoning = (effort) => (effort === "xhigh" ? "high" : effort);
|