Skip to content

Commit 52321e8

Browse files
committed
fix: CR round 1 — missing reasoning item_id, multi-fco assistantCount inflation, e2e turnIndex test
1 parent cdab8af commit 52321e8

2 files changed

Lines changed: 154 additions & 8 deletions

File tree

src/__tests__/responses.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,6 +1719,129 @@ describe("Bug 6: item_reference for assistant text turns counted in assistantCou
17191719
});
17201720
});
17211721

1722+
// ─── Bug fix: reasoning_summary_text.done must include item_id ──────────────
1723+
1724+
describe("reasoning_summary_text.done includes item_id", () => {
1725+
it("reasoning_summary_text.done has item_id matching the reasoning item", () => {
1726+
const events = buildTextStreamEvents("result", "gpt-4", 100, "thinking hard");
1727+
const textDone = events.find((e) => e.type === "response.reasoning_summary_text.done");
1728+
expect(textDone).toBeDefined();
1729+
expect(textDone!.item_id).toBeDefined();
1730+
expect(typeof textDone!.item_id).toBe("string");
1731+
1732+
// Verify it matches the reasoning item id
1733+
const reasoningAdded = events.find(
1734+
(e) =>
1735+
e.type === "response.output_item.added" &&
1736+
(e.item as { type: string })?.type === "reasoning",
1737+
);
1738+
const reasoningId = (reasoningAdded!.item as { id: string }).id;
1739+
expect(textDone!.item_id).toBe(reasoningId);
1740+
});
1741+
});
1742+
1743+
// ─── Bug fix: multi-fco after single item_reference ─────────────────────────
1744+
1745+
describe("multi-fco after single item_reference", () => {
1746+
it("[user, item_reference, fco_A, fco_B] produces assistantCount=1 with 2 tool_calls", () => {
1747+
const messages = responsesInputToMessages({
1748+
model: "gpt-4",
1749+
input: [
1750+
{ role: "user", content: "hello" },
1751+
{ type: "item_reference", id: "ref_multi_fc" },
1752+
{ type: "function_call_output", call_id: "call_A", output: '{"a":1}' },
1753+
{ type: "function_call_output", call_id: "call_B", output: '{"b":2}' },
1754+
],
1755+
});
1756+
1757+
const assistantMsgs = messages.filter((m) => m.role === "assistant");
1758+
expect(assistantMsgs).toHaveLength(1);
1759+
expect(assistantMsgs[0].tool_calls).toHaveLength(2);
1760+
expect(assistantMsgs[0].tool_calls![0].id).toBe("call_A");
1761+
expect(assistantMsgs[0].tool_calls![1].id).toBe("call_B");
1762+
1763+
const toolMsgs = messages.filter((m) => m.role === "tool");
1764+
expect(toolMsgs).toHaveLength(2);
1765+
});
1766+
1767+
it("[user, item_reference, fco_A, fco_B, user] produces assistantCount=1", () => {
1768+
const messages = responsesInputToMessages({
1769+
model: "gpt-4",
1770+
input: [
1771+
{ role: "user", content: "hello" },
1772+
{ type: "item_reference", id: "ref_multi_fc" },
1773+
{ type: "function_call_output", call_id: "call_A", output: '{"a":1}' },
1774+
{ type: "function_call_output", call_id: "call_B", output: '{"b":2}' },
1775+
{ role: "user", content: "next question" },
1776+
],
1777+
});
1778+
1779+
const assistantCount = messages.filter((m) => m.role === "assistant").length;
1780+
expect(assistantCount).toBe(1);
1781+
});
1782+
});
1783+
1784+
// ─── e2e: turnIndex + item_reference via Responses API ──────────────────────
1785+
1786+
describe("turnIndex + item_reference via Responses API (e2e)", () => {
1787+
it("selects turnIndex:1 fixture when input has item_reference + fco (assistantCount=1)", async () => {
1788+
const turn0Fixture: Fixture = {
1789+
match: { userMessage: "turn-index-test", turnIndex: 0 },
1790+
response: { content: "turn zero response" },
1791+
};
1792+
const turn1Fixture: Fixture = {
1793+
match: { userMessage: "turn-index-test", turnIndex: 1 },
1794+
response: { content: "turn one response" },
1795+
};
1796+
instance = await createServer([turn0Fixture, turn1Fixture]);
1797+
1798+
// Input: [user, item_reference, function_call_output, user]
1799+
// This should produce assistantCount=1 → turnIndex 1 match
1800+
const res = await post(`${instance.url}/v1/responses`, {
1801+
model: "gpt-4",
1802+
input: [
1803+
{ role: "user", content: "first question" },
1804+
{ type: "item_reference", id: "ref_prev_assistant" },
1805+
{ type: "function_call_output", call_id: "call_prev", output: '{"done":true}' },
1806+
{ role: "user", content: "turn-index-test" },
1807+
],
1808+
stream: false,
1809+
});
1810+
1811+
expect(res.status).toBe(200);
1812+
const body = JSON.parse(res.body);
1813+
expect(body.output[0].content[0].text).toBe("turn one response");
1814+
});
1815+
1816+
it("multi-fco [user, item_reference, fco_A, fco_B, user] produces assistantCount=1", async () => {
1817+
const turn0Fixture: Fixture = {
1818+
match: { userMessage: "multi-fco-turn-test", turnIndex: 0 },
1819+
response: { content: "should not match" },
1820+
};
1821+
const turn1Fixture: Fixture = {
1822+
match: { userMessage: "multi-fco-turn-test", turnIndex: 1 },
1823+
response: { content: "correct turn one" },
1824+
};
1825+
instance = await createServer([turn0Fixture, turn1Fixture]);
1826+
1827+
const res = await post(`${instance.url}/v1/responses`, {
1828+
model: "gpt-4",
1829+
input: [
1830+
{ role: "user", content: "initial" },
1831+
{ type: "item_reference", id: "ref_2tool_assistant" },
1832+
{ type: "function_call_output", call_id: "call_X", output: '{"x":1}' },
1833+
{ type: "function_call_output", call_id: "call_Y", output: '{"y":2}' },
1834+
{ role: "user", content: "multi-fco-turn-test" },
1835+
],
1836+
stream: false,
1837+
});
1838+
1839+
expect(res.status).toBe(200);
1840+
const body = JSON.parse(res.body);
1841+
expect(body.output[0].content[0].text).toBe("correct turn one");
1842+
});
1843+
});
1844+
17221845
// ─── Debug logging in handleResponses ───────────────────────────────────────
17231846

