Skip to content

Fix Codex model catalog routing for API-key sessions - #183

Merged
lawrencecchen merged 4 commits into
mainfrom
fix/codex-model-catalog
Aug 6, 2026
Merged

lawrencecchen merged 4 commits into
mainfrom
fix/codex-model-catalog

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • recognize Codex model-catalog requests by the documented client_version query
  • lease an OAuth credential for catalog metadata without changing the response session assignment
  • keep ordinary OpenAI /models requests on their selected API-key account
  • keep terminal OAuth refresh failover inside the OAuth pool

Verification

  • reproduced Codex 0.146.1 rejecting the API-key {object,data} model list because it expects {models:[...]}
  • focused proxy tests pass
  • focused race tests pass
  • full go test ./... reached one pre-existing macOS deployment-script timeout; the failing test also times out alone on unchanged main, while all proxy packages passed

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes Codex model catalog routing to always use ChatGPT OAuth and preserve the caller’s session assignment, preventing fallback to the incompatible API-key /models response.

  • Bug Fixes
    • Detect catalog requests via client_version and route them to the OAuth Codex upstream, using internal session internal:codex-model-catalog without changing response assignment.
    • Strip account/model headers during credential selection and broker leasing for catalog routing.
    • Require OAuth for catalog leases (brokered and bound session); reject API-key leases.
    • Keep refresh failover within the OAuth pool; return 503 if no OAuth credential remains.
    • Keep ordinary OpenAI /models requests on the selected API-key account when client_version is absent.

Written for commit bb29b01. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved Codex model catalog requests to consistently use OAuth authentication.
    • Prevented unintended API-key fallback when OAuth refresh or account selection fails.
    • Preserved response assignment and routing behavior for catalog requests.
    • Enforced OAuth requirements for broker and bound-session access.
    • Allowed valid catalog requests without applying standard model validation.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Codex model-catalog requests are detected by method, path, and client_version. They use dedicated sessions, sanitized routing inputs, OAuth-only account selection, OAuth-preserving refresh failover, and OAuth-only lease authorization. Integration tests cover routing, refresh, broker leases, and bound sessions.

Changes

Codex catalog OAuth flow

Layer / File(s) Summary
Catalog detection and OAuth routing
internal/proxy/proxy.go, internal/proxy/codex_model_catalog_test.go
Catalog requests use a dedicated session and sanitized routing request. Account selection, refresh, and failover remain OAuth-only. Tests cover OAuth routing, response assignment, refresh behavior, API-key rejection, and non-catalog API-key behavior.
Catalog lease authorization
internal/proxy/session_lease.go, internal/proxy/codex_model_catalog_test.go
Catalog leases require OAuth authentication and skip model validation. Tests cover broker lease fields and bound-session authorization.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Proxy
  participant CredentialBroker
  participant OAuthUpstream
  Client->>Proxy: GET /models with client_version
  Proxy->>CredentialBroker: Select OAuth catalog account
  CredentialBroker-->>Proxy: OAuth account and token
  Proxy->>OAuthUpstream: Send catalog request
  OAuthUpstream-->>Proxy: Return model catalog
  Proxy-->>Client: Return response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing Codex model catalog routing for API-key sessions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 fix/codex-model-catalog

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/proxy/codex_model_catalog_test.go (1)

102-104: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Synchronize the codexAuth capture.

The httptest handler goroutine writes codexAuth at line 104. The test goroutine reads it at line 168. No explicit synchronization edge exists between the two. The other counters in this file use atomic.Int32 for exactly this reason. Under go test -race this write and read pair can be reported as a data race.

Use an atomic value to make the capture explicit.

♻️ Proposed fix
-	var codexAuth string
+	var codexAuth atomic.Value
 	codexUpstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
-		codexAuth = request.Header.Get("Authorization")
+		codexAuth.Store(request.Header.Get("Authorization"))
 		w.Header().Set("Content-Type", "application/json")
 		_, _ = io.WriteString(w, `{"models":[]}`)
 	}))
