Skip to content

add Codex subscription provider with device auth#2715

Open
yaronshanisima wants to merge 4 commits intomaximhq:mainfrom
yaronshanisima:yaron/codex-subscription-provider
Open

add Codex subscription provider with device auth#2715
yaronshanisima wants to merge 4 commits intomaximhq:mainfrom
yaronshanisima:yaron/codex-subscription-provider

Conversation

@yaronshanisima
Copy link
Copy Markdown

@yaronshanisima yaronshanisima commented Apr 14, 2026

Summary

  • add a first-class codex provider with ChatGPT Plus/Pro device auth, DB-backed credential persistence, and a provider-specific UI flow
  • make Codex requests production-safe by normalizing request shape, stripping unsupported params, persisting refreshed credentials, and supporting both non-stream and stream OpenAI-compatible calls
  • add Codex pricing mode support and targeted backend/UI test coverage while keeping unrelated local lockfile changes out of the PR

Related Issue

Validation

  • go test ./core/...
  • go test ./framework/configstore
  • go test ./framework/modelcatalog
  • go test ./transports/bifrost-http/lib
  • go test ./transports/bifrost-http/handlers
  • go test ./transports/bifrost-http/server
  • go test ./plugins/maxim
  • npx tsc --noEmit

Screenshots

Codex screenshot 1
Codex screenshot 2
Codex screenshot 3

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 34014a75-4688-44ec-80e2-d9eed0817f20

📥 Commits

Reviewing files that changed from the base of the PR and between bd63f24 and 51ec950.

📒 Files selected for processing (4)
  • docs/docs.json
  • docs/providers/supported-providers/overview.mdx
  • transports/config.schema.json
  • ui/lib/constants/icons.tsx
✅ Files skipped from review due to trivial changes (1)
  • docs/docs.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • transports/config.schema.json
👮 Files not reviewed due to content moderation or server errors (1)
  • ui/lib/constants/icons.tsx

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added Codex provider support with device/browser/manual auth, token refresh, streaming chat/responses, and credential persistence.
    • UI: provider key forms, interactive device-auth controls, polling status, and connect/reconnect flows.
    • Server/API: device-auth endpoints (start/status/cancel) and middleware to persist refreshed Codex credentials.
  • Documentation

    • Added Codex provider docs and updated supported-providers overview.
  • Tests

    • Added unit tests for Codex auth helpers and provider request/stream transformations.
  • Chores

    • Database migrations and config schema updates for Codex credentials.

Walkthrough

Adds a new "codex" provider with OAuth device-code and refresh flows, provider wiring, credential persistence (DB + migrations), HTTP auth endpoints, frontend device-auth UI and RTK endpoints, request/response normalization for Chat/Responses, and tests.

Changes

