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
5 changes: 3 additions & 2 deletions packages/llm/src/protocols/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const OpenAIResponsesReasoningSummaryText = Schema.Struct({

const OpenAIResponsesReasoningItem = Schema.Struct({
type: Schema.tag("reasoning"),
id: Schema.optionalKey(Schema.String),
id: Schema.String,
summary: Schema.Array(OpenAIResponsesReasoningSummaryText),
encrypted_content: optionalNull(Schema.String),
})
Expand Down Expand Up @@ -103,7 +103,7 @@ type OpenAIResponsesReasoningInput = {
summary: Array<{ type: "summary_text"; text: string }>
encrypted_content?: string | null
}
type OpenAIResponsesReasoningReplay = Omit<OpenAIResponsesReasoningInput, "id">
type OpenAIResponsesReasoningReplay = OpenAIResponsesReasoningInput

const OpenAIResponsesTool = Schema.Struct({
type: Schema.tag("function"),
Expand Down Expand Up @@ -400,6 +400,7 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
}
const replay = {
type: reasoning.type,
id: reasoning.id,
summary: reasoning.summary,
encrypted_content: reasoning.encrypted_content,
}
Expand Down
41 changes: 41 additions & 0 deletions packages/llm/test/provider/openai-responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,7 @@ describe("OpenAI Responses route", () => {
expect(prepared.body.input).toEqual([
{
type: "reasoning",
id: "rs_1",
encrypted_content: "encrypted-state",
summary: [
{ type: "summary_text", text: "First" },
Expand Down Expand Up @@ -1469,4 +1470,44 @@ describe("OpenAI Responses route", () => {
expect(error.message).toContain("HTTP 400")
}),
)
it.effect("includes id in stateless reasoning replay item", () =>
Effect.gen(function* () {
// Regression for the bug introduced by PR #34027: the replay object
// built in lowerMessages explicitly omitted id via the Omit type alias,
// causing HTTP 400 from the Responses API when encrypted_content was
// present (the API requires id and encrypted_content as a matched pair).
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
id: "req_reasoning_replay_id",
model,
messages: [
Message.assistant([
{
type: "reasoning",
text: "I considered the question carefully.",
providerMetadata: {
openai: {
itemId: "rs_abc123",
reasoningEncryptedContent: "encrypted-abc123",
},
},
},
]),
],
providerOptions: { openai: { store: false } },
}),
)

const reasoningItem = prepared.body.input.find(
(item) => "type" in item && item.type === "reasoning",
)
expect(reasoningItem).toBeDefined()
expect(reasoningItem).toEqual({
type: "reasoning",
id: "rs_abc123",
encrypted_content: "encrypted-abc123",
summary: [{ type: "summary_text", text: "I considered the question carefully." }],
})
}),
)
})
Loading