164 lines
5.7 KiB
TypeScript
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");
|
|
});
|