Skip to content

fix(codex): harden tier-aware model catalog refresh#2024

Open
excelwang wants to merge 5 commits intorouter-for-me:devfrom
excelwang:feat/codex-model-catalog
Open

fix(codex): harden tier-aware model catalog refresh#2024
excelwang wants to merge 5 commits intorouter-for-me:devfrom
excelwang:feat/codex-model-catalog

Conversation

@excelwang
Copy link

Summary

This is a follow-up hardening PR for #2013.

It keeps the network-backed Codex model catalog direction, but fixes the parts that make model visibility and routing unsafe or inconsistent.

What this changes

  • adds a dedicated Codex tier catalog with embedded fallback data
  • refreshes the Codex catalog asynchronously instead of blocking service startup
  • honors configured outbound proxy settings during catalog refresh
  • restores plan_type from Codex OAuth metadata / id_token and persists it back to auth files
  • applies tier-aware model registration only when plan_type is actually known
  • falls back to the full Codex union model set when plan_type is unknown, preserving manual model routing compatibility
  • makes static codex model lookup search the full Codex union instead of a single implicit tier

Why this is needed

After #2013, the model catalog direction is correct, but there are still a few sharp edges:

  1. Codex OAuth auths need tier-aware registration, otherwise low-tier accounts can expose higher-tier models.
  2. Codex API keys do not have chatgpt_plan_type, so applying OAuth tier logic to them breaks existing manual model selection in config.
  3. Startup should not block on external catalog fetches.
  4. Static Codex model lookup should be closed over all Codex tiers, not just one bucket.

This PR fixes those issues without changing the overall network-catalog design.

Behavior after this PR

  • explicit codex.models[] config still has highest priority
  • known Codex OAuth plan_type => tier-aware registration
  • unknown plan_type => full Codex union fallback
  • Codex API keys without explicit models[] keep manual model-name compatibility

Tests

