Skip to content

fix(providers/openai): skip reasoning that can't go inline - #81

Draft
mafredri wants to merge 6 commits into
ethan/openai-web-search-replay-recordfrom
mafredri/skip-unresolvable-reasoning
Draft

mafredri wants to merge 6 commits into
ethan/openai-web-search-replay-recordfrom
mafredri/skip-unresolvable-reasoning

Conversation

@mafredri

@mafredri mafredri commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Stacked on #77.

Reasoning that can't be sent inline, because it has no finalized encrypted content, was sent as an item_reference whenever store was true. That resolves only while OpenAI still holds the source response. Otherwise the whole request gets a 404, and since a chat resends the same history, every later turn fails the same way.

In coder/coder this affects reasoning saved before coder/coder#30298 (2026-10-13), which has no finalized flag. Reasoning saved since then always has encrypted content and goes inline. The reference fails when:

  • the org has zero data retention, where OpenAI treats store as false;
  • store was false when the message was saved;
  • OpenAI no longer has the item, since stored responses are only kept "for at least 30 days" (data controls).

Such reasoning is now skipped in both store modes. The model loses it: by default models before GPT-5.6 don't render reasoning from earlier turns, GPT-5.6 and later do (reasoning guide). An assistant message whose only content is such reasoning is now dropped with the existing empty-message warning, as it already was with store: false.

With #77 and this, prompt conversion no longer reads store, so the parameter is removed.

Evidence

  • item_reference probes through the AI Gateway on gpt-5-mini: reasoning from a completed stored response got 200; from a store: false response, 404; from a stored response whose stream was cut after the reasoning started, 404 at 5s and 60s.
  • Live two-turn runs through agent.Stream and agent.Generate on gpt-5-mini, with the turn 1 messages round-tripped through JSON and finalized removed, turn 2 with store: true. On fix(providers/openai): replay web searches and computer calls without item IDs #77: turn 1 store: false got 404 "Item with id 'rs_...' not found", turn 1 store: true got 200. With this PR both got 200.
  • Zero data retention wasn't probed, since the gateway's org isn't one.

Validation

  • go test ./... -count=1, go vet ./... and golangci-lint v2.15.0 pass. gofumpt only flags two existing blank lines in openai_test.go.
  • Sending the reference again fails two tests.
Decisions
  • Skip, not reference: a reference works only for reasoning OpenAI still holds, and failure breaks the whole chat. Skipping costs earlier-turn reasoning on GPT-5.6 and later, only for messages saved before 2026-10-13.
  • Retry without references after a 404: rejected. It needs 404 parsing in coder/coder for a set of messages that only shrinks.
  • Remove store rather than rename it _: revive flags the unused parameter, and the tests that ran once per store value checked the same output twice.

🤖 This PR was created with the help of Coder Agents, and will be reviewed by a human. 🏂🏻

Reasoning without finalized encrypted content was sent as an
item_reference whenever store was true. That only resolves while OpenAI
still holds the source response. A reference to reasoning from an
unstored response, or from a stored response whose stream was cut, gets
a 404 that fails the whole request, so every later turn of the chat
fails the same way.

Such reasoning is now skipped in both store modes. By default, models
before GPT-5.6 ignore reasoning from earlier turns anyway.
Prompt conversion no longer reads store: searches and reasoning go
inline or not at all in both modes. Tests that ran once per store value
now run once.
@mafredri

Copy link
Copy Markdown
Member Author

@codex review
/coder-agents-review

🤖

@coder-agents-review

coder-agents-review Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Chat: Review posted | View chat
Requested: 2026-10-29 23:20 UTC by @mafredri

Review history
  • R1 (2026-10-29): 9 reviewers, 2 Nit, 1 P2, 3 P3, 4 P4, COMMENT. Review

deep-review v0.13.0 | Round 1 | 9ec26d9..13e36ee

Last posted: Round 1, 10 findings (1 P2, 3 P3, 4 P4, 2 Nit), COMMENT. Review

Finding inventory

