Merge 25989c1e91 into 09be5d45d5
This commit is contained in:
commit
2ef0d49be8
@ -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 }> = [
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user