Added coverage for:

  • recovering plan_type from Codex id_token
  • tier-aware Codex model registration for free/team/plus
  • unknown-plan fallback to union
  • API-key fallback to union when no explicit models[] are configured
  • explicit models[] overriding union fallback

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41cf3135ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +149 to +153
for _, key := range []string{"plan_type", "chatgpt_plan_type"} {
if raw, ok := metadata[key].(string); ok {
if plan := NormalizeCodexPlanType(raw); plan != "" {
if current, _ := metadata["plan_type"].(string); NormalizeCodexPlanType(current) != plan {
metadata["plan_type"] = plan

Choose a reason for hiding this comment

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

P1 Badge Recompute plan_type from refreshed id_token

EnsureCodexPlanTypeMetadata returns immediately when plan_type/chatgpt_plan_type is already present, so it never reconciles against a newer JWT payload. This becomes incorrect after token refreshes because CodexExecutor.Refresh updates metadata.id_token but does not refresh metadata.plan_type; if a user’s subscription changes (especially downgrade), the stale stored plan can continue to register higher-tier Codex models until the auth file is manually reset. Please reconcile plan_type with the current id_token before returning.

Useful? React with 👍 / 👎.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly hardens the Codex model catalog refresh and registration process. It introduces a dedicated, asynchronously updated catalog with embedded fallback data, ensuring model availability and preventing startup blocks. A core focus is on implementing tier-aware model visibility based on user plan types, which is crucial for security and correct model routing. The changes also enhance robustness by providing intelligent fallbacks for unknown plan types and API keys, and by respecting proxy configurations during network operations.

Highlights

  • Dedicated Codex Tier Catalog: Introduced a new codex_catalog.go file to manage Codex models, including embedded fallback data and asynchronous refreshing from remote sources, ensuring the service does not block on startup.
  • Tier-Aware Model Registration: Implemented logic to resolve and apply plan_type from Codex OAuth metadata (e.g., JWT id_token) to enable tier-aware model visibility, preventing lower-tier accounts from accessing higher-tier models.
  • Robust Fallback Mechanisms: Ensured that when plan_type is unknown or for API keys without explicit model configurations, the system falls back to a union of all Codex models, preserving compatibility with manual model routing.
  • Proxy Support for Catalog Refresh: Configured the Codex model catalog refresh mechanism to honor outbound proxy settings, improving network compatibility and reliability.
  • Improved Model Lookup: Updated static Codex model lookup to search across the full union of all Codex tiers, rather than a single implicit tier, enhancing model discovery.
  • Plan Type Persistence: Added functionality to persist the extracted plan_type back into authentication files for Codex OAuth credentials, ensuring consistency across sessions.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • internal/registry/codex_catalog.go
    • Added new file to define Codex model catalog structure and management functions.
    • Implemented asynchronous catalog refresh with embedded fallback data.
    • Included functions to extract and normalize Codex plan types from JWTs and metadata.
    • Provided methods to retrieve tier-specific model lists (Free, Team, Plus, Pro) and a union of all models.
    • Added HTTP client creation with proxy support for fetching model data.
  • internal/registry/codex_catalog_test.go
    • Added new file containing unit tests for Codex plan type extraction from JWTs.
    • Included tests for safe fallback behavior when an unknown plan type is provided.
    • Added tests to verify the union of static model definitions for the 'codex' channel.
  • internal/registry/model_definitions.go
    • Modified GetStaticModelDefinitionsByChannel to return GetCodexModelsUnion() for 'codex' channel.
    • Updated LookupStaticModelInfo to include GetCodexModelsUnion() in its search scope.
  • internal/registry/models/codex_models.json
    • Added new file containing embedded JSON data for Codex models, categorized by free, team, plus, and pro tiers.
  • internal/watcher/synthesizer/file.go
    • Imported the registry package.
    • Added logic to synthesizeFileAuths to ensure Codex plan_type metadata is extracted and set from authentication files.
  • sdk/auth/codex_device.go
    • Modified buildAuthRecord to include the planType in the authentication metadata if available.
  • sdk/auth/filestore.go
    • Imported the registry package.
    • Added logic to readAuthFile to extract and persist plan_type from Codex ID tokens to the auth file.
    • Updated readAuthFile to resolve and set the plan_type attribute on the authentication object for Codex providers.
  • sdk/auth/filestore_codex_plan_test.go
    • Added new file with a test case to verify that readAuthFile correctly recovers and persists the plan_type from a Codex ID token.
  • sdk/cliproxy/service.go
    • Added rebindCodexModels function to re-register Codex models for active authentications.
    • Integrated StartCodexModelsUpdater into the Run method to asynchronously refresh Codex models and trigger re-binding.
    • Refactored registerModelsForAuth for 'codex' provider to implement tier-aware model registration based on plan_type or fall back to the union of models.
  • sdk/cliproxy/service_codex_models_test.go
    • Added new file containing tests for tier-aware model registration for Codex free, team, and plus plans.
    • Included tests for fallback behavior when the Codex plan is unknown or for API keys without explicit model configurations.
    • Added a test to confirm explicit API key models override the union fallback.
Activity
  • No human activity has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request significantly hardens the Codex model catalog handling by introducing tier-aware models, asynchronous catalog refreshing, and persisting the user's plan type. However, the implementation introduces several security and reliability weaknesses. The file persistence logic in the filestore is not atomic, which could lead to credential loss during updates. Additionally, the tier-aware restriction can be bypassed due to the proxy falling back to granting access to all models when the plan type is unknown, and its reliance on unverified JWT data. There are also opportunities to improve error handling and logging for better debuggability and robustness.

Comment on lines +204 to +207
if file, errOpen := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0o600); errOpen == nil {
_, _ = file.Write(raw)
_ = file.Close()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The current logic for updating the Codex plan type in readAuthFile overwrites the authentication file non-atomically using os.O_TRUNC. This poses a significant reliability risk, as an interruption during the write operation could corrupt the file and lead to permanent credential loss. To ensure data integrity and atomicity, it is strongly recommended to write the updated metadata to a temporary file and then use os.Rename to replace the original file. Additionally, the current implementation silently ignores several potential errors during file persistence, including JSON marshaling and file I/O, which could prevent plan_type from being saved and hinder debugging. Robust error handling and logging for these operations would improve the system's resilience and debuggability.

Comment on lines +860 to +870
codexPlanType := registry.ResolveCodexPlanType(a.Attributes, a.Metadata)
if a.Attributes == nil {
a.Attributes = make(map[string]string)
}
if codexPlanType != "" {
a.Attributes["plan_type"] = codexPlanType
models = registry.GetCodexModelsForPlan(codexPlanType)
} else {
delete(a.Attributes, "plan_type")
models = registry.GetCodexModelsUnion()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The Codex model registration logic contains a fallback mechanism that bypasses tier-based restrictions. If the plan_type is unrecognized or cannot be determined (e.g., due to a missing or invalid id_token), the proxy defaults to registry.GetCodexModelsUnion(), which grants access to all models across all tiers. This behavior defeats the intended security hardening for tier-aware registration. Additionally, the plan_type is extracted from a JWT that is decoded without signature verification (in internal/registry/codex_catalog.go), making it trivial to forge. It is recommended to use a safe default (e.g., the 'free' model set) when the plan type is unknown and to verify JWT signatures if they are used for access control decisions.

Comment on lines +171 to +173
if err != nil {
return "", false
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The error from extractCodexPlanTypeFromJWT is silently ignored. This can make it difficult to debug issues with JWT parsing, such as an invalid format or claims structure. It would be beneficial to log this error at a debug level for better observability.

if err != nil {
	log.Debugf("failed to extract codex plan type from JWT: %v", err)
	return "", false
}

Copy link
Collaborator

@luispater luispater left a comment

Choose a reason for hiding this comment

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

Summary

Thanks for the hardening follow-up. The tier-aware registration and fallback direction looks right, but I found one blocking issue and one compatibility issue before merge.

Key findings

  • Blocking: startup is still synchronously blocked by registry.StartModelsUpdater(...) in cmd/server/main.go, so the new async Codex refresh does not actually remove the existing network wait on process startup.
  • Non-blocking: plan aliases like business, enterprise, and edu are normalized to team for routing, but device login still persists the raw JWT plan into the credential filename and metadata, which can create inconsistent filenames and skip the team-account hash naming path.

Test plan

  • go build -o test-output ./cmd/server
  • go test ./...

@xkonjin
Copy link

xkonjin commented Mar 10, 2026

Code Review

This PR introduces tier-aware model catalog refresh with embedded fallback. The architecture is sound.

Strengths

  • Good use of embedded models.json as fallback when network fetch fails
  • Proper timeout handling (15s) for external fetches
  • Clone pattern on getters prevents external mutation
  • Multiple URL sources for redundancy

Potential Issues

1. Race Condition in Initial Load

The init() function parses embedded catalog, but StartCodexModelsUpdater runs asynchronously. There is a window where:

  • Embedded catalog loads successfully
  • RefreshCodexModelsCatalog starts and fails
  • onUpdated callback is skipped (due to error return)

The consumer gets the embedded catalog but never receives the onUpdated signal. Consider:

func StartCodexModelsUpdater(ctx context.Context, cfg *sdkconfig.SDKConfig, onUpdated func()) {
    codexUpdaterOnce.Do(func() {
        go func() {
            // Always call onUpdated, even if refresh fails
            err := refreshCodexModelsCatalog(ctx, cfg)
            if err != nil {
                log.Warnf("codex models refresh failed, using embedded catalog: %v", err)
            }
            if onUpdated != nil {
                onUpdated()
            }
        }()
    })
}

2. No Retry Logic

If the fetch fails, it is never retried. Consider adding exponential backoff:

func refreshCodexModelsCatalog(ctx context.Context, cfg *sdkconfig.SDKConfig) error {
    const maxRetries = 3
    const baseDelay = 1 * time.Second
    
    for attempt := 0; attempt < maxRetries; attempt++ {
        if err := tryFetchCodexModels(ctx, cfg); err == nil {
            return nil
        } else if attempt < maxRetries-1 {
            time.Sleep(baseDelay * time.Duration(1<<uint(attempt)))
        }
    }
    return fmt.Errorf("failed after %d retries", maxRetries)
}

3. No Health Check

Consumers cannot tell if the catalog is using embedded vs remote data. Consider adding a status endpoint or health check that reports:

  • Last successful refresh time
  • Whether using embedded or remote data
  • Number of models per tier

4. Memory Usage

Clone on every getter creates unnecessary allocations. Consider caching clones or returning immutable data structures.

Recommendations

  • Always call onUpdated even if refresh fails, so consumers can react
  • Add retry logic with exponential backoff
  • Consider adding a health/status check for catalog state
  • Evaluate if cloning on every getter is necessary (could use immutable structs)

Good architecture overall. Just needs some operational hardening.

@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@excelwang
Copy link
Author

Addressed the two blocking points from the latest review.

What changed

  • Rebased the branch onto current dev so the follow-up now applies to the code path introduced by Fetch model catalog from network #2013.
  • Removed the separate codex-only catalog store/updater and now reuse the shared models.json catalog already introduced on dev.
  • Moved startup model refresh off the synchronous cmd/server/main.go path. registry.StartModelsUpdater(...) now runs once in the background and the service triggers a model rebind callback after the startup attempt finishes, so process startup is no longer blocked on the network fetch.
  • Normalized Codex plan aliases like business, enterprise, edu, and education to team before persistence. This now applies to filename generation and to persisted plan_type metadata / attributes for device login and the management login flow.

Tests

  • go build -o /tmp/cliproxyapi-pr2024-test ./cmd/server
  • go test ./... -count=1

Additional coverage added

  • startup updater returns immediately and still invokes the callback after the startup attempt
  • alias plan types normalize to team in metadata persistence
  • device login record generation persists normalized team and uses the team hashed filename path

Please take another look when convenient.

@xkonjin
Copy link

xkonjin commented Mar 10, 2026

Code Review:

Overview:
Good hardening of the Codex model catalog refresh logic with proper plan type normalization. The changes improve reliability and maintainability.

Strengths:

  • Comprehensive test coverage added across multiple files
  • Proper normalization of plan type aliases (business/enterprise/edu → team)
  • Background startup refresh pattern is correct
  • JWT parsing and metadata extraction is robust

Issues:

  1. Missing error handling in filestore.go
    The code silently ignores write errors when updating auth files. Could lead to stale auth state. Should log errors or propagate them.

  2. Potential race condition in model_updater
    If StartModelsUpdater is called multiple times rapidly before the goroutine starts, only one callback will execute. Consider documenting this limitation or using a mutex-protected counter.

  3. Missing nil check in EnsureCodexPlanTypeMetadata
    If metadata is nil, the code will panic. Add nil check at function start.

Recommendations:

  • Add error logging for auth file write failures
  • Document the single-callback limitation in StartModelsUpdater
  • Add nil guard in EnsureCodexPlanTypeMetadata
  • Consider adding integration test for the full auth refresh flow

Test Coverage:
Good coverage of normalization logic. Consider adding a test that verifies the background refresh does not block service startup.

@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@excelwang
Copy link
Author

Follow-up on xkonjin's review:

I picked up the two actionable points from that comment set.

Updated

  • sdk/auth/filestore.go: the codex plan normalization write-back on read no longer silently ignores persistence failures. It now logs explicit warnings when the normalized metadata cannot be marshaled or written back.
  • internal/registry/model_updater.go: StartModelsUpdater(...) no longer has an implicit first-callback-only behavior. Multiple callers can now register callbacks for the same one-time startup refresh attempt, and each callback fires after that attempt completes. I also added coverage for callbacks registered after the refresh has already started.

On the nil-check point: EnsureCodexPlanTypeMetadata(...) already had a nil guard before this follow-up, so I did not add a redundant change there.

Validation

  • go test ./internal/registry ./sdk/auth -count=1
  • go test ./... -count=1

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