Cohort / File(s) Summary
Codex Provider Core
core/providers/codex/codex.go, core/providers/codex/auth.go, core/providers/codex/auth_test.go, core/providers/codex/codex_test.go
New CodexProvider implementation and auth helpers: device/OAuth flows, token refresh, auth header management with singleflight, request normalization, stream/non‑stream Chat+Responses handling, credential persistence hooks, and unit tests.
Provider Registration & Runtime
core/bifrost.go, core/providers/openai/openai.go, core/providers/mistral/mistral.go, core/utils.go
Wired Codex into provider factory; exempted Codex from specific non‑SSE stream drain error path; adjusted Mistral provider naming and operation gating; allow empty provider key for Codex.
Core Schemas & Context
core/schemas/account.go, core/schemas/bifrost.go, core/schemas/context.go, core/schemas/provider.go
Added Codex model provider constant, StandardProviders entry, CodexKeyConfig and CodexAuthMethod types, context key for credential persister, and minor formatting changes.
Config Store & DB
framework/configstore/migrations.go, framework/configstore/rdb.go, framework/configstore/store.go, framework/configstore/clientconfig.go, framework/configstore/tables/codexauth.go, framework/configstore/tables/key.go
Migrations and new codex_auth_sessions table; extended TableKey to store encrypted Codex credential fields and runtime CodexKeyConfig; added RDB/store methods for persisting Codex key config and auth-session CRUD; redaction and key-hash inclusion.
HTTP Transport & Handlers
transports/bifrost-http/handlers/codexauth.go, transports/bifrost-http/server/server.go, transports/bifrost-http/lib/config.go, transports/bifrost-http/lib/config_test.go
New CodexAuthHandler (start/status/cancel device auth), route registration, middleware to expose credential persister to requests, server helper to persist refreshed credentials, and test mocks.
Config Schema
transports/config.schema.json
Added providers.codex schema and codex_key_config definition (refresh/access token, expires_at, account_id, auth_method) and provider-level config shape.
Frontend Types, API & Constants
ui/lib/types/config.ts, ui/lib/types/schemas.ts, ui/lib/constants/config.ts, ui/lib/constants/logs.ts, ui/lib/store/apis/providersApi.ts
Added CodexKeyConfig types, zod schema refinements, DefaultCodexKeyConfig, UI constants (placeholders/labels), and RTK Query endpoints/hooks for starting/polling/cancelling auth sessions.
Frontend UI Components
ui/app/workspace/providers/fragments/codexAuthControls.tsx, ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx, ui/app/workspace/providers/fragments/index.ts, ui/app/workspace/providers/views/providerKeyForm.tsx, ui/app/workspace/providers/dialogs/providerConfigSheet.tsx
Added CodexAuthControls component and integrated UI changes to support device/manual credentials, session polling, connect/reconnect/cancel flows, and key persistence behavior.
Miscellaneous & Docs
framework/modelcatalog/*, plugins/maxim/plugin_test.go, transports/bifrost-http/lib/ctx.go, docs/*, ui/*
Formatting/test tweaks, added documentation page and nav entry for Codex, docs matrix update, and small UI import/prop adjustments.

Sequence Diagram

sequenceDiagram
    participant User as User/Client
    participant UI as Frontend UI
    participant API as CodexAuthHandler
    participant Store as Config Store
    participant Codex as Codex OAuth Service
    participant Provider as CodexProvider

    User->>UI: Click "Connect with OpenAI"
    UI->>API: POST /codex/auth/start (keyId)
    API->>Codex: StartDeviceAuthorization()
    Codex-->>API: device_auth_id, user_code, interval
    API->>Store: CreateCodexAuthSession(pending)
    API-->>UI: session_id, user_code, verification_uri
    UI->>User: Display user code & open verification URL

    loop User authorizes device
      UI->>API: GET /codex/auth/session/{sessionId} (poll)
      API->>Store: GetCodexAuthSessionByID()
      API->>Codex: PollDeviceAuthorization()
      alt authorized
        Codex-->>API: authorization_code, code_verifier
        API->>Codex: ExchangeDeviceAuthorizationCode()
        Codex-->>API: access_token, refresh_token, id_token
        API->>Store: UpdateCodexAuthSession(authorized) & PersistCodexKeyConfig
        API-->>UI: status: authorized
      else pending / retry
        API-->>UI: status: pending
      end
    end

    UI->>Provider: Make request with Codex key
    Provider->>Store: Read Codex credentials
    Provider->>Codex: Chat/Responses request (with access token)
    Codex-->>Provider: Response
    Provider-->>UI: Response result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #2386: Modifies provider key validation logic in core/utils.go and provider wiring, intersecting with Codex registration and key handling.

Suggested reviewers

  • akshaydeo
  • Pratham-Mishra04

Poem

🐰 A tiny rabbit hops to say,

"Codex joined the Bifrost way!
Device codes and tokens snugly stored,
UI and server keep credentials adored.
Hop—now ChatGPT paths roam restored!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.44% 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
Title check ✅ Passed The title 'add Codex subscription provider with device auth' accurately summarizes the main change: introducing a first-class Codex provider with device authentication, making it clear and specific.
Description check ✅ Passed The PR description covers the summary, changes, related issues, and validation steps. However, it lacks explicit sections for 'Type of change', 'Affected areas', 'Breaking changes', and 'Checklist' checkboxes as specified in the template.

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

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

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.

@akshaydeo
Copy link
Copy Markdown
Contributor

@yaronshanisima thanks for the PR. But this is an extremely big PR which will definitely take time from our side to review and test properly.

Could you please attach a demo video of the entire flow if possible?

@yaronshanisima yaronshanisima force-pushed the yaron/codex-subscription-provider branch from f32567a to 2111eb9 Compare April 14, 2026 14:22
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 14, 2026

Confidence Score: 4/5

  • Safe to merge after the persistTokensToKey TOCTOU race is fixed; remaining findings are style-level.
  • One P1 remains: persistTokensToKey uses the same full-provider rebuild that was flagged and fixed in PersistCodexKeyCredentials, but the fix was not applied here. The two P2 UI issues (missing error handling, popup not closed on cancel) are non-blocking. Prior P1 issues from earlier rounds have been addressed.
  • transports/bifrost-http/handlers/codexauth.go — persistTokensToKey needs to be rewritten to use the narrow single-key DB update.

Important Files Changed

Filename Overview
core/providers/codex/codex.go New Codex provider: converts chat requests to responses API, handles singleflight refresh, streaming accumulation, and unsupported-param stripping. Core logic is solid; outstanding singleflight race and access-token-expiry issues flagged in prior threads remain open.
core/providers/codex/auth.go Device auth helpers (start, poll, exchange, token refresh). Dead code generateRandomString present; no signature validation on JWT parsing (acceptable for self-received tokens).
transports/bifrost-http/handlers/codexauth.go Device auth session handler. persistTokensToKey uses full-provider UpdateProviderConfig, introducing the same TOCTOU race (key deletion) previously flagged for PersistCodexKeyCredentials. completeSession silently discards DB errors. failSession is dead code.
transports/bifrost-http/lib/config.go Adds PersistCodexKeyCredentials using the targeted PersistCodexKeyConfig DB update — the correct narrow-write fix. In-memory update is properly lock-guarded.
framework/configstore/rdb.go Adds Codex CRUD methods and the targeted PersistCodexKeyConfig (single-key GORM Save). Standard GORM Save still writes all columns but the race window is narrow and acceptable for the refresh path.
framework/configstore/tables/key.go Adds flat Codex credential columns with correct BeforeSave serialization and AfterFind reconstruction; encryption/decryption hooks integrated consistently with other provider fields.
framework/configstore/tables/codexauth.go New TableCodexAuthSession table with BeforeSave/AfterFind encryption hooks for DeviceAuthID. Schema and encryption handling look correct.
ui/app/workspace/providers/fragments/codexAuthControls.tsx New Codex device-auth React component. beginDeviceFlow lacks error handling (silent failures), and handleCancel doesn't close the verification popup. Polling interval logic and session-state display are correct.
framework/configstore/migrations.go Adds two migrations for Codex key columns and auth sessions table with proper HasColumn/HasTable guards and rollback support.
core/schemas/account.go Adds CodexKeyConfig and CodexCredentialPersister types. Struct design is clean; RefreshToken is non-pointer (always required), AccessToken/AccountID are optional pointers.

Reviews (5): Last reviewed commit: "clean up codex docs and schema diffs" | Re-trigger Greptile

Comment thread transports/bifrost-http/lib/config.go
Comment thread transports/bifrost-http/handlers/codexauth.go
Comment thread transports/bifrost-http/handlers/codexauth.go
Comment thread core/providers/codex/auth.go
@yaronshanisima yaronshanisima force-pushed the yaron/codex-subscription-provider branch from 2111eb9 to ec6efc1 Compare April 14, 2026 14:27
Comment thread core/providers/codex/codex.go
Copy link
Copy Markdown
Contributor

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
framework/configstore/rdb.go (2)

423-433: ⚠️ Potential issue | 🟠 Major

UpdateProvider still drops provider-level Codex settings.

Save(&dbProvider) never sees configCopy.CodexConfig because this method doesn't assign it before the save. Any pricing-mode or other provider-level Codex edits made through the update path are silently lost.

Suggested fix
 	dbProvider.CustomProviderConfig = configCopy.CustomProviderConfig
 	dbProvider.OpenAIConfig = configCopy.OpenAIConfig
+	dbProvider.CodexConfig = configCopy.CodexConfig
 	dbProvider.PricingOverrides = configCopy.PricingOverrides
 	dbProvider.ConfigHash = configCopy.ConfigHash

Also applies to: 474-475

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

In `@framework/configstore/rdb.go` around lines 423 - 433, The UpdateProvider path
is not copying provider-level Codex settings so Save(&dbProvider) never persists
configCopy.CodexConfig; update the assignment block that currently sets
NetworkConfig, ConcurrencyAndBufferSize, ProxyConfig, SendBackRawRequest,
SendBackRawResponse, StoreRawRequestResponse, CustomProviderConfig,
OpenAIConfig, PricingOverrides, ConfigHash to also assign dbProvider.CodexConfig
= configCopy.CodexConfig before calling Save(&dbProvider), and make the same
addition at the other update location referenced around the 474-475 area so
provider-level Codex edits are not lost.

563-573: ⚠️ Potential issue | 🟠 Major

AddProvider doesn't persist CodexKeyConfig on new keys.

The provider row now stores CodexConfig, but the TableKey literal still omits key.CodexKeyConfig. Creating a Codex key with preloaded/manual credentials will insert the key and drop those credentials immediately.

Suggested fix
 		dbKey := tables.TableKey{
 			Provider:           dbProvider.Name,
 			ProviderID:         dbProvider.ID,
 			KeyID:              key.ID,
 			Name:               key.Name,
 			Value:              key.Value,
 			Models:             key.Models,
 			BlacklistedModels:  key.BlacklistedModels,
 			Weight:             &key.Weight,
 			Enabled:            key.Enabled,
 			UseForBatchAPI:     key.UseForBatchAPI,
 			AzureKeyConfig:     key.AzureKeyConfig,
 			VertexKeyConfig:    key.VertexKeyConfig,
 			BedrockKeyConfig:   key.BedrockKeyConfig,
 			ReplicateKeyConfig: key.ReplicateKeyConfig,
 			VLLMKeyConfig:      key.VLLMKeyConfig,
+			CodexKeyConfig:     key.CodexKeyConfig,
 			ConfigHash:         key.ConfigHash,
 			Status:             string(key.Status),
 			Description:        key.Description,
 		}

Also applies to: 583-599

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

In `@framework/configstore/rdb.go` around lines 563 - 573, The AddProvider path is
building tables.TableKey entries but omits key.CodexKeyConfig, so Codex
credentials are dropped; update the TableKey literal(s) created alongside
dbProvider (look for the TableKey construction near dbProvider :=
tables.TableProvider and the similar block at the later occurrence) to include
CodexKeyConfig: key.CodexKeyConfig, ensuring both places persist the Codex key
config when inserting keys; no other structural changes required.
🧹 Nitpick comments (8)
core/providers/codex/auth_test.go (1)

5-19: Basic test coverage for auth helpers.

Tests cover the happy path for ExtractAccountID and ExpiresAtFromNow. Consider adding edge case tests for:

  • ExtractAccountID(nil)
  • Malformed JWT tokens
  • Tokens without account ID claims
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/codex/auth_test.go` around lines 5 - 19, Add unit tests
covering edge cases for ExtractAccountID and ExpiresAtFromNow: add a test that
calls ExtractAccountID(nil) and asserts it returns an empty string (or the
expected zero value), tests that pass malformed JWT strings to ExtractAccountID
and assert it safely returns empty/error without panicking, and a test with a
well-formed JWT that lacks the account ID claim to assert empty result; also add
a test for ExpiresAtFromNow with zero/negative input to assert expected behavior
(empty or RFC3339 handling). Target the existing test file and the functions
ExtractAccountID and ExpiresAtFromNow when adding these negative-case tests.
ui/app/workspace/providers/fragments/index.ts (1)

3-4: Prefer @/... alias imports for new UI exports.

These new exports continue relative paths; please switch them to the UI alias pattern for consistency with current repo conventions.

♻️ Suggested update
-export { CodexAuthControls } from "./codexAuthControls";
-export { CodexConfigFormFragment } from "./codexConfigFormFragment";
+export { CodexAuthControls } from "@/app/workspace/providers/fragments/codexAuthControls";
+export { CodexConfigFormFragment } from "@/app/workspace/providers/fragments/codexConfigFormFragment";

Based on learnings: In the UI codebase, prefer alias imports using @/... over relative imports.

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

In `@ui/app/workspace/providers/fragments/index.ts` around lines 3 - 4, Replace
the relative export paths with the UI alias pattern: change the two exports for
CodexAuthControls and CodexConfigFormFragment to re-export from the
corresponding "@/..." alias modules (use the module paths that map to the same
files under the UI alias) so the file exports CodexAuthControls and
CodexConfigFormFragment via the alias imports instead of "./codexAuthControls"
and "./codexConfigFormFragment".
ui/lib/store/apis/providersApi.ts (1)

124-147: Seed getCodexAuthSession from these mutation results.

startCodex* and cancelCodexAuthSession return the same session payload that getCodexAuthSession reads, but nothing patches the cached session entry. The session view will stay stale until the next poll/refetch, which is especially visible right after cancel.

Based on learnings "In ui/lib/store/apis/, optimistically patch the cache with onQueryStarted + routingRulesApi.util.updateQueryData for mutations ... instead of using invalidatesTags."

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

In `@ui/lib/store/apis/providersApi.ts` around lines 124 - 147, The
getCodexAuthSession query cache is never updated when startCodexBrowserAuth,
startCodexDeviceAuth, or cancelCodexAuthSession mutate the session; add
onQueryStarted handlers to those three mutations that await the mutation
response and then call providersApi.util.updateQueryData for getCodexAuthSession
(using the sessionId from the response) to replace/seed the cached
CodexAuthSessionResponse so the UI reflects the new session state immediately;
ensure you handle errors by reverting the update if the mutation fails.
ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx (1)

281-288: Subscribe to key.id before passing it to CodexAuthControls.

getValues() is just a snapshot. If the create flow persists the key and writes key.id back into the form, this prop can stay undefined until some unrelated rerender, so CodexAuthControls won't reliably see the new key ID.

♻️ Proposed refactor
-import { Control, UseFormReturn } from "react-hook-form";
+import { Control, UseFormReturn, useWatch } from "react-hook-form";
...
+	const keyId = useWatch({ control, name: "key.id" });
...
 				<CodexAuthControls
 					providerName={providerName}
-					keyId={form.getValues("key.id")}
+					keyId={keyId}
 					form={form}
 					isEditing={isEditing}
 					isConfigManaged={isConfigManaged}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx` around lines
281 - 288, Replace the snapshot call form.getValues("key.id") with a reactive
subscription so CodexAuthControls always receives updates when the form writes
back the persisted key id; e.g. use form.watch("key.id") or React Hook Form's
useWatch tied to the same form instance and pass that watchedKeyId to
CodexAuthControls (referencing CodexAuthControls, form, and the "key.id" field).
transports/bifrost-http/handlers/codexauth.go (1)

243-257: Silently ignored errors in terminal state updates.

The completeSession and failSession methods ignore errors from UpdateCodexAuthSession. While this is acceptable for terminal states (the session won't be polled again), consider logging these failures for observability.

💡 Suggested improvement for observability
 func (h *CodexAuthHandler) completeSession(session *configstoreTables.TableCodexAuthSession) {
 	now := time.Now()
 	session.Status = codexAuthSessionSucceeded
 	session.CompletedAt = &now
 	session.LastError = nil
-	_ = h.store.ConfigStore.UpdateCodexAuthSession(context.Background(), session)
+	if err := h.store.ConfigStore.UpdateCodexAuthSession(context.Background(), session); err != nil {
+		// Log but don't fail - session is already in terminal state
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@transports/bifrost-http/handlers/codexauth.go` around lines 243 - 257, The
terminal-state helpers completeSession and failSession currently discard errors
from h.store.ConfigStore.UpdateCodexAuthSession; change both to capture the
returned error and log it (e.g., using an existing logger on the handler such as
h.logger or processLogger) including context like the session ID and whether it
was a success or failure so failed updates are observable; still treat the state
change as terminal but do not ignore the UpdateCodexAuthSession error—log it
with a clear message and the error value after calling UpdateCodexAuthSession.
ui/app/workspace/providers/fragments/codexConfigFormFragment.tsx (1)

37-41: Consider removing form from the dependency array.

Including form in the dependency array may cause unnecessary form resets since the form object reference can change on re-renders. The reset should only trigger when provider.codex_config?.pricing_mode or provider.name changes.

💡 Suggested refinement
 	useEffect(() => {
 		form.reset({
 			pricing_mode: provider.codex_config?.pricing_mode ?? "included_zero",
 		});
-	}, [form, provider.codex_config?.pricing_mode, provider.name]);
+	// eslint-disable-next-line react-hooks/exhaustive-deps
+	}, [provider.codex_config?.pricing_mode, provider.name]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/providers/fragments/codexConfigFormFragment.tsx` around
lines 37 - 41, The useEffect currently resets the form via form.reset when any
of its dependencies change; remove form from the dependency array so the effect
only runs when provider.codex_config?.pricing_mode or provider.name change.
Locate the useEffect that calls form.reset (reference symbols: useEffect,
form.reset, provider.codex_config?.pricing_mode, provider.name) and update the
dependency array to exclude form to avoid unnecessary resets while keeping the
two provider-based dependencies.
core/providers/codex/codex.go (1)

83-143: Consider reusing the HTTP client for token refresh.

The authHeaders method creates a new http.Client on line 95 for each token refresh. While functional, this could be optimized by reusing h.httpClient (which appears to be intended for auth operations but isn't stored on the provider).

💡 Suggested improvement

Consider adding an authHTTPClient *http.Client field to CodexProvider initialized in the constructor, similar to CodexAuthHandler, to avoid repeated client creation during token refresh.

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

In `@core/providers/codex/codex.go` around lines 83 - 143, The authHeaders method
on CodexProvider creates a new http.Client per refresh; add a reusable http
client field (e.g., authHTTPClient *http.Client) to CodexProvider, initialize it
in the provider constructor, and use that client when calling RefreshAccessToken
instead of creating &http.Client{Timeout: 20*time.Second}; update authHeaders to
call RefreshAccessToken(requestCtx, provider.authHTTPClient, refreshToken) and
ensure any related methods like persistRefreshedCredentials and
RefreshAccessToken still work with the reused client.
ui/app/workspace/providers/fragments/codexAuthControls.tsx (1)

147-160: Consider cleaning up the popup window on component unmount.

If the user navigates away while the auth dialog is open, the popup window will remain open. Consider adding cleanup in a useEffect.

💡 Suggested cleanup on unmount
+	useEffect(() => {
+		return () => {
+			if (popupRef.current && !popupRef.current.closed) {
+				popupRef.current.close();
+			}
+		};
+	}, []);
+
 	const beginDeviceFlow = async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx` around lines 147
- 160, The popup opened in beginDeviceFlow (popupRef.current = window.open(...))
is not closed if the component unmounts; add a useEffect cleanup that on unmount
checks popupRef.current, calls .close() if it's open, and clears
popupRef.current to avoid leaking the window; also consider closing the popup
when setSession(null) or when the dialog is dismissed. Locate popupRef usage and
implement the cleanup in a useEffect with a return cleanup function that closes
and nulls popupRef.current.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/bifrost.go`:
- Around line 3640-3641: The Codex provider branch returns
codex.NewCodexProvider(config, bifrost.logger) but must return the
Provider,error tuple like the other providers; change the return in the
schemas.Codex case to return codex.NewCodexProvider(config, bifrost.logger), nil
(or adjust to match NewCodexProvider's signature) so it returns (Provider,
error). Also add CODEX_API_KEY into the workflow environment blocks in
pr-tests.yml and release-pipeline.yml — in each job env section where
OPENAI_API_KEY and ANTHROPIC_API_KEY are set (around 6–7 job env blocks), add
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} so Codex is available to CI jobs.

In `@core/providers/codex/auth.go`:
- Around line 185-196: Remove the dead/unreferenced helper function
generateRandomString from the file (the function declaration and its body) since
it isn't used anywhere; delete the entire generateRandomString function
definition to eliminate dead code and keep the codebase clean, and run
tests/linters to ensure no references remain to generateRandomString.

In `@core/providers/codex/codex_test.go`:
- Around line 82-88: The test currently only checks block types and non-nil
output text; update it to assert the actual preserved string contents so
normalization hasn't altered or blanked them: after locating the blocks via
request.Input[0].Content.ContentBlocks[0] and
request.Input[1].Content.ContentBlocks[0] assert that the original user message
text equals the input block's text field and that the original assistant/output
message text equals the ResponsesOutputMessageContentText (or its text field)
value; modify the assertions in codex_test.go to compare the expected literal
strings to these fields rather than only checking type/non-nil.

In `@core/providers/openai/openai.go`:
- Line 1119: The branch that checks for schemas.Codex using providerName is
incorrect for aliased providers; update the call sites (where
providerUtils.DrainNonSSEStreamResponse is invoked) to use the canonical/base
provider type or an explicit capability flag instead of providerName so aliased
Codex providers are treated as Codex for streaming behavior; propagate the base
discriminator from the provider (the same value used by
providerUtils.CheckOperationAllowed) or add a boolean like
supportsCodexStreaming to the shared helper signatures, and ensure
provider.GetProviderKey() continues to be used only for alias/error attribution
while the base type/capability is used for branching in
DrainNonSSEStreamResponse and related logic.

In `@framework/configstore/migrations.go`:
- Around line 3992-3999: The two new fields CodexAccessTokenExpiresAt and
CodexAuthMethod on TableKey are not being encrypted/decrypted like the other
Codex fields; update their types to the same pointer EnvVar type used by
CodexRefreshToken/CodexAccessToken/CodexAccountID (or otherwise wrap them
consistently) and modify the TableKey.BeforeSave() logic to call
encryptEnvVarPtr() for CodexAccessTokenExpiresAt and CodexAuthMethod and the
TableKey.AfterFind() logic to call decryptEnvVarPtr() for those same fields so
they go through the same encryption/decryption hooks as the other Codex
credential fields.
- Around line 280-287: Don't modify historical migrations; instead add a new
forward-only migration (e.g., migrationRecomputeProviderConfigHashWithCodex) and
call it immediately after migrationAddCodexConfigColumns in the migration
sequence. The new migration should scan providers/users that need backfilling,
recompute their config_hash including the new codex_config_json field, and
update only rows that are missing or outdated (idempotent and forward-only),
leaving migrationAddCodexConfigColumns, migrationAddCodexAuthSessionsTable, and
migrationCleanupCodexBrowserAuthColumns unchanged.

In `@framework/configstore/tables/codexauth.go`:
- Around line 36-46: The current encryption block only sets s.EncryptionStatus =
EncryptionStatusEncrypted when at least one field was actually encrypted,
leaving rows with empty DeviceAuthID at plain_text; change the logic in the
encrypt.IsEnabled() branch (around s.DeviceAuthID and encryptString usage) to
always set s.EncryptionStatus = EncryptionStatusEncrypted whenever
encrypt.IsEnabled() returns true, even if no individual field was encrypted, so
the repo invariant for encrypted-row status is preserved.

In `@transports/bifrost-http/handlers/codexauth.go`:
- Line 218: Update the error messages created with fmt.Errorf to follow Go
conventions: start with a lowercase letter and no trailing punctuation. Replace
messages like "Codex key %s not found" with "codex key %s not found" in the
return fmt.Errorf(...) that references keyID, and make the same change for the
other occurrence noted (the similar fmt.Errorf at the second location). Ensure
both fmt.Errorf calls are updated so error strings are lowercase and have no
trailing punctuation.

In `@transports/bifrost-http/lib/config.go`:
- Around line 3123-3174: The current flow snapshots c.Providers[schemas.Codex]
under c.Mu.RLock, releases the lock, persists the full updated provider, then
overwrites c.Providers with a stale snapshot; instead serialize the
read-modify-write by doing the modify-and-persist while holding a proper
write/compare step: inside ConfigStore.ExecuteTransaction (or just before it)
read the latest provider state (use UpdateProvider/GetProvider API) into a local
variable, apply only the per-key updates to that latest provider (modify the
matching Keys entry by ID and replace only CodexKeyConfig fields), persist via
c.ConfigStore.UpdateProvider, and only after a successful transaction acquire
c.Mu.Lock() and merge/replace the provider in c.Providers (or check that the
in-memory provider hasn’t diverged) before unlocking; this avoids clobbering
concurrent UpdateProviderConfig/RemoveProvider operations and ensures the update
is atomic with respect to other goroutines.

In `@transports/bifrost-http/server/server.go`:
- Around line 1161-1166: The persistCodexCredentialRefresh function uses
context.Background()/schemas.NoDeadline which can block indefinitely; modify
persistCodexCredentialRefresh (or add an overload) to accept a caller
context.Context (or derive a bounded context from an existing server
shutdown/context field) and create a cancellable/timeout context before calling
s.Config.PersistCodexKeyCredentials so DB/config store calls can't hang forever;
update callers to pass through their request context (or use a configured
timeout on BifrostHTTPServer) and ensure the call uses that derived context
instead of context.Background().
- Around line 1142-1149: The credential persister middleware is being appended
into commonMiddlewares after TransportInterceptorMiddleware runs, so the
interceptor's getBifrostContextFromFastHTTP materializes a BifrostContext
without the persister; move the persister setup (the func that calls
ctx.SetUserValue(schemas.BifrostContextKeyCodexCredentialPersister,
schemas.CodexCredentialPersister(s.persistCodexCredentialRefresh))) to execute
before TransportInterceptorMiddleware (either insert it before you prepend
TransportInterceptorMiddleware at the prepend point around where
TracingMiddleware is handled, or prepend it separately ahead of
TransportInterceptorMiddleware at line ~1373), and ensure AuthMiddleware is also
arranged so it runs after the persister but not before the interceptor;
reference TransportInterceptorMiddleware, getBifrostContextFromFastHTTP,
TracingMiddleware, AuthMiddleware,
schemas.BifrostContextKeyCodexCredentialPersister and
s.persistCodexCredentialRefresh when locating where to move the code.

In `@transports/config.schema.json`:
- Around line 2698-2704: Add a new schema definition "#/$defs/codex_key" that
extends "base_key" and requires the provider-specific "codex_key_config"
property (mirroring how "azure_key" and "vllm_key" are defined), then update the
"provider_with_codex_config.keys" items reference to point to
"#/$defs/codex_key" instead of "#/$defs/base_key" so keys must include the
Codex-specific config; ensure "codex_key" appears alongside other key defs in
"$defs" and that "codex_key_config" is referenced as required within that
definition.

In `@ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx`:
- Around line 209-274: Add stable data-testid attributes to the new Codex
manual-auth UI elements so e2e tests can target them: add
data-testid="apikey-codex-manual-credentials-trigger" to the CollapsibleTrigger,
data-testid="apikey-codex-refresh-token-input" to the EnvVarInput rendering
`key.codex_key_config.refresh_token`,
data-testid="apikey-codex-access-token-input" to the EnvVarInput rendering
`key.codex_key_config.access_token`, data-testid="apikey-codex-account-id-input"
to the EnvVarInput rendering `key.codex_key_config.account_id`, and
data-testid="apikey-codex-access-token-expiry-input" to the Input rendering
`key.codex_key_config.access_token_expires_at`; ensure attributes are placed on
the interactive component instances (CollapsibleTrigger, EnvVarInput, Input)
rather than wrappers to match existing `apikey-*` selectors convention.

---

Outside diff comments:
In `@framework/configstore/rdb.go`:
- Around line 423-433: The UpdateProvider path is not copying provider-level
Codex settings so Save(&dbProvider) never persists configCopy.CodexConfig;
update the assignment block that currently sets NetworkConfig,
ConcurrencyAndBufferSize, ProxyConfig, SendBackRawRequest, SendBackRawResponse,
StoreRawRequestResponse, CustomProviderConfig, OpenAIConfig, PricingOverrides,
ConfigHash to also assign dbProvider.CodexConfig = configCopy.CodexConfig before
calling Save(&dbProvider), and make the same addition at the other update
location referenced around the 474-475 area so provider-level Codex edits are
not lost.
- Around line 563-573: The AddProvider path is building tables.TableKey entries
but omits key.CodexKeyConfig, so Codex credentials are dropped; update the
TableKey literal(s) created alongside dbProvider (look for the TableKey
construction near dbProvider := tables.TableProvider and the similar block at
the later occurrence) to include CodexKeyConfig: key.CodexKeyConfig, ensuring
both places persist the Codex key config when inserting keys; no other
structural changes required.

---

Nitpick comments:
In `@core/providers/codex/auth_test.go`:
- Around line 5-19: Add unit tests covering edge cases for ExtractAccountID and
ExpiresAtFromNow: add a test that calls ExtractAccountID(nil) and asserts it
returns an empty string (or the expected zero value), tests that pass malformed
JWT strings to ExtractAccountID and assert it safely returns empty/error without
panicking, and a test with a well-formed JWT that lacks the account ID claim to
assert empty result; also add a test for ExpiresAtFromNow with zero/negative
input to assert expected behavior (empty or RFC3339 handling). Target the
existing test file and the functions ExtractAccountID and ExpiresAtFromNow when
adding these negative-case tests.

In `@core/providers/codex/codex.go`:
- Around line 83-143: The authHeaders method on CodexProvider creates a new
http.Client per refresh; add a reusable http client field (e.g., authHTTPClient
*http.Client) to CodexProvider, initialize it in the provider constructor, and
use that client when calling RefreshAccessToken instead of creating
&http.Client{Timeout: 20*time.Second}; update authHeaders to call
RefreshAccessToken(requestCtx, provider.authHTTPClient, refreshToken) and ensure
any related methods like persistRefreshedCredentials and RefreshAccessToken
still work with the reused client.

In `@transports/bifrost-http/handlers/codexauth.go`:
- Around line 243-257: The terminal-state helpers completeSession and
failSession currently discard errors from
h.store.ConfigStore.UpdateCodexAuthSession; change both to capture the returned
error and log it (e.g., using an existing logger on the handler such as h.logger
or processLogger) including context like the session ID and whether it was a
success or failure so failed updates are observable; still treat the state
change as terminal but do not ignore the UpdateCodexAuthSession error—log it
with a clear message and the error value after calling UpdateCodexAuthSession.

In `@ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx`:
- Around line 281-288: Replace the snapshot call form.getValues("key.id") with a
reactive subscription so CodexAuthControls always receives updates when the form
writes back the persisted key id; e.g. use form.watch("key.id") or React Hook
Form's useWatch tied to the same form instance and pass that watchedKeyId to
CodexAuthControls (referencing CodexAuthControls, form, and the "key.id" field).

In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx`:
- Around line 147-160: The popup opened in beginDeviceFlow (popupRef.current =
window.open(...)) is not closed if the component unmounts; add a useEffect
cleanup that on unmount checks popupRef.current, calls .close() if it's open,
and clears popupRef.current to avoid leaking the window; also consider closing
the popup when setSession(null) or when the dialog is dismissed. Locate popupRef
usage and implement the cleanup in a useEffect with a return cleanup function
that closes and nulls popupRef.current.

In `@ui/app/workspace/providers/fragments/codexConfigFormFragment.tsx`:
- Around line 37-41: The useEffect currently resets the form via form.reset when
any of its dependencies change; remove form from the dependency array so the
effect only runs when provider.codex_config?.pricing_mode or provider.name
change. Locate the useEffect that calls form.reset (reference symbols:
useEffect, form.reset, provider.codex_config?.pricing_mode, provider.name) and
update the dependency array to exclude form to avoid unnecessary resets while
keeping the two provider-based dependencies.

In `@ui/app/workspace/providers/fragments/index.ts`:
- Around line 3-4: Replace the relative export paths with the UI alias pattern:
change the two exports for CodexAuthControls and CodexConfigFormFragment to
re-export from the corresponding "@/..." alias modules (use the module paths
that map to the same files under the UI alias) so the file exports
CodexAuthControls and CodexConfigFormFragment via the alias imports instead of
"./codexAuthControls" and "./codexConfigFormFragment".

In `@ui/lib/store/apis/providersApi.ts`:
- Around line 124-147: The getCodexAuthSession query cache is never updated when
startCodexBrowserAuth, startCodexDeviceAuth, or cancelCodexAuthSession mutate
the session; add onQueryStarted handlers to those three mutations that await the
mutation response and then call providersApi.util.updateQueryData for
getCodexAuthSession (using the sessionId from the response) to replace/seed the
cached CodexAuthSessionResponse so the UI reflects the new session state
immediately; ensure you handle errors by reverting the update if the mutation
fails.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d06c0331-a1db-4728-b565-731397115f2d

📥 Commits

Reviewing files that changed from the base of the PR and between 1bda46a and 2111eb9.

📒 Files selected for processing (44)
  • core/bifrost.go
  • core/providers/codex/auth.go
  • core/providers/codex/auth_test.go
  • core/providers/codex/codex.go
  • core/providers/codex/codex_test.go
  • core/providers/mistral/mistral.go
  • core/providers/openai/openai.go
  • core/schemas/account.go
  • core/schemas/bifrost.go
  • core/schemas/context.go
  • core/schemas/provider.go
  • core/utils.go
  • framework/configstore/clientconfig.go
  • framework/configstore/migrations.go
  • framework/configstore/rdb.go
  • framework/configstore/store.go
  • framework/configstore/tables/codexauth.go
  • framework/configstore/tables/key.go
  • framework/configstore/tables/provider.go
  • framework/modelcatalog/codex.go
  • framework/modelcatalog/codex_test.go
  • framework/modelcatalog/main.go
  • framework/modelcatalog/main_test.go
  • framework/modelcatalog/pricing.go
  • plugins/maxim/plugin_test.go
  • transports/bifrost-http/handlers/codexauth.go
  • transports/bifrost-http/handlers/providers.go
  • transports/bifrost-http/lib/config.go
  • transports/bifrost-http/lib/config_test.go
  • transports/bifrost-http/lib/ctx.go
  • transports/bifrost-http/server/server.go
  • transports/config.schema.json
  • ui/app/workspace/providers/dialogs/providerConfigSheet.tsx
  • ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx
  • ui/app/workspace/providers/fragments/codexAuthControls.tsx
  • ui/app/workspace/providers/fragments/codexConfigFormFragment.tsx
  • ui/app/workspace/providers/fragments/index.ts
  • ui/app/workspace/providers/views/providerKeyForm.tsx
  • ui/lib/constants/config.ts
  • ui/lib/constants/icons.tsx
  • ui/lib/constants/logs.ts
  • ui/lib/store/apis/providersApi.ts
  • ui/lib/types/config.ts
  • ui/lib/types/schemas.ts
👮 Files not reviewed due to content moderation or server errors (1)
  • ui/lib/constants/icons.tsx

Comment thread core/bifrost.go
Comment on lines +3640 to +3641
case schemas.Codex:
return codex.NewCodexProvider(config, bifrost.logger)
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Provider registration check =="
rg -n --type=go -C2 'case\s+schemas\.Codex|NewCodexProvider' core/bifrost.go

echo
echo "== Workflow Codex env/config check =="
for f in .github/workflows/pr-tests.yml .github/workflows/release-pipeline.yml; do
  if [[ -f "$f" ]]; then
    echo "---- $f ----"
    rg -n -C3 'codex|CODEX' "$f" || true
  else
    echo "Missing expected workflow file: $f"
  fi
done

Repository: maximhq/bifrost

Length of output: 507


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== All workflow files in .github/workflows =="
fd -e yml -e yaml . .github/workflows/ 2>/dev/null | sort

echo
echo "== Check what env vars/configs are in pr-tests.yml (full file) =="
head -100 .github/workflows/pr-tests.yml

echo
echo "== Check what env vars/configs are in release-pipeline.yml (full file) =="
head -100 .github/workflows/release-pipeline.yml

echo
echo "== Search for any existing provider env patterns to understand the structure =="
rg -n 'OPENAI|ANTHROPIC' .github/workflows/ -C 2

Repository: maximhq/bifrost

Length of output: 19520


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check return statements for all providers in bifrost.go =="
rg -n 'return.*Provider.*config.*bifrost.logger' core/bifrost.go | head -20

Repository: maximhq/bifrost

Length of output: 1447


Add CODEX_API_KEY to workflow environments and fix return statement.

The Codex provider registration exists but has two issues:

  1. Missing , nil in return statement (line 3641) — inconsistent with providers like OpenAI, Anthropic, Mistral, Gemini that return (Provider, error).
  2. CODEX_API_KEY not wired into workflows — add CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} to env sections in pr-tests.yml and release-pipeline.yml (approximately 6–7 job env blocks where OPENAI_API_KEY and ANTHROPIC_API_KEY appear).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/bifrost.go` around lines 3640 - 3641, The Codex provider branch returns
codex.NewCodexProvider(config, bifrost.logger) but must return the
Provider,error tuple like the other providers; change the return in the
schemas.Codex case to return codex.NewCodexProvider(config, bifrost.logger), nil
(or adjust to match NewCodexProvider's signature) so it returns (Provider,
error). Also add CODEX_API_KEY into the workflow environment blocks in
pr-tests.yml and release-pipeline.yml — in each job env section where
OPENAI_API_KEY and ANTHROPIC_API_KEY are set (around 6–7 job env blocks), add
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} so Codex is available to CI jobs.

Comment on lines +185 to +196
func generateRandomString(length int) (string, error) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
result := make([]byte, length)
for i, value := range bytes {
result[i] = chars[int(value)%len(chars)]
}
return string(result), nil
}
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for usages of generateRandomString across the codebase
rg -n 'generateRandomString' --type=go

Repository: maximhq/bifrost

Length of output: 147


Remove unused generateRandomString function.

This function is not called anywhere in the codebase and should be removed as dead code. If it is intended for future use, it can be added back later when needed.

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

In `@core/providers/codex/auth.go` around lines 185 - 196, Remove the
dead/unreferenced helper function generateRandomString from the file (the
function declaration and its body) since it isn't used anywhere; delete the
entire generateRandomString function definition to eliminate dead code and keep
the codebase clean, and run tests/linters to ensure no references remain to
generateRandomString.

Comment on lines +82 to +88
require.Len(t, request.Input, 2)
require.Len(t, request.Input[0].Content.ContentBlocks, 1)
require.Equal(t, schemas.ResponsesInputMessageContentBlockTypeText, request.Input[0].Content.ContentBlocks[0].Type)
require.Len(t, request.Input[1].Content.ContentBlocks, 1)
require.Equal(t, schemas.ResponsesOutputMessageContentTypeText, request.Input[1].Content.ContentBlocks[0].Type)
require.NotNil(t, request.Input[1].Content.ContentBlocks[0].ResponsesOutputMessageContentText)
}
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 | 🟡 Minor

Assert the preserved text value, not just that a typed block exists.

This test still passes if normalization rewrites or blanks the user/assistant payload, because it only checks the block type and a non-nil output-text field. Since the contract here is preservation, assert the actual text contents too.

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

In `@core/providers/codex/codex_test.go` around lines 82 - 88, The test currently
only checks block types and non-nil output text; update it to assert the actual
preserved string contents so normalization hasn't altered or blanked them: after
locating the blocks via request.Input[0].Content.ContentBlocks[0] and
request.Input[1].Content.ContentBlocks[0] assert that the original user message
text equals the input block's text field and that the original assistant/output
message text equals the ResponsesOutputMessageContentText (or its text field)
value; modify the assertions in codex_test.go to compare the expected literal
strings to these fields rather than only checking type/non-nil.

// Skip scanner for non-SSE responses — avoids bufio.Scanner buffer bloat
// on non-line-delimited data (e.g. provider returned JSON instead of SSE).
if providerUtils.DrainNonSSEStreamResponse(resp) {
if providerName != schemas.Codex && providerUtils.DrainNonSSEStreamResponse(resp) {
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

Branch this Codex exception on the base provider, not providerName.

Line 1119 and Line 1721 compare the resolved providerName to schemas.Codex, but providerName is also the alias-facing key written into response/error metadata. A custom-named Codex provider won't hit this bypass and will still take the old DrainNonSSEStreamResponse path, so the new Codex streaming behavior won't apply for aliased Codex configs. Please plumb the canonical provider type, or an explicit capability flag, into these shared helpers and branch on that instead.

Based on learnings: providerUtils.CheckOperationAllowed uses the base provider type discriminator, while provider.GetProviderKey() is reserved for alias/error attribution.

Also applies to: 1721-1721

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

In `@core/providers/openai/openai.go` at line 1119, The branch that checks for
schemas.Codex using providerName is incorrect for aliased providers; update the
call sites (where providerUtils.DrainNonSSEStreamResponse is invoked) to use the
canonical/base provider type or an explicit capability flag instead of
providerName so aliased Codex providers are treated as Codex for streaming
behavior; propagate the base discriminator from the provider (the same value
used by providerUtils.CheckOperationAllowed) or add a boolean like
supportsCodexStreaming to the shared helper signatures, and ensure
provider.GetProviderKey() continues to be used only for alias/error attribution
while the base type/capability is used for branching in
DrainNonSSEStreamResponse and related logic.

Comment thread framework/configstore/migrations.go Outdated
Comment on lines +280 to +287
if err := migrationAddCodexConfigColumns(ctx, db); err != nil {
return err
}
if err := migrationAddCodexAuthSessionsTable(ctx, db); err != nil {
return err
}
if err := migrationCleanupCodexBrowserAuthColumns(ctx, db); err != nil {
return err
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 | 🔴 Critical

Add a new forward-only Codex hash migration here instead of editing old ones.

Threading Codex into add_config_hash_column / add_store_raw_request_response_column is unsafe in both directions: databases that already applied those IDs will never rerun the new backfill, and databases upgrading from a release that predates add_config_hash_column will hit that historical migration before codex_config_json exists. Keep the old migrations pre-Codex and add a new migration immediately after migrationAddCodexConfigColumns that recomputes provider config_hash with the new field.

Suggested follow-up
 if err := migrationAddCodexConfigColumns(ctx, db); err != nil {
 	return err
 }
+if err := migrationBackfillCodexProviderConfigHash(ctx, db); err != nil {
+	return err
+}
 if err := migrationAddCodexAuthSessionsTable(ctx, db); err != nil {
 	return err
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/configstore/migrations.go` around lines 280 - 287, Don't modify
historical migrations; instead add a new forward-only migration (e.g.,
migrationRecomputeProviderConfigHashWithCodex) and call it immediately after
migrationAddCodexConfigColumns in the migration sequence. The new migration
should scan providers/users that need backfilling, recompute their config_hash
including the new codex_config_json field, and update only rows that are missing
or outdated (idempotent and forward-only), leaving
migrationAddCodexConfigColumns, migrationAddCodexAuthSessionsTable, and
migrationCleanupCodexBrowserAuthColumns unchanged.

Comment thread transports/bifrost-http/lib/config.go Outdated
Comment thread transports/bifrost-http/server/server.go
Comment on lines +1161 to +1166
func (s *BifrostHTTPServer) persistCodexCredentialRefresh(keyID string, refreshed *schemas.CodexKeyConfig) error {
if s.Config == nil || refreshed == nil {
return nil
}
ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
return s.Config.PersistCodexKeyCredentials(ctx, keyID, refreshed)
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 | 🔴 Critical

Don't write refreshed credentials with an unbounded background context.

PersistCodexKeyCredentials can hit the DB/config store from the inference path. Using context.Background() plus schemas.NoDeadline means a slow store can stall token refresh and request completion indefinitely.

Suggested fix
 func (s *BifrostHTTPServer) persistCodexCredentialRefresh(keyID string, refreshed *schemas.CodexKeyConfig) error {
 	if s.Config == nil || refreshed == nil {
 		return nil
 	}
-	ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
+	ctx := schemas.NewBifrostContext(context.Background(), time.Now().Add(5*time.Second))
+	defer ctx.Cancel()
 	return s.Config.PersistCodexKeyCredentials(ctx, keyID, refreshed)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@transports/bifrost-http/server/server.go` around lines 1161 - 1166, The
persistCodexCredentialRefresh function uses
context.Background()/schemas.NoDeadline which can block indefinitely; modify
persistCodexCredentialRefresh (or add an overload) to accept a caller
context.Context (or derive a bounded context from an existing server
shutdown/context field) and create a cancellable/timeout context before calling
s.Config.PersistCodexKeyCredentials so DB/config store calls can't hang forever;
update callers to pass through their request context (or use a configured
timeout on BifrostHTTPServer) and ensure the call uses that derived context
instead of context.Background().

Comment on lines +2698 to +2704
"keys": {
"type": "array",
"items": {
"$ref": "#/$defs/base_key"
},
"minItems": 1,
"description": "Codex subscription credentials for this provider"
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

🧩 Analysis chain

🏁 Script executed:

# First, let's check the structure and find the codex.keys section
fd config.schema.json transports/

Repository: maximhq/bifrost

Length of output: 88


🏁 Script executed:

# Now let's examine the relevant section and surrounding context
cat -n transports/config.schema.json | sed -n '2690,2720p'

Repository: maximhq/bifrost

Length of output: 1156


🏁 Script executed:

# Let's search for base_key, azure_key, vllm_key, and codex_key definitions
rg "\"(base_key|azure_key|vllm_key|codex_key)\"" transports/config.schema.json -A 10

Repository: maximhq/bifrost

Length of output: 855


🏁 Script executed:

# Check if there's any codex_key_config mentioned in the schema
rg "codex_key_config" transports/config.schema.json

Repository: maximhq/bifrost

Length of output: 484


🏁 Script executed:

# Let's also check the git history to see if there are related PRs or changes
git log --oneline -20 transports/config.schema.json

Repository: maximhq/bifrost

Length of output: 120


🏁 Script executed:

# Get full azure_key and vllm_key definitions
rg "\"azure_key\"|\"vllm_key\"" transports/config.schema.json -A 30 | head -100

Repository: maximhq/bifrost

Length of output: 1870


🏁 Script executed:

# Search for any existing codex_key definition
rg "\"codex_key\"" transports/config.schema.json

Repository: maximhq/bifrost

Length of output: 41


🏁 Script executed:

# Get the full context of codex_key_config usage
rg "codex_key_config" transports/config.schema.json -B 5 -A 10

Repository: maximhq/bifrost

Length of output: 3586


🏁 Script executed:

# Check the recent commit that added codex
git show ec6efc1d --stat

Repository: maximhq/bifrost

Length of output: 50372


🏁 Script executed:

# Look for codex_key definition (not codex_key_config)
rg "\"codex_key\"" transports/config.schema.json -B 2 -A 40

Repository: maximhq/bifrost

Length of output: 41


🏁 Script executed:

# Get the full base_key definition to see if it validates codex_key_config properly
rg "\"base_key\"" transports/config.schema.json -A 60 | head -80

Repository: maximhq/bifrost

Length of output: 1806


🏁 Script executed:

# Search for where azure_key and vllm_key are actually used in provider schemas
rg "provider_with_azure_config|provider_with_vllm_config" transports/config.schema.json -A 15

Repository: maximhq/bifrost

Length of output: 1871


🏁 Script executed:

# Check the full context of how keys are validated across all providers
rg "provider_with_.*_config.*items" transports/config.schema.json -B 2 -A 8

Repository: maximhq/bifrost

Length of output: 41


🏁 Script executed:

# Search more broadly for codex_key in the entire config file
grep -n "codex_key" transports/config.schema.json | head -20

Repository: maximhq/bifrost

Length of output: 524


Create a dedicated codex_key schema definition for the Codex provider.

The provider_with_codex_config.keys currently references only base_key, which does not validate Codex-specific credentials. This allows invalid configurations like { "name": "...", "weight": 1 } to pass validation. Other specialized providers (azure_key, vllm_key) define their own key schemas extending base_key with required provider-specific config fields. Codex should follow the same pattern: create a codex_key definition that enforces codex_key_config as required, then update line 2701 to reference it.

Proposed fix
         "keys": {
           "type": "array",
           "items": {
-            "$ref": "#/$defs/base_key"
+            "$ref": "#/$defs/codex_key"
           },
           "minItems": 1,
           "description": "Codex subscription credentials for this provider"
         }

Add #/$defs/codex_key alongside azure_key and vllm_key that extends base_key and requires codex_key_config.

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

In `@transports/config.schema.json` around lines 2698 - 2704, Add a new schema
definition "#/$defs/codex_key" that extends "base_key" and requires the
provider-specific "codex_key_config" property (mirroring how "azure_key" and
"vllm_key" are defined), then update the "provider_with_codex_config.keys" items
reference to point to "#/$defs/codex_key" instead of "#/$defs/base_key" so keys
must include the Codex-specific config; ensure "codex_key" appears alongside
other key defs in "$defs" and that "codex_key_config" is referenced as required
within that definition.

Comment on lines +209 to +274
<CollapsibleTrigger className="flex w-full items-center justify-between text-left">
<div className="space-y-1">
<div className="text-sm font-medium">Manual credentials (advanced)</div>
<p className="text-muted-foreground text-xs">
Most users should use the connect flow above. Only open this if you already have Codex tokens and want to manage them
manually.
</p>
</div>
<ChevronDown className={`h-4 w-4 shrink-0 transition-transform ${showManualCodexCredentials ? "rotate-180" : ""}`} />
</CollapsibleTrigger>
<CollapsibleContent className="data-[state=closed]:animate-collapse-up data-[state=open]:animate-collapse-down overflow-hidden">
<div className="mt-4 space-y-4">
<FormField
control={control}
name={`key.codex_key_config.refresh_token`}
render={({ field }) => (
<FormItem>
<FormLabel>Refresh Token</FormLabel>
<FormControl>
<EnvVarInput placeholder="Refresh token or env.CODEX_REFRESH_TOKEN" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.codex_key_config.access_token`}
render={({ field }) => (
<FormItem>
<FormLabel>Cached Access Token</FormLabel>
<FormControl>
<EnvVarInput placeholder="Optional access token or env.CODEX_ACCESS_TOKEN" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid gap-4 md:grid-cols-2">
<FormField
control={control}
name={`key.codex_key_config.account_id`}
render={({ field }) => (
<FormItem>
<FormLabel>ChatGPT Account ID</FormLabel>
<FormControl>
<EnvVarInput placeholder="Optional account id or env.CODEX_ACCOUNT_ID" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.codex_key_config.access_token_expires_at`}
render={({ field }) => (
<FormItem>
<FormLabel>Access Token Expiry</FormLabel>
<FormControl>
<Input placeholder="2026-04-14T12:00:00Z" type="text" {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
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 | 🟡 Minor

Add stable selectors for the new Codex manual-auth controls.

The new trigger and credential inputs are all interactive, but none expose stable selectors like the sibling apikey-* controls already in this file. That will make the Codex setup flow harder to cover in e2e/regression tests.

Examples here would be enough: apikey-codex-manual-credentials-trigger, apikey-codex-refresh-token-input, apikey-codex-access-token-input, apikey-codex-account-id-input, and apikey-codex-access-token-expiry-input.

As per coding guidelines, ui/**/*.{tsx,ts} should add new interactive UI elements with data-testid attributes following data-testid="<entity>-<element>-<qualifier>".

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

