Skip to content

feat: auto-detect ChatGPT subscription tokens and route to chatgpt.com#4548

Open
Purvi09 wants to merge 12 commits into
maximhq:devfrom
Purvi09:feat/chatgpt-passthrough-auto-detect
Open

feat: auto-detect ChatGPT subscription tokens and route to chatgpt.com#4548
Purvi09 wants to merge 12 commits into
maximhq:devfrom
Purvi09:feat/chatgpt-passthrough-auto-detect

Conversation

@Purvi09

@Purvi09 Purvi09 commented Jun 18, 2026

Copy link
Copy Markdown

Summary

When a user logs into Codex CLI using their ChatGPT subscription (codex login with ChatGPT OAuth), the resulting bearer token is a JWT containing a chatgpt_account_id claim. Sending this token to api.openai.com/v1/responses returns 401 because it is a subscription token, not an API key.

This PR auto-detects that token in Bifrost's /v1/responses pre-hook and transparently reroutes the request to chatgpt.com/backend-api/codex/responses — the endpoint that actually accepts it. No config change or URL change is required from the user.

Changes

  • core/providers/openai/chatgpt_passthrough.go — new file with ParseChatGPTJWT (decodes the JWT payload and checks for the chatgpt_account_id claim under https://api.openai.com/auth) and IsChatGPTPassthrough (reads the detection flag from context)
  • core/schemas/bifrost.go — adds BifrostContextKeyChatGPTPassthrough context key (bool) set by the pre-hook and read by the provider
  • transports/bifrost-http/integrations/openai.go — pre-hook for POST /v1/responses that calls ParseChatGPTJWT on the incoming bearer token; if detected, sets the URL override to chatgpt.com/backend-api/codex/responses, skips Bifrost key selection, and forwards the original bearer token
  • core/providers/openai/openai.goResponses() and ResponsesStream() force store: false when IsChatGPTPassthrough is true (required by the chatgpt.com backend)
  • core/providers/openai/chatgpt_passthrough_test.go — unit tests for ParseChatGPTJWT covering valid JWT, missing claim, malformed tokens, non-JWT API keys, and edge cases

Design decision: JWT signature is intentionally not verified — the token is used only as a routing hint. The upstream chatgpt.com endpoint validates it for real.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

Unit tests:

cd core
go test ./providers/openai/ -run TestParseChatGPTJWT -v

Breaking changes

  • Yes
  • No

Related issues

Closes #4459

Security considerations

  • The JWT is decoded (base64) but not signature-verified — it is used only as a routing hint. The chatgpt.com backend performs real authentication with the token.
  • No credentials are stored or logged; the bearer token passes through in-memory only.
  • BifrostContextKeySkipKeySelection is set so Bifrost does not attempt to inject a stored API key over the user's token.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

roroghost17 and others added 10 commits June 18, 2026 17:39
## Summary

Fixes a bug where OTEL plugin headers were being overwritten with redacted placeholder values when saving a plugin configuration. After the multi-profile change, header values stored as plain strings inside the `profiles` array were not being restored from the database before saving, causing real credentials to be replaced with masked values like `****`.

## Changes

- Extracted `restoreRedactedValue` as a standalone recursive helper, replacing the inline logic in `restoreRedactedFromExisting`. This allows the restoration logic to descend into both nested maps and slices.
- Added slice traversal support (index-aligned) so that elements within arrays like the OTEL `profiles` array are individually checked and restored.
- Added plain-string redaction detection so that header values stored as raw strings (rather than `EnvVar` objects) are also restored from the existing DB config when they carry a redaction artifact. Empty strings are intentionally left as-is to allow clearing a value.
- Added `TestRestoreRedacted_OTELProfilesHeaders` to cover both failure modes: slice traversal and plain-string secret restoration. Also asserts that genuinely new (non-redacted) values pass through unchanged.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/handlers/...
```

Verify that saving an OTEL plugin configuration with multiple profiles, after a GET that returns redacted header values, does not overwrite the stored credentials in the database. Confirm that providing a genuinely new header value still persists correctly.

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

N/A

## Security considerations

This fix ensures that redacted credential placeholders returned to the client are never written back over real secrets stored in the database. The restoration logic only replaces values that are confirmed redaction artifacts; empty strings and non-redacted values are always passed through as-is, preserving the ability to clear a credential intentionally.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added ChatGPT subscription passthrough capability, enabling authenticated ChatGPT users to route traffic directly
    • Implemented JWT token recognition and validation for ChatGPT accounts
  • Tests

    • Added comprehensive test coverage for token parsing, including valid tokens, malformed tokens, and edge cases

Walkthrough

Adds ChatGPT subscription JWT auto-detection to Bifrost's OpenAI /responses route. When a bearer token's JWT payload contains a chatgpt_account_id claim, the HTTP transport pre-hook sets passthrough context flags and forwards the original bearer token, while the OpenAI provider skips the responses→chat-completions fallback, disables store, and routes the request to chatgpt.com/backend-api/codex/responses.

Changes

ChatGPT Passthrough Routing

Layer / File(s) Summary
Context key constant, JWT parsing helpers, and tests
core/schemas/bifrost.go, core/providers/openai/chatgpt_passthrough.go, core/providers/openai/chatgpt_passthrough_test.go
Defines BifrostContextKeyChatGPTPassthrough constant, ChatGPTCodexURL and auth-claim constants, ParseChatGPTJWT (base64url-decodes the JWT payload and extracts chatgpt_account_id), IsChatGPTPassthrough (reads the boolean flag from BifrostContext), and unit tests covering valid tokens, missing claims, wrong namespaces, invalid base64, non-JSON payloads, and malformed token shapes.
HTTP transport pre-hook detection and context wiring
transports/bifrost-http/integrations/openai.go
In the responses route PreCallback, reads the Authorization header; on successful ParseChatGPTJWT, sets ChatGPTPassthrough=true, disables key selection, and propagates the original bearer token via BifrostContextKeyExtraHeaders.
Provider store disabling and URL rerouting for passthrough requests
core/providers/openai/openai.go
In Responses and ResponsesStream, guards the fallback-to-chat-completions path with !IsChatGPTPassthrough(ctx), sets request.Params.Store=false when passthrough is active (initializing Params if nil), and swaps the endpoint URL to ChatGPTCodexURL for passthrough requests before dispatching.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Codex CLI
  participant PreCallback as HTTP PreCallback
  participant ParseChatGPTJWT
  participant OpenAIProvider
  participant ChatGPTBackend as chatgpt.com/backend-api/codex/responses

  Client->>PreCallback: POST /openai/responses\nAuthorization: Bearer <JWT>
  PreCallback->>ParseChatGPTJWT: ParseChatGPTJWT(token)
  ParseChatGPTJWT-->>PreCallback: accountID, ok=true
  PreCallback->>PreCallback: set ChatGPTPassthrough=true\nSkipKeySelection=true\nExtraHeaders[Authorization]=Bearer token
  PreCallback->>OpenAIProvider: Responses(ctx, request)
  OpenAIProvider->>OpenAIProvider: IsChatGPTPassthrough(ctx)==true\nskip fallback, Store=false\nURL=ChatGPTCodexURL
  OpenAIProvider->>ChatGPTBackend: POST chatgpt.com/backend-api/codex/responses\nAuthorization: Bearer <original JWT>
  ChatGPTBackend-->>OpenAIProvider: response
  OpenAIProvider-->>Client: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • maximhq/bifrost#3505: Modifies the same Responses/ResponsesStream fallback-to-ChatCompletions control flow in core/providers/openai/openai.go, adding a different conditional gate to the same branching logic this PR touches.

Suggested reviewers

  • danpiths
  • Pratham-Mishra04
  • akshaydeo

Poem

🐇 Hop hop, a JWT arrived at the gate,
Its payload decoded — chatgpt_account_id great!
No toggling needed, the claim tells the route,
Straight to codex endpoint, no poking about.
The rabbit grins wide: passthrough detected,
Bearer forwarded, store flags corrected! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main feature: auto-detection of ChatGPT subscription tokens and transparent rerouting to the correct endpoint.
Description check ✅ Passed The description comprehensively covers all required template sections including summary, changes, type of change, affected areas, testing, security, and checklist items.
Linked Issues check ✅ Passed The code changes fully address issue #4459 objectives: auto-detection of ChatGPT JWTs (via ParseChatGPTJWT), routing to chatgpt.com endpoint, skipping key selection, and forcing store=false.
Out of Scope Changes check ✅ Passed All changes are within scope: JWT parsing, context flag handling, endpoint rerouting, and store parameter enforcement are all necessary for the stated ChatGPT passthrough feature.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@transports/bifrost-http/integrations/openai.go`:
- Around line 765-767: The SetValue call on BifrostContextKeyExtraHeaders is
replacing the entire map with only the Authorization header, which drops any
previously collected forwarded headers for the request. Instead of replacing the
map, retrieve the existing BifrostContextKeyExtraHeaders map from bifrostCtx
first (handling the case where it may not exist yet), add the Authorization
header to that map, and then set the updated map back. This ensures the ChatGPT
Authorization passthrough is added as a special case without breaking other
forwarded headers that were collected according to client.header_filter_config.
- Around line 759-760: The Bearer scheme matching in the authHeader validation
uses case-sensitive string comparison with strings.HasPrefix checking for
"Bearer " (capital B), but the HTTP Authorization header scheme is
case-insensitive per RFC 7235. Modify the code to perform case-insensitive
comparison by converting authHeader to lowercase before checking with
strings.HasPrefix, and similarly convert the scheme when extracting the token
using strings.TrimPrefix to handle variations like "bearer", "BEARER", or
"Bearer ".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4925a162-1fdc-4493-99ab-f3433109b899

📥 Commits

Reviewing files that changed from the base of the PR and between 96bb2bd and a601215.

📒 Files selected for processing (5)
  • core/providers/openai/chatgpt_passthrough.go
  • core/providers/openai/chatgpt_passthrough_test.go
  • core/providers/openai/openai.go
  • core/schemas/bifrost.go
  • transports/bifrost-http/integrations/openai.go

Comment thread transports/bifrost-http/integrations/openai.go Outdated
Comment thread transports/bifrost-http/integrations/openai.go Outdated
@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge; the routing, Authorization forwarding, fallback guard, and store override are all correct.

The JWT detection, flag propagation, URL override, and fallback guard work correctly end-to-end. The only findings are a pre-existing in-place mutation of request.Params (consistent with the disableStore pattern that already existed) and missing unit tests for the provider-level overrides — neither blocks the feature from functioning correctly.

No files require special attention; the in-place Params mutation in openai.go is worth a follow-up but does not affect correctness in the common case.

Important Files Changed

Filename Overview
core/providers/openai/chatgpt_passthrough.go New file; JWT base64 decode + JSON claim extraction is correct; no signature verification is intentional and documented.
core/providers/openai/chatgpt_passthrough_test.go Good table-driven coverage of ParseChatGPTJWT; pre-hook detection logic, URL routing, store override, and Authorization forwarding are not tested.
core/providers/openai/openai.go Fallback guard, store override, and URL routing added correctly for both Responses and ResponsesStream; store mutation is in-place on the shared request pointer, consistent with pre-existing disableStore behavior.
core/schemas/bifrost.go New BifrostContextKeyChatGPTPassthrough context key added with clear comment; follows existing key naming and documentation conventions.
transports/bifrost-http/integrations/openai.go Pre-hook correctly detects ChatGPT JWT, sets passthrough and skip-key-selection flags, and copies the Authorization header into context extra headers; applies to all three /v1/responses path aliases and to both streaming and non-streaming paths.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client as Codex CLI
    participant Transport as bifrost-http
    participant PreHook as PreCallback
    participant Core as OpenAI Provider
    participant CGPT as chatgpt.com
    participant OAPI as api.openai.com

    Client->>Transport: POST /v1/responses Authorization: Bearer JWT
    Transport->>PreHook: fires PreCallback
    PreHook->>PreHook: ParseChatGPTJWT(token)
    alt ChatGPT JWT detected
        PreHook->>PreHook: "SetValue ChatGPTPassthrough=true"
        PreHook->>PreHook: "SetValue SkipKeySelection=true"
        PreHook->>PreHook: "SetValue ExtraHeaders Authorization=Bearer JWT"
    end
    Transport->>Core: Responses(ctx, emptyKey, request)
    Core->>Core: IsChatGPTPassthrough skip fallback-to-chat
    Core->>Core: "force store=false"
    Core->>Core: "url = ChatGPTCodexURL"
    Core->>CGPT: POST chatgpt.com/backend-api/codex/responses
    CGPT-->>Core: 200 response
    Core-->>Client: response

    Note over Core,OAPI: Normal OpenAI API key path
    Core->>OAPI: POST api.openai.com/v1/responses
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client as Codex CLI
    participant Transport as bifrost-http
    participant PreHook as PreCallback
    participant Core as OpenAI Provider
    participant CGPT as chatgpt.com
    participant OAPI as api.openai.com

    Client->>Transport: POST /v1/responses Authorization: Bearer JWT
    Transport->>PreHook: fires PreCallback
    PreHook->>PreHook: ParseChatGPTJWT(token)
    alt ChatGPT JWT detected
        PreHook->>PreHook: "SetValue ChatGPTPassthrough=true"
        PreHook->>PreHook: "SetValue SkipKeySelection=true"
        PreHook->>PreHook: "SetValue ExtraHeaders Authorization=Bearer JWT"
    end
    Transport->>Core: Responses(ctx, emptyKey, request)
    Core->>Core: IsChatGPTPassthrough skip fallback-to-chat
    Core->>Core: "force store=false"
    Core->>Core: "url = ChatGPTCodexURL"
    Core->>CGPT: POST chatgpt.com/backend-api/codex/responses
    CGPT-->>Core: 200 response
    Core-->>Client: response

    Note over Core,OAPI: Normal OpenAI API key path
    Core->>OAPI: POST api.openai.com/v1/responses
Loading

Reviews (3): Last reviewed commit: "fix: skip responses-to-chat fallback for..." | Re-trigger Greptile

Comment on lines +19 to +21
// ParseChatGPTJWT parses a raw bearer token, checks for the ChatGPT subscription
// JWT claim, and returns the chatgpt_account_id. No signature verification is
// Returns ("", false) for any non-ChatGPT or malformed token.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The GoDoc comment for ParseChatGPTJWT has a truncated sentence on the second line. "No signature verification is" is missing its predicate, making the doc incomplete and potentially confusing.

Suggested change
// ParseChatGPTJWT parses a raw bearer token, checks for the ChatGPT subscription
// JWT claim, and returns the chatgpt_account_id. No signature verification is
// Returns ("", false) for any non-ChatGPT or malformed token.
// ParseChatGPTJWT parses a raw bearer token, checks for the ChatGPT subscription
// JWT claim, and returns the chatgpt_account_id. No signature verification is
// performed — the token is used only as a routing hint.
// Returns ("", false) for any non-ChatGPT or malformed token.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@coderabbitai coderabbitai Bot requested a review from akshaydeo June 18, 2026 21:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@transports/bifrost-http/integrations/openai.go`:
- Around line 764-770: The code is setting a new Authorization header but does
not remove existing Authorization entries that may use different casing (e.g.,
"authorization", "AUTHORIZATION"). Since HTTP header names are case-insensitive,
this can cause both the old and new credentials to be sent. When iterating
through the existing headers in the for loop before copying them to the headers
map, add a case-insensitive check to skip any Authorization header variants so
that only the new Bearer token authorization is present. Use a case-insensitive
comparison approach when filtering out the existing authorization headers during
the copy operation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b2943818-ffe4-416a-aef3-50f68052c5ac

📥 Commits

Reviewing files that changed from the base of the PR and between a601215 and 6783c5a.

📒 Files selected for processing (2)
  • core/providers/openai/openai.go
  • transports/bifrost-http/integrations/openai.go

Comment on lines +764 to +770
existing, _ := bifrostCtx.Value(schemas.BifrostContextKeyExtraHeaders).(map[string][]string)
headers := make(map[string][]string, len(existing)+1)
for k, v := range existing {
headers[k] = v
}
headers["Authorization"] = []string{"Bearer " + token}
bifrostCtx.SetValue(schemas.BifrostContextKeyExtraHeaders, headers)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Drop existing Authorization entries case-insensitively before injecting the passthrough token.

headers["Authorization"] does not replace an existing authorization/AUTHORIZATION entry. Since auth header names are case-insensitive, downstream header merging can send or choose the wrong credential; remove all existing Authorization variants while copying.

🛡️ Proposed fix
 						existing, _ := bifrostCtx.Value(schemas.BifrostContextKeyExtraHeaders).(map[string][]string)
 						headers := make(map[string][]string, len(existing)+1)
 						for k, v := range existing {
-							headers[k] = v
+							if strings.EqualFold(k, "Authorization") {
+								continue
+							}
+							headers[k] = append([]string(nil), v...)
 						}
 						headers["Authorization"] = []string{"Bearer " + token}
 						bifrostCtx.SetValue(schemas.BifrostContextKeyExtraHeaders, headers)

As per coding guidelines, client.header_filter_config controls forwarded headers and should not accidentally allow sensitive auth headers beyond the passthrough pre-hook.

🤖 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 `@transports/bifrost-http/integrations/openai.go` around lines 764 - 770, The
code is setting a new Authorization header but does not remove existing
Authorization entries that may use different casing (e.g., "authorization",
"AUTHORIZATION"). Since HTTP header names are case-insensitive, this can cause
both the old and new credentials to be sent. When iterating through the existing
headers in the for loop before copying them to the headers map, add a
case-insensitive check to skip any Authorization header variants so that only
the new Bearer token authorization is present. Use a case-insensitive comparison
approach when filtering out the existing authorization headers during the copy
operation.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Auto-detected ChatGPT/Codex OAuth passthrough (forward the official client's existing token, no toggle)

6 participants