-	if codexAuth != "Bearer healthy-token" {
-		t.Fatalf("Codex upstream authorization = %q, want healthy OAuth token", codexAuth)
+	if got, _ := codexAuth.Load().(string); got != "Bearer healthy-token" {
+		t.Fatalf("Codex upstream authorization = %q, want healthy OAuth token", got)
 	}

Also applies to: 168-170

🤖 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 `@internal/proxy/codex_model_catalog_test.go` around lines 102 - 104, Replace
the plain codexAuth variable with an atomic value, store the Authorization
header from the httptest handler via its atomic store operation, and load it in
the test assertion around the existing read at line 168. Preserve the current
captured-header assertion while using the same atomic synchronization pattern as
the other counters.
🤖 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 `@internal/proxy/proxy.go`:
- Around line 4337-4349: The Codex catalog routing flow must neutralize
query-based model overrides on both paths. In internal/proxy/proxy.go lines
4337-4349, update codexModelCatalogRoutingRequest to remove the model query
parameter from the cloned URL as well as the routing headers; in
internal/proxy/session_lease.go lines 563-565, narrow validateRequestModel’s
catalog early return to only the body-model requirement so the existing
query-model conflict validation remains active for bound leases.
- Around line 2239-2243: Update the request handling around modelCatalogRequest,
sessionID, and the subsequent session/account selection flow to set userEmail to
"" for Codex model-catalog requests when no lease is bound. Preserve normal
caller email extraction for non-catalog requests and catalog requests within a
bound lease, preventing the shared codexModelCatalogSessionID record from being
updated with the caller’s identity.

In `@internal/proxy/session_lease.go`:
- Around line 563-565: Update the lease validation flow around the Codex
catalog-request handling so the request body requirement is bypassed without
returning before the query-model conflict check. Preserve the existing query
loop that validates the requested model against lease.Model, while retaining the
early bypass behavior only for body validation.

---

Nitpick comments:
In `@internal/proxy/codex_model_catalog_test.go`:
- Around line 102-104: Replace the plain codexAuth variable with an atomic
value, store the Authorization header from the httptest handler via its atomic
store operation, and load it in the test assertion around the existing read at
line 168. Preserve the current captured-header assertion while using the same
atomic synchronization pattern as the other counters.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c9630fb4-e866-4cbb-b304-1b801317e3a7

📥 Commits

Reviewing files that changed from the base of the PR and between fdb21c1 and bb29b01.

📒 Files selected for processing (3)
  • internal/proxy/codex_model_catalog_test.go
  • internal/proxy/proxy.go
  • internal/proxy/session_lease.go

Comment thread internal/proxy/proxy.go
Comment on lines +2239 to +2243
modelCatalogRequest := requestProvider == accounts.ProviderCodex && codexModelCatalogRequest(r)
sessionID := session.ExtractID(r, s.MaxBodyBytes)
if modelCatalogRequest {
sessionID = codexModelCatalogSessionID
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect ExtractUserEmail sources and Sessions.Put write path.
set -euo pipefail
fd -t f 'extract.go' session | xargs rg -n -C5 'func ExtractUserEmail|userEmailHeaderCandidates'
rg -n -C3 'func .*Store\) Put\(' --type=go

Repository: manaflow-ai/subrouter

Length of output: 674


🏁 Script executed:

#!/bin/bash
set -u

echo "== files =="
git ls-files | rg '(^|/)proxy\.go$|session' | head -100

echo "== proxy.go relevant lines =="
if [ -f internal/proxy/proxy.go ]; then
  sed -n '2210,2350p' internal/proxy/proxy.go | nl -ba -v2210
  echo "== around accountForSessionProviderWithOptions =="
  rg -n -C8 'accountForSessionProviderWithOptions|codexModelCatalogSessionID|Sessions\.Put|userEmail' internal/proxy/proxy.go || true
fi

echo "== session package/store files =="
fd -t f . . | rg 'session' | while read -r f; do
  echo "--- $f ---"
  rg -n -C5 'func .*Put|Store|Sessions|Put\(' "$f" || true
done

echo "== deterministic source checks =="
python3 - <<'PY'
from pathlib import Path
s = Path('internal/proxy/proxy.go').read_text()
checks = {
    "catalog session constant exists": "codexModelCatalogSessionID" in s,
    "handler replaces session id when modelCatalogRequest": "sessionID = codexModelCatalogSessionID" in s,
    "user email extracted before Sessions.Put account path": "userEmail := session.ExtractUserEmail(r)" in s,
    "Put called with userEmail": "s.Sessions.Put(agentType, sessionID, assignment.AccountID, userEmail)" in s,
}
for k,v in checks.items():
    print(f"{k}: {v}")
# Locate line ranges containing the key symbols.
lines = s.splitlines()
for needle in ["codexModelCatalogSessionID", "requestProvider == accounts.ProviderCodex && codexModelCatalogRequest(r)", "userEmail := session.ExtractUserEmail(r)", "s.Sessions.Put(agentType, sessionID, assignment.AccountID, userEmail)"]:
    for i,l in enumerate(lines,1):
        if needle in l:
            print(f"line {i}: {l.strip()}")
            start=max(1,i-8); end=min(len(lines),i+8)
            for j in range(start,end+1):
                print(f"  {j}: {lines[j-1]}")
            break
PY

Repository: manaflow-ai/subrouter

Length of output: 50377


Clear caller email for catalog requests on a shared session slot.

codexModelCatalogSessionID is one global session key. Codex catalog requests still call session.ExtractUserEmail(r) and pass that value into account selection, which writes the caller’s email onto internal:codex-model-catalog when it differs from the stored UserEmail. This mixes caller identity on a shared session record and causes extra Sessions.Put calls for catalog polling. Clear userEmail to "" for model-catalog requests outside any bound lease.

🤖 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 `@internal/proxy/proxy.go` around lines 2239 - 2243, Update the request
handling around modelCatalogRequest, sessionID, and the subsequent
session/account selection flow to set userEmail to "" for Codex model-catalog
requests when no lease is bound. Preserve normal caller email extraction for
non-catalog requests and catalog requests within a bound lease, preventing the
shared codexModelCatalogSessionID record from being updated with the caller’s
identity.

Comment thread internal/proxy/proxy.go
Comment on lines +4337 to +4349
func codexModelCatalogRoutingRequest(r *http.Request) *http.Request {
request := r.Clone(r.Context())
request.Header = r.Header.Clone()
for _, header := range []string{
"X-Subrouter-Account-ID",
"X-Subrouter-Account",
"X-Subrouter-Model",
"X-Model",
} {
request.Header.Del(header)
}
return request
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

A model query parameter on a Codex catalog request is never neutralized on either routing path. session.ExtractModel reads the model query parameter in addition to headers (session/extract.go:129-139). The unbound path sanitizes only headers, and the bound-lease path skips model validation entirely, so ?model= survives in both cases and reaches scheduler pool selection and the broker Model field.

  • internal/proxy/proxy.go#L4337-L4349: in codexModelCatalogRoutingRequest, delete the model query parameter from a cloned URL in addition to the routing headers.
  • internal/proxy/session_lease.go#L563-L565: in validateRequestModel, restrict the catalog early return to the body-model requirement, and keep the query-model conflict loop at lines 576-581 so a bound lease still rejects a conflicting ?model=.
📍 Affects 2 files
  • internal/proxy/proxy.go#L4337-L4349 (this comment)
  • internal/proxy/session_lease.go#L563-L565
🤖 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 `@internal/proxy/proxy.go` around lines 4337 - 4349, The Codex catalog routing
flow must neutralize query-based model overrides on both paths. In
internal/proxy/proxy.go lines 4337-4349, update codexModelCatalogRoutingRequest
to remove the model query parameter from the cloned URL as well as the routing
headers; in internal/proxy/session_lease.go lines 563-565, narrow
validateRequestModel’s catalog early return to only the body-model requirement
so the existing query-model conflict validation remains active for bound leases.

Comment on lines +563 to +565
if lease.Provider == accounts.ProviderCodex && codexModelCatalogRequest(r) {
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The model bypass also removes the query-model conflict check.

The early return skips the query loop at lines 576-581. A bound-lease catalog request such as GET /v1/models?client_version=0.146.1&model=other-model is no longer checked against lease.Model. The lease account stays pinned by allowsAccount in internal/proxy/proxy.go at line 2383, so the account cannot be changed. The value still reaches session.ExtractModel and influences scheduler pool selection and the broker Model field.

Bypass only the body requirement, and keep the query conflict check.

🤖 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 `@internal/proxy/session_lease.go` around lines 563 - 565, Update the lease
validation flow around the Codex catalog-request handling so the request body
requirement is bypassed without returning before the query-model conflict check.
Preserve the existing query loop that validates the requested model against
lease.Model, while retaining the early bypass behavior only for body validation.

@lawrencecchen
lawrencecchen merged commit b026825 into main Aug 6, 2026
7 checks passed
@lawrencecchen
lawrencecchen deleted the fix/codex-model-catalog branch August 6, 2026 07:00
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.

1 participant