mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 07:03:25 +00:00
test(ai): add failing test for orphaned function_call without reasoning item
Reproduces issue #886 where function_call is sent without its required paired reasoning item, causing Azure/OpenAI 400 error.
This commit is contained in:
parent
b7cef51f3f
commit
de58391085
1 changed files with 98 additions and 1 deletions
|
|
@ -2,7 +2,7 @@ import { Type } from "@sinclair/typebox";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getModel } from "../src/models.js";
|
import { getModel } from "../src/models.js";
|
||||||
import { complete, getEnvApiKey } from "../src/stream.js";
|
import { complete, getEnvApiKey } from "../src/stream.js";
|
||||||
import type { AssistantMessage, Context, Message, Tool } from "../src/types.js";
|
import type { AssistantMessage, Context, Message, ThinkingContent, Tool, ToolCall } from "../src/types.js";
|
||||||
|
|
||||||
const testToolSchema = Type.Object({
|
const testToolSchema = Type.Object({
|
||||||
value: Type.Number({ description: "A number to double" }),
|
value: Type.Number({ description: "A number to double" }),
|
||||||
|
|
@ -78,4 +78,101 @@ describe.skipIf(!process.env.OPENAI_API_KEY)("OpenAI Responses reasoning replay
|
||||||
// Model should respond (text or tool call)
|
// Model should respond (text or tool call)
|
||||||
expect(response.content.length).toBeGreaterThan(0);
|
expect(response.content.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("drops orphaned tool calls when reasoning signature is missing", { retry: 2 }, async () => {
|
||||||
|
// This tests the scenario where:
|
||||||
|
// 1. A completed turn has reasoning + function_call
|
||||||
|
// 2. The thinking signature gets lost (e.g., cross-provider handoff, isSameModel=false filtering)
|
||||||
|
// 3. The toolCall remains but reasoning is gone
|
||||||
|
// 4. Without the fix: Azure/OpenAI returns 400 "function_call without required reasoning item"
|
||||||
|
// 5. With the fix: orphaned toolCalls are dropped, conversation continues
|
||||||
|
|
||||||
|
const model = getModel("openai", "gpt-5-mini");
|
||||||
|
|
||||||
|
const apiKey = getEnvApiKey("openai");
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error("Missing OPENAI_API_KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
role: "user",
|
||||||
|
content: "Use the double_number tool to double 21.",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get a real response with reasoning + tool call
|
||||||
|
const assistantResponse = await complete(
|
||||||
|
model,
|
||||||
|
{
|
||||||
|
systemPrompt: "You are a helpful assistant. Always use the tool when asked.",
|
||||||
|
messages: [userMessage],
|
||||||
|
tools: [testTool],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiKey,
|
||||||
|
reasoningEffort: "high",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const thinkingBlock = assistantResponse.content.find(
|
||||||
|
(block) => block.type === "thinking" && block.thinkingSignature,
|
||||||
|
) as ThinkingContent | undefined;
|
||||||
|
const toolCallBlock = assistantResponse.content.find((block) => block.type === "toolCall") as
|
||||||
|
| ToolCall
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (!thinkingBlock) {
|
||||||
|
throw new Error("Missing thinking block from OpenAI Responses");
|
||||||
|
}
|
||||||
|
if (!toolCallBlock) {
|
||||||
|
throw new Error("Missing tool call from OpenAI Responses - model did not use the tool");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate corruption: keep toolCall but strip thinkingSignature
|
||||||
|
// This mimics what happens when isSameModel=false and thinking text is empty
|
||||||
|
const corruptedThinking: ThinkingContent = {
|
||||||
|
type: "thinking",
|
||||||
|
thinking: thinkingBlock.thinking,
|
||||||
|
// thinkingSignature intentionally omitted - simulates it being lost
|
||||||
|
};
|
||||||
|
|
||||||
|
const corruptedAssistant: AssistantMessage = {
|
||||||
|
...assistantResponse,
|
||||||
|
content: [corruptedThinking, toolCallBlock],
|
||||||
|
stopReason: "toolUse", // Completed successfully, not aborted
|
||||||
|
};
|
||||||
|
|
||||||
|
// Provide a tool result to continue the conversation
|
||||||
|
const toolResult: Message = {
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: toolCallBlock.id,
|
||||||
|
toolName: toolCallBlock.name,
|
||||||
|
content: [{ type: "text", text: "42" }],
|
||||||
|
isError: false,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const followUp: Message = {
|
||||||
|
role: "user",
|
||||||
|
content: "What was the result?",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const context: Context = {
|
||||||
|
systemPrompt: "You are a helpful assistant.",
|
||||||
|
messages: [userMessage, corruptedAssistant, toolResult, followUp],
|
||||||
|
tools: [testTool],
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await complete(model, context, {
|
||||||
|
apiKey,
|
||||||
|
reasoningEffort: "high",
|
||||||
|
});
|
||||||
|
|
||||||
|
// The key assertion: no 400 error from orphaned function_call
|
||||||
|
// Error would be: "function_call was provided without its required reasoning item"
|
||||||
|
expect(response.stopReason, `Error: ${response.errorMessage}`).not.toBe("error");
|
||||||
|
expect(response.errorMessage).toBeFalsy();
|
||||||
|
expect(response.content.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue