fix(openai-codex): support GPT-5.6 Luna Responses Lite#889
Conversation
📝 WalkthroughWalkthroughThe Codex provider now supports GPT-5.6 Luna Responses Lite requests through exact model gating, body transformation, session-aware headers, consistent streaming/fallback handling, and ChangesLuna Responses Lite support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant OpenAiCodexHandler
participant CodexSDK
participant SSEEndpoint
OpenAiCodexHandler->>OpenAiCodexHandler: Resolve session and transform Luna body
OpenAiCodexHandler->>CodexSDK: Send transformed body and headers
CodexSDK-->>OpenAiCodexHandler: Return stream or fail
OpenAiCodexHandler->>SSEEndpoint: Retry with the same body and headers
SSEEndpoint-->>OpenAiCodexHandler: Return SSE stream
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/api/providers/__tests__/openai-codex.spec.ts (1)
339-339: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueHoist the Luna version string into a shared test constant.
"0.144.0"is repeated in three assertions; centralizing it avoids updating multiple spots when the header version changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/api/providers/__tests__/openai-codex.spec.ts` at line 339, Define a shared test constant for the Luna version near the other test fixtures in openai-codex.spec.ts, then replace all three hardcoded "0.144.0" assertion values with that constant. Keep the existing assertions and expected version unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/api/providers/__tests__/openai-codex.spec.ts`:
- Line 339: Define a shared test constant for the Luna version near the other
test fixtures in openai-codex.spec.ts, then replace all three hardcoded
"0.144.0" assertion values with that constant. Keep the existing assertions and
expected version unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c2922768-3277-4e17-9d7b-91f33bc97186
📒 Files selected for processing (4)
docs/luna-responses-lite-analysis.mdsrc/api/providers/__tests__/openai-codex-native-tool-calls.spec.tssrc/api/providers/__tests__/openai-codex.spec.tssrc/api/providers/openai-codex.ts
|
@zoomote please review this PR |
|
@roomote please review |
|
No code issues found. Reviewed 19b7327 |
edelauna
left a comment
There was a problem hiding this comment.
Main findings were around types, otherwise included some test nits.
| const baseRequestBody = this.buildRequestBody(model, formattedInput, systemPrompt, reasoningEffort, metadata) | ||
| const requestBody = | ||
| model.id === LUNA_MODEL_ID | ||
| ? transformLunaResponsesLiteBody(baseRequestBody, effectiveSessionId) |
There was a problem hiding this comment.
If transformLunaResponsesLiteBody throws here (e.g. malformed input), the error propagates through createMessage without hitting the try-catch at line 257 — so no TelemetryService.captureException is called. Worth wrapping this in a try/catch that logs before rethrowing.
| }) | ||
| expect(body).not.toHaveProperty("tools") | ||
| expect(body).not.toHaveProperty("instructions") | ||
| expect(body.input[0]).toMatchObject({ type: "additional_tools", role: "developer" }) |
There was a problem hiding this comment.
This only spot-checks input[0] and input[1]. Could we also assert body.input length and that the user message is forwarded at input[2]? A bug that drops the user turn would pass as-is.
| const LUNA_MODEL_ID = "gpt-5.6-luna" | ||
| const LUNA_CODEX_VERSION = "0.144.0" | ||
|
|
||
| type ResponsesRequestBody = Record<string, any> |
There was a problem hiding this comment.
There is also a stricter interface ResponsesRequestBody declared inside buildRequestBody (line ~352). The transformLunaResponsesLiteBody signature uses this weaker Record<string, any> alias — callers get no type guidance. Could we drop this alias and use a concrete type or any directly on the function signature?
| tool_choice: "auto", | ||
| parallel_tool_calls: false, | ||
| prompt_cache_key: effectiveSessionId, | ||
| reasoning: { ...reasoning, context: "all_turns" }, |
There was a problem hiding this comment.
Because context: "all_turns" comes after the spread, it overwrites any context value already in reasoning. Is that intentional? Worth a comment here, and probably a test case where the input already has a context set.
|
|
||
| expect(retryBody).toEqual(firstBody) | ||
| expect(firstBody.prompt_cache_key).toBe("task-retry") | ||
| expect(firstOptions.headers).toMatchObject({ |
There was a problem hiding this comment.
The retry test checks session-id and x-session-affinity on both calls but misses x-openai-internal-codex-responses-lite and version. A regression that strips those headers on retry wouldn't be caught here.
| const firstBody = JSON.parse(firstOptions.body) | ||
| const retryBody = JSON.parse(retryOptions.body) | ||
|
|
||
| expect(retryBody).toEqual(firstBody) |
There was a problem hiding this comment.
If the implementation regenerated the transformed body on retry (bug), retryBody would still equal firstBody because the inputs are identical. Could we spy on transformLunaResponsesLiteBody and assert it was called exactly once to actually verify the single-build contract?
| const fetchOptions = mockFetch.mock.calls[0][1] | ||
| const body = JSON.parse(fetchOptions.body) | ||
| const sessionId = body.prompt_cache_key | ||
| expect(sessionId).toEqual(expect.any(String)) |
There was a problem hiding this comment.
expect.any(String) would also pass for "" or a stringified undefined. Since this.sessionId is always a uuidv7, could we match the shape instead?
| expect(sessionId).toEqual(expect.any(String)) | |
| expect(sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) |
| ...transformedInput, | ||
| ], | ||
| tool_choice: "auto", | ||
| parallel_tool_calls: false, |
There was a problem hiding this comment.
These silently discard any tool_choice or parallelToolCalls the caller supplied. Worth a short comment explaining Luna Responses Lite requires these values.
| }) | ||
|
|
||
| describe("transformLunaResponsesLiteBody", () => { | ||
| it("creates the exact Responses Lite body while preserving unrelated fields and reasoning", () => { |
There was a problem hiding this comment.
Both tests here pass a reasoning without a pre-existing context key — so they only test that context is added, not that it overwrites an existing value. Could we add a case with reasoning: { effort: "high", context: "current_turn" } and assert the output has context: "all_turns"?
Summary and root cause
Zoo Code already catalogs the exact model ID
gpt-5.6-luna, but ChatGPT OAuth requests to that model failed because Luna expects the Responses Lite request contract rather than the normal Responses request used by the OpenAI Codex provider. The standard request body and headers were therefore incompatible with Luna.This change detects only the exact
gpt-5.6-lunamodel on the ChatGPT OAuth Codex path, transforms the outgoing body into the Responses Lite-compatible shape, and adds the Luna-only headers required for routing, affinity, and prompt caching. Non-Luna requests continue using the existing standard Responses contract.The initial request referred to “5.5 Luna.” However, upstream anomalyco/opencode#36685, its corresponding commit, and Zoo Code's existing model catalog identify the model as
gpt-5.6-luna. Exact equality is intentional: this does not change GPT-5.5, Sol, Terra, model aliases, API-key providers, or any other transport.Request handling
For exact-model Luna requests, the provider now:
session-idandx-session-affinityheaders in addition to the existing Codexsession_idheader;prompt_cache_keyso request affinity and prompt caching remain stable;completePrompt, keeping one-shot completion behavior aligned with streaming behavior.flowchart LR subgraph Before[Before] B1[ChatGPT OAuth request] --> B2{Exact model gpt-5.6-luna?} B2 --> B3[Normal Responses body and headers] B3 --> B4[Luna rejects incompatible contract] end subgraph After[After] A1[ChatGPT OAuth request] --> A2{Exact model gpt-5.6-luna?} A2 -- No --> A3[Existing normal Responses path] A2 -- Yes --> A4[Transform to Responses Lite body] A4 --> A5[Add Luna-only session and affinity headers] A5 --> A6[SDK streaming, SSE fallback, auth retry, or completePrompt] A6 --> A7[Luna request succeeds] endtaskIdand provider session boundaryNo core task ID was removed, replaced, regenerated, or mutated.
Before this change, request metadata
taskIdwas already used only inside this provider as the Codexsession_idheader. The provider now resolves that value once into a provider-localeffectiveSessionId, using the handler UUID only as a fallback when request metadata does not supply ataskId. That single provider-local value is then reused consistently for:session_id;session-id;x-session-affinity; andprompt_cache_key.Core task execution, tool dispatch, subtask routing, persistence, and task lookup remain unchanged. The provider consumes core metadata to derive transport metadata; it does not write a provider session value back into core task state.
flowchart LR subgraph Core[Core task domain] T[Existing taskId] E[Task execution] D[Tool and subtask dispatch] P[Persistence and lookup] T --> E T --> D T --> P end subgraph Provider[OpenAI Codex provider boundary] M[Read request metadata taskId] F[Fallback: handler UUID] S[Resolve effectiveSessionId once] H[session_id / session-id / x-session-affinity] C[prompt_cache_key] M --> S F --> S S --> H S --> C end T -- read-only metadata --> M S -. no mutation or write-back .-> TScope and non-goals
gpt-5.6-lunarequests through the ChatGPT OAuth OpenAI Codex provider.Tests and verification
srctype check passed.srclint passed.References
docs/luna-responses-lite-analysis.mdSummary by CodeRabbit
New Features
gpt-5.6-lunamodel through Responses Lite request handling.Documentation
Tests