Finding inventory: coder/fantasy PR 81

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Open responses_language_model.go:682 No test for reasoning-only assistant message now dropped with empty-message warning R1 Netero Yes
CRF-2 P2 Open responses_language_model.go:682 Store true without IncludeReasoningEncryptedContent now drops all finalized reasoning silently R1 Pariston P2, Mafuuu P2, Melody P2, Razor P3, Leorio P4 Yes
CRF-3 P3 Open responses_language_model.go:685 New comment says references fail for items OpenAI did not store; they also fail for stored items R1 Gon P2, Leorio P4 Yes
CRF-4 P3 Open openai_test.go:4751 "skips reasoning when item id missing" no longer tests the ItemID check R1 Ryosuke Yes
CRF-5 P4 Open openai_test.go:4865 Combined reasoning tests now only re-test the skip branch; names describe ordering not checked R1 Bisky Yes
CRF-6 P4 Open responses_reasoning_replay_test.go:157 New store true/false row pairs build the same input R1 Bisky P4, Mafuuu Nit Yes
CRF-7 P4 Open responses_options.go:152 Finalized doc says unfinalized metadata is not replayed inline; it is not sent at all R1 Leorio Yes
CRF-8 P4 Open responses_reasoning_replay_test.go:127 unfinalized field comment names one of two sources of unfinalized metadata R1 Gon Yes
CRF-9 Nit Open openai_test.go:4786 Test comment repeats subtest name R1 Gon P2 Yes
CRF-10 Nit Open responses_web_search_test.go:593 Renamed cases keep a PR number as their only description R1 Gon Yes
CRF-11 OOS Out of scope (body) responses_reasoning_replay_test.go:209 context.Background in unchanged test line R1 Ging-Go No
CRF-12 OOS Out of scope (body) responses_web_search_test.go:22 wantInline comment describes a referenced case no code produces R1 Gon No
CRF-13 OOS Out of scope (body) responses_language_model.go:468 allowUnpairedFunctionCallOutputs passed as bare bool R1 Gon No
CRF-14 OOS Out of scope (body) responses_language_model.go:1117 Unchanged comment repeats function name R1 Gon P2 No
CRF-15 Note Note (body) responses_language_model.go:1119 OfItemReference removal verified: no non-test producer remains R1 Pariston, Melody, Razor No
CRF-16 Note Note (body) responses_language_model.go:682 Restoring the reference fails four tests, reproduced R1 Netero, Bisky, Leorio, Razor No

Contested and acknowledged

Round log

Round 1

Round type: round 1, full panel. Netero first pass: 1 P3. Law not run (effective additions 105). Panel: ging-go, ryosuke, melody (specialists), pariston, mafuuu (core), bisky (tests changed), gon, leorio (once-per-PR floor), razor (wildcard). Size 8 + wildcard, focused Go PR.
Cross-check: CRF-2 convergent across 5 reviewers; severity P2 (higher wins over Razor P3). Leorio's no-warning finding merged into CRF-2 since the proposed CallWarning covers both legacy and no-include cases. CRF-3 Gon P2 vs Leorio P4 settled at P3: four tests fail if the reference branch returns (reproduced by Netero, Bisky, Leorio, Razor), so the misleading comment cannot silently reintroduce the bug. CRF-9 Gon P2 downgraded to Nit: comment restating a name is a convention issue, no behavior consequence. CRF-14 moved out of scope: comment line unchanged and still accurate. Gon's pattern Note dropped as a summary of CRF-3, CRF-9. CRF-6 interacts with CRF-2: if references come back for store true, the pairs are needed. 1 P2, 3 P3, 4 P4, 2 Nit, 4 OOS, 2 Note. Reviewed against 9ec26d9..13e36ee.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T23:21:56.215404Z 13e36ee Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 13e36ee99d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR stops sending reasoning without finalized encrypted content as an item_reference and removes store from prompt conversion. Findings: 1 P2, 3 P3, 4 P4, 2 Nit.