17241847
describe("handleResponses debug logging", () => {

src/responses.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,17 +150,39 @@ export function responsesInputToMessages(req: ResponsesRequest): ChatMessage[] {
150150
];
151151
itemReferencePlaceholders.delete(lastMsg);
152152
} else {
153-
messages.push({
154-
role: "assistant",
155-
content: null,
156-
tool_calls: [
157-
{
153+
// Multi-fco case: look for a recent assistant with tool_calls that
154+
// belongs to the same turn. After the first fco upgrades a placeholder,
155+
// subsequent fco's see [assistant(call_A), tool(call_A)] — the last
156+
// assistant with tool_calls (right before the trailing tool messages)
157+
// is the correct target.
158+
let appended = false;
159+
for (let k = messages.length - 1; k >= 0; k--) {
160+
const m = messages[k];
161+
if (m.role === "assistant" && m.tool_calls) {
162+
m.tool_calls.push({
158163
id: item.call_id ?? generateToolCallId(),
159164
type: "function",
160165
function: { name: "", arguments: "" },
161-
},
162-
],
163-
});
166+
});
167+
appended = true;
168+
break;
169+
}
170+
// Stop scanning if we hit a user message — different turn
171+
if (m.role === "user") break;
172+
}
173+
if (!appended) {
174+
messages.push({
175+
role: "assistant",
176+
content: null,
177+
tool_calls: [
178+
{
179+
id: item.call_id ?? generateToolCallId(),
180+
type: "function",
181+
function: { name: "", arguments: "" },
182+
},
183+
],
184+
});
185+
}
164186
}
165187
}
166188
messages.push({
@@ -439,6 +461,7 @@ function buildReasoningStreamEvents(
439461

440462
events.push({
441463
type: "response.reasoning_summary_text.done",
464+
item_id: reasoningId,
442465
output_index: 0,
443466
summary_index: 0,
444467
text: reasoning,

0 commit comments

Comments
 (0)