Skip to content

Commit f254476

Browse files
authored
fix(llm): omit stateless response item ids (anomalyco#34027)
1 parent 639c8e6 commit f254476

7 files changed

Lines changed: 38 additions & 27 deletions

File tree

packages/llm/src/protocols/openai-responses.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const OpenAIResponsesReasoningSummaryText = Schema.Struct({
5353

5454
const OpenAIResponsesReasoningItem = Schema.Struct({
5555
type: Schema.tag("reasoning"),
56-
id: Schema.String,
56+
id: Schema.optionalKey(Schema.String),
5757
summary: Schema.Array(OpenAIResponsesReasoningSummaryText),
5858
encrypted_content: optionalNull(Schema.String),
5959
})
@@ -101,6 +101,7 @@ type OpenAIResponsesReasoningInput = {
101101
summary: Array<{ type: "summary_text"; text: string }>
102102
encrypted_content?: string | null
103103
}
104+
type OpenAIResponsesReasoningReplay = Omit<OpenAIResponsesReasoningInput, "id">
104105

105106
const OpenAIResponsesTool = Schema.Struct({
106107
type: Schema.tag("function"),
@@ -366,7 +367,7 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
366367

367368
if (message.role === "assistant") {
368369
const content: TextPart[] = []
369-
const reasoningItems: Record<string, OpenAIResponsesReasoningInput> = {}
370+
const reasoningItems: Record<string, OpenAIResponsesReasoningReplay> = {}
370371
const reasoningReferences = new Set<string>()
371372
const hostedToolReferences = new Set<string>()
372373
const flushText = () => {
@@ -383,7 +384,7 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
383384
flushText()
384385
const reasoning = lowerReasoning(part)
385386
if (!reasoning) continue
386-
if (store !== false && reasoning.id) {
387+
if (store !== false) {
387388
if (!reasoningReferences.has(reasoning.id)) input.push({ type: "item_reference", id: reasoning.id })
388389
reasoningReferences.add(reasoning.id)
389390
continue
@@ -395,8 +396,13 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
395396
existing.encrypted_content = reasoning.encrypted_content
396397
continue
397398
}
398-
reasoningItems[reasoning.id] = reasoning
399-
input.push(reasoning)
399+
const replay = {
400+
type: reasoning.type,
401+
summary: reasoning.summary,
402+
encrypted_content: reasoning.encrypted_content,
403+
}
404+
reasoningItems[reasoning.id] = replay
405+
input.push(replay)
400406
continue
401407
}
402408
if (part.type === "tool-call") {
@@ -974,6 +980,7 @@ export const route = Route.make({
974980
endpoint,
975981
auth,
976982
transport: httpTransport,
983+
defaults: { providerOptions: { openai: { store: false } } },
977984
})
978985

979986
const decodeWebSocketMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesWebSocketMessage))
@@ -1001,6 +1008,7 @@ export const webSocketRoute = Route.make({
10011008
endpoint,
10021009
auth,
10031010
transport: webSocketTransport,
1011+
defaults: { providerOptions: { openai: { store: false } } },
10041012
})
10051013

10061014
export * as OpenAIResponses from "./openai-responses"

packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"headers": {
4545
"content-type": "application/json"
4646
},
47-
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}"
47+
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}"
4848
},
4949
"response": {
5050
"status": 200,

packages/llm/test/provider/openai-responses.test.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe("OpenAI Responses route", () => {
5050
{ role: "system", content: "You are concise." },
5151
{ role: "user", content: [{ type: "input_text", text: "Say hello." }] },
5252
],
53+
store: false,
5354
stream: true,
5455
max_output_tokens: 20,
5556
temperature: 0,
@@ -161,16 +162,16 @@ describe("OpenAI Responses route", () => {
161162
Effect.gen(function* () {
162163
const prepared = yield* LLMClient.prepare(
163164
LLM.updateRequest(request, {
164-
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responsesWebSocket(
165-
"gpt-4.1-mini",
166-
),
165+
model: OpenAIResponses.webSocketRoute
166+
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
167+
.model({ id: "gpt-4.1-mini" }),
167168
}),
168169
)
169170

170171
expect(prepared.route).toBe("openai-responses-websocket")
171172
expect(prepared.protocol).toBe("openai-responses")
172173
expect(prepared.metadata).toEqual({ transport: "websocket-json" })
173-
expect(prepared.body).toMatchObject({ model: "gpt-4.1-mini", stream: true })
174+
expect(prepared.body).toMatchObject({ model: "gpt-4.1-mini", store: false, stream: true })
174175
}),
175176
)
176177

@@ -356,7 +357,13 @@ describe("OpenAI Responses route", () => {
356357
{ type: "function_call", call_id: "call_1", name: "lookup", arguments: '{"query":"weather"}' },
357358
{ type: "function_call_output", call_id: "call_1", output: '{"forecast":"sunny"}' },
358359
],
360+
store: false,
359361
stream: true,
362+
max_output_tokens: undefined,
363+
temperature: undefined,
364+
tool_choice: undefined,
365+
tools: undefined,
366+
top_p: undefined,
360367
})
361368
}),
362369
)
@@ -519,7 +526,6 @@ describe("OpenAI Responses route", () => {
519526
},
520527
{
521528
type: "reasoning",
522-
id: "rs_continuation_1",
523529
encrypted_content: "encrypted-continuation-state",
524530
summary: [{ type: "summary_text", text: "I inspected the previous turn." }],
525531
},
@@ -864,7 +870,9 @@ describe("OpenAI Responses route", () => {
864870

865871
it.effect("closes reasoning summary parts when storage is not disabled", () =>
866872
Effect.gen(function* () {
867-
const response = yield* LLMClient.generate(request).pipe(
873+
const response = yield* LLMClient.generate(
874+
LLM.updateRequest(request, { providerOptions: { openai: { store: true } } }),
875+
).pipe(
868876
Effect.provide(
869877
fixedResponse(
870878
sseEvents(
@@ -925,19 +933,20 @@ describe("OpenAI Responses route", () => {
925933
dynamicResponse((input) =>
926934
Effect.gen(function* () {
927935
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
928-
expect(yield* Effect.promise(() => web.json())).toMatchObject({
936+
const body = yield* Effect.promise(() => web.json())
937+
expect(body).toMatchObject({
929938
input: [
930939
{ role: "user", content: [{ type: "input_text", text: "What changed?" }] },
931940
{
932941
type: "reasoning",
933-
id: "rs_1",
934942
encrypted_content: "encrypted-state",
935943
summary: [{ type: "summary_text", text: "Checked the previous diff." }],
936944
},
937945
{ role: "assistant", content: [{ type: "output_text", text: "The parser changed." }] },
938946
{ role: "user", content: [{ type: "input_text", text: "Summarize it." }] },
939947
],
940948
})
949+
expect(body.input[1]).not.toHaveProperty("id")
941950
return input.respond(
942951
sseEvents(
943952
{ type: "response.output_text.delta", item_id: "msg_1", delta: "Parser now round-trips reasoning." },
@@ -984,7 +993,6 @@ describe("OpenAI Responses route", () => {
984993
{ role: "assistant", content: [{ type: "output_text", text: "Before." }] },
985994
{
986995
type: "reasoning",
987-
id: "rs_1",
988996
encrypted_content: "encrypted-state",
989997
summary: [{ type: "summary_text", text: "Checked order." }],
990998
},
@@ -1078,7 +1086,6 @@ describe("OpenAI Responses route", () => {
10781086
expect(prepared.body.input).toEqual([
10791087
{
10801088
type: "reasoning",
1081-
id: "rs_1",
10821089
encrypted_content: "encrypted-state",
10831090
summary: [
10841091
{ type: "summary_text", text: "First" },

packages/llm/test/tool-runtime.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ describe("LLMClient tools", () => {
598598
include: ["reasoning.encrypted_content"],
599599
input: [
600600
{ role: "user" },
601-
{ type: "reasoning", id: "rs_1", summary: [], encrypted_content: "encrypted-state" },
601+
{ type: "reasoning", summary: [], encrypted_content: "encrypted-state" },
602602
{ type: "function_call", call_id: "call_1", name: "get_weather" },
603603
{ type: "function_call_output", call_id: "call_1" },
604604
],

packages/opencode/test/fixtures/recordings/session/native-openai-oauth-tool-loop.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"headers": {
3434
"content-type": "application/json"
3535
},
36-
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"reasoning\",\"id\":\"rs_0812d6cbe7a2b19b016a1214d32f6881998bcd9ff2e739d7f2\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEhTUCQT4XELlBu6r5VHqqtu5Il5WdX4m1upE8li0mPmIwgIykAmUTZWiE0213kmviuAgIrmhhiL4B8DXbWQD2vOEkQMhpZq_UCqc22SOg-4DpQLrebMWkzgAPL618VPu9mXNUIH9BW1sRhPdDSbbtK5_bitzsn-FMJGcO3UN7Ga2RW1Rdvt1M3m7J4MRlTutH8cwY8SthzgvOFEBS-_IrAhiwKVz4Se9Jlu3pVNMqhPF7kdrQOfDYui0v-AT8VrHBVomqekJl_dWESww0eWo6bS1PxZB4cLQHWp9JJi5pEECvU9Ntcz3GxuGJEtTKq5mFcRvCanXHOwZGmbBcWMNdVyikk3fxgIE2g9t8rCKJmhNXznMERtrfG2tey19qWbsVbo2YmBbg_5N02AA4NmEVvdfgHJx58nOfEEc2OZYk0YQ1fHBOkpBnwY61hxtrWFdj48QnTEKuvjAyNpX-KKFmMzL4531yLbEEzpaERlr11fDeoMpKofUoMsg3Jz8aTaZ1CpzI3O7iFzGDEV6gKh8vQYGrKOaOXnfBVDXDo8iJhZywpcQY6xB4NNf4pyyjFkR-vjgvBYV2hejlq2V1j8vQHgy8CsZJ6lW5oaTNMfP76MAHlwUwyMYj-cFmuX0epJdDWv8GDznUpOS-v2X5eNsvyx9qvvcTEMLsKJ--3_odisilj4vPhw16P9fB8eLmvESZmJRYmWM4mO7hPTVXOooOa-zxRHGhRQH9ouUea9UHSuH1A0o54qTEPr-JqYlQggugW449IuYW4HSMNMyeGdUNJfodWRu5cL0VPgk6zwTU3ArBq28FDgG7NZMk3njfCId351GZ8VRlTMA6U522_6FFaZ8-5gxsidOm0WULOwyTTo54tJsJFv2pgYUKs0VFWSwi3rvNMVMOgwOVIdSgZt1hFTxBImZh8HUIXUPvdOVKZzQmWT5M6uOTUsm5xsufhj8m79RuYZh2J0bkVOBzZ1As8zH-4v_r9d7e8464EuWXCln_6LAJdrTYgE2gVfHK0zeUaAMbIKhirOf0AVQZyfVsGvJ_CPqrPE_QSECeSA2D4TSa5Tc_IRY-Fb2_HKNCMEP2uvy\"},{\"type\":\"function_call\",\"call_id\":\"call_Ix5Urx04RtKsUJ75K0vTTgFF\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_Ix5Urx04RtKsUJ75K0vTTgFF\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"instructions\":\"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"prompt_cache_key\":\"session-recorded-openai-oauth-loop\",\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"stream\":true}"
36+
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"reasoning\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEhTUCQT4XELlBu6r5VHqqtu5Il5WdX4m1upE8li0mPmIwgIykAmUTZWiE0213kmviuAgIrmhhiL4B8DXbWQD2vOEkQMhpZq_UCqc22SOg-4DpQLrebMWkzgAPL618VPu9mXNUIH9BW1sRhPdDSbbtK5_bitzsn-FMJGcO3UN7Ga2RW1Rdvt1M3m7J4MRlTutH8cwY8SthzgvOFEBS-_IrAhiwKVz4Se9Jlu3pVNMqhPF7kdrQOfDYui0v-AT8VrHBVomqekJl_dWESww0eWo6bS1PxZB4cLQHWp9JJi5pEECvU9Ntcz3GxuGJEtTKq5mFcRvCanXHOwZGmbBcWMNdVyikk3fxgIE2g9t8rCKJmhNXznMERtrfG2tey19qWbsVbo2YmBbg_5N02AA4NmEVvdfgHJx58nOfEEc2OZYk0YQ1fHBOkpBnwY61hxtrWFdj48QnTEKuvjAyNpX-KKFmMzL4531yLbEEzpaERlr11fDeoMpKofUoMsg3Jz8aTaZ1CpzI3O7iFzGDEV6gKh8vQYGrKOaOXnfBVDXDo8iJhZywpcQY6xB4NNf4pyyjFkR-vjgvBYV2hejlq2V1j8vQHgy8CsZJ6lW5oaTNMfP76MAHlwUwyMYj-cFmuX0epJdDWv8GDznUpOS-v2X5eNsvyx9qvvcTEMLsKJ--3_odisilj4vPhw16P9fB8eLmvESZmJRYmWM4mO7hPTVXOooOa-zxRHGhRQH9ouUea9UHSuH1A0o54qTEPr-JqYlQggugW449IuYW4HSMNMyeGdUNJfodWRu5cL0VPgk6zwTU3ArBq28FDgG7NZMk3njfCId351GZ8VRlTMA6U522_6FFaZ8-5gxsidOm0WULOwyTTo54tJsJFv2pgYUKs0VFWSwi3rvNMVMOgwOVIdSgZt1hFTxBImZh8HUIXUPvdOVKZzQmWT5M6uOTUsm5xsufhj8m79RuYZh2J0bkVOBzZ1As8zH-4v_r9d7e8464EuWXCln_6LAJdrTYgE2gVfHK0zeUaAMbIKhirOf0AVQZyfVsGvJ_CPqrPE_QSECeSA2D4TSa5Tc_IRY-Fb2_HKNCMEP2uvy\"},{\"type\":\"function_call\",\"call_id\":\"call_Ix5Urx04RtKsUJ75K0vTTgFF\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_Ix5Urx04RtKsUJ75K0vTTgFF\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"instructions\":\"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"prompt_cache_key\":\"session-recorded-openai-oauth-loop\",\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"stream\":true}"
3737
},
3838
"response": {
3939
"status": 200,

0 commit comments

Comments
 (0)