In `@ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx` around lines
209 - 274, Add stable data-testid attributes to the new Codex manual-auth UI
elements so e2e tests can target them: add
data-testid="apikey-codex-manual-credentials-trigger" to the CollapsibleTrigger,
data-testid="apikey-codex-refresh-token-input" to the EnvVarInput rendering
`key.codex_key_config.refresh_token`,
data-testid="apikey-codex-access-token-input" to the EnvVarInput rendering
`key.codex_key_config.access_token`, data-testid="apikey-codex-account-id-input"
to the EnvVarInput rendering `key.codex_key_config.account_id`, and
data-testid="apikey-codex-access-token-expiry-input" to the Input rendering
`key.codex_key_config.access_token_expires_at`; ensure attributes are placed on
the interactive component instances (CollapsibleTrigger, EnvVarInput, Input)
rather than wrappers to match existing `apikey-*` selectors convention.

@yaronshanisima
Copy link
Copy Markdown
Author

@akshaydeo regrading a video

Yaron.Shani_s.Video.-.Apr.14.2026.mp4

@akshaydeo
Copy link
Copy Markdown
Contributor

I actually skimmed through the PR - there are too many redflags at the moment including a static OAuth client ID etc. Ill review it by end of the week once Greptile and CR are done

@yaronshanisima
Copy link
Copy Markdown
Author

I actually skimmed through the PR - there are too many redflags at the moment including a static OAuth client ID etc. Ill review it by end of the week once Greptile and CR are done

@akshaydeo thanks, fully understand. The app client id was taken from opencode. It also being used in other repos, but its indeed red flag.

Copy link
Copy Markdown
Contributor

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

Actionable comments posted: 6

♻️ Duplicate comments (5)
framework/configstore/migrations.go (1)

280-287: ⚠️ Potential issue | 🔴 Critical

Add a forward-only Codex config-hash backfill here.

Threading Codex into the old add_config_hash_column / add_store_raw_request_response_column migrations is still unsafe. Databases that already applied those IDs will never recompute config_hash, and databases upgrading from a pre-Codex release can hit the historical hash migration before codex_config_json exists. Keep the old migrations pre-Codex and add a new idempotent backfill immediately after migrationAddCodexConfigColumns.

Suggested follow-up
 if err := migrationAddCodexConfigColumns(ctx, db); err != nil {
 	return err
 }
+if err := migrationBackfillCodexProviderConfigHash(ctx, db); err != nil {
+	return err
+}
 if err := migrationAddCodexAuthSessionsTable(ctx, db); err != nil {
 	return err
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/configstore/migrations.go` around lines 280 - 287, Add an
idempotent, forward-only backfill immediately after calling
migrationAddCodexConfigColumns: implement a new function (e.g.,
migrationBackfillCodexConfigHash) that finds records where codex_config_json IS
NOT NULL and config_hash IS NULL, computes and sets the config_hash only for
those rows (skip rows that already have config_hash), and do not implement a
down/rollback path; then invoke migrationBackfillCodexConfigHash(ctx, db) right
after migrationAddCodexConfigColumns and before
migrationAddCodexAuthSessionsTable/migrationCleanupCodexBrowserAuthColumns to
ensure safe incremental upgrades.
ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx (1)

198-278: ⚠️ Potential issue | 🟡 Minor

Add stable selectors to the new Codex manual-auth controls.

The new CollapsibleTrigger and credential inputs are interactive, but they still don't expose the repo's required data-testid selectors. That will make the Codex setup flow harder to cover in e2e/regression tests. Examples here would be enough: apikey-codex-manual-credentials-trigger, apikey-codex-refresh-token-input, apikey-codex-access-token-input, apikey-codex-account-id-input, and apikey-codex-access-token-expiry-input.

As per coding guidelines, ui/**/*.{tsx,ts} should add new interactive UI elements with data-testid attributes following data-testid="<entity>-<element>-<qualifier>".

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

In `@ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx` around lines
198 - 278, The CollapsibleTrigger and the manual Codex input controls lack
stable test selectors; add data-testid attributes to CollapsibleTrigger (e.g.,
data-testid="apikey-codex-manual-credentials-trigger"), the refresh token
EnvVarInput (apikey-codex-refresh-token-input), the cached access token
EnvVarInput (apikey-codex-access-token-input), the account id EnvVarInput
(apikey-codex-account-id-input), and the access token expiry Input
(apikey-codex-access-token-expiry-input); update the JSX for the
CollapsibleTrigger, the EnvVarInput instances used in the FormField names
key.codex_key_config.refresh_token, key.codex_key_config.access_token,
key.codex_key_config.account_id, and the Input used for
key.codex_key_config.access_token_expires_at to include these data-testid
attributes following the project pattern.
framework/configstore/tables/codexauth.go (1)

36-46: ⚠️ Potential issue | 🟠 Major

Always mark the row as encrypted when encryption is enabled.

Rows without DeviceAuthID currently keep the default "plain_text" status, so the startup encryption pass will keep treating them as unprocessed. Set s.EncryptionStatus = EncryptionStatusEncrypted whenever encrypt.IsEnabled() is true, regardless of whether a specific field was encrypted.

🛠️ Proposed fix
 	if encrypt.IsEnabled() {
-		encrypted := false
+		s.EncryptionStatus = EncryptionStatusEncrypted
 		if s.DeviceAuthID != nil && *s.DeviceAuthID != "" {
 			if err := encryptString(s.DeviceAuthID); err != nil {
 				return fmt.Errorf("failed to encrypt codex device auth id: %w", err)
 			}
-			encrypted = true
-		}
-		if encrypted {
-			s.EncryptionStatus = EncryptionStatusEncrypted
 		}
 	}

Based on learnings, ensure EncryptionStatus is set to "encrypted" whenever encryption is enabled, even if no fields are actually encrypted.

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

In `@framework/configstore/tables/codexauth.go` around lines 36 - 46, The
encryption status is only set to EncryptionStatusEncrypted when a field was
actually encrypted, leaving rows with no DeviceAuthID marked as plain_text;
modify the logic in the codexauth save/prepare code (around encrypt.IsEnabled(),
s.DeviceAuthID, encryptString) so that whenever encrypt.IsEnabled() returns true
you unconditionally set s.EncryptionStatus = EncryptionStatusEncrypted (move or
add the assignment outside the conditional that checks the encrypted boolean) so
rows are marked "encrypted" even if no specific field was changed.
transports/bifrost-http/server/server.go (2)

1161-1167: ⚠️ Potential issue | 🔴 Critical

Don't persist refreshed credentials on an unbounded background context.

This runs on the request path after token refresh. Using context.Background() with schemas.NoDeadline means a slow config store/DB can stall request completion indefinitely. Thread the caller context through, or derive a short timeout and cancel it explicitly.

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

In `@transports/bifrost-http/server/server.go` around lines 1161 - 1167, The
persistCodexCredentialRefresh function uses
schemas.NewBifrostContext(context.Background(), schemas.NoDeadline) which
creates an unbounded background context and can hang request handling; change it
to accept and use the caller context (e.g., add ctx context.Context parameter to
persistCodexCredentialRefresh and pass that into schemas.NewBifrostContext) or,
if signature change is not possible, derive a bounded context via
context.WithTimeout and cancel it, then call s.Config.PersistCodexKeyCredentials
with that bounded context; update all callers of persistCodexCredentialRefresh
accordingly and keep references to schemas.NewBifrostContext and
s.Config.PersistCodexKeyCredentials intact.

1142-1149: ⚠️ Potential issue | 🔴 Critical

Inject the credential persister before TransportInterceptorMiddleware.

This middleware is appended into commonMiddlewares, but the inference chain later prepends handlers.TransportInterceptorMiddleware(s.Config) ahead of that slice. TransportInterceptorMiddleware materializes the *schemas.BifrostContext first, so refreshed Codex tokens never see schemas.BifrostContextKeyCodexCredentialPersister. Move this setup ahead of the interceptor in the inference middleware order.

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

In `@transports/bifrost-http/server/server.go` around lines 1142 - 1149, The
credential persister middleware (setting
schemas.BifrostContextKeyCodexCredentialPersister with
schemas.CodexCredentialPersister(s.persistCodexCredentialRefresh)) is appended
to commonMiddlewares after which
handlers.TransportInterceptorMiddleware(s.Config) is later prepended, so the
interceptor runs before the persister; change the order so the persister
middleware is inserted/Prepended before TransportInterceptorMiddleware (i.e.,
ensure the middleware that sets the CodexCredentialPersister on the request
context runs earlier than handlers.TransportInterceptorMiddleware by moving its
addition ahead of where TransportInterceptorMiddleware is placed or by
prepending it to the middleware slice).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/schemas/provider.go`:
- Around line 504-514: The CodexConfig currently allows any non-empty string for
PricingMode which leads to silent zero-rating; add explicit validation to reject
unknown values by ensuring CodexPricingMode only accepts the defined constants
(CodexPricingModeIncludedZero and CodexPricingModeOpenAIEquivalent). Implement
an IsValid() method on CodexPricingMode or a custom UnmarshalJSON/UnmarshalText
for CodexPricingMode that returns an error for unknown values, and call/trigger
that validation when decoding/constructing CodexConfig (or add a Validate() on
CodexConfig) so invalid pricing_mode values are rejected rather than treated as
zero-priced; update any config-loading paths to surface the error.

In `@transports/bifrost-http/handlers/codexauth.go`:
- Around line 243-257: The helper functions completeSession and failSession
currently swallow UpdateCodexAuthSession errors and use context.Background();
change their signatures to accept a context.Context (e.g., completeSession(ctx
context.Context, session *configstoreTables.TableCodexAuthSession) error and
failSession(ctx context.Context, session
*configstoreTables.TableCodexAuthSession, message string) error), update the
session fields the same way but call
h.store.ConfigStore.UpdateCodexAuthSession(ctx, session) and return any error
from that call instead of discarding it, and then update the caller
refreshSessionState (and any other callers) to pass down the incoming context
and handle/return the error so persistence failures bubble up instead of being
best-effort.
- Around line 90-126: The getAuthSessionStatus and cancelAuthSession handlers
currently dereference h.store.ConfigStore and can panic when the server runs
without DB-backed config storage; add the same guard used by
getEditableCodexKey/startDeviceAuth: check that h.store != nil and
h.store.ConfigStore != nil at the start of both getAuthSessionStatus and
cancelAuthSession and if missing return SendError(ctx,
fasthttp.StatusServiceUnavailable, "Config store unavailable") (or equivalent
503 message) before any calls to h.store.ConfigStore or refreshSessionState;
ensure both functions return immediately after sending the 503.
- Around line 43-46: RegisterRoutes is missing the browser-auth route used by
the frontend; add a POST route for
"/api/providers/codex/keys/{keyId}/auth/browser/start" in
CodexAuthHandler.RegisterRoutes and wire it to the appropriate handler (e.g.,
h.startBrowserAuth) using lib.ChainMiddlewares like the other routes; if
startBrowserAuth does not exist, implement a handler function with the same
signature/behavior pattern as startDeviceAuth (validate keyId, start the browser
auth flow, return session info and errors).

In `@transports/bifrost-http/handlers/providers.go`:
- Around line 370-381: mergeKeys() currently preserves redacted secrets for
Azure/Vertex/Bedrock/VLLM but not for Codex, which causes device-auth fields to
be overwritten with redacted/empty UI values; update mergeKeys() to detect
redacted placeholders or empty values on incoming schemas.Key Codex fields and,
when detected, keep the existing stored values for CodexRefreshToken,
CodexAccessToken, CodexAccessTokenExpiresAt, CodexAccountID and CodexAuthMethod
instead of replacing them; ensure this logic mirrors the existing preservation
checks used for Azure/Vertex/Bedrock/VLLM and operates on the same key merge
path for provider keys (schemas.Key).

In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx`:
- Around line 147-160: beginDeviceFlow currently can reject from
ensureKeyID("device") or startDeviceAuth(...).unwrap() and the caller ignores
the returned promise, causing unhandled rejections and no user feedback; wrap
the body of beginDeviceFlow in a try/catch, catch any errors from ensureKeyID or
startDeviceAuth, set a visible error via existing UI paths (e.g. call
setStatusMessage with a user-friendly message and error details or trigger the
toast/notification utility used in this component), and return/exit after
handling; keep the success path that calls setSession, setIsOpen and opens
popupRef unchanged so valid flows are unaffected.

---

Duplicate comments:
In `@framework/configstore/migrations.go`:
- Around line 280-287: Add an idempotent, forward-only backfill immediately
after calling migrationAddCodexConfigColumns: implement a new function (e.g.,
migrationBackfillCodexConfigHash) that finds records where codex_config_json IS
NOT NULL and config_hash IS NULL, computes and sets the config_hash only for
those rows (skip rows that already have config_hash), and do not implement a
down/rollback path; then invoke migrationBackfillCodexConfigHash(ctx, db) right
after migrationAddCodexConfigColumns and before
migrationAddCodexAuthSessionsTable/migrationCleanupCodexBrowserAuthColumns to
ensure safe incremental upgrades.

In `@framework/configstore/tables/codexauth.go`:
- Around line 36-46: The encryption status is only set to
EncryptionStatusEncrypted when a field was actually encrypted, leaving rows with
no DeviceAuthID marked as plain_text; modify the logic in the codexauth
save/prepare code (around encrypt.IsEnabled(), s.DeviceAuthID, encryptString) so
that whenever encrypt.IsEnabled() returns true you unconditionally set
s.EncryptionStatus = EncryptionStatusEncrypted (move or add the assignment
outside the conditional that checks the encrypted boolean) so rows are marked
"encrypted" even if no specific field was changed.

In `@transports/bifrost-http/server/server.go`:
- Around line 1161-1167: The persistCodexCredentialRefresh function uses
schemas.NewBifrostContext(context.Background(), schemas.NoDeadline) which
creates an unbounded background context and can hang request handling; change it
to accept and use the caller context (e.g., add ctx context.Context parameter to
persistCodexCredentialRefresh and pass that into schemas.NewBifrostContext) or,
if signature change is not possible, derive a bounded context via
context.WithTimeout and cancel it, then call s.Config.PersistCodexKeyCredentials
with that bounded context; update all callers of persistCodexCredentialRefresh
accordingly and keep references to schemas.NewBifrostContext and
s.Config.PersistCodexKeyCredentials intact.
- Around line 1142-1149: The credential persister middleware (setting
schemas.BifrostContextKeyCodexCredentialPersister with
schemas.CodexCredentialPersister(s.persistCodexCredentialRefresh)) is appended
to commonMiddlewares after which
handlers.TransportInterceptorMiddleware(s.Config) is later prepended, so the
interceptor runs before the persister; change the order so the persister
middleware is inserted/Prepended before TransportInterceptorMiddleware (i.e.,
ensure the middleware that sets the CodexCredentialPersister on the request
context runs earlier than handlers.TransportInterceptorMiddleware by moving its
addition ahead of where TransportInterceptorMiddleware is placed or by
prepending it to the middleware slice).

In `@ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx`:
- Around line 198-278: The CollapsibleTrigger and the manual Codex input
controls lack stable test selectors; add data-testid attributes to
CollapsibleTrigger (e.g.,
data-testid="apikey-codex-manual-credentials-trigger"), the refresh token
EnvVarInput (apikey-codex-refresh-token-input), the cached access token
EnvVarInput (apikey-codex-access-token-input), the account id EnvVarInput
(apikey-codex-account-id-input), and the access token expiry Input
(apikey-codex-access-token-expiry-input); update the JSX for the
CollapsibleTrigger, the EnvVarInput instances used in the FormField names
key.codex_key_config.refresh_token, key.codex_key_config.access_token,
key.codex_key_config.account_id, and the Input used for
key.codex_key_config.access_token_expires_at to include these data-testid
attributes following the project pattern.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c084bbef-1bbd-4920-abd2-9c090813f74a

📥 Commits

Reviewing files that changed from the base of the PR and between 2111eb9 and ec6efc1.

⛔ Files ignored due to path filters (3)
  • docs/media/ui-codex-screenshot-1.png is excluded by !**/*.png
  • docs/media/ui-codex-screenshot-2.png is excluded by !**/*.png
  • docs/media/ui-codex-screenshot-3.png is excluded by !**/*.png
📒 Files selected for processing (44)
  • core/bifrost.go
  • core/providers/codex/auth.go
  • core/providers/codex/auth_test.go
  • core/providers/codex/codex.go
  • core/providers/codex/codex_test.go
  • core/providers/mistral/mistral.go
  • core/providers/openai/openai.go
  • core/schemas/account.go
  • core/schemas/bifrost.go
  • core/schemas/context.go
  • core/schemas/provider.go
  • core/utils.go
  • framework/configstore/clientconfig.go
  • framework/configstore/migrations.go
  • framework/configstore/rdb.go
  • framework/configstore/store.go
  • framework/configstore/tables/codexauth.go
  • framework/configstore/tables/key.go
  • framework/configstore/tables/provider.go
  • framework/modelcatalog/codex.go
  • framework/modelcatalog/codex_test.go
  • framework/modelcatalog/main.go
  • framework/modelcatalog/main_test.go
  • framework/modelcatalog/pricing.go
  • plugins/maxim/plugin_test.go
  • transports/bifrost-http/handlers/codexauth.go
  • transports/bifrost-http/handlers/providers.go
  • transports/bifrost-http/lib/config.go
  • transports/bifrost-http/lib/config_test.go
  • transports/bifrost-http/lib/ctx.go
  • transports/bifrost-http/server/server.go
  • transports/config.schema.json
  • ui/app/workspace/providers/dialogs/providerConfigSheet.tsx
  • ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx
  • ui/app/workspace/providers/fragments/codexAuthControls.tsx
  • ui/app/workspace/providers/fragments/codexConfigFormFragment.tsx
  • ui/app/workspace/providers/fragments/index.ts
  • ui/app/workspace/providers/views/providerKeyForm.tsx
  • ui/lib/constants/config.ts
  • ui/lib/constants/icons.tsx
  • ui/lib/constants/logs.ts
  • ui/lib/store/apis/providersApi.ts
  • ui/lib/types/config.ts
  • ui/lib/types/schemas.ts
✅ Files skipped from review due to trivial changes (10)
  • ui/lib/constants/config.ts
  • transports/bifrost-http/lib/ctx.go
  • core/providers/codex/auth_test.go
  • plugins/maxim/plugin_test.go
  • ui/app/workspace/providers/fragments/index.ts
  • framework/modelcatalog/main.go
  • core/providers/openai/openai.go
  • framework/modelcatalog/codex_test.go
  • core/providers/codex/codex_test.go
  • framework/modelcatalog/main_test.go
🚧 Files skipped from review as they are similar to previous changes (16)
  • ui/app/workspace/providers/dialogs/providerConfigSheet.tsx
  • ui/lib/constants/logs.ts
  • core/schemas/context.go
  • framework/configstore/store.go
  • ui/app/workspace/providers/fragments/codexConfigFormFragment.tsx
  • core/bifrost.go
  • framework/configstore/clientconfig.go
  • framework/configstore/rdb.go
  • transports/config.schema.json
  • framework/configstore/tables/key.go
  • transports/bifrost-http/lib/config_test.go
  • ui/lib/types/schemas.ts
  • core/providers/mistral/mistral.go
  • core/providers/codex/auth.go
  • core/schemas/account.go
  • core/providers/codex/codex.go
👮 Files not reviewed due to content moderation or server errors (1)
  • ui/lib/constants/icons.tsx

Comment thread core/schemas/provider.go Outdated
Comment on lines +504 to +514
type CodexPricingMode string

const (
CodexPricingModeIncludedZero CodexPricingMode = "included_zero"
CodexPricingModeOpenAIEquivalent CodexPricingMode = "openai_equivalent"
)

// CodexConfig holds provider-level settings for the ChatGPT Plus/Pro-backed Codex provider.
type CodexConfig struct {
PricingMode CodexPricingMode `json:"pricing_mode,omitempty"`
}
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

Reject unknown pricing_mode values instead of silently zero-rating Codex.

Any non-empty string survives normalization here. Downstream, framework/modelcatalog/pricing.go only treats openai_equivalent specially; every other value falls back to zeroPricing(...). That means a typo like "openai-equivalant" will underbill all Codex traffic instead of failing fast. Please validate against the known enum values before this config is accepted.

Also applies to: 551-553

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

In `@core/schemas/provider.go` around lines 504 - 514, The CodexConfig currently
allows any non-empty string for PricingMode which leads to silent zero-rating;
add explicit validation to reject unknown values by ensuring CodexPricingMode
only accepts the defined constants (CodexPricingModeIncludedZero and
CodexPricingModeOpenAIEquivalent). Implement an IsValid() method on
CodexPricingMode or a custom UnmarshalJSON/UnmarshalText for CodexPricingMode
that returns an error for unknown values, and call/trigger that validation when
decoding/constructing CodexConfig (or add a Validate() on CodexConfig) so
invalid pricing_mode values are rejected rather than treated as zero-priced;
update any config-loading paths to surface the error.

Comment on lines +43 to +46
func (h *CodexAuthHandler) RegisterRoutes(r *router.Router, middlewares ...schemas.BifrostHTTPMiddleware) {
r.POST("/api/providers/codex/keys/{keyId}/auth/device/start", lib.ChainMiddlewares(h.startDeviceAuth, middlewares...))
r.GET("/api/providers/codex/auth/sessions/{id}", lib.ChainMiddlewares(h.getAuthSessionStatus, middlewares...))
r.DELETE("/api/providers/codex/auth/sessions/{id}", lib.ChainMiddlewares(h.cancelAuthSession, middlewares...))
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

The browser-auth endpoint is missing from the handler.

ui/lib/store/apis/providersApi.ts now exposes startCodexBrowserAuth against /api/providers/codex/keys/{keyId}/auth/browser/start, but this handler only registers auth/device/start, session status, and cancel. Any browser-auth call will 404 until the matching route is added or the hook is removed.

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

In `@transports/bifrost-http/handlers/codexauth.go` around lines 43 - 46,
RegisterRoutes is missing the browser-auth route used by the frontend; add a
POST route for "/api/providers/codex/keys/{keyId}/auth/browser/start" in
CodexAuthHandler.RegisterRoutes and wire it to the appropriate handler (e.g.,
h.startBrowserAuth) using lib.ChainMiddlewares like the other routes; if
startBrowserAuth does not exist, implement a handler function with the same
signature/behavior pattern as startDeviceAuth (validate keyId, start the browser
auth flow, return session info and errors).

Comment on lines +90 to +126
func (h *CodexAuthHandler) getAuthSessionStatus(ctx *fasthttp.RequestCtx) {
sessionID := ctx.UserValue("id").(string)
session, err := h.store.ConfigStore.GetCodexAuthSessionByID(ctx, sessionID)
if err != nil {
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to get auth session: %v", err))
return
}
if session == nil {
SendError(ctx, fasthttp.StatusNotFound, "Auth session not found")
return
}
if err := h.refreshSessionState(ctx, session); err != nil {
SendError(ctx, fasthttp.StatusBadGateway, fmt.Sprintf("Failed to refresh auth session: %v", err))
return
}
SendJSON(ctx, h.sessionResponse(session))
}

func (h *CodexAuthHandler) cancelAuthSession(ctx *fasthttp.RequestCtx) {
sessionID := ctx.UserValue("id").(string)
session, err := h.store.ConfigStore.GetCodexAuthSessionByID(ctx, sessionID)
if err != nil {
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to get auth session: %v", err))
return
}
if session == nil {
SendError(ctx, fasthttp.StatusNotFound, "Auth session not found")
return
}
session.Status = codexAuthSessionCancelled
now := time.Now()
session.CompletedAt = &now
if err := h.store.ConfigStore.UpdateCodexAuthSession(ctx, session); err != nil {
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to cancel auth session: %v", err))
return
}
SendJSON(ctx, h.sessionResponse(session))
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 | 🔴 Critical

Guard session GET/DELETE when DB-backed config storage is unavailable.

startDeviceAuth() routes through getEditableCodexKey() and returns a clean error when h.store/h.store.ConfigStore is missing, but getAuthSessionStatus() and cancelAuthSession() dereference h.store.ConfigStore directly. Since server.RegisterAPIRoutes() always registers this handler, these endpoints can panic in non-DB deployments instead of returning 503.

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

In `@transports/bifrost-http/handlers/codexauth.go` around lines 90 - 126, The
getAuthSessionStatus and cancelAuthSession handlers currently dereference
h.store.ConfigStore and can panic when the server runs without DB-backed config
storage; add the same guard used by getEditableCodexKey/startDeviceAuth: check
that h.store != nil and h.store.ConfigStore != nil at the start of both
getAuthSessionStatus and cancelAuthSession and if missing return SendError(ctx,
fasthttp.StatusServiceUnavailable, "Config store unavailable") (or equivalent
503 message) before any calls to h.store.ConfigStore or refreshSessionState;
ensure both functions return immediately after sending the 503.

Comment on lines +243 to +257
func (h *CodexAuthHandler) completeSession(session *configstoreTables.TableCodexAuthSession) {
now := time.Now()
session.Status = codexAuthSessionSucceeded
session.CompletedAt = &now
session.LastError = nil
_ = h.store.ConfigStore.UpdateCodexAuthSession(context.Background(), session)
}

func (h *CodexAuthHandler) failSession(session *configstoreTables.TableCodexAuthSession, message string) {
now := time.Now()
session.Status = codexAuthSessionFailed
session.CompletedAt = &now
session.LastError = &message
_ = h.store.ConfigStore.UpdateCodexAuthSession(context.Background(), session)
}
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

Don't make terminal session persistence best-effort.

After tokens are written, completeSession() updates the DB with context.Background() and discards the error. If that write fails, the current poll returns authorized from memory while the stored session stays pending, so later polls can retry the exchange or show stale state. Persist the terminal state with the caller context and bubble failures back to refreshSessionState().

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

In `@transports/bifrost-http/handlers/codexauth.go` around lines 243 - 257, The
helper functions completeSession and failSession currently swallow
UpdateCodexAuthSession errors and use context.Background(); change their
signatures to accept a context.Context (e.g., completeSession(ctx
context.Context, session *configstoreTables.TableCodexAuthSession) error and
failSession(ctx context.Context, session
*configstoreTables.TableCodexAuthSession, message string) error), update the
session fields the same way but call
h.store.ConfigStore.UpdateCodexAuthSession(ctx, session) and return any error
from that call instead of discarding it, and then update the caller
refreshSessionState (and any other callers) to pass down the incoming context
and handle/return the error so persistence failures bubble up instead of being
best-effort.

Comment on lines 370 to +381
var payload = struct {
Keys []schemas.Key `json:"keys"` // API keys for the provider
NetworkConfig schemas.NetworkConfig `json:"network_config"` // Network-related settings
ConcurrencyAndBufferSize schemas.ConcurrencyAndBufferSize `json:"concurrency_and_buffer_size"` // Concurrency settings
ProxyConfig *schemas.ProxyConfig `json:"proxy_config,omitempty"` // Proxy configuration
SendBackRawRequest *bool `json:"send_back_raw_request,omitempty"` // Include raw request in BifrostResponse
SendBackRawResponse *bool `json:"send_back_raw_response,omitempty"` // Include raw response in BifrostResponse
Keys []schemas.Key `json:"keys"` // API keys for the provider
NetworkConfig schemas.NetworkConfig `json:"network_config"` // Network-related settings
ConcurrencyAndBufferSize schemas.ConcurrencyAndBufferSize `json:"concurrency_and_buffer_size"` // Concurrency settings
ProxyConfig *schemas.ProxyConfig `json:"proxy_config,omitempty"` // Proxy configuration
SendBackRawRequest *bool `json:"send_back_raw_request,omitempty"` // Include raw request in BifrostResponse
SendBackRawResponse *bool `json:"send_back_raw_response,omitempty"` // Include raw response in BifrostResponse
StoreRawRequestResponse *bool `json:"store_raw_request_response,omitempty"` // Capture raw request/response for internal logging only
CustomProviderConfig *schemas.CustomProviderConfig `json:"custom_provider_config,omitempty"` // Custom provider configuration
OpenAIConfig *schemas.OpenAIConfig `json:"openai_config,omitempty"` // OpenAI-specific configuration
PricingOverrides []schemas.ProviderPricingOverride `json:"pricing_overrides,omitempty"` // Provider-level pricing overrides
CustomProviderConfig *schemas.CustomProviderConfig `json:"custom_provider_config,omitempty"` // Custom provider configuration
OpenAIConfig *schemas.OpenAIConfig `json:"openai_config,omitempty"` // OpenAI-specific configuration
CodexConfig *schemas.CodexConfig `json:"codex_config,omitempty"` // Codex-specific configuration
PricingOverrides []schemas.ProviderPricingOverride `json:"pricing_overrides,omitempty"` // Provider-level pricing overrides
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 | 🔴 Critical

Preserve redacted Codex key credentials during updates.

This path now round-trips Codex-managed keys through mergeKeys(), but mergeKeys() still only restores redacted secrets for Azure/Vertex/Bedrock/VLLM. A normal edit/save of a connected Codex key will overwrite the stored device-auth fields (CodexRefreshToken, CodexAccessToken, CodexAccessTokenExpiresAt, CodexAccountID, CodexAuthMethod) with redacted placeholders or empties from the UI, breaking subsequent requests. Please add the same redaction-preservation logic for the Codex fields before shipping.

Also applies to: 517-517

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

In `@transports/bifrost-http/handlers/providers.go` around lines 370 - 381,
mergeKeys() currently preserves redacted secrets for Azure/Vertex/Bedrock/VLLM
but not for Codex, which causes device-auth fields to be overwritten with
redacted/empty UI values; update mergeKeys() to detect redacted placeholders or
empty values on incoming schemas.Key Codex fields and, when detected, keep the
existing stored values for CodexRefreshToken, CodexAccessToken,
CodexAccessTokenExpiresAt, CodexAccountID and CodexAuthMethod instead of
replacing them; ensure this logic mirrors the existing preservation checks used
for Azure/Vertex/Bedrock/VLLM and operates on the same key merge path for
provider keys (schemas.Key).

Comment on lines +147 to +160
const beginDeviceFlow = async () => {
setStatusMessage(null);
const resolvedKeyID = await ensureKeyID("device");
const nextSession = await startDeviceAuth(resolvedKeyID).unwrap();
setSession(nextSession);
setIsOpen(true);
if (nextSession.verification_uri) {
popupRef.current = window.open(
nextSession.verification_uri,
"codex_device_auth",
"width=640,height=760,resizable=yes,scrollbars=yes",
);
}
};
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

Handle device-flow startup failures instead of dropping the promise.

beginDeviceFlow() can reject when draft persistence fails (for example, missing key name) or when startDeviceAuth(...).unwrap() errors. The caller does void beginDeviceFlow(), so those failures turn into unhandled promises and the user gets no feedback. Catch the error here and surface it via toast or an inline alert before returning.

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

In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx` around lines 147
- 160, beginDeviceFlow currently can reject from ensureKeyID("device") or
startDeviceAuth(...).unwrap() and the caller ignores the returned promise,
causing unhandled rejections and no user feedback; wrap the body of
beginDeviceFlow in a try/catch, catch any errors from ensureKeyID or
startDeviceAuth, set a visible error via existing UI paths (e.g. call
setStatusMessage with a user-friendly message and error details or trigger the
toast/notification utility used in this component), and return/exit after
handling; keep the success path that calls setSession, setIsOpen and opens
popupRef unchanged so valid flows are unaffected.

@yaronshanisima
Copy link
Copy Markdown
Author

I will go over the comments and try to fix those best I can.
I also tried to add functionality of "adjusted pricing", the idea that subscription call do not really have cost, but maybe we can simulate it. Anyway, the PR is already too large, if its something we would like to implmented, we will do in a seperate PR, I will work on removing this logic as well.

Copy link
Copy Markdown
Contributor

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
transports/bifrost-http/lib/config_test.go (1)

447-461: Keep MockConfigStore.PersistCodexKeyConfig minimal in this test double.

This new in-memory mutation logic makes the mock behaviorful and can diverge from real store semantics. Prefer keeping this method as a simple stub here and validating Codex key persistence via SQLite-backed integration tests (createTestSQLiteConfigStore path).

Based on learnings: "In tests under transports/bifrost-http/lib/config_test.go, keep MockConfigStore methods (e.g., GetVirtualKeysPaginated) as simple, returning zero/nil values. Do not embed business logic (filtering/pagination) in the mock. Cover behavior with SQLite-backed integration tests using createTestSQLiteConfigStore instead to validate end-to-end behavior."

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

In `@transports/bifrost-http/lib/config_test.go` around lines 447 - 461, Remove
the in-memory mutation/filtering logic inside
MockConfigStore.PersistCodexKeyConfig and make it a minimal stub: do not read or
write m.providers, do not attempt to find keys, and simply return a zero/nil
result (e.g., return nil) so the mock stays behaviorless; rely on the
SQLite-backed integration tests via createTestSQLiteConfigStore to validate
actual persistence semantics.
framework/configstore/rdb.go (1)

871-889: Prefer column-scoped update for Codex credential persistence

Current flow does SELECT then Save of the full row. A targeted update (Model(...).Where(...).Update(...)) is safer and avoids unintentionally writing unrelated stale fields.

♻️ Proposed refactor
 func (s *RDBConfigStore) PersistCodexKeyConfig(ctx context.Context, keyID string, keyConfig *schemas.CodexKeyConfig) error {
 	if keyConfig == nil {
 		return nil
 	}
 
-	var dbKey tables.TableKey
-	if err := s.db.WithContext(ctx).Where("provider = ? AND key_id = ?", string(schemas.Codex), keyID).First(&dbKey).Error; err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return ErrNotFound
-		}
-		return fmt.Errorf("failed to load codex key for persistence: %w", err)
-	}
-
-	dbKey.CodexKeyConfig = keyConfig
-	if err := s.db.WithContext(ctx).Save(&dbKey).Error; err != nil {
-		return fmt.Errorf("failed to persist codex key config: %w", err)
-	}
+	result := s.db.WithContext(ctx).
+		Model(&tables.TableKey{}).
+		Where("provider = ? AND key_id = ?", string(schemas.Codex), keyID).
+		Update("codex_key_config", keyConfig)
+	if result.Error != nil {
+		return fmt.Errorf("failed to persist codex key config: %w", result.Error)
+	}
+	if result.RowsAffected == 0 {
+		return ErrNotFound
+	}
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/configstore/rdb.go` around lines 871 - 889, The
PersistCodexKeyConfig method in RDBConfigStore currently selects the full
tables.TableKey row then calls Save, which can overwrite unrelated fields;
change it to perform a column-scoped update instead: locate
PersistCodexKeyConfig and replace the SELECT+Save flow with a targeted update
using s.db.WithContext(ctx).Model(&tables.TableKey{}).Where("provider = ? AND
key_id = ?", string(schemas.Codex), keyID).Update("codex_key_config", keyConfig)
(or Updates if necessary) and return ErrNotFound if the initial lookup should
detect missing rows, ensuring only the CodexKeyConfig column is modified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@framework/configstore/rdb.go`:
- Around line 302-303: The code copies key.CodexKeyConfig directly into the
persisted record causing existing Codex credentials to be wiped when an update
payload omits them; update the save/update paths (where Save is called for
existing keys) to first load the existing record (using the same
store/repository methods already present), and if key.CodexKeyConfig is nil,
preserve the existingRecord.CodexKeyConfig instead of overwriting it; apply this
pattern for all update branches that set CodexKeyConfig (references:
CodexKeyConfig field assignment and the Save calls used for existing-key
updates).

---

Nitpick comments:
In `@framework/configstore/rdb.go`:
- Around line 871-889: The PersistCodexKeyConfig method in RDBConfigStore
currently selects the full tables.TableKey row then calls Save, which can
overwrite unrelated fields; change it to perform a column-scoped update instead:
locate PersistCodexKeyConfig and replace the SELECT+Save flow with a targeted
update using s.db.WithContext(ctx).Model(&tables.TableKey{}).Where("provider = ?
AND key_id = ?", string(schemas.Codex), keyID).Update("codex_key_config",
keyConfig) (or Updates if necessary) and return ErrNotFound if the initial
lookup should detect missing rows, ensuring only the CodexKeyConfig column is
modified.

In `@transports/bifrost-http/lib/config_test.go`:
- Around line 447-461: Remove the in-memory mutation/filtering logic inside
MockConfigStore.PersistCodexKeyConfig and make it a minimal stub: do not read or
write m.providers, do not attempt to find keys, and simply return a zero/nil
result (e.g., return nil) so the mock stays behaviorless; rely on the
SQLite-backed integration tests via createTestSQLiteConfigStore to validate
actual persistence semantics.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b293b5c5-bdc0-40a7-a6de-7629aa4e576b

📥 Commits

Reviewing files that changed from the base of the PR and between ec6efc1 and d98fbe7.

📒 Files selected for processing (18)
  • core/providers/codex/codex.go
  • core/schemas/provider.go
  • framework/configstore/clientconfig.go
  • framework/configstore/migrations.go
  • framework/configstore/rdb.go
  • framework/configstore/store.go
  • framework/modelcatalog/main.go
  • framework/modelcatalog/main_test.go
  • framework/modelcatalog/pricing.go
  • transports/bifrost-http/handlers/codexauth.go
  • transports/bifrost-http/lib/config.go
  • transports/bifrost-http/lib/config_test.go
  • transports/bifrost-http/server/server.go
  • transports/config.schema.json
  • ui/app/workspace/providers/dialogs/providerConfigSheet.tsx
  • ui/app/workspace/providers/fragments/index.ts
  • ui/lib/types/config.ts
  • ui/lib/types/schemas.ts
✅ Files skipped from review due to trivial changes (4)
  • framework/modelcatalog/main.go
  • ui/app/workspace/providers/fragments/index.ts
  • ui/app/workspace/providers/dialogs/providerConfigSheet.tsx
  • core/schemas/provider.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • transports/bifrost-http/server/server.go
  • transports/bifrost-http/handlers/codexauth.go
  • core/providers/codex/codex.go
👮 Files not reviewed due to content moderation or server errors (4)
  • framework/configstore/clientconfig.go
  • transports/bifrost-http/lib/config.go
  • ui/lib/types/config.ts
  • framework/configstore/store.go

Comment on lines +302 to 303
CodexKeyConfig: key.CodexKeyConfig,
ConfigHash: keyHash,
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

Prevent accidental Codex credential wipe during key updates

CodexKeyConfig is copied directly from incoming config, but existing-key paths later call Save (Line 361, Line 376, Line 522). If incoming config omits Codex credentials (nil), persisted DB credentials get overwritten and lost.

💡 Proposed fix
@@
 			if result.Error == nil {
 				// Update existing key with new data
 				dbKey.ID = existingKey.ID                             // Keep the same database ID
 				dbKey.ProviderID = existingKey.ProviderID             // Preserve the existing ProviderID
 				dbKey.Enabled = existingKey.Enabled                   // Preserve the existing Enabled status
 				dbKey.Status = existingKey.Status                     // Preserve status (UI-managed)
 				dbKey.Description = existingKey.Description           // Preserve description (UI-managed)
 				dbKey.EncryptionStatus = existingKey.EncryptionStatus // Preserve encryption status
+				if dbKey.CodexKeyConfig == nil {
+					dbKey.CodexKeyConfig = existingKey.CodexKeyConfig
+				}
 				if err := txDB.WithContext(ctx).Save(&dbKey).Error; err != nil {
 					return s.parseGormError(err)
 				}
@@
 				if result.Error == nil {
 					// Found by name - update existing key, preserve original KeyID
 					dbKey.ID = existingKey.ID                             // Keep the same database ID
 					dbKey.KeyID = existingKey.KeyID                       // Preserve original KeyID
 					dbKey.ProviderID = existingKey.ProviderID             // Preserve the existing ProviderID
 					dbKey.Enabled = existingKey.Enabled                   // Preserve the existing Enabled status
 					dbKey.Status = existingKey.Status                     // Preserve status (UI-managed)
 					dbKey.Description = existingKey.Description           // Preserve description (UI-managed)
 					dbKey.EncryptionStatus = existingKey.EncryptionStatus // Preserve encryption status
+					if dbKey.CodexKeyConfig == nil {
+						dbKey.CodexKeyConfig = existingKey.CodexKeyConfig
+					}
 					if err := txDB.WithContext(ctx).Save(&dbKey).Error; err != nil {
 						return s.parseGormError(err)
 					}
@@
 		if existingKey, exists := existingKeysMap[key.ID]; exists {
 			dbKey.ID = existingKey.ID                             // Keep the same database ID
 			dbKey.ConfigHash = existingKey.ConfigHash             // Preserve config hash
 			dbKey.Status = existingKey.Status                     // Preserve status (UI-managed)
 			dbKey.Description = existingKey.Description           // Preserve description (UI-managed)
 			dbKey.EncryptionStatus = existingKey.EncryptionStatus // Preserve encryption status
+			if dbKey.CodexKeyConfig == nil {
+				dbKey.CodexKeyConfig = existingKey.CodexKeyConfig
+			}
 			if err := txDB.WithContext(ctx).Save(&dbKey).Error; err != nil {
 				return s.parseGormError(err)
 			}

Also applies to: 473-474

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

In `@framework/configstore/rdb.go` around lines 302 - 303, The code copies
key.CodexKeyConfig directly into the persisted record causing existing Codex
credentials to be wiped when an update payload omits them; update the
save/update paths (where Save is called for existing keys) to first load the
existing record (using the same store/repository methods already present), and
if key.CodexKeyConfig is nil, preserve the existingRecord.CodexKeyConfig instead
of overwriting it; apply this pattern for all update branches that set
CodexKeyConfig (references: CodexKeyConfig field assignment and the Save calls
used for existing-key updates).

Comment on lines +529 to +536
func accessTokenExpired(expiresAt *string) bool {
if expiresAt == nil || strings.TrimSpace(*expiresAt) == "" {
return true
}
parsed, err := time.Parse(time.RFC3339, *expiresAt)
if err != nil {
return true
}
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.

P1 Manual access token silently ignored when expiry is unset

accessTokenExpired returns true when AccessTokenExpiresAt is nil or empty. In the "manual" auth path a user may supply only an access_token with no expiry; the code treats the token as expired, falls through to the refresh-token branch, and if no refresh token is present it returns HTTP 401 — the manually-supplied access token is never used. The fix should skip the expiry guard when the auth method is manual or, at minimum, fall back to the access token when no refresh token is available:

func accessTokenExpired(expiresAt *string) bool {
	if expiresAt == nil || strings.TrimSpace(*expiresAt) == "" {
		return false // treat unknown expiry as still valid; caller should refresh on 401
	}
	parsed, err := time.Parse(time.RFC3339, *expiresAt)
	if err != nil {
		return true
	}
	return time.Now().After(parsed.Add(-30 * time.Second))
}

Copy link
Copy Markdown
Contributor

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
ui/app/workspace/providers/fragments/codexAuthControls.tsx (1)

147-160: ⚠️ Potential issue | 🟠 Major

Handle device-flow startup failures instead of dropping the promise.

beginDeviceFlow() can reject when ensureKeyID() fails or when startDeviceAuth(...).unwrap() errors. The caller uses void beginDeviceFlow(), so failures become unhandled promise rejections with no user feedback.

🛡️ Proposed fix to handle errors
 const beginDeviceFlow = async () => {
+  try {
     setStatusMessage(null);
     const resolvedKeyID = await ensureKeyID("device");
     const nextSession = await startDeviceAuth(resolvedKeyID).unwrap();
     setSession(nextSession);
     setIsOpen(true);
     if (nextSession.verification_uri) {
       popupRef.current = window.open(
         nextSession.verification_uri,
         "codex_device_auth",
         "width=640,height=760,resizable=yes,scrollbars=yes",
       );
     }
+  } catch (error) {
+    setStatusMessage(getErrorMessage(error));
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx` around lines 147
- 160, beginDeviceFlow currently lets rejections from ensureKeyID or
startDeviceAuth(...).unwrap() bubble into unhandled promise rejections; wrap the
function body in a try/catch, call ensureKeyID("device") and
startDeviceAuth(resolvedKeyID).unwrap() inside the try, and on success keep the
existing setSession, setIsOpen and popupRef.open logic; in the catch call
setStatusMessage(error?.message || String(error)) and clean up state (close
popupRef.current if opened, setSession(null) or setIsOpen(false) as appropriate)
and optionally log the error; reference beginDeviceFlow, ensureKeyID,
startDeviceAuth, setStatusMessage, setSession, setIsOpen, and popupRef when
making these changes.
🧹 Nitpick comments (3)
docs/providers/supported-providers/overview.mdx (1)

11-11: Link Cohere here for consistency with the other providers in the same sentence.

It is underlined but not clickable, unlike Anthropic/Gemini/Bedrock.

Suggested doc tweak
-Bifrost can also act as a provider-compatible gateway (for example, <u>[Anthropic](../../integrations/anthropic-sdk/overview)</u>, <u>[Google Gemini](../../integrations/genai-sdk/overview)</u>, <u>Cohere</u>, <u>[Bedrock](../../integrations/bedrock-sdk/overview)</u>, and others), exposing provider-specific endpoints so you can use existing provider SDKs or integrations with no code changes, see [What is an integration?](../../integrations/what-is-an-integration) for details.
+Bifrost can also act as a provider-compatible gateway (for example, <u>[Anthropic](../../integrations/anthropic-sdk/overview)</u>, <u>[Google Gemini](../../integrations/genai-sdk/overview)</u>, <u>[Cohere](./cohere)</u>, <u>[Bedrock](../../integrations/bedrock-sdk/overview)</u>, and others), exposing provider-specific endpoints so you can use existing provider SDKs or integrations with no code changes, see [What is an integration?](../../integrations/what-is-an-integration) for details.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/providers/supported-providers/overview.mdx` at line 11, The sentence in
docs/providers/supported-providers/overview.mdx lists providers but "Cohere" is
underlined plain text instead of a clickable link; change the "Cohere" token in
that sentence to use the same markdown link pattern as the others (e.g., replace
Cohere with [Cohere](../../integrations/cohere-sdk/overview) or the correct
Cohere integration path) so it becomes a clickable link consistent with
"Anthropic", "Google Gemini", and "Bedrock".
ui/app/workspace/providers/fragments/codexAuthControls.tsx (2)

49-49: Clean up popup window on component unmount.

If the component unmounts while the auth dialog is open, the popup window remains open with no way to close it programmatically.

♻️ Suggested cleanup effect
 const popupRef = useRef<Window | null>(null);
+
+useEffect(() => {
+  return () => {
+    if (popupRef.current && !popupRef.current.closed) {
+      popupRef.current.close();
+    }
+  };
+}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx` at line 49, Add a
cleanup effect to CodexAuthControls that closes the auth popup when the
component unmounts: implement a useEffect in the codexAuthControls.tsx component
that returns a cleanup function which checks popupRef.current, calls
popupRef.current.close() if it's still open, and then nulls popupRef.current;
also remove any related event listeners or polling tied to that popup inside the
same cleanup so no orphaned handlers remain.

110-130: Consider using server-provided polling interval.

The polling interval is hardcoded to 2000ms, but the backend returns interval_seconds in the session response (see context snippet 1). Using the server-recommended interval respects rate limits and adapts to backend configuration changes.

♻️ Suggested improvement
 useEffect(() => {
   if (!session || session.status !== "pending") {
     return;
   }
+  const intervalMs = (session.interval_seconds ?? 2) * 1000;
-  const timer = window.setInterval(async () => {
+  const timer = window.setInterval(async () => {
     try {
       const nextSession = await getCodexAuthSession(session.id).unwrap();
       setSession(nextSession);
       // ...
     } catch (error) {
       setStatusMessage(getErrorMessage(error));
     }
-  }, 2000);
+  }, intervalMs);
   return () => window.clearInterval(timer);
 }, [getCodexAuthSession, session, syncFormFromProvider]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx` around lines 110
- 130, The effect currently hardcodes a 2000ms poll interval; update the polling
in the useEffect that uses getCodexAuthSession/session to respect the
server-provided interval_seconds from the session response: when fetching
nextSession (and also on initial session) read nextSession.interval_seconds (or
session.interval_seconds) and use that value (converted to ms) to set the
window.setInterval delay, restarting/clearing the timer whenever the interval
value or session.id changes; keep the same logic for setting setSession,
setStatusMessage, syncFormFromProvider, and error handling (getErrorMessage) but
replace the fixed 2000 with the server-provided value and fall back to a
sensible default if interval_seconds is missing or invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx`:
- Around line 102-108: syncFormFromProvider currently uses the original keyId
prop which can be stale after ensureKeyID/onEnsurePersisted persists a new key;
instead, store the resolved key id returned by ensureKeyID (e.g., resolvedKeyID)
in component state and update it after persistence, then update
syncFormFromProvider to look up the key using that resolvedKeyID (referencing
functions getProvider and syncFormFromProvider) so it always finds the persisted
key; ensure the state is included in the syncFormFromProvider dependency array
and set when ensureKeyID/onEnsurePersisted completes.
- Around line 267-276: Add data-testid attributes to the interactive Buttons
inside DialogFooter: for the cancel button rendered when session?.status ===
"pending" (the Button that calls handleCancel) add a data-testid like
"codexauth-cancel-button" (or use entity-element-qualifier matching repo
conventions), and for the close button (the Button that calls handleCloseDialog)
add a data-testid like "codexauth-close-button"; update the JSX for those Button
elements in codexAuthControls.tsx (the Button calling handleCancel and the
Button calling handleCloseDialog) to include the corresponding data-testid
attributes following the <entity>-<element>-<qualifier> pattern.

---

Duplicate comments:
In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx`:
- Around line 147-160: beginDeviceFlow currently lets rejections from
ensureKeyID or startDeviceAuth(...).unwrap() bubble into unhandled promise
rejections; wrap the function body in a try/catch, call ensureKeyID("device")
and startDeviceAuth(resolvedKeyID).unwrap() inside the try, and on success keep
the existing setSession, setIsOpen and popupRef.open logic; in the catch call
setStatusMessage(error?.message || String(error)) and clean up state (close
popupRef.current if opened, setSession(null) or setIsOpen(false) as appropriate)
and optionally log the error; reference beginDeviceFlow, ensureKeyID,
startDeviceAuth, setStatusMessage, setSession, setIsOpen, and popupRef when
making these changes.

---

Nitpick comments:
In `@docs/providers/supported-providers/overview.mdx`:
- Line 11: The sentence in docs/providers/supported-providers/overview.mdx lists
providers but "Cohere" is underlined plain text instead of a clickable link;
change the "Cohere" token in that sentence to use the same markdown link pattern
as the others (e.g., replace Cohere with
[Cohere](../../integrations/cohere-sdk/overview) or the correct Cohere
integration path) so it becomes a clickable link consistent with "Anthropic",
"Google Gemini", and "Bedrock".

In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx`:
- Line 49: Add a cleanup effect to CodexAuthControls that closes the auth popup
when the component unmounts: implement a useEffect in the codexAuthControls.tsx
component that returns a cleanup function which checks popupRef.current, calls
popupRef.current.close() if it's still open, and then nulls popupRef.current;
also remove any related event listeners or polling tied to that popup inside the
same cleanup so no orphaned handlers remain.
- Around line 110-130: The effect currently hardcodes a 2000ms poll interval;
update the polling in the useEffect that uses getCodexAuthSession/session to
respect the server-provided interval_seconds from the session response: when
fetching nextSession (and also on initial session) read
nextSession.interval_seconds (or session.interval_seconds) and use that value
(converted to ms) to set the window.setInterval delay, restarting/clearing the
timer whenever the interval value or session.id changes; keep the same logic for
setting setSession, setStatusMessage, syncFormFromProvider, and error handling
(getErrorMessage) but replace the fixed 2000 with the server-provided value and
fall back to a sensible default if interval_seconds is missing or invalid.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf511e36-d2d2-4859-aa79-1f0feb216dec

📥 Commits

Reviewing files that changed from the base of the PR and between d98fbe7 and bd63f24.

📒 Files selected for processing (12)
  • core/schemas/account.go
  • docs/docs.json
  • docs/providers/supported-providers/codex.mdx
  • docs/providers/supported-providers/overview.mdx
  • framework/configstore/migrations.go
  • framework/modelcatalog/pricing.go
  • transports/config.schema.json
  • ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx
  • ui/app/workspace/providers/fragments/codexAuthControls.tsx
  • ui/app/workspace/providers/views/providerKeyForm.tsx
  • ui/lib/types/config.ts
  • ui/lib/types/schemas.ts
💤 Files with no reviewable changes (1)
  • framework/modelcatalog/pricing.go
✅ Files skipped from review due to trivial changes (1)
  • docs/providers/supported-providers/codex.mdx
🚧 Files skipped from review as they are similar to previous changes (4)
  • core/schemas/account.go
  • framework/configstore/migrations.go
  • ui/app/workspace/providers/views/providerKeyForm.tsx
  • ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx

Comment on lines +102 to +108
const syncFormFromProvider = useCallback(async () => {
const updatedProvider = await getProvider(providerName).unwrap();
const updatedKey = updatedProvider.keys.find((key) => key.id === keyId);
if (updatedKey?.codex_key_config) {
form.setValue("key.codex_key_config", updatedKey.codex_key_config, { shouldDirty: false });
}
}, [form, getProvider, keyId, providerName]);
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 | 🟡 Minor

Potential stale keyId after persistence.

When syncFormFromProvider runs after device auth completes, it searches for the key using the original keyId prop. However, if onEnsurePersisted was called (for new keys), the actual persisted key may have a different ID. The resolvedKeyID from ensureKeyID should be used instead.

Consider storing the resolved key ID in state after ensureKeyID returns, then using that in syncFormFromProvider.

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

In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx` around lines 102
- 108, syncFormFromProvider currently uses the original keyId prop which can be
stale after ensureKeyID/onEnsurePersisted persists a new key; instead, store the
resolved key id returned by ensureKeyID (e.g., resolvedKeyID) in component state
and update it after persistence, then update syncFormFromProvider to look up the
key using that resolvedKeyID (referencing functions getProvider and
syncFormFromProvider) so it always finds the persisted key; ensure the state is
included in the syncFormFromProvider dependency array and set when
ensureKeyID/onEnsurePersisted completes.

Comment on lines +267 to +276
<DialogFooter>
{session?.status === "pending" ? (
<Button type="button" variant="outline" onClick={() => void handleCancel()}>
Cancel authorization
</Button>
) : null}
<Button type="button" variant="outline" onClick={handleCloseDialog}>
Close
</Button>
</DialogFooter>
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.

🛠️ Refactor suggestion | 🟠 Major

Add missing data-testid attributes for interactive buttons.

Per coding guidelines, interactive UI elements should have data-testid attributes following the <entity>-<element>-<qualifier> pattern.

♻️ Proposed fix
           {session?.status === "pending" ? (
-            <Button type="button" variant="outline" onClick={() => void handleCancel()}>
+            <Button type="button" variant="outline" onClick={() => void handleCancel()} data-testid="codex-cancel-auth-btn">
               Cancel authorization
             </Button>
           ) : null}
-          <Button type="button" variant="outline" onClick={handleCloseDialog}>
+          <Button type="button" variant="outline" onClick={handleCloseDialog} data-testid="codex-close-dialog-btn">
             Close
           </Button>

As per coding guidelines: ui/**/*.{tsx,ts}: Add new interactive UI elements with data-testid attributes following the pattern: data-testid="<entity>-<element>-<qualifier>".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<DialogFooter>
{session?.status === "pending" ? (
<Button type="button" variant="outline" onClick={() => void handleCancel()}>
Cancel authorization
</Button>
) : null}
<Button type="button" variant="outline" onClick={handleCloseDialog}>
Close
</Button>
</DialogFooter>
<DialogFooter>
{session?.status === "pending" ? (
<Button type="button" variant="outline" onClick={() => void handleCancel()} data-testid="codex-cancel-auth-btn">
Cancel authorization
</Button>
) : null}
<Button type="button" variant="outline" onClick={handleCloseDialog} data-testid="codex-close-dialog-btn">
Close
</Button>
</DialogFooter>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/providers/fragments/codexAuthControls.tsx` around lines 267
- 276, Add data-testid attributes to the interactive Buttons inside
DialogFooter: for the cancel button rendered when session?.status === "pending"
(the Button that calls handleCancel) add a data-testid like
"codexauth-cancel-button" (or use entity-element-qualifier matching repo
conventions), and for the close button (the Button that calls handleCloseDialog)
add a data-testid like "codexauth-close-button"; update the JSX for those Button
elements in codexAuthControls.tsx (the Button calling handleCancel and the
Button calling handleCloseDialog) to include the corresponding data-testid
attributes following the <entity>-<element>-<qualifier> pattern.

Comment on lines +185 to +215
func (h *CodexAuthHandler) persistTokensToKey(ctx context.Context, keyID string, tokens *providerCodex.TokenResponse, authMethod schemas.CodexAuthMethod) error {
providerConfig, _, err := h.getEditableCodexKey(keyID)
if err != nil {
return err
}
updatedKeys := append([]schemas.Key(nil), providerConfig.Keys...)
for idx := range updatedKeys {
if updatedKeys[idx].ID != keyID {
continue
}
var existingAccountID *schemas.EnvVar
if updatedKeys[idx].CodexKeyConfig != nil {
existingAccountID = updatedKeys[idx].CodexKeyConfig.AccountID
}
refreshToken := schemas.NewEnvVar(tokens.RefreshToken)
accessToken := schemas.NewEnvVar(tokens.AccessToken)
accountIDValue := providerCodex.ExtractAccountID(tokens)
accessTokenExpiresAt := providerCodex.ExpiresAtFromNow(tokens.ExpiresIn)
updatedKeys[idx].CodexKeyConfig = &schemas.CodexKeyConfig{
RefreshToken: *refreshToken,
AccessToken: accessToken,
AccessTokenExpiresAt: &accessTokenExpiresAt,
AuthMethod: authMethod,
}
if accountIDValue != "" {
updatedKeys[idx].CodexKeyConfig.AccountID = schemas.NewEnvVar(accountIDValue)
} else if existingAccountID != nil {
updatedKeys[idx].CodexKeyConfig.AccountID = existingAccountID
}
providerConfig.Keys = updatedKeys
return h.store.UpdateProviderConfig(ctx, schemas.Codex, *providerConfig)
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.

P1 Same TOCTOU race as PersistCodexKeyCredentials

persistTokensToKey reads the full Codex provider config via getEditableCodexKey, modifies one key in the snapshot, and then calls UpdateProviderConfig. UpdateProvider (the DB write underneath) deletes all keys not present in the snapshot and re-inserts them. Any key added between the read on line 186 and the write on line 215 is permanently deleted from the database — the same race flagged in PersistCodexKeyCredentials.

The fix used by PersistCodexKeyCredentials applies here too: build the CodexKeyConfig, call h.store.ConfigStore.PersistCodexKeyConfig(ctx, keyID, codexKeyConfig) for the DB update, then update the in-memory cache with a targeted single-key write instead of a full provider rebuild.

@yaronshanisima
Copy link
Copy Markdown
Author

yaronshanisima commented Apr 15, 2026

Its also worth mentioning that if you guys dont want this thing in your upstream, using projects like https://github.com/EvanZhouDev/openai-oauth as custom provider also works. I tested this project with bifrost custom provider, and it works. Maybe mentioning this as part of the docs might be an alternative is upstream support if not possible.

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.

3 participants