This commit is contained in:
Ayush Ojha 2026-01-30 23:54:09 +08:00 committed by GitHub
commit 2ef0d49be8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 95 additions and 2 deletions

View File

@ -206,6 +206,52 @@ describe("sanitizeSessionHistory", () => {
expect(result).toEqual(messages);
});
it("normalizes toolCall blocks missing arguments to have empty object", async () => {
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
const messages: AgentMessage[] = [
{
role: "assistant",
content: [
// Missing arguments field entirely (the bug in #4345)
{ type: "toolCall", id: "call_1", name: "session_status" } as never,
{ type: "toolCall", id: "call_2", name: "sessions_list", arguments: { limit: 10 } },
],
},
// Include matching tool results so repair doesn't add synthetic ones
{
role: "toolResult",
toolCallId: "call_1",
toolName: "session_status",
content: [{ type: "text", text: "ok" }],
} as AgentMessage,
{
role: "toolResult",
toolCallId: "call_2",
toolName: "sessions_list",
content: [{ type: "text", text: "ok" }],
} as AgentMessage,
];
const result = await sanitizeSessionHistory({
messages,
modelApi: "anthropic-messages",
provider: "anthropic",
sessionManager: mockSessionManager,
sessionId: "test-session",
});
expect(result).toHaveLength(3);
const assistant = result[0] as {
role: string;
content: Array<{ type: string; arguments?: unknown }>;
};
// The first tool call should now have arguments: {}
expect(assistant.content[0]).toHaveProperty("arguments", {});
// The second tool call should keep its original arguments
expect(assistant.content[1]).toHaveProperty("arguments", { limit: 10 });
});
it("downgrades openai reasoning only when the model changes", async () => {
const sessionEntries: Array<{ type: string; customType: string; data: unknown }> = [
{

View File

@ -336,6 +336,10 @@ export async function sanitizeSessionHistory(params: {
? sanitizeToolUseResultPairing(sanitizedThinking)
: sanitizedThinking;
// Ensure every toolCall/toolUse block has an arguments/input field (default {}).
// Some models omit the field for optional-args tools, which causes API rejections.
const normalizedToolArgs = normalizeToolCallArguments(repairedTools);
const isOpenAIResponsesApi =
params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses";
const hasSnapshot = Boolean(params.provider || params.modelApi || params.modelId);
@ -350,8 +354,8 @@ export async function sanitizeSessionHistory(params: {
: false;
const sanitizedOpenAI =
isOpenAIResponsesApi && modelChanged
? downgradeOpenAIReasoningBlocks(repairedTools)
: repairedTools;
? downgradeOpenAIReasoningBlocks(normalizedToolArgs)
: normalizedToolArgs;
if (hasSnapshot && (!priorSnapshot || modelChanged)) {
appendModelSnapshot(params.sessionManager, {
@ -373,3 +377,46 @@ export async function sanitizeSessionHistory(params: {
sessionId: params.sessionId,
}).messages;
}
/**
* Ensure every toolCall/toolUse block has its arguments/input field.
* Some models omit the field entirely for tools with only optional parameters,
* which causes API rejections ("input: Field required").
*/
function normalizeToolCallArguments(messages: AgentMessage[]): AgentMessage[] {
let changed = false;
const out: AgentMessage[] = [];
for (const msg of messages) {
if (!msg || typeof msg !== "object" || (msg as { role?: unknown }).role !== "assistant") {
out.push(msg);
continue;
}
const assistant = msg as Extract<AgentMessage, { role: "assistant" }>;
const content = assistant.content;
if (!Array.isArray(content)) {
out.push(msg);
continue;
}
let blockChanged = false;
const nextContent = content.map((block) => {
if (!block || typeof block !== "object") return block;
const rec = block as unknown as Record<string, unknown>;
if (rec.type === "toolCall" && rec.arguments === undefined) {
blockChanged = true;
return { ...rec, arguments: {} };
}
if (rec.type === "toolUse" && rec.input === undefined) {
blockChanged = true;
return { ...rec, input: {} };
}
return block;
});
if (blockChanged) {
changed = true;
out.push({ ...assistant, content: nextContent } as AgentMessage);
} else {
out.push(msg);
}
}
return changed ? out : messages;
}