Skip to content

[codex] strip codex responses max_output_tokens#399

Merged
cita-777 merged 2 commits intomainfrom
codex/strip-codex-responses-max-output-tokens
Apr 2, 2026
Merged

[codex] strip codex responses max_output_tokens#399
cita-777 merged 2 commits intomainfrom
codex/strip-codex-responses-max-output-tokens

Conversation

@cita-777
Copy link
Copy Markdown
Owner

@cita-777 cita-777 commented Apr 2, 2026

This PR fixes a Codex upstream contract mismatch in the /v1/messages and /v1/responses proxy paths.

When a downstream Claude-style /v1/messages request was bridged into the ChatGPT Codex OAuth upstream /responses endpoint, Metapi preserved or synthesized a max_output_tokens field in the outgoing Codex request body. The current Codex upstream rejects that field with Unsupported parameter: max_output_tokens, which surfaced to users as a fast 400 failure even though the downstream request itself was otherwise valid.

The root cause was that Codex-specific response shaping still assumed an older native /responses contract. We already force store: false and normalize a few Codex-only compatibility details in buildUpstreamEndpointRequest(...), but token-limit fields were still allowed to pass through that final Codex request-shaping step. That meant the same incompatibility could leak in from OpenAI chat input, Claude /v1/messages input, native /v1/responses payloads, or even configured payload rules.

The fix adds a Codex-only cleanup step in the final /responses request builder so max_output_tokens, max_completion_tokens, and max_tokens are stripped before the request is sent upstream, and again after payload rules are applied so configuration cannot reintroduce the unsupported field. The associated tests were updated to lock the current contract for all affected entry paths, including /v1/messages -> /responses, OpenAI/chat-derived Codex requests, native Codex responses requests, and payload-rule overrides.

Validation was done with focused proxy tests plus the repo drift guardrail:

  • npm test -- src/server/routes/proxy/upstreamEndpoint.test.ts src/server/routes/proxy/chat.codex-oauth.test.ts src/server/routes/proxy/responses.codex-oauth.test.ts
  • npm run repo:drift-check

Summary by CodeRabbit

  • Bug Fixes

    • Removed unsupported token-limit fields from Codex-style requests so upstream requests no longer include them.
  • New Features

    • Added a dedicated Codex responses normalizer to centralize compatibility handling for Codex vs. non-Codex requests.
  • Tests

    • Updated and added tests to verify token-field stripping and Codex-specific response normalization behavior.

@github-actions github-actions bot added area: server Server-side API and backend changes size: XS Less than 50 lines changed labels Apr 2, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Codex responses normalization was moved from route-layer helpers into a new transformer function normalizeCodexResponsesBodyForProxy. Upstream request building now calls that normalizer (before and after payload rules); tests were updated/added to validate token-limit fields are stripped and normalization delegation.

Changes

Cohort / File(s) Summary
Transformer: new module & tests
src/server/transformers/openai/responses/codexCompatibility.ts, src/server/transformers/openai/responses/codexCompatibility.test.ts
Added normalizeCodexResponsesBodyForProxy(...) which rewrites systemdeveloper roles, ensures instructions is a string, sets store: false, and removes max_output_tokens, max_completion_tokens, and max_tokens for sitePlatform === 'codex'. Tests validate codex vs openai behavior.
Route logic updates
src/server/routes/proxy/upstreamEndpoint.ts
Removed in-file Codex normalization helpers and now imports/uses normalizeCodexResponsesBodyForProxy(...). Calls the normalizer both before payload-rule application and after payload-rule configuration for /responses flows.
Tests: expectations & architecture boundary
src/server/routes/proxy/chat.codex-oauth.test.ts, src/server/routes/proxy/upstreamEndpoint.test.ts, src/server/routes/proxy/architecture-boundaries.test.ts
Updated tests to expect token-limit fields (max_output_tokens, max_tokens, max_completion_tokens) to be undefined. Added an architecture-boundary test asserting route-layer no longer implements Codex-normalization helpers and imports the transformer module.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Proxy as UpstreamEndpoint (proxy)
  participant Transformer as normalizeCodexResponsesBodyForProxy
  participant Upstream as Upstream API

  Client->>Proxy: POST /responses (Codex-style body)
  Proxy->>Transformer: normalizeCodexResponsesBodyForProxy(body, 'codex')
  Transformer-->>Proxy: normalized body (roles rewritten, token fields removed, store=false)
  Proxy->>Proxy: applyConfiguredPayloadRules(normalized body)
  Proxy->>Transformer: normalizeCodexResponsesBodyForProxy(bodyAfterRules, 'codex')
  Transformer-->>Proxy: final normalized body
  Proxy->>Upstream: forward normalized request
  Upstream-->>Proxy: response
  Proxy-->>Client: proxied response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex, size: M

