fix(pi-embedded): inject body.reasoning when payload has no prior reasoning (#70904)#70911
Conversation
…soning (openclaw#70904) `createOpenAIThinkingLevelWrapper` only reacted to three prior states of `payloadObj.reasoning`: "off" (delete), `"none"` string (replace), and an existing `{ effort }` object (mutate in place). Because pi-ai's Responses/Codex-Responses clients only populate `body.reasoning` when `options.reasoningEffort` is threaded through — which OpenClaw does not do for this wrapper — `payloadObj.reasoning` is `undefined` in every real run. Result: `thinkingDefault` / `thinkingLevel` was silently cosmetic for `openai-codex/gpt-5.x` and `openai/gpt-5.x` on the wrapper path, and server- side defaults applied regardless of user configuration. Mirror `normalizeProxyReasoningPayload`'s `!existingReasoning` branch (proxy-stream-wrappers.ts:43): when the payload arrives without a reasoning field and thinkingLevel is active, populate `{ effort: mapThinkingLevelToReasoningEffort(thinkingLevel) }`. Gated behind the existing `shouldApplyOpenAIReasoningCompatibility` check, so non-reasoning-capable models are untouched. Added regression test: - "injects reasoning on reasoning-capable model when payload has no prior reasoning (openclaw#70904)" Closes openclaw#70904
Greptile SummaryThis PR adds the missing
Confidence Score: 4/5The runtime fix is correct, but a pre-existing test contradicts the new behavior and will fail in CI. One P1 finding: the test at line 82–88 is rendered incorrect by the new branch and will fail, since src/agents/pi-embedded-runner/openai-stream-wrappers.test.ts — the stale 'non-reasoning models' test at line 82 needs updating.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 23ed33c6e6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| void wrapped(codexModel, { messages: [] }, {}); | ||
|
|
||
| expect(payloads[0]?.reasoning).toEqual({ effort: "high" }); |
There was a problem hiding this comment.
Reconcile conflicting missing-reasoning expectations
This new case asserts that absent payload.reasoning should be populated, but the existing test does not add reasoning for non-reasoning models without existing reasoning payload in the same file still expects undefined for openaiModel when reasoning is absent. Because shouldApplyOpenAIReasoningCompatibility covers both openai/openai-responses and openai-codex/openai-codex-responses, these expectations diverge after this change and will leave the test suite red unless the older fixture/expectation is updated.
Useful? React with 👍 / 👎.
|
Reporter here. Confirming the runtime change in Have had the equivalent manual patch active on the bundled On the 3 |
…rl (openclaw#70904) The existing test asserted "does not add reasoning for non-reasoning models without existing reasoning payload" but the fixture used { api: "openai-responses", provider: "openai", id: "gpt-5.2" } with no baseUrl — resolveOpenAIRequestCapabilities treats that as the default endpoint class, which has supportsOpenAIReasoningCompatPayload === true. The test was passing only because the pre-fix wrapper silently skipped undefined payload.reasoning. After the openclaw#70904 fix, the new !existingReasoning branch correctly injects { effort: "medium" } for that compat-capable route, so the assertion breaks. Rename the test to reflect what it should actually cover (proxy routes that fail the compat gate) and add baseUrl: "https://proxy.example.com/v1" to force shouldApplyOpenAIReasoningCompatibility === false. With that, the wrapper returns early and payload.reasoning stays undefined, as intended. Diagnosis + suggested fix from the openclaw#70904 reporter (@douglferreira977) and Greptile.
|
Thanks for the runtime A/B confirmation and the Greptile diagnosis — both spot-on. Pushed your suggested test fix in 4dbf49a: renamed to "proxy-routed models without prior reasoning" and added |
steipete
left a comment
There was a problem hiding this comment.
Codex review: verified current main still has the bug from #70904: createOpenAIThinkingLevelWrapper only patches body.reasoning when it is "none" or an existing object, so the common undefined case never gets reasoning.effort (src/agents/pi-embedded-runner/openai-stream-wrappers.ts:246).
This PR is the right minimal fix. It adds the missing undefined/null branch and the stale non-reasoning fixture was already corrected to a proxy-routed model. Current check rollup is green. I would land this.
steipete
left a comment
There was a problem hiding this comment.
Codex follow-up: I’m retracting my earlier “land this” recommendation unless we can identify a real broken runtime path.
I traced the normal embedded-agent path more deeply:
- OpenClaw passes the selected/default think level into
createAgentSessionasthinkingLevel: mapThinkingLevel(params.thinkLevel)(src/agents/pi-embedded-runner/run/attempt.ts:1235). pi-agent-coreforwards that session thinking level into model calls asoptions.reasoning(node_modules/@mariozechner/pi-agent-core/dist/agent.js:280).pi-aiOpenAI Responses mapsoptions.reasoningtoreasoningEffortbefore callingbuildParams(node_modules/@mariozechner/pi-ai/dist/providers/openai-responses.js:105-109), then emitsparams.reasoning = { effort, summary }whenreasoningEffortis present (:169-175).pi-aiOpenAI Codex Responses does the same atnode_modules/@mariozechner/pi-ai/dist/providers/openai-codex-responses.js:197-200and:231-235.
That means the wrapper’s missing existingReasoning === undefined branch does not prove production breakage. In the normal Pi run path, the upstream provider should already create the reasoning object from options.reasoning; the OpenClaw wrapper then only has to override existing reasoning or the "none" sentinel.
The current PR test exercises a bare wrapper with a synthetic base stream that does not model pi-agent-core/pi-ai option propagation, so it proves the helper’s isolated behavior but not the reported runtime bug.
Could you point to the concrete production code path that is broken? Specifically: where OpenClaw has a selected/default think level, but the OpenAI/Codex stream call reaches pi-ai without options.reasoning, resulting in an outbound request with no body.reasoning. A captured outbound payload or a production-shaped regression test through applyExtraParamsToAgent + the real Pi options would make this landable.
Until then I would not merge this as a bug fix. At most it is defensive hardening, and it risks masking a test that is currently not representative of the actual run path.
|
Thanks for the follow-up and the runtime A/B notes. I dug through the production path and the upstream Pi layers before deciding this. The normal embedded OpenAI/Codex path is already covered by the session-level reasoning path:
I added a maintainer-side regression contract on main in 0211280 (
Verification on current main:
Given that, I do not want to land this wrapper change as-is. It makes If you can show the concrete production code path where a selected/default thinking level exists but |
|
Thanks for the detailed path trace and the regression contract in 0211280 — that's the right place to guarantee the behavior. Agreed the wrapper change would have hidden a lower-level contract break. Not reopening; will leave the wrapper alone and file a fresh PR only if I can produce a concrete repro on the embedded runtime path that reaches the wrapper with missing options.reasoning. |
Summary
Closes #70904.
`createOpenAIThinkingLevelWrapper` (`src/agents/pi-embedded-runner/openai-stream-wrappers.ts`) only reacted to three prior states of `payloadObj.reasoning`:
pi-ai's Responses / Codex-Responses clients only set `body.reasoning` when `options.reasoningEffort` is threaded through, which OpenClaw does not do for this wrapper. So `payloadObj.reasoning` is `undefined` in every real run, none of the three branches fires, and `thinkingDefault` / `thinkingLevel` is silently cosmetic for `openai-codex/gpt-5.x` and `openai/gpt-5.x` — the server-side default applies regardless of user configuration.
The reporter confirmed via A/B runtime instrumentation on 2026.4.22.
Change
Add the missing `!existingReasoning` branch, mirroring `normalizeProxyReasoningPayload`'s already-shipped handling in `proxy-stream-wrappers.ts:43`. Still gated behind the existing `shouldApplyOpenAIReasoningCompatibility` check, so non-reasoning-capable models are untouched.
Test plan