fix(ai): preserve response message phases

This commit is contained in:
Aiden Cline 2026-07-24 16:05:22 -05:00 committed by Aiden Cline
commit 92ee702eeb
6 changed files with 634 additions and 12 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,89 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, Message } from "../../src"
import { configure } from "../../src/providers/openai"
import { OpenAIResponses } from "../../src/protocols/openai-responses"
import { LLMClient } from "../../src/route"
import { weatherTool } from "../recorded-scenarios"
import { recordedTests } from "../recorded-test"
const model = configure({
apiKey: process.env.OPENAI_API_KEY ?? "fixture",
}).responses("gpt-5.6-sol")
const recorded = recordedTests({
prefix: "openai-responses-phase",
provider: "openai",
protocol: "openai-responses",
requires: ["OPENAI_API_KEY"],
})
describe("OpenAI Responses phase recorded", () => {
recorded.effect.with("round-trips commentary into a final answer", { tags: ["phase", "tool"] }, () =>
Effect.gen(function* () {
const user = Message.user("What is the weather in Paris?")
const first = yield* LLMClient.generate(
LLM.request({
model,
system:
"Before calling get_weather, briefly tell the user you are checking. Then call get_weather exactly once. Do not provide the final answer until its result is available.",
messages: [user],
tools: [weatherTool],
generation: { maxTokens: 100 },
}),
)
const call = first.toolCalls[0]
if (!call) throw new Error("OpenAI Responses did not return the expected weather tool call")
expect(call).toMatchObject({ name: "get_weather", input: { city: "Paris" } })
const commentary = first.message.content.find(
(part) => part.type === "text" && part.providerMetadata?.openai?.phase === "commentary",
)
if (!commentary || commentary.type !== "text") throw new Error("OpenAI Responses did not return commentary text")
const itemID = commentary.providerMetadata?.openai?.itemId
if (typeof itemID !== "string") throw new Error("OpenAI Responses commentary did not include an item ID")
expect(commentary).toEqual({
type: "text",
text: "Ill check the current weather in Paris.",
providerMetadata: {
openai: { itemId: itemID, phase: "commentary", status: "completed", annotations: [] },
},
})
const continuation = LLM.request({
model,
system:
"Before calling get_weather, briefly tell the user you are checking. Then call get_weather exactly once. After its result, answer exactly: Paris is sunny.",
messages: [
user,
first.message,
Message.tool({
id: call.id,
name: call.name,
result: { temperature: 22, condition: "sunny" },
}),
],
tools: [weatherTool],
generation: { maxTokens: 100 },
})
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(continuation)
expect(prepared.body.input).toContainEqual({
type: "message",
id: itemID,
status: "completed",
role: "assistant",
content: [{ type: "output_text", text: commentary.text, annotations: [] }],
phase: "commentary",
})
const second = yield* LLMClient.generate(continuation)
expect(second.text.trim()).toBe("Paris is sunny.")
expect(
second.message.content.some(
(part) => part.type === "text" && part.providerMetadata?.openai?.phase === "final_answer",
),
).toBeTrue()
}),
)
})

View file

@ -920,6 +920,7 @@ describe("OpenAI Responses route", () => {
sseEvents(
{ type: "response.output_text.delta", item_id: "msg_1", delta: "First" },
{ type: "response.output_text.done", item_id: "msg_1" },
{ type: "response.output_item.done", item: { type: "message", id: "msg_1" } },
{ type: "response.output_text.delta", item_id: "msg_2", delta: "Second" },
{ type: "response.output_item.done", item: { type: "message", id: "msg_2" } },
{ type: "response.completed", response: { id: "resp_1" } },
@ -939,6 +940,94 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("preserves and replays OpenAI response message phases", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
{
type: "response.output_item.added",
item: { type: "message", id: "msg_commentary", status: "in_progress", phase: "commentary" },
},
{ type: "response.output_text.delta", item_id: "msg_commentary", delta: "Checking." },
{
type: "response.output_item.done",
item: {
type: "message",
id: "msg_commentary",
status: "completed",
phase: "commentary",
content: [{ type: "output_text", text: "Checking.", annotations: [] }],
},
},
{
type: "response.output_item.added",
item: { type: "message", id: "msg_final", status: "in_progress", phase: "final_answer" },
},
{ type: "response.output_text.done", item_id: "msg_final", content_index: 0, text: "Finished." },
{
type: "response.output_item.done",
item: {
type: "message",
id: "msg_final",
status: "incomplete",
phase: "final_answer",
content: [{ type: "output_text", text: "Finished.", annotations: [{ type: "test" }] }],
},
},
{ type: "response.completed", response: { id: "resp_1" } },
),
),
),
)
expect(response.message.content).toEqual([
{
type: "text",
text: "Checking.",
providerMetadata: {
openai: { itemId: "msg_commentary", status: "completed", phase: "commentary", annotations: [] },
},
},
{
type: "text",
text: "Finished.",
providerMetadata: {
openai: {
itemId: "msg_final",
status: "incomplete",
phase: "final_answer",
annotations: [{ type: "test" }],
},
},
},
])
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ model, messages: [response.message] }),
)
expect(prepared.body.input).toEqual([
{
type: "message",
id: "msg_commentary",
status: "completed",
role: "assistant",
phase: "commentary",
content: [{ type: "output_text", text: "Checking.", annotations: [] }],
},
{
type: "message",
id: "msg_final",
status: "incomplete",
role: "assistant",
phase: "final_answer",
content: [{ type: "output_text", text: "Finished.", annotations: [{ type: "test" }] }],
},
])
}),
)
it.effect("parses reasoning summary stream fixtures", () =>
Effect.gen(function* () {
const body = sseEvents(
@ -1347,7 +1436,13 @@ describe("OpenAI Responses route", () => {
},
},
},
{ type: "text", text: "The parser changed." },
{
type: "text",
text: "The parser changed.",
providerMetadata: {
openai: { itemId: "msg_1", phase: "final_answer", status: "completed" },
},
},
]),
Message.user("Summarize it."),
],
@ -1358,7 +1453,7 @@ describe("OpenAI Responses route", () => {
expect(prepared.body).toMatchObject({
input: [
{ role: "user", content: [{ type: "input_text", text: "What changed?" }] },
{ role: "assistant", content: [{ type: "output_text", text: "The parser changed." }] },
{ role: "assistant", content: "The parser changed.", phase: "final_answer" },
{ role: "user", content: [{ type: "input_text", text: "Summarize it." }] },
],
store: false,