Poem

🐇 In tunnels of code I hop and nod,
I strip the tokens where Codex trod.
Roles softened, instructions set true,
Store turned off — a tidy view.
Hoppity-hop, the proxy's new!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[codex] strip codex responses max_output_tokens' directly summarizes the main change: removing unsupported max_output_tokens field from Codex responses requests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/strip-codex-responses-max-output-tokens

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/server/routes/proxy/chat.codex-oauth.test.ts (1)

213-214: Add an explicit max_tokens assertion to fully lock the Codex strip contract.

You now assert max_output_tokens and max_completion_tokens; adding max_tokens here would cover all three stripped aliases in this path.

🧪 Suggested test addition
     expect(forwardedBody.max_output_tokens).toBeUndefined();
+    expect(forwardedBody.max_tokens).toBeUndefined();
     expect(forwardedBody.max_completion_tokens).toBeUndefined();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/routes/proxy/chat.codex-oauth.test.ts` around lines 213 - 214, The
test in chat.codex-oauth.test.ts currently asserts that
forwardedBody.max_output_tokens and forwardedBody.max_completion_tokens are
undefined but misses the max_tokens alias; update the test to also assert that
forwardedBody.max_tokens is undefined (add
expect(forwardedBody.max_tokens).toBeUndefined(); alongside the existing
assertions for max_output_tokens and max_completion_tokens) so the Codex strip
contract is fully locked for all three token aliases.
src/server/routes/proxy/upstreamEndpoint.test.ts (1)

837-877: Extend Codex stripping assertions to include all token-limit aliases.

These updated tests now pin max_output_tokens, but the production strip helper also removes max_tokens and max_completion_tokens. Add both to prevent partial-regression gaps.

🧪 Suggested coverage extension
       responsesOriginalBody: {
         model: 'gpt-5.4',
         input: 'hello codex',
         stream: false,
         store: true,
@@
         temperature: 0.3,
         top_p: 0.8,
+        max_completion_tokens: 256,
+        max_tokens: 128,
         max_output_tokens: 512,
       },
@@
     expect(request.body.top_p).toBe(0.8);
+    expect(request.body.max_completion_tokens).toBeUndefined();
+    expect(request.body.max_tokens).toBeUndefined();
     expect(request.body.max_output_tokens).toBeUndefined();
           params: {
             'text.verbosity': 'low',
+            max_completion_tokens: 48,
+            max_tokens: 32,
             max_output_tokens: 64,
             store: true,
           },
@@
     expect(request.body.safety_identifier).toBeUndefined();
+    expect(request.body.max_completion_tokens).toBeUndefined();
+    expect(request.body.max_tokens).toBeUndefined();
     expect(request.body.max_output_tokens).toBeUndefined();
     expect(request.body.store).toBe(false);

Also applies to: 1005-1043

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/routes/proxy/upstreamEndpoint.test.ts` around lines 837 - 877, The
test for codex token-limit stripping currently only checks max_output_tokens;
update the assertions generated from buildUpstreamEndpointRequest (using the
responsesOriginalBody payload) to also assert that request.body.max_tokens and
request.body.max_completion_tokens are undefined (in addition to
max_output_tokens) so the test mirrors the production strip helper behavior;
apply the same addition to the other related test block referenced around lines
1005-1043.
src/server/routes/proxy/upstreamEndpoint.ts (1)

972-991: Move Codex request-shaping into shared transformer/proxy-core logic.