Out of scope (needs a ticket or explicit acceptance by a human):

  • providers/openai/responses_reasoning_replay_test.go:209: the test calls Generate(context.Background(), ...) instead of t.Context(), so the context is not cancelled when the test ends.
  • providers/openai/responses_web_search_test.go:22: the wantInline comment says "when it is not referenced", but no code references a search anymore, so the comment describes a case that does not exist.
  • providers/openai/responses_language_model.go:468: allowUnpairedFunctionCallOutputs is a bare bool, so the caller at line 229 passes true with no name at the call site.
  • providers/openai/responses_language_model.go:1117: the comment Check if we added any assistant content parts from this message repeats the function name and loop.

Notes:

  • No non-test code in providers/openai builds an item_reference after this PR, so removing OfItemReference from hasVisibleResponsesAssistantContent hides nothing.
  • Restoring the item_reference branch fails four tests, as the description states (reproduced with go test ./providers/openai -count=1).

providers/openai/responses_options.go:152

P4 [CRF-7] The exported Finalized doc says unfinalized metadata is not "replayed inline", which implies another replay path; after this PR it is not sent at all. (Leorio)

Callers such as coder/coder read this doc to decide whether old rows need migration. Suggested: // Finalized marks metadata copied from the completed reasoning output item. Only finalized metadata with encrypted content is sent on replay; reasoning from streaming placeholders or saved before this field existed is skipped.

🤖

🤖 This review was automatically generated with Coder Agents.

@@ -682,13 +681,10 @@ func toResponsesPromptWithValidation(prompt fantasy.Prompt, systemMessageMode st
}
if item, ok := responsesReasoningInputItem(meta); ok {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-2] With Store: true and no IncludeReasoningEncryptedContent in Include, all reasoning, including reasoning from completed responses, is now dropped from every later request with no warning. (Pariston, Mafuuu, Melody, Razor, Leorio)

Fantasy never adds reasoning.encrypted_content itself (prepareParams only copies openaiOptions.Include, lines 312-313), so finalResponsesReasoningMetadata gives Finalized: true, EncryptedContent: nil; the description's "always has encrypted content" holds for coder/coder only because chatopenai.EnsureResponseIncludes forces the include. Before this PR that item went back as an item_reference to a stored, completed response, which your probe shows returns 200, and the no blob store true case now asserts nothing is sent, so these callers lose reasoning between tool-call steps of a single agent run. Fix one of: add IncludeReasoningEncryptedContent in prepareParams for reasoning models so replay always has a blob; keep the reference for Finalized items when store is true and skip only unfinalized ones; or emit a CallWarning per skipped reasoning item with an ItemID (as dropping web search ... does) and document on Store and Include that reasoning is replayed only with that include. The warning also gives a signal for the pre-coder/coder#30298 rows this PR targets.

🤖

@@ -682,13 +681,10 @@ func toResponsesPromptWithValidation(prompt fantasy.Prompt, systemMessageMode st
}
if item, ok := responsesReasoningInputItem(meta); ok {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-1] No test covers an assistant message whose only content is unfinalized reasoning, which this PR changes from kept (as an item_reference) to dropped with the empty-message warning. (Netero)

A scratch test with [user "q", assistant {ReasoningPart{ItemID: "rs_1"}}, user "q2"] returned 2 items and dropping empty assistant message (contains neither user-facing content nor tool calls). The only test asserting that warning, TestResponsesToPrompt_DropsEmptyMessages, uses an assistant message with no parts, so a change that keeps or errors on reasoning-only messages passes the suite. Add a subtest there with a reasoning-only assistant message asserting the drop and the warning.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7752a17: added a reasoning-only subtest to TestResponsesToPrompt_DropsEmptyMessages.

🤖

// content cannot be resolved by the API.
continue
}
// Reasoning that cannot be sent inline is skipped, not

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-3] The comment says a reference fails for "an item OpenAI did not store", which implies a reference is safe when store was true. (Gon, Leorio)

