fix: cap tool call IDs for OpenAI/OpenRouter (#875) (thanks @j1philli)
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
### Fixes
|
### Fixes
|
||||||
- Browser: add tests for snapshot labels/efficient query params and labeled image responses.
|
- Browser: add tests for snapshot labels/efficient query params and labeled image responses.
|
||||||
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
|
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
|
||||||
|
- Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.
|
||||||
- Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.
|
- Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.
|
||||||
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
|
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
|
||||||
- Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.
|
- Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ export function downgradeGeminiThinkingBlocks(messages: AgentMessage[]): AgentMe
|
|||||||
|
|
||||||
// Gemini rejects thinking blocks that lack a signature; downgrade to text for safety.
|
// Gemini rejects thinking blocks that lack a signature; downgrade to text for safety.
|
||||||
let hasDowngraded = false;
|
let hasDowngraded = false;
|
||||||
const nextContent = assistantMsg.content.flatMap((block) => {
|
type AssistantContentBlock = (typeof assistantMsg.content)[number];
|
||||||
if (!block || typeof block !== "object") return [block];
|
const nextContent = assistantMsg.content.flatMap((block): AssistantContentBlock[] => {
|
||||||
|
if (!block || typeof block !== "object") return [block as AssistantContentBlock];
|
||||||
const record = block as GeminiThinkingBlock;
|
const record = block as GeminiThinkingBlock;
|
||||||
if (record.type !== "thinking") return [block];
|
if (record.type !== "thinking") return [block];
|
||||||
const signature =
|
const signature =
|
||||||
@@ -63,6 +64,7 @@ export function downgradeGeminiThinkingBlocks(messages: AgentMessage[]): AgentMe
|
|||||||
const trimmed = thinking.trim();
|
const trimmed = thinking.trim();
|
||||||
hasDowngraded = true;
|
hasDowngraded = true;
|
||||||
if (!trimmed) return [];
|
if (!trimmed) return [];
|
||||||
|
<<<<<<< HEAD
|
||||||
return [{ type: "text" as const, text: thinking }];
|
return [{ type: "text" as const, text: thinking }];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -170,4 +170,36 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
|||||||
};
|
};
|
||||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sanitizes tool call ids for OpenAI-compatible APIs", async () => {
|
||||||
|
const sessionManager = SessionManager.inMemory();
|
||||||
|
const longId = `call_${"a".repeat(60)}`;
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "toolCall", id: longId, name: "read", arguments: {} }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: longId,
|
||||||
|
toolName: "read",
|
||||||
|
content: [{ type: "text", text: "ok" }],
|
||||||
|
},
|
||||||
|
] satisfies AgentMessage[];
|
||||||
|
|
||||||
|
const out = await sanitizeSessionHistory({
|
||||||
|
messages: input,
|
||||||
|
modelApi: "openai-responses",
|
||||||
|
sessionManager,
|
||||||
|
sessionId: "session:openai",
|
||||||
|
});
|
||||||
|
|
||||||
|
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
|
||||||
|
const toolCall = assistant.content?.[0] as { id?: string };
|
||||||
|
expect(toolCall.id).toBeDefined();
|
||||||
|
expect(toolCall.id?.length).toBeLessThanOrEqual(40);
|
||||||
|
|
||||||
|
const toolResult = out[1] as Extract<AgentMessage, { role: "toolResult" }>;
|
||||||
|
expect(toolResult.toolCallId).toBe(toolCall.id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,6 +37,17 @@ const GOOGLE_SCHEMA_UNSUPPORTED_KEYWORDS = new Set([
|
|||||||
"minProperties",
|
"minProperties",
|
||||||
"maxProperties",
|
"maxProperties",
|
||||||
]);
|
]);
|
||||||
|
const OPENAI_TOOL_CALL_ID_APIS = new Set([
|
||||||
|
"openai",
|
||||||
|
"openai-completions",
|
||||||
|
"openai-responses",
|
||||||
|
"openai-codex-responses",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function shouldSanitizeToolCallIds(modelApi?: string | null): boolean {
|
||||||
|
if (!modelApi) return false;
|
||||||
|
return isGoogleModelApi(modelApi) || OPENAI_TOOL_CALL_ID_APIS.has(modelApi);
|
||||||
|
}
|
||||||
|
|
||||||
function findUnsupportedSchemaKeywords(schema: unknown, path: string): string[] {
|
function findUnsupportedSchemaKeywords(schema: unknown, path: string): string[] {
|
||||||
if (!schema || typeof schema !== "object") return [];
|
if (!schema || typeof schema !== "object") return [];
|
||||||
@@ -145,7 +156,7 @@ export async function sanitizeSessionHistory(params: {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
}): Promise<AgentMessage[]> {
|
}): Promise<AgentMessage[]> {
|
||||||
const sanitizedImages = await sanitizeSessionMessagesImages(params.messages, "session:history", {
|
const sanitizedImages = await sanitizeSessionMessagesImages(params.messages, "session:history", {
|
||||||
sanitizeToolCallIds: isGoogleModelApi(params.modelApi),
|
sanitizeToolCallIds: shouldSanitizeToolCallIds(params.modelApi),
|
||||||
enforceToolCallLast: params.modelApi === "anthropic-messages",
|
enforceToolCallLast: params.modelApi === "anthropic-messages",
|
||||||
});
|
});
|
||||||
const repairedTools = sanitizeToolUseResultPairing(sanitizedImages);
|
const repairedTools = sanitizeToolUseResultPairing(sanitizedImages);
|
||||||
|
|||||||
@@ -65,4 +65,48 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
|||||||
expect(r1.toolCallId).toBe(a.id);
|
expect(r1.toolCallId).toBe(a.id);
|
||||||
expect(r2.toolCallId).toBe(b.id);
|
expect(r2.toolCallId).toBe(b.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("caps tool call IDs at 40 chars while preserving uniqueness", () => {
|
||||||
|
const longA = `call_${"a".repeat(60)}`;
|
||||||
|
const longB = `call_${"a".repeat(59)}b`;
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{ type: "toolCall", id: longA, name: "read", arguments: {} },
|
||||||
|
{ type: "toolCall", id: longB, name: "read", arguments: {} },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: longA,
|
||||||
|
toolName: "read",
|
||||||
|
content: [{ type: "text", text: "one" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: longB,
|
||||||
|
toolName: "read",
|
||||||
|
content: [{ type: "text", text: "two" }],
|
||||||
|
},
|
||||||
|
] satisfies AgentMessage[];
|
||||||
|
|
||||||
|
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
|
||||||
|
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
|
||||||
|
const a = assistant.content?.[0] as { id?: string };
|
||||||
|
const b = assistant.content?.[1] as { id?: string };
|
||||||
|
|
||||||
|
expect(typeof a.id).toBe("string");
|
||||||
|
expect(typeof b.id).toBe("string");
|
||||||
|
expect(a.id).not.toBe(b.id);
|
||||||
|
expect(a.id?.length).toBeLessThanOrEqual(40);
|
||||||
|
expect(b.id?.length).toBeLessThanOrEqual(40);
|
||||||
|
expect(isValidCloudCodeAssistToolId(a.id as string)).toBe(true);
|
||||||
|
expect(isValidCloudCodeAssistToolId(b.id as string)).toBe(true);
|
||||||
|
|
||||||
|
const r1 = out[1] as Extract<AgentMessage, { role: "toolResult" }>;
|
||||||
|
const r2 = out[2] as Extract<AgentMessage, { role: "toolResult" }>;
|
||||||
|
expect(r1.toolCallId).toBe(a.id);
|
||||||
|
expect(r2.toolCallId).toBe(b.id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user