Lines 972-991 add more Codex protocol-conversion behavior directly in the route adapter path. Consider extracting this Codex normalization chain into shared transformation logic (single helper/module) and calling it here to keep route code thin.

As per coding guidelines src/server/routes/**/*.ts: “Route files in src/server/routes/** are adapters... and must not own protocol conversion...”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/routes/proxy/upstreamEndpoint.ts` around lines 972 - 991, The
route is performing Codex protocol conversion inline using the chain of
functions ensureCodexResponsesStoreFalse, stripCodexUnsupportedResponsesFields,
ensureCodexResponsesInstructions, applyCodexResponsesCompatibility and
applyConfiguredPayloadRules; extract that chain into a shared transformer (e.g.,
export a function normalizeCodexResponses(sanitizedResponsesBody, sitePlatform)
from the proxy-core/transformer module) that encapsulates the ordering
(compatibility -> instructions -> strip -> store-flag -> configured payload
rules), update upstreamEndpoint.ts to import and call normalizeCodexResponses to
produce body and configuredResponsesBody (preserving the
applyConfiguredPayloadRules step and sitePlatform parameter), and add unit tests
for the new helper and replace the inline chain with the single helper call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/server/routes/proxy/chat.codex-oauth.test.ts`:
- Around line 213-214: The test in chat.codex-oauth.test.ts currently asserts
that forwardedBody.max_output_tokens and forwardedBody.max_completion_tokens are
undefined but misses the max_tokens alias; update the test to also assert that
forwardedBody.max_tokens is undefined (add
expect(forwardedBody.max_tokens).toBeUndefined(); alongside the existing
assertions for max_output_tokens and max_completion_tokens) so the Codex strip
contract is fully locked for all three token aliases.

In `@src/server/routes/proxy/upstreamEndpoint.test.ts`:
- Around line 837-877: The test for codex token-limit stripping currently only
checks max_output_tokens; update the assertions generated from
buildUpstreamEndpointRequest (using the responsesOriginalBody payload) to also
assert that request.body.max_tokens and request.body.max_completion_tokens are
undefined (in addition to max_output_tokens) so the test mirrors the production
strip helper behavior; apply the same addition to the other related test block
referenced around lines 1005-1043.

In `@src/server/routes/proxy/upstreamEndpoint.ts`:
- Around line 972-991: The route is performing Codex protocol conversion inline
using the chain of functions ensureCodexResponsesStoreFalse,
stripCodexUnsupportedResponsesFields, ensureCodexResponsesInstructions,
applyCodexResponsesCompatibility and applyConfiguredPayloadRules; extract that
chain into a shared transformer (e.g., export a function
normalizeCodexResponses(sanitizedResponsesBody, sitePlatform) from the
proxy-core/transformer module) that encapsulates the ordering (compatibility ->
instructions -> strip -> store-flag -> configured payload rules), update
upstreamEndpoint.ts to import and call normalizeCodexResponses to produce body
and configuredResponsesBody (preserving the applyConfiguredPayloadRules step and
sitePlatform parameter), and add unit tests for the new helper and replace the
inline chain with the single helper call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2900399a-4cd8-4f26-8221-7c4c2b43cebb

📥 Commits

Reviewing files that changed from the base of the PR and between 1407f75 and af9f284.

📒 Files selected for processing (3)
  • src/server/routes/proxy/chat.codex-oauth.test.ts
  • src/server/routes/proxy/upstreamEndpoint.test.ts
  • src/server/routes/proxy/upstreamEndpoint.ts

@github-actions github-actions bot added size: M 200 to 499 lines changed and removed size: XS Less than 50 lines changed labels Apr 2, 2026
@cita-777
Copy link
Copy Markdown
Owner Author

cita-777 commented Apr 2, 2026

Addressed the review follow-up in 8c4802f.

  • Added explicit max_tokens / max_completion_tokens strip assertions alongside max_output_tokens in the Codex route tests.
  • Moved Codex /responses request normalization out of upstreamEndpoint.ts into src/server/transformers/openai/responses/codexCompatibility.ts.
  • Added unit coverage for the new helper and an architecture-boundary assertion so the route layer no longer owns that Codex normalization chain.

Validation rerun:

  • npm test -- src/server/transformers/openai/responses/codexCompatibility.test.ts src/server/routes/proxy/architecture-boundaries.test.ts src/server/routes/proxy/upstreamEndpoint.test.ts src/server/routes/proxy/chat.codex-oauth.test.ts src/server/routes/proxy/responses.codex-oauth.test.ts
  • npm run repo:drift-check

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/server/transformers/openai/responses/codexCompatibility.test.ts (1)

4-45: Add an explicit idempotence case.

normalizeCodexResponsesBodyForProxy() is intentionally called twice in src/server/routes/proxy/upstreamEndpoint.ts, so a normalize(normalize(body)) assertion here would lock that contract at the unit-test layer too.

♻️ Suggested test
 describe('normalizeCodexResponsesBodyForProxy', () => {
   it('normalizes codex responses bodies before proxying upstream', () => {
     const body = normalizeCodexResponsesBodyForProxy({
       input: [
         {
@@
     expect(body).toEqual({
       input: [
         {
           type: 'message',
           role: 'developer',
           content: [{ type: 'input_text', text: 'be precise' }],
         },
       ],
       instructions: '',
       store: false,
       temperature: 0.3,
     });
   });
+
+  it('is value-idempotent for codex bodies', () => {
+    const source = {
+      input: [
+        {
+          type: 'message',
+          role: 'system',
+          content: [{ type: 'input_text', text: 'be precise' }],
+        },
+      ],
+      max_output_tokens: 512,
+      store: true,
+    };
+
+    const once = normalizeCodexResponsesBodyForProxy(source, 'codex');
+    const twice = normalizeCodexResponsesBodyForProxy(once, 'codex');
+
+    expect(twice).toEqual(once);
+  });
 
   it('leaves non-codex bodies untouched', () => {
     const source = {
       input: 'hello',
       max_output_tokens: 512,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/transformers/openai/responses/codexCompatibility.test.ts` around
lines 4 - 45, Add an idempotence unit test in codexCompatibility.test.ts that
asserts calling normalizeCodexResponsesBodyForProxy twice yields the same result
(e.g., const once = normalizeCodexResponsesBodyForProxy(source, 'codex'); const
twice = normalizeCodexResponsesBodyForProxy(once, 'codex');
expect(twice).toEqual(once)); this locks the contract used by
src/server/routes/proxy/upstreamEndpoint.ts where
normalizeCodexResponsesBodyForProxy is invoked twice and ensures
normalizeCodexResponsesBodyForProxy remains stable when re-applied.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/server/transformers/openai/responses/codexCompatibility.test.ts`:
- Around line 4-45: Add an idempotence unit test in codexCompatibility.test.ts
that asserts calling normalizeCodexResponsesBodyForProxy twice yields the same
result (e.g., const once = normalizeCodexResponsesBodyForProxy(source, 'codex');
const twice = normalizeCodexResponsesBodyForProxy(once, 'codex');
expect(twice).toEqual(once)); this locks the contract used by
src/server/routes/proxy/upstreamEndpoint.ts where
normalizeCodexResponsesBodyForProxy is invoked twice and ensures
normalizeCodexResponsesBodyForProxy remains stable when re-applied.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 95d5dc73-9cdb-468e-96b1-27bb3c15a95a

📥 Commits

Reviewing files that changed from the base of the PR and between af9f284 and 8c4802f.

📒 Files selected for processing (6)
  • src/server/routes/proxy/architecture-boundaries.test.ts
  • src/server/routes/proxy/chat.codex-oauth.test.ts
  • src/server/routes/proxy/upstreamEndpoint.test.ts
  • src/server/routes/proxy/upstreamEndpoint.ts
  • src/server/transformers/openai/responses/codexCompatibility.test.ts
  • src/server/transformers/openai/responses/codexCompatibility.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/server/routes/proxy/chat.codex-oauth.test.ts
  • src/server/routes/proxy/upstreamEndpoint.test.ts

@cita-777 cita-777 merged commit 6ad9ec6 into main Apr 2, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: server Server-side API and backend changes size: M 200 to 499 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant