Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 13 additions & 17 deletions packages/openai-adapters/src/apis/openaiResponses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
ResponseInputImage,
ResponseInputText,
ResponseOutputItem,
ResponseOutputMessage,
ResponseOutputRefusal,
ResponseOutputText,
ResponseReasoningSummaryTextDeltaEvent,
Expand Down Expand Up @@ -288,7 +287,6 @@ export function toResponsesInput(
messages: ChatCompletionMessageParam[],
): ResponseInput {
const inputItems: ResponseInput = [];
let assistantMessageCounter = 0;

for (const message of messages) {
if (message.role === "tool") {
Expand Down Expand Up @@ -337,18 +335,19 @@ export function toResponsesInput(
assistantMessage.refusal ?? null,
);
if (assistantContentParts.length > 0) {
const providedId = (message as any).id;
const assistantId =
typeof providedId === "string" && providedId.startsWith("msg_")
? providedId
: `msg_${(assistantMessageCounter++).toString().padStart(4, "0")}`;
// Chat Completions history does not carry the Responses reasoning item
// associated with an output message. Replay it as an easy input message
// without inventing a msg_ ID that would require that missing reasoning.
const content = assistantContentParts
.map((part) =>
part.type === "output_text" ? part.text : part.refusal,
)
.join("");
inputItems.push({
type: "message",
role: "assistant",
content: assistantContentParts,
id: assistantId,
status: "completed",
} as ResponseOutputMessage as any);
content,
});
}
if (assistantMessage.tool_calls?.length) {
assistantMessage.tool_calls.forEach((toolCall, index) => {
Expand All @@ -360,12 +359,9 @@ export function toResponsesInput(
name: toolCall.function.name ?? "",
arguments: toolCall.function.arguments ?? "{}",
};
if (
typeof toolCall.id === "string" &&
toolCall.id.startsWith("fc_")
) {
functionCall.id = toolCall.id;
}
// Chat Completions exposes only the call ID, not the Responses
// output-item ID. Reusing it as `id` can make the Responses API
// require a reasoning item that this representation cannot carry.
inputItems.push(functionCall);
}
});
Expand Down
37 changes: 11 additions & 26 deletions packages/openai-adapters/src/test/openai-responses.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from "../apis/openaiResponses.js";

describe("toResponsesInput", () => {
it("maps assistant text content to output_text with generated msg ids", () => {
it("does not invent Responses output item ids for assistant history", () => {
const messages: ChatCompletionMessageParam[] = [
{
role: "assistant",
Expand All @@ -40,14 +40,9 @@ describe("toResponsesInput", () => {
expect(assistant).toMatchObject({
type: "message",
role: "assistant",
id: "msg_0000",
});
expect(assistant.content).toMatchObject([
{
type: "output_text",
text: "Hello there!",
},
]);
expect(assistant).not.toHaveProperty("id");
expect(assistant.content).toBe("Hello there!");
});

it("maps assistant refusal content to refusal output items", () => {
Expand All @@ -63,12 +58,7 @@ describe("toResponsesInput", () => {

expect(inputItems).toHaveLength(1);
const assistant = inputItems[0] as any;
expect(assistant.content).toEqual([
{
type: "refusal",
refusal: "I must decline.",
},
]);
expect(assistant.content).toBe("I must decline.");
});

it("converts assistant structured content into output_text items", () => {
Expand All @@ -82,12 +72,7 @@ describe("toResponsesInput", () => {
const inputItems = toResponsesInput(messages);

const assistant = inputItems[0] as any;
expect(assistant.content).toMatchObject([
{
type: "output_text",
text: "Structured hello.",
},
]);
expect(assistant.content).toBe("Structured hello.");
});

it("converts chat messages, multimodal content, and tool interactions into Responses input items", () => {
Expand All @@ -107,7 +92,7 @@ describe("toResponsesInput", () => {
role: "assistant",
tool_calls: [
{
id: "fc_call_1",
id: "call_1",
type: "function",
function: {
name: "searchDocs",
Expand Down Expand Up @@ -146,8 +131,7 @@ describe("toResponsesInput", () => {
},
{
type: "function_call",
call_id: "fc_call_1",
id: "fc_call_1",
call_id: "call_1",
name: "searchDocs",
arguments: '{"query":"vitest expectations"}',
},
Expand All @@ -157,16 +141,17 @@ describe("toResponsesInput", () => {
output: "Found 3 relevant documents.",
},
]);
expect(inputItems[2]).not.toHaveProperty("id");
});
});

it("omits function_call id when tool call id lacks fc_ prefix", () => {
it("does not treat a Chat Completions tool call id as a Responses item id", () => {
const messages: ChatCompletionMessageParam[] = [
{
role: "assistant",
tool_calls: [
{
id: "call_custom",
id: "fc_custom",
type: "function",
function: {
name: "lookup",
Expand All @@ -184,7 +169,7 @@ it("omits function_call id when tool call id lacks fc_ prefix", () => {
) as any;

expect(functionCall).toBeTruthy();
expect(functionCall.call_id).toBe("call_custom");
expect(functionCall.call_id).toBe("fc_custom");
expect(functionCall).not.toHaveProperty("id");
});

Expand Down
Loading