The PR's evidence shows references to stored items also return 404: a stored response whose stream was cut, zero data retention, and expired items. That is the reason the else if store branch was removed. Suggested: // Never sent as an item_reference: a reference to an item OpenAI no longer holds fails the whole request with 404.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a4b7d52, reworded to an item OpenAI does not hold.

🤖

})

t.Run("store true skips reasoning when item id missing", func(t *testing.T) {
t.Run("skips reasoning when item id missing", func(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-4] The subtest skips reasoning when item id missing no longer tests the meta.ItemID == "" check at responses_language_model.go:679, and no other test does. (Ryosuke)

Its fixture has no Finalized and no encrypted content, so responsesReasoningInputItem rejects it with or without the ID check; changing the check to if meta == nil { leaves go test ./providers/openai/ -run 'Reasoning|Responses' passing. The check still matters: responsesReasoningInputItem accepts finalized metadata with encrypted content and an empty ItemID and marshals {"id":"",...,"type":"reasoning"} (API response to an empty id unverified). Move the ItemID == "" check into responsesReasoningInputItem and add a TestResponsesReplay_FinalizedReasoningInput case with finalized encrypted content and an empty ItemID.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7752a17: the fixture now has finalized encrypted content, so only the ID check rejects it. Removing the check fails the subtest.

🤖

Comment thread providers/openai/openai_test.go Outdated
t.Parallel()

input, warnings, err := toResponsesPrompt(prompt, "system", false)
input, warnings, err := toResponsesPrompt(prompt, "system")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4 [CRF-5] TestResponsesToPrompt_ReasoningWithWebSearchCombined and TestResponsesToPrompt_ReasoningWithFunctionCallCombined now only check that unfinalized reasoning is missing, the same branch skips unfinalized reasoning covers, while their names describe ordering they no longer check. (Bisky)

The skip is a continue at responses_language_model.go:688 that does not read the adjacent parts. Ordering of finalized reasoning before a function call and before a web search is checked in TestResponsesReplay_FinalizedReasoningInput by the store true and store true before web search rows. Delete both Combined tests.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7752a17, both deleted.

🤖

name: "no blob store true",
store: true,
want: reference,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4 [CRF-6] The new store true/store false row pairs for no blob and unfinalized build the same input, the duplication the description gives as a reason to remove store. (Bisky, Mafuuu)

Store here only sets the request's store field, which the test does not read. If replay stays independent of store (see the Store: true reasoning finding), keep the store true row of each pair, since store: true is the mode that used to send the reference, and drop the store false rows.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7752a17: dropped the unfinalized store false row. The no blob store false row predates this PR, so I left it.

🤖

encrypted *string
summary []string
webSearch bool
// unfinalized marks metadata saved before Finalized existed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4 [CRF-8] The unfinalized field comment names only metadata saved before Finalized existed; streaming placeholders (responses_language_model.go:1511) are also unfinalized, and the cut-stream 404 in the description is that case. (Gon)

Suggested: // unfinalized clears Finalized, as in streaming placeholders and metadata saved before Finalized existed.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7752a17.

🤖

Comment thread providers/openai/openai_test.go Outdated

// Without an ItemID we cannot reference the reasoning
// item. Order: user, assistant text, user.
// Without an ItemID the reasoning cannot be sent. Order: user,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-9] "Without an ItemID the reasoning cannot be sent." repeats the subtest name. (Gon)

Keep only the order list, // user, assistant text, user., as the sibling subtest does at line 4803.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7752a17.

🤖

},
{
name: "UnstoredPre62SearchWithoutStatus",
name: "Pre62SearchWithoutStatus",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-10] The renamed cases Pre62SearchWithoutStatus and Pre62FindInPageWithoutStatus use a PR number as their only description. (Gon)

This diff already renames them, so name them after the data: LegacySearchWithoutStatus and LegacyFindInPageWithoutStatus. The comment at line 604 can keep #62.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7752a17.

🤖

The item ID test now uses finalized encrypted reasoning, so only the ID
check rejects it. A reasoning-only assistant message is dropped with the
empty-message warning. The two Combined tests only rechecked the skip,
and the store false row repeated the store true one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant