Files
diegosouzapw--omniroute/tests/unit/translator-openai-responses-custom-tool-1007.test.ts
2026-07-13 13:39:12 +08:00

164 lines
5.7 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const { openaiResponsesToOpenAIRequest } =
await import("../../open-sse/translator/request/openai-responses.ts");
const { openaiToOpenAIResponsesResponse } =
await import("../../open-sse/translator/response/openai-responses.ts");
const { initState } = await import("../../open-sse/translator/index.ts");
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
function collectEvents(chunks) {
const state = initState(FORMATS.OPENAI_RESPONSES);
const events = [];
for (const chunk of chunks) {
const result = openaiToOpenAIResponsesResponse(chunk, state);
if (result) events.push(...result);
}
return events;
}
// Request side: a Codex custom/freeform tool (type:"custom", no `parameters`) must be
// normalized to a { input: string } function schema — NOT an empty function schema.
test("Responses -> Chat: custom tool is normalized to a { input: string } function schema (#1007)", () => {
const result = openaiResponsesToOpenAIRequest(
"gpt-5.3-codex",
{
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }],
tools: [
{
type: "custom",
name: "apply_patch",
description: "Apply a code patch",
format: { type: "grammar", syntax: "lark", definition: "..." },
},
],
},
false,
{}
);
assert.equal(Array.isArray(result.tools), true);
const tool = result.tools[0];
assert.equal(tool.type, "function");
assert.equal(tool.function.name, "apply_patch");
// The regression: without normalization, parameters is undefined / empty and the model
// invokes apply_patch with {}, breaking the Codex runtime.
assert.deepEqual(tool.function.parameters, {
type: "object",
properties: { input: { type: "string" } },
required: ["input"],
additionalProperties: false,
});
});
// Request side: custom_tool_call / custom_tool_call_output input items round-trip.
test("Responses -> Chat: custom_tool_call + output items map to tool_calls and tool role (#1007)", () => {
const result = openaiResponsesToOpenAIRequest(
"gpt-5.3-codex",
{
input: [
{ type: "message", role: "user", content: [{ type: "input_text", text: "patch it" }] },
{
type: "custom_tool_call",
call_id: "call_patch_1",
name: "apply_patch",
input: "*** Begin Patch\n*** End Patch",
},
{
type: "custom_tool_call_output",
call_id: "call_patch_1",
output: '{"output":"applied","metadata":{"ok":true}}',
},
],
},
false,
{}
);
const assistant = result.messages.find(
(m) => m.role === "assistant" && Array.isArray(m.tool_calls)
);
assert.ok(assistant, "expected an assistant message carrying the custom tool call");
const tc = assistant.tool_calls[0];
assert.equal(tc.id, "call_patch_1");
assert.equal(tc.type, "function");
assert.equal(tc.function.name, "apply_patch");
assert.deepEqual(JSON.parse(tc.function.arguments), {
input: "*** Begin Patch\n*** End Patch",
});
const toolMsg = result.messages.find((m) => m.role === "tool");
assert.ok(toolMsg, "expected a tool result message");
assert.equal(toolMsg.tool_call_id, "call_patch_1");
// JSON-wrapped {"output":...} is unwrapped to the plain string.
assert.equal(toolMsg.content, "applied");
});
// Response side: an apply_patch tool call must stream as custom_tool_call_input.* events
// and the raw patch string is unwrapped from the {"input":"..."} JSON the model produced.
test("OpenAI -> Responses: apply_patch streams as custom_tool_call with raw input (#1007)", () => {
const events = collectEvents([
{
id: "chatcmpl-1",
model: "gpt-5.3-codex",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: "call_1",
type: "function",
function: { name: "apply_patch", arguments: '{"input":"PATCH' },
},
],
},
finish_reason: null,
},
],
},
{
id: "chatcmpl-1",
model: "gpt-5.3-codex",
choices: [
{
index: 0,
delta: { tool_calls: [{ index: 0, function: { arguments: '_BODY"}' } }] },
finish_reason: "tool_calls",
},
],
},
]);
const added = events.find((e) => e.event === "response.output_item.added");
assert.ok(added);
assert.equal(added.data.item.type, "custom_tool_call");
assert.equal(added.data.item.name, "apply_patch");
assert.ok(
events.some((e) => e.event === "response.custom_tool_call_input.delta"),
"expected a custom_tool_call_input.delta event"
);
// No function_call_arguments.* events should leak for a custom tool.
assert.ok(!events.some((e) => e.event === "response.function_call_arguments.delta"));
assert.ok(!events.some((e) => e.event === "response.function_call_arguments.done"));
const inputDone = events.find((e) => e.event === "response.custom_tool_call_input.done");
assert.ok(inputDone);
assert.equal(inputDone.data.input, "PATCH_BODY");
const itemDone = events.find(
(e) => e.event === "response.output_item.done" && e.data.item.type === "custom_tool_call"
);
assert.ok(itemDone);
assert.equal(itemDone.data.item.input, "PATCH_BODY");
const completed = events.find((e) => e.event === "response.completed");
assert.ok(completed);
const customItem = completed.data.response.output.find((o) => o.type === "custom_tool_call");
assert.ok(customItem, "final snapshot should carry the custom_tool_call item");
assert.equal(customItem.input, "PATCH_BODY");
});