fix(translator): preserve Responses Lite tools across Chat providers
Codex Responses Lite clients routed to a chat-native OpenAI-compatible provider lost tool use in three places: non-streaming Chat responses leaked the raw chat.completion envelope instead of Responses output items, internal reasoning continuity fields leaked into the outbound Chat body causing some upstreams to reject the request, and the Responses to Chat request translator ignored additional_tools, custom_tool_call, and custom_tool_call_output items entirely. Also fixes apiType (chat vs responses) for openai-compatible nodes being resolved from the immutable provider ID instead of the stored node config, so editing a node's API Type had no runtime effect.
This commit is contained in:
80
tests/unit/continuity-strip.test.js
Normal file
80
tests/unit/continuity-strip.test.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/usageDb.js", () => ({
|
||||
appendRequestLog: vi.fn(async () => {}),
|
||||
saveRequestDetail: vi.fn(async () => {}),
|
||||
saveRequestUsage: vi.fn(async () => {})
|
||||
}));
|
||||
|
||||
const { stripContinuityFields } = await import("../../open-sse/handlers/chatCore.js");
|
||||
const { openaiResponsesToOpenAIRequest } = await import("../../open-sse/translator/request/openai-responses.js");
|
||||
|
||||
// Multi-turn Codex-style Responses input: reasoning item carrying a
|
||||
// store=false encrypted_content continuity blob between tool turns.
|
||||
const makeResponsesBody = () => ({
|
||||
model: "x",
|
||||
instructions: "You are Codex.",
|
||||
store: false,
|
||||
include: ["reasoning.encrypted_content"],
|
||||
reasoning: { effort: "low", summary: "auto" },
|
||||
input: [
|
||||
{ role: "user", content: [{ type: "input_text", text: "Run: echo hi" }] },
|
||||
{
|
||||
type: "reasoning",
|
||||
id: "rs_1",
|
||||
summary: [{ type: "summary_text", text: "thinking" }],
|
||||
encrypted_content: "B".repeat(5000)
|
||||
},
|
||||
{ type: "function_call", id: "fc_1", call_id: "call_1", name: "shell", arguments: "{\"command\":\"echo hi\"}" },
|
||||
{ type: "function_call_output", call_id: "call_1", output: "hi" },
|
||||
{ role: "user", content: [{ type: "input_text", text: "Now run: echo bye" }] }
|
||||
],
|
||||
tools: [{
|
||||
type: "function",
|
||||
name: "shell",
|
||||
description: "Run a shell command",
|
||||
parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] }
|
||||
}]
|
||||
});
|
||||
|
||||
describe("stripContinuityFields (outbound boundary)", () => {
|
||||
it("removes continuity blobs from assistant messages", () => {
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "user", content: "hi" },
|
||||
{ role: "assistant", content: null, reasoning_content: "thinking",
|
||||
encrypted_content: "B".repeat(500), reasoning_encrypted_content: "alias",
|
||||
tool_calls: [{ id: "call_1", type: "function", function: { name: "shell", arguments: "{}" } }] }
|
||||
]
|
||||
};
|
||||
stripContinuityFields(body);
|
||||
const assistant = body.messages[1];
|
||||
expect(assistant).not.toHaveProperty("encrypted_content");
|
||||
expect(assistant).not.toHaveProperty("reasoning_encrypted_content");
|
||||
// legitimate fields survive
|
||||
expect(assistant.reasoning_content).toBe("thinking");
|
||||
expect(assistant.tool_calls[0].function.name).toBe("shell");
|
||||
});
|
||||
|
||||
it("is a no-op for bodies without a messages array", () => {
|
||||
const body = { input: [], instructions: "x" };
|
||||
expect(stripContinuityFields(body)).toBe(body);
|
||||
expect(stripContinuityFields(null)).toBe(null);
|
||||
});
|
||||
|
||||
it("end-to-end: Responses multi-turn translation stripped before dispatch", () => {
|
||||
const translated = openaiResponsesToOpenAIRequest("x", makeResponsesBody(), false, {});
|
||||
// The translator stashes the blob for internal round-trip symmetry...
|
||||
const assistantBefore = translated.messages.find((m) => m.role === "assistant");
|
||||
expect(assistantBefore.encrypted_content).toBe("B".repeat(5000));
|
||||
// ...and the outbound boundary removes it before it reaches any upstream.
|
||||
stripContinuityFields(translated);
|
||||
expect(JSON.stringify(translated)).not.toContain("encrypted_content");
|
||||
expect(JSON.stringify(translated)).not.toContain("B".repeat(100));
|
||||
const assistant = translated.messages.find((m) => m.role === "assistant");
|
||||
expect(assistant.reasoning_content).toContain("thinking");
|
||||
expect(assistant.tool_calls[0].function.name).toBe("shell");
|
||||
const roles = translated.messages.map((m) => m.role);
|
||||
expect(roles).toEqual(["system", "user", "assistant", "tool", "user"]);
|
||||
});
|
||||
});
|
||||
76
tests/unit/openai-compatible-apitype-resolution.test.js
Normal file
76
tests/unit/openai-compatible-apitype-resolution.test.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// Locks openai-compatible apiType resolution: the stored apiType on the
|
||||
// connection's providerSpecificData (kept in sync with the node) is
|
||||
// authoritative, and the node-ID substring is only a legacy fallback.
|
||||
//
|
||||
// Regression for: editing a node's apiType to "responses" had no effect because
|
||||
// runtime derived chat/responses from the immutable node ID string
|
||||
// (`openai-compatible-<chat|responses>-<uuid>`) instead of the stored value.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { resolveOpenAICompatibleApiType, getTargetFormat } from "open-sse/services/provider.js";
|
||||
import { DefaultExecutor } from "open-sse/executors/default.js";
|
||||
import { BaseExecutor } from "open-sse/executors/base.js";
|
||||
|
||||
const CHAT_ID = "openai-compatible-chat-3d8d3de8-1206-47ee-a42f-22113a5f2387";
|
||||
const RESPONSES_ID = "openai-compatible-responses-11111111-2222-3333-4444-555555555555";
|
||||
const BASE = "https://api.ericding.io.vn/v1";
|
||||
|
||||
function creds(apiType) {
|
||||
return { providerSpecificData: apiType === undefined ? { baseUrl: BASE } : { baseUrl: BASE, apiType } };
|
||||
}
|
||||
|
||||
describe("resolveOpenAICompatibleApiType", () => {
|
||||
it("prefers stored apiType over the ID substring (edited node on a legacy -chat- ID)", () => {
|
||||
expect(resolveOpenAICompatibleApiType(CHAT_ID, creds("responses"))).toBe("responses");
|
||||
expect(resolveOpenAICompatibleApiType(RESPONSES_ID, creds("chat"))).toBe("chat");
|
||||
});
|
||||
|
||||
it("falls back to the ID substring when apiType is absent", () => {
|
||||
expect(resolveOpenAICompatibleApiType(CHAT_ID, creds(undefined))).toBe("chat");
|
||||
expect(resolveOpenAICompatibleApiType(RESPONSES_ID, creds(undefined))).toBe("responses");
|
||||
expect(resolveOpenAICompatibleApiType(CHAT_ID, null)).toBe("chat");
|
||||
expect(resolveOpenAICompatibleApiType(RESPONSES_ID, null)).toBe("responses");
|
||||
});
|
||||
|
||||
it("ignores an invalid stored apiType and falls back to the ID", () => {
|
||||
expect(resolveOpenAICompatibleApiType(RESPONSES_ID, creds("bogus"))).toBe("responses");
|
||||
expect(resolveOpenAICompatibleApiType(CHAT_ID, creds(""))).toBe("chat");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTargetFormat", () => {
|
||||
it("selects openai-responses when the stored apiType is responses (even on a -chat- ID)", () => {
|
||||
expect(getTargetFormat(CHAT_ID, creds("responses"))).toBe("openai-responses");
|
||||
});
|
||||
|
||||
it("selects openai for chat, and honors stored chat over a -responses- ID", () => {
|
||||
expect(getTargetFormat(CHAT_ID, creds(undefined))).toBe("openai");
|
||||
expect(getTargetFormat(RESPONSES_ID, creds("chat"))).toBe("openai");
|
||||
});
|
||||
|
||||
it("keeps the ID-based fallback when credentials are absent", () => {
|
||||
expect(getTargetFormat(RESPONSES_ID)).toBe("openai-responses");
|
||||
expect(getTargetFormat(CHAT_ID)).toBe("openai");
|
||||
});
|
||||
});
|
||||
|
||||
describe("executor buildUrl endpoint path", () => {
|
||||
for (const [name, Ex] of [["DefaultExecutor", DefaultExecutor], ["BaseExecutor", BaseExecutor]]) {
|
||||
describe(name, () => {
|
||||
const ex = new Ex(CHAT_ID);
|
||||
|
||||
it("routes to /responses when stored apiType is responses, despite the -chat- ID", () => {
|
||||
expect(ex.buildUrl("cx/gpt-5.6-sol", true, 0, creds("responses"))).toBe(`${BASE}/responses`);
|
||||
});
|
||||
|
||||
it("routes to /chat/completions when apiType is chat", () => {
|
||||
expect(ex.buildUrl("cx/gpt-5.6-sol", true, 0, creds("chat"))).toBe(`${BASE}/chat/completions`);
|
||||
});
|
||||
|
||||
it("falls back to the ID substring (legacy) when apiType is absent", () => {
|
||||
expect(ex.buildUrl("cx/gpt-5.6-sol", true, 0, creds(undefined))).toBe(`${BASE}/chat/completions`);
|
||||
const exResp = new Ex(RESPONSES_ID);
|
||||
expect(exResp.buildUrl("m", true, 0, creds(undefined))).toBe(`${BASE}/responses`);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
153
tests/unit/openai-responses-custom-tools.test.js
Normal file
153
tests/unit/openai-responses-custom-tools.test.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
openaiResponsesToOpenAIRequest,
|
||||
} from "../../open-sse/translator/request/openai-responses.js";
|
||||
import { openaiToOpenAIResponsesResponse } from "../../open-sse/translator/response/openai-responses.js";
|
||||
import { initState } from "../../open-sse/translator/index.js";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||
|
||||
const EXEC_TOOL = {
|
||||
type: "custom",
|
||||
name: "exec",
|
||||
description: "Run JavaScript code to orchestrate tool calls.",
|
||||
format: {
|
||||
type: "grammar",
|
||||
syntax: "lark",
|
||||
definition: "start: /(.|\\n)+/",
|
||||
},
|
||||
};
|
||||
|
||||
describe("Codex Responses Lite custom tools → OpenAI Chat", () => {
|
||||
it("promotes additional_tools custom declarations into Chat tools", () => {
|
||||
const out = openaiResponsesToOpenAIRequest("cx/gpt-5.6-sol", {
|
||||
input: [
|
||||
{ type: "additional_tools", role: "developer", tools: [EXEC_TOOL] },
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "Run pwd" }] },
|
||||
],
|
||||
tool_choice: "auto",
|
||||
}, true, null);
|
||||
|
||||
expect(out.tools).toHaveLength(1);
|
||||
expect(out.tools[0]).toMatchObject({
|
||||
type: "function",
|
||||
function: {
|
||||
name: "exec",
|
||||
parameters: {
|
||||
type: "object",
|
||||
required: ["input"],
|
||||
properties: { input: { type: "string" } },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(out._customToolNames).toEqual(["exec"]);
|
||||
expect(out.messages.some((message) => message.role === "developer")).toBe(false);
|
||||
});
|
||||
|
||||
it("translates custom tool call/output history into Chat assistant/tool messages", () => {
|
||||
const program = "const result = await tools.shell({command: 'pwd'});\nreturn result;";
|
||||
const out = openaiResponsesToOpenAIRequest("cx/gpt-5.6-sol", {
|
||||
input: [
|
||||
{ type: "additional_tools", role: "developer", tools: [EXEC_TOOL] },
|
||||
{ type: "custom_tool_call", call_id: "call_exec_1", name: "exec", input: program },
|
||||
{ type: "custom_tool_call_output", call_id: "call_exec_1", output: "/srv/app" },
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "Continue" }] },
|
||||
],
|
||||
}, true, null);
|
||||
|
||||
const assistant = out.messages.find((message) => message.role === "assistant");
|
||||
expect(assistant.tool_calls[0]).toMatchObject({
|
||||
id: "call_exec_1",
|
||||
type: "function",
|
||||
function: { name: "exec" },
|
||||
});
|
||||
expect(JSON.parse(assistant.tool_calls[0].function.arguments)).toEqual({ input: program });
|
||||
expect(out.messages.find((message) => message.role === "tool")).toEqual({
|
||||
role: "tool",
|
||||
tool_call_id: "call_exec_1",
|
||||
content: "/srv/app",
|
||||
});
|
||||
});
|
||||
|
||||
it("merges additional_tools with normal top-level function tools", () => {
|
||||
const out = openaiResponsesToOpenAIRequest("cx/gpt-5.6-sol", {
|
||||
input: [{ type: "additional_tools", role: "developer", tools: [EXEC_TOOL] }],
|
||||
tools: [{ type: "function", name: "search", parameters: { type: "object", properties: {} } }],
|
||||
}, true, null);
|
||||
|
||||
expect(out.tools.map((tool) => tool.function.name)).toEqual(["search", "exec"]);
|
||||
expect(out._customToolNames).toEqual(["exec"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenAI Chat stream → Codex custom_tool_call", () => {
|
||||
it("unwraps the Chat input parameter and emits custom-tool events", () => {
|
||||
const state = initState(FORMATS.OPENAI_RESPONSES);
|
||||
state.customToolNames = new Set(["exec"]);
|
||||
const chunks = [
|
||||
{
|
||||
id: "chatcmpl-custom",
|
||||
choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: "call_exec_2", type: "function", function: { name: "exec", arguments: "" } }] }, finish_reason: null }],
|
||||
},
|
||||
{
|
||||
id: "chatcmpl-custom",
|
||||
choices: [{ index: 0, delta: { tool_calls: [{ index: 0, function: { arguments: "{\"input\":\"const x = await tools.shell({command: 'pwd'});\"}" } }] }, finish_reason: null }],
|
||||
},
|
||||
{ id: "chatcmpl-custom", choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }] },
|
||||
];
|
||||
|
||||
const events = chunks.flatMap((chunk) => openaiToOpenAIResponsesResponse(chunk, state));
|
||||
const added = events.find((event) => event.event === "response.output_item.added");
|
||||
const delta = events.find((event) => event.event === "response.custom_tool_call_input.delta");
|
||||
const done = events.find((event) => event.event === "response.output_item.done");
|
||||
|
||||
expect(added.data.item).toMatchObject({
|
||||
type: "custom_tool_call",
|
||||
call_id: "call_exec_2",
|
||||
name: "exec",
|
||||
input: "",
|
||||
});
|
||||
expect(delta.data.delta).toBe("const x = await tools.shell({command: 'pwd'});");
|
||||
expect(done.data.item).toMatchObject({
|
||||
type: "custom_tool_call",
|
||||
call_id: "call_exec_2",
|
||||
name: "exec",
|
||||
input: "const x = await tools.shell({command: 'pwd'});",
|
||||
});
|
||||
expect(events.some((event) => event.event === "response.function_call_arguments.delta")).toBe(false);
|
||||
});
|
||||
|
||||
it("waits for the function name when id and name arrive in separate chunks", () => {
|
||||
const state = initState(FORMATS.OPENAI_RESPONSES);
|
||||
state.customToolNames = new Set(["exec"]);
|
||||
const chunks = [
|
||||
{ id: "chatcmpl-split", choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: "call_split", type: "function", function: { arguments: "" } }] }, finish_reason: null }] },
|
||||
{ id: "chatcmpl-split", choices: [{ index: 0, delta: { tool_calls: [{ index: 0, function: { name: "exec", arguments: "{\"input\":\"return 1;\"}" } }] }, finish_reason: null }] },
|
||||
{ id: "chatcmpl-split", choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }] },
|
||||
];
|
||||
|
||||
const events = chunks.flatMap((chunk) => openaiToOpenAIResponsesResponse(chunk, state));
|
||||
const added = events.filter((event) => event.event === "response.output_item.added");
|
||||
expect(added).toHaveLength(1);
|
||||
expect(added[0].data.item).toMatchObject({
|
||||
type: "custom_tool_call",
|
||||
call_id: "call_split",
|
||||
name: "exec",
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves normal Chat tool calls as Responses function_call events", () => {
|
||||
const state = initState(FORMATS.OPENAI_RESPONSES);
|
||||
state.customToolNames = new Set(["exec"]);
|
||||
const events = [
|
||||
{ id: "chatcmpl-normal", choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: "call_search", type: "function", function: { name: "search", arguments: "{\"q\":\"x\"}" } }] }, finish_reason: null }] },
|
||||
{ id: "chatcmpl-normal", choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }] },
|
||||
].flatMap((chunk) => openaiToOpenAIResponsesResponse(chunk, state));
|
||||
|
||||
expect(events.find((event) => event.event === "response.output_item.added").data.item.type).toBe("function_call");
|
||||
expect(events.find((event) => event.event === "response.output_item.done").data.item).toMatchObject({
|
||||
type: "function_call",
|
||||
name: "search",
|
||||
arguments: "{\"q\":\"x\"}",
|
||||
});
|
||||
});
|
||||
});
|
||||
148
tests/unit/openai-responses-nonstream.test.js
Normal file
148
tests/unit/openai-responses-nonstream.test.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/usageDb.js", () => ({
|
||||
appendRequestLog: vi.fn(async () => {}),
|
||||
saveRequestDetail: vi.fn(async () => {}),
|
||||
saveRequestUsage: vi.fn(async () => {})
|
||||
}));
|
||||
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.js");
|
||||
const { translateNonStreamingResponse } = await import("../../open-sse/handlers/chatCore/nonStreamingHandler.js");
|
||||
const { handleForcedSSEToJson } = await import("../../open-sse/handlers/chatCore/sseToJsonHandler.js");
|
||||
|
||||
// A chat.completion body as returned by a chat-native upstream (e.g. op-ericding)
|
||||
const CHAT_TOOL_BODY = {
|
||||
id: "chatcmpl-abc123",
|
||||
object: "chat.completion",
|
||||
created: 1700000000,
|
||||
model: "cl/claude-haiku-4-5",
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [{ id: "call_1", type: "function", function: { name: "shell", arguments: "{\"cmd\":\"ls\"}" } }]
|
||||
},
|
||||
finish_reason: "tool_calls"
|
||||
}],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
||||
};
|
||||
|
||||
describe("non-stream Chat upstream for a Responses-API client (op-ericding bug)", () => {
|
||||
it("translates chat.completion tool_calls into Responses function_call output", () => {
|
||||
// translateNonStreamingResponse(body, targetFormat=PROVIDER format, sourceFormat=CLIENT format)
|
||||
const out = translateNonStreamingResponse(CHAT_TOOL_BODY, FORMATS.OPENAI, FORMATS.OPENAI_RESPONSES);
|
||||
expect(out.object).toBe("response");
|
||||
expect(out).not.toHaveProperty("choices");
|
||||
const fc = (out.output || []).find((o) => o.type === "function_call");
|
||||
expect(fc).toBeTruthy();
|
||||
expect(fc.call_id).toBe("call_1");
|
||||
expect(fc.name).toBe("shell");
|
||||
expect(fc.arguments).toBe("{\"cmd\":\"ls\"}");
|
||||
});
|
||||
|
||||
it("translates marked Chat tools into Responses custom_tool_call output", () => {
|
||||
const customBody = structuredClone(CHAT_TOOL_BODY);
|
||||
customBody.choices[0].message.tool_calls[0] = {
|
||||
id: "call_exec",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "exec",
|
||||
arguments: "{\"input\":\"return await tools.shell({command: 'pwd'});\"}"
|
||||
}
|
||||
};
|
||||
const out = translateNonStreamingResponse(
|
||||
customBody,
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI_RESPONSES,
|
||||
new Set(["exec"])
|
||||
);
|
||||
const call = (out.output || []).find((item) => item.type === "custom_tool_call");
|
||||
expect(call).toMatchObject({
|
||||
call_id: "call_exec",
|
||||
name: "exec",
|
||||
input: "return await tools.shell({command: 'pwd'});"
|
||||
});
|
||||
expect(out.output.some((item) => item.type === "function_call")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps chat.completion text content as a Responses message item", () => {
|
||||
const body = {
|
||||
...CHAT_TOOL_BODY,
|
||||
choices: [{ index: 0, message: { role: "assistant", content: "hello" }, finish_reason: "stop" }]
|
||||
};
|
||||
const out = translateNonStreamingResponse(body, FORMATS.OPENAI, FORMATS.OPENAI_RESPONSES);
|
||||
const msg = (out.output || []).find((o) => o.type === "message");
|
||||
expect(msg).toBeTruthy();
|
||||
expect(msg.content[0].type).toBe("output_text");
|
||||
expect(msg.content[0].text).toBe("hello");
|
||||
});
|
||||
|
||||
it("leaves chat->chat untouched", () => {
|
||||
const out = translateNonStreamingResponse(CHAT_TOOL_BODY, FORMATS.OPENAI, FORMATS.OPENAI);
|
||||
expect(out.object).toBe("chat.completion");
|
||||
expect(out.choices[0].message.tool_calls[0].function.name).toBe("shell");
|
||||
});
|
||||
});
|
||||
|
||||
describe("forced-SSE JSON path for a Responses-API client behind a chat upstream", () => {
|
||||
const sseCtx = (sourceFormat, targetFormat) => {
|
||||
const encoder = new TextEncoder();
|
||||
const raw = [
|
||||
'data: {"id":"chatcmpl-sse","object":"chat.completion.chunk","created":1700000000,"model":"gpt-x","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_9","type":"function","function":{"name":"shell","arguments":""}}]},"finish_reason":null}]}',
|
||||
'data: {"id":"chatcmpl-sse","object":"chat.completion.chunk","created":1700000000,"model":"gpt-x","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"cmd\\":\\"pwd\\"}"}}]},"finish_reason":null}]}',
|
||||
'data: {"id":"chatcmpl-sse","object":"chat.completion.chunk","created":1700000000,"model":"gpt-x","choices":[{"delta":{},"finish_reason":"tool_calls"}]}',
|
||||
"data: [DONE]",
|
||||
""
|
||||
].join("\n\n");
|
||||
return {
|
||||
providerResponse: new Response(new ReadableStream({
|
||||
start(controller) { controller.enqueue(encoder.encode(raw)); controller.close(); }
|
||||
}), { headers: { "content-type": "text/event-stream" } }),
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
provider: "op-test-chat",
|
||||
model: "gpt-x",
|
||||
body: { model: "gpt-x", messages: [] },
|
||||
stream: false,
|
||||
requestStartTime: Date.now(),
|
||||
connectionId: "test-connection",
|
||||
clientRawRequest: { endpoint: "/v1/responses" },
|
||||
trackDone: vi.fn(),
|
||||
appendLog: vi.fn()
|
||||
};
|
||||
};
|
||||
|
||||
it("parses chat SSE chunks and returns a Responses function_call body", async () => {
|
||||
const result = await handleForcedSSEToJson(sseCtx(FORMATS.OPENAI_RESPONSES, FORMATS.OPENAI));
|
||||
expect(result.success).toBe(true);
|
||||
const json = await result.response.json();
|
||||
expect(json.object).toBe("response");
|
||||
const fc = (json.output || []).find((o) => o.type === "function_call");
|
||||
expect(fc).toBeTruthy();
|
||||
expect(fc.name).toBe("shell");
|
||||
expect(fc.arguments).toBe("{\"cmd\":\"pwd\"}");
|
||||
});
|
||||
|
||||
it("returns a custom_tool_call for a marked tool", async () => {
|
||||
const ctx = sseCtx(FORMATS.OPENAI_RESPONSES, FORMATS.OPENAI);
|
||||
ctx.customToolNames = new Set(["shell"]);
|
||||
const result = await handleForcedSSEToJson(ctx);
|
||||
expect(result.success).toBe(true);
|
||||
const json = await result.response.json();
|
||||
const call = (json.output || []).find((item) => item.type === "custom_tool_call");
|
||||
expect(call).toMatchObject({
|
||||
call_id: "call_9",
|
||||
name: "shell",
|
||||
input: "{\"cmd\":\"pwd\"}"
|
||||
});
|
||||
});
|
||||
|
||||
it("still returns chat.completion for a plain chat client", async () => {
|
||||
const result = await handleForcedSSEToJson(sseCtx(FORMATS.OPENAI, FORMATS.OPENAI));
|
||||
expect(result.success).toBe(true);
|
||||
const json = await result.response.json();
|
||||
expect(json.object).toBe("chat.completion");
|
||||
expect(json.choices[0].message.tool_calls[0].function.name).toBe("shell");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user