Skip to content

Preserve custom provider lineage during ID migrations#1025

Merged
PeterDaveHello merged 1 commit into
ChatGPTBox-dev:masterfrom
PeterDaveHello:harden-provider-model-routing
Jul 23, 2026
Merged

Preserve custom provider lineage during ID migrations#1025
PeterDaveHello merged 1 commit into
ChatGPTBox-dev:masterfrom
PeterDaveHello:harden-provider-model-routing

Conversation

@PeterDaveHello

@PeterDaveHello PeterDaveHello commented Jul 22, 2026

Copy link
Copy Markdown
Member

Safely migrate custom providers whose IDs become reserved by built-in providers without losing API keys or historical session routing.

Retain legacy provider IDs for deterministic recovery and keep ambiguous or disabled matches fail-closed.

Summary by CodeRabbit

  • Bug Fixes
    • Improved matching and recovery when provider IDs differ by casing, formatting, or legacy IDs.
    • Enhanced disambiguation for API modes and sessions when multiple providers/custom URLs are similar.
    • Prevented disabled providers from being selected via legacy identifiers.
    • Improved provider-secret migration and import handling to avoid missing or misplaced secrets.
  • Data Migration
    • Added one-time tracking for migrations involving newly reserved built-in provider IDs, with safer secret preservation.
    • Preserved legacy provider relationships to support reliable future lookups.
  • Tests
    • Expanded unit coverage for migration, provider matching, and sessionCompat selection edge cases.

@PeterDaveHello
PeterDaveHello requested a review from Copilot July 22, 2026 15:00
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Provider migration now preserves legacy provider IDs, normalizes endpoint matching, handles reserved builtin ID collisions and secrets, and improves runtime provider resolution across API modes, sessions, disabled providers, and imports.

Changes

Provider migration and resolution

Layer / File(s) Summary
Provider normalization and migration state
src/config/index.mjs
Normalizes provider identifiers and endpoint paths, stores legacy provider lineage, and tracks completed builtin migrations.
Provider collision and secret migration
src/config/index.mjs
Resolves canonical and raw ID collisions while remapping, preserving, or deleting provider secrets.
Custom mode provider materialization
src/config/index.mjs
Migrates custom API modes through canonical signatures, carries legacy IDs, promotes keys to providers, and persists migration results.
Runtime provider matching and compatibility
src/popup/sections/api-modes-provider-utils.mjs, src/popup/sections/ApiModes.jsx, src/popup/sections/import-data-cleanup.mjs, src/services/apis/provider-registry.mjs, src/utils/model-name-convert.mjs
Matches current and legacy IDs, preserves builtin secrets during imports, disambiguates providers by URL or mode labels, includes disabled providers where needed, and applies equivalent-ID comparisons.
Migration and resolution validation
tests/unit/config/*, tests/unit/popup/*, tests/unit/services/apis/*, tests/unit/utils/*
Adds coverage for secret migration, reserved-ID collisions, imports, legacy lineage, URL disambiguation, disabled providers, and compatible API mode selection.

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

Sequence Diagram(s)

sequenceDiagram
  participant Session
  participant resolveOpenAICompatibleRequest
  participant ProviderRegistry
  participant CustomApiMode
  Session->>resolveOpenAICompatibleRequest: provide providerId, customUrl, and mode metadata
  resolveOpenAICompatibleRequest->>ProviderRegistry: match current and legacy provider IDs
  ProviderRegistry->>CustomApiMode: filter candidates by stable mode label
  CustomApiMode-->>resolveOpenAICompatibleRequest: return an unambiguous provider
  resolveOpenAICompatibleRequest-->>Session: return providerId, requestUrl, and apiKey
Loading

Possibly related PRs

Suggested labels: Review effort 4/5

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: preserving custom provider lineage through provider ID migrations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate 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.

@qodo-code-review

qodo-code-review Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Preserve custom provider lineage during ID migrations

🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Preserve custom provider IDs across renames/collisions to keep sessions and secrets recoverable.
• Fail-closed on ambiguous or disabled provider matches during legacy session recovery.
• Track one-time migrations for newly reserved built-in provider IDs and rerun on imports.
Diagram

graph TD
  A[("User config storage")] --> B["Config migration"] --> C["Providers + secrets"] --> D["Session provider resolver"] --> E[/"Popup API modes UI"/]
  F["Import cleanup"] --> B
  G["Unit tests"] --> B --> D
  subgraph Legend
    direction LR
    _db[("Storage")] ~~~ _proc["Logic"] ~~~ _ui[/"UI"/]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize provider ID equivalence + normalization utilities
  • ➕ Avoids duplicated normalize/equivalence logic across config/UI/utils modules
  • ➕ Reduces risk of future divergence in edge-case handling
  • ➖ Requires refactoring imports and may create dependency cycles (config ↔ utils ↔ popup)
  • ➖ Bigger PR scope than a targeted migration fix
2. Schema-versioned migration instead of completedBuiltinProviderIdMigrations list
  • ➕ Simpler state: a single schema bump can gate one-time migrations
  • ➕ Less per-provider bookkeeping as new built-ins are added
  • ➖ Harder to selectively rerun only specific collisions (e.g., import reintroduces xai)
  • ➖ May force broader migrations on unrelated upgrades
3. Persist provider UUIDs and reference sessions by UUID instead of ID strings
  • ➕ Eliminates most rename/collision problems permanently
  • ➕ Cleaner model for long-term session routing stability
  • ➖ Requires data model change across storage, UI, and resolver
  • ➖ More invasive migration and higher backward-compatibility risk

Recommendation: The PR’s approach (explicit legacyProviderIds lineage plus conservative resolver rules) is the best fit for a safe, incremental migration: it preserves determinism without a storage model rewrite. If follow-up work is planned, consider centralizing provider ID equivalence logic to reduce duplication, but keep that separate from this risk-sensitive migration change.

Files changed (11) +1634 / -136

Bug fix (5) +629 / -131
index.mjsTrack reserved built-in ID migrations and preserve provider lineage/secrets +390/-63

Track reserved built-in ID migrations and preserve provider lineage/secrets

• Adds one-time migration tracking for newly reserved built-in provider IDs (xai, nvidia-nim, mistral) and persists it in config. Introduces legacyProviderIds support and path normalization for custom providers, plus more conservative secret migration logic using snapshots and collision-aware disambiguation.

src/config/index.mjs

ApiModes.jsxTreat canonical-equivalent provider IDs as unchanged in editor +2/-1

Treat canonical-equivalent provider IDs as unchanged in editor

• Updates provider-change detection to use canonical ID equivalence, preventing unnecessary clearing of provider-derived fields when casing/format changes only.

src/popup/sections/ApiModes.jsx

api-modes-provider-utils.mjsAdd provider ID equivalence + legacy ID matching for sessions and UI labels +102/-35

Add provider ID equivalence + legacy ID matching for sessions and UI labels

• Introduces areProviderIdsEquivalent and uses it for provider reference checks and selection changes. Extends provider matching to include legacyProviderIds, improves recovery behavior for ambiguous matches (URL-based disambiguation), and ensures disabled providers don’t get incorrectly selected except for explicit reference tracking.

src/popup/sections/api-modes-provider-utils.mjs

provider-registry.mjsResolve sessions using legacyProviderIds with safe disambiguation and fail-closed rules +90/-28

Resolve sessions using legacyProviderIds with safe disambiguation and fail-closed rules

• Custom provider normalization now persists legacyProviderIds. Session resolution considers exact IDs, normalized IDs, and legacy IDs as candidates, then disambiguates via URL and (when safe) stable mode labels, while preventing disabled-provider bypass through legacy identifiers.

src/services/apis/provider-registry.mjs

model-name-convert.mjsSessionCompat selection supports canonical/legacy provider IDs and URL disambiguation +45/-4

SessionCompat selection supports canonical/legacy provider IDs and URL disambiguation

• Adds canonical provider ID equivalence checks and legacyProviderIds support when matching selected API modes in session-compat mode. When multiple modes match, uses customUrl to pick a unique match to avoid accidental routing.

src/utils/model-name-convert.mjs

Tests (5) +999 / -5
migrate-user-config.test.mjsAdd extensive migration coverage for reserved IDs, duplicates, and lineage +554/-4

Add extensive migration coverage for reserved IDs, duplicates, and lineage

• Adds tests validating legacyProviderIds persistence, path normalization, idempotency, secret migration behavior under canonical collisions, and newly reserved built-in ID collision handling. Also validates end-to-end routing via resolveOpenAICompatibleRequest for migrated modes/providers.

tests/unit/config/migrate-user-config.test.mjs

api-modes-provider-utils.test.mjsTest legacyProviderIds behavior in popup provider utils +90/-0

Test legacyProviderIds behavior in popup provider utils

• Adds tests ensuring createProviderId avoids legacy IDs, sanitize clears legacyProviderIds for non-custom modes, provider reference checks are canonical-equivalent, and sessions remain linked to renamed providers with URL-based collision handling.

tests/unit/popup/api-modes-provider-utils.test.mjs

import-data-cleanup.test.mjsTest import-time migration marker injection/preservation +26/-0

Test import-time migration marker injection/preservation

• Adds tests confirming provider-state imports without completedBuiltinProviderIdMigrations get a default empty marker list, while existing markers remain untouched.

tests/unit/popup/import-data-cleanup.test.mjs

provider-registry.test.mjsTest resolver disambiguation by URL/label and disabled-provider fail-closed rules +270/-1

Test resolver disambiguation by URL/label and disabled-provider fail-closed rules

• Adds coverage for resolver behavior when exact/legacy/normalized provider IDs collide, verifying URL-based disambiguation and stable-label fallback. Includes regression tests preventing disabled providers from being selected via legacy IDs.

tests/unit/services/apis/provider-registry.test.mjs

model-name-convert.test.mjsTest canonical-equivalent provider selection and URL collision disambiguation +59/-0

Test canonical-equivalent provider selection and URL collision disambiguation

• Adds tests for sessionCompat selection when provider IDs differ only by canonical normalization and for uniquely selecting between colliding modes using customUrl.

tests/unit/utils/model-name-convert.test.mjs

Other (1) +6 / -0
import-data-cleanup.mjsForce rerun of built-in provider ID migrations on provider-state imports +6/-0

Force rerun of built-in provider ID migrations on provider-state imports

• When imported data includes providers or secrets but lacks completedBuiltinProviderIdMigrations, injects an empty list so migrations rerun deterministically during getUserConfig.

src/popup/sections/import-data-cleanup.mjs

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5422198963

ℹ️ 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 thread src/config/index.mjs
Comment on lines +1155 to +1157
const legacyProviderIds = getLegacyProviderIds(
[...(Array.isArray(provider.legacyProviderIds) ? provider.legacyProviderIds : []), originalId],
id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reserve legacy IDs before assigning migrated provider IDs

When two stored providers canonicalize to the same ID (for example, My Proxy and my-proxy), the first remains my-proxy while the second is renamed to my-proxy-2 and receives my-proxy as a legacy ID here. Historical sessions using my-proxy without a distinguishing URL or unique mode label then match both providers in resolveOpenAICompatibleRequest and fail closed, so those conversations can no longer send requests. Reserve already-declared/derived legacy IDs while assigning IDs (or avoid retaining an alias already owned by another provider) so lineage does not create an ambiguous current-ID collision.

Useful? React with 👍 / 👎.

Comment thread src/popup/sections/import-data-cleanup.mjs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR strengthens backward-compatible resolution for custom OpenAI-compatible providers when provider IDs change or become newly reserved by built-in providers, preserving API keys and maintaining deterministic recovery for historical sessions while failing closed in ambiguous/disabled cases.

Changes:

  • Introduces provider ID lineage (legacyProviderIds) and canonical-equivalence matching to keep sessions and UI selections linked across ID migrations.
  • Adds URL- and label-based disambiguation to resolve collisions safely, including explicit “fail closed” behavior when matches are ambiguous or disabled.
  • Persists and replays one-time migration markers (completedBuiltinProviderIdMigrations) during config migration and import normalization.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/unit/utils/model-name-convert.test.mjs Adds coverage for provider-ID equivalence and URL-based disambiguation in API mode selection helpers.
tests/unit/services/apis/provider-registry.test.mjs Adds coverage for collision disambiguation (URL/label) and disabled-provider fail-closed behavior in request resolution.
tests/unit/popup/import-data-cleanup.test.mjs Verifies import normalization behavior for builtin provider ID migration markers.
tests/unit/popup/api-modes-provider-utils.test.mjs Adds coverage for legacy provider IDs affecting provider ID generation, referencing, and conversation labeling.
tests/unit/config/migrate-user-config.test.mjs Adds extensive coverage for provider ID collision migrations, secret preservation, and lineage persistence.
src/utils/model-name-convert.mjs Implements canonical provider ID equivalence and URL disambiguation for session-compatible API mode selection.
src/services/apis/provider-registry.mjs Enhances custom provider resolution to handle normalized/legacy ID collisions with URL/label-based disambiguation and fail-closed rules.
src/popup/sections/import-data-cleanup.mjs Ensures imported provider state triggers builtin provider ID migration replay when markers are missing.
src/popup/sections/ApiModes.jsx Uses canonical provider ID equivalence when deciding whether to clear provider-derived fields on edit.
src/popup/sections/api-modes-provider-utils.mjs Adds provider ID equivalence utilities, expands collision handling using URL, and avoids reusing legacy provider IDs.
src/config/index.mjs Extends user-config migration to preserve lineage, normalize paths, migrate colliding IDs/secrets safely, and persist migration completion markers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@qodo-code-review

qodo-code-review Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 5 rules

Grey Divider


Action required

1. Import overwrites builtin secret 🐞 Bug ≡ Correctness ⭐ New
Description
preserveExistingBuiltinSecrets() only restores stored builtin secrets from
providerSecrets[providerId] and ignores the legacy-key values it already fetches, so a provider-only
import can overwrite a valid legacy-only builtin key with an empty/stale imported legacy field. This
can silently drop API credentials for newly reserved builtin providers after import.
Code

src/popup/sections/import-data-cleanup.mjs[R45-77]

+  const legacyKeys = relevantProviderIds.map(
+    (providerId) => LEGACY_API_KEY_FIELD_BY_PROVIDER_ID[providerId],
+  )
+  const stored = await storageArea.get([
+    'completedBuiltinProviderIdMigrations',
+    'customOpenAIProviders',
+    'providerSecrets',
+    ...legacyKeys,
+  ])
+  const completedMigrations = new Set(
+    Array.isArray(stored.completedBuiltinProviderIdMigrations)
+      ? stored.completedBuiltinProviderIdMigrations.map(normalizeProviderId).filter(Boolean)
+      : [],
+  )
+  const existingProviderIds = new Set(
+    (Array.isArray(stored.customOpenAIProviders) ? stored.customOpenAIProviders : [])
+      .map((provider) => normalizeProviderId(provider?.id))
+      .filter(Boolean),
+  )
+  const existingProviderSecrets =
+    stored.providerSecrets && typeof stored.providerSecrets === 'object'
+      ? stored.providerSecrets
+      : {}
+
+  for (const providerId of relevantProviderIds) {
+    const legacyKey = LEGACY_API_KEY_FIELD_BY_PROVIDER_ID[providerId]
+    if (
+      completedMigrations.has(providerId) &&
+      !existingProviderIds.has(providerId) &&
+      Object.hasOwn(existingProviderSecrets, providerId)
+    ) {
+      normalizedData[legacyKey] = existingProviderSecrets[providerId]
+    }
Evidence
The import helper fetches legacy key fields from storage but never reads them, and only restores
secrets from stored.providerSecrets[providerId]. Because builtin secrets are still supported via
legacy key fields (fallback lookup), overwriting those fields during import can drop credentials
when providerSecrets doesn’t contain the entry.

src/popup/sections/import-data-cleanup.mjs[45-78]
src/popup/sections/import-data-cleanup.mjs[143-152]
src/services/apis/provider-registry.mjs[438-446]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`preserveExistingBuiltinSecrets()` fetches legacy builtin key fields (e.g., `xaiApiKey`) from storage but never uses their stored values. If the existing correct secret is only present in the legacy key field (or `providerSecrets[providerId]` is missing/stale) and the import payload contains that legacy key as empty/stale, `importDataIntoStorage()` will overwrite the correct value.

### Issue Context
- This helper runs specifically when importing `customOpenAIProviders` **without** importing `providerSecrets`, which is exactly when protecting existing secrets matters most.
- The code already requests `...legacyKeys` from storage, so the intent appears to be to preserve those values, but the implementation only consults `stored.providerSecrets`.

### Fix Focus Areas
- src/popup/sections/import-data-cleanup.mjs[27-79]

### Suggested fix
- When iterating `relevantProviderIds`, compute the existing builtin secret with a precedence such as:
 1) `stored.providerSecrets[providerId]` if present and non-empty, else
 2) `stored[legacyKey]` (trimmed) if present and non-empty.
- If that existing secret is non-empty and the import would otherwise set `normalizedData[legacyKey]` to empty/stale (at minimum: empty/missing), write `normalizedData[legacyKey] = existingSecret`.
- Add/extend a unit test for the case where:
 - `completedBuiltinProviderIdMigrations` contains the providerId,
 - `stored.providerSecrets` is missing the providerId entry,
 - `stored[legacyKey]` has the real secret,
 - import payload would set `legacyKey` to `''` (or includes it as empty),
 - and verify the stored secret is preserved after import + subsequent `getUserConfig()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Wrong provider picked by label ✓ Resolved 🐞 Bug ≡ Correctness
Description
In resolveOpenAICompatibleRequest(), the label-based disambiguation normalizes the configured
providerId but then searches customProviders using raw id equality, so providers whose stored ids
are not already normalized (e.g., 'Foo') won’t match and may cause selecting the wrong provider
(e.g., 'foo') or failing recovery. This can misroute a recovered session to the wrong endpoint/API
key in canonical-collision scenarios.
Code

src/services/apis/provider-registry.mjs[R782-783]

      const matchedConfiguredProviderId = normalizeProviderId(matchedConfiguredApiMode?.providerId)
      if (matchedConfiguredProviderId) {
Evidence
Provider ids are not canonicalized during provider normalization (trim-only), but the recovery
branch canonicalizes the configured id and then compares using raw equality, creating a mismatch
that can select the wrong provider or none.

src/services/apis/provider-registry.mjs[334-371]
src/services/apis/provider-registry.mjs[761-796]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The label-based session recovery branch normalizes `matchedConfiguredApiMode.providerId` but then compares it to `item.id` using raw equality (`item.id === matchedConfiguredProviderId`). Because `normalizeCustomProvider()` preserves provider ids as-trimmed (not canonicalized), this can fail to match the intended provider (or match a different one) when ids differ only by case/format.

### Issue Context
This code runs in the disambiguation path meant to safely recover routing when provider IDs collide canonically. A raw-vs-normalized mismatch undermines that disambiguation.

### Fix Focus Areas
- src/services/apis/provider-registry.mjs[777-796]

### Implementation guidance
- Use the configured provider id as-is for exact match first (trimmed), then fall back to canonical comparison.
- Prefer resolving within `providerIdCandidates` when available, and fail-closed if canonical comparison matches multiple enabled candidates.
- Example shape:
 - `const configuredIdRaw = toStringOrEmpty(matchedConfiguredApiMode?.providerId).trim()`
 - Try `providerIdCandidates.find(p => p.enabled !== false && p.id === configuredIdRaw)`
 - Else canonical match: filter candidates by `normalizeProviderId(p.id) === normalizeProviderId(configuredIdRaw)` and only accept if exactly one enabled match.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Long test() description line 📘 Rule violation ⚙ Maintainability ⭐ New
Description
A newly added test() declaration line exceeds the 100-character maximum, reducing readability and
increasing diff noise. This violates the repository line-length compliance requirement.
Code

tests/unit/services/apis/provider-registry.test.mjs[134]

+test('resolveOpenAICompatibleRequest disambiguates legacy and normalized ID matches by URL', () => {
Evidence
PR Compliance ID 2261946 requires all non-comment source lines to be 100 characters or fewer. The
added test(...) declaration lines shown in the cited locations are single physical lines with very
long descriptions, exceeding that limit.

Rule 2261946: Limit source line length to 100 characters
tests/unit/services/apis/provider-registry.test.mjs[134-134]
tests/unit/services/apis/provider-registry.test.mjs[189-189]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
One or more newly added lines exceed the 100 character line-length limit.

## Issue Context
The compliance checklist requires source lines to be <= 100 characters.

## Fix Focus Areas
- tests/unit/services/apis/provider-registry.test.mjs[134-134]
- tests/unit/services/apis/provider-registry.test.mjs[189-189]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit d76707a

Results up to commit 5422198 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Wrong provider picked by label ✓ Resolved 🐞 Bug ≡ Correctness
Description
In resolveOpenAICompatibleRequest(), the label-based disambiguation normalizes the configured
providerId but then searches customProviders using raw id equality, so providers whose stored ids
are not already normalized (e.g., 'Foo') won’t match and may cause selecting the wrong provider
(e.g., 'foo') or failing recovery. This can misroute a recovered session to the wrong endpoint/API
key in canonical-collision scenarios.
Code

src/services/apis/provider-registry.mjs[R782-783]

      const matchedConfiguredProviderId = normalizeProviderId(matchedConfiguredApiMode?.providerId)
      if (matchedConfiguredProviderId) {
Evidence
Provider ids are not canonicalized during provider normalization (trim-only), but the recovery
branch canonicalizes the configured id and then compares using raw equality, creating a mismatch
that can select the wrong provider or none.

src/services/apis/provider-registry.mjs[334-371]
src/services/apis/provider-registry.mjs[761-796]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The label-based session recovery branch normalizes `matchedConfiguredApiMode.providerId` but then compares it to `item.id` using raw equality (`item.id === matchedConfiguredProviderId`). Because `normalizeCustomProvider()` preserves provider ids as-trimmed (not canonicalized), this can fail to match the intended provider (or match a different one) when ids differ only by case/format.

### Issue Context
This code runs in the disambiguation path meant to safely recover routing when provider IDs collide canonically. A raw-vs-normalized mismatch undermines that disambiguation.

### Fix Focus Areas
- src/services/apis/provider-registry.mjs[777-796]

### Implementation guidance
- Use the configured provider id as-is for exact match first (trimmed), then fall back to canonical comparison.
- Prefer resolving within `providerIdCandidates` when available, and fail-closed if canonical comparison matches multiple enabled candidates.
- Example shape:
 - `const configuredIdRaw = toStringOrEmpty(matchedConfiguredApiMode?.providerId).trim()`
 - Try `providerIdCandidates.find(p => p.enabled !== false && p.id === configuredIdRaw)`
 - Else canonical match: filter candidates by `normalizeProviderId(p.id) === normalizeProviderId(configuredIdRaw)` and only accept if exactly one enabled match.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment thread src/services/apis/provider-registry.mjs Outdated
Safely migrate custom providers whose IDs become reserved by built-in
providers without losing API keys or historical session routing.

Retain legacy provider IDs for deterministic recovery. Preserve completed
migration state across partial imports, and keep existing built-in secrets
from being assigned to newly imported custom providers.

Use exact raw provider IDs when canonical siblings collide so label and
secret recovery stays isolated. Keep ambiguous or disabled matches
fail-closed.
@PeterDaveHello
PeterDaveHello force-pushed the harden-provider-model-routing branch from 5422198 to d76707a Compare July 22, 2026 21:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/services/apis/provider-registry.mjs (1)

392-401: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated canonical-collision check into a shared helper. The getAllOpenAIProviders(config).filter(p => normalizeProviderId(p.id) === normalizedProviderId).length > 1 computation is repeated verbatim in three places; since this drives secret routing under reserved-ID collisions, divergence between copies would silently misroute secrets. A single hasCanonicalProviderIdCollision(config, normalizedProviderId) helper keeps them in lock-step.

  • src/services/apis/provider-registry.mjs#L392-L401: have resolveSecretProviderId call the new helper.
  • src/services/apis/provider-registry.mjs#L213-L216: replace the inline filter in getConfiguredCustomApiModesForProvider with the helper.
  • src/services/apis/provider-registry.mjs#L462-L465: replace the inline filter in getProviderSecret with the helper (the boolean is still needed locally for canUseApiModeApiKey).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/apis/provider-registry.mjs` around lines 392 - 401, Extract the
repeated canonical-collision computation into a shared
hasCanonicalProviderIdCollision(config, normalizedProviderId) helper. Update
resolveSecretProviderId, getConfiguredCustomApiModesForProvider, and
getProviderSecret to call it, preserving the local boolean in getProviderSecret
for canUseApiModeApiKey. Apply this in src/services/apis/provider-registry.mjs
at lines 392-401, 213-216, and 462-465 respectively.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/services/apis/provider-registry.mjs`:
- Around line 392-401: Extract the repeated canonical-collision computation into
a shared hasCanonicalProviderIdCollision(config, normalizedProviderId) helper.
Update resolveSecretProviderId, getConfiguredCustomApiModesForProvider, and
getProviderSecret to call it, preserving the local boolean in getProviderSecret
for canUseApiModeApiKey. Apply this in src/services/apis/provider-registry.mjs
at lines 392-401, 213-216, and 462-465 respectively.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a038b7fa-2038-4335-be71-fd87731eb1a6

📥 Commits

Reviewing files that changed from the base of the PR and between 5422198 and d76707a.

📒 Files selected for processing (11)
  • src/config/index.mjs
  • src/popup/sections/ApiModes.jsx
  • src/popup/sections/api-modes-provider-utils.mjs
  • src/popup/sections/import-data-cleanup.mjs
  • src/services/apis/provider-registry.mjs
  • src/utils/model-name-convert.mjs
  • tests/unit/config/migrate-user-config.test.mjs
  • tests/unit/popup/api-modes-provider-utils.test.mjs
  • tests/unit/popup/import-data-cleanup.test.mjs
  • tests/unit/services/apis/provider-registry.test.mjs
  • tests/unit/utils/model-name-convert.test.mjs
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/popup/sections/ApiModes.jsx
  • tests/unit/utils/model-name-convert.test.mjs
  • tests/unit/services/apis/provider-registry.test.mjs
  • src/utils/model-name-convert.mjs
  • tests/unit/popup/api-modes-provider-utils.test.mjs
  • tests/unit/config/migrate-user-config.test.mjs
  • src/popup/sections/api-modes-provider-utils.mjs
  • src/config/index.mjs

assert.equal(resolved.apiKey, 'proxy-key')
})

test('resolveOpenAICompatibleRequest disambiguates legacy and normalized ID matches by URL', () => {

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.

Informational

1. Long test() description line 📘 Rule violation ⚙ Maintainability

A newly added test() declaration line exceeds the 100-character maximum, reducing readability and
increasing diff noise. This violates the repository line-length compliance requirement.
Agent Prompt
## Issue description
One or more newly added lines exceed the 100 character line-length limit.

## Issue Context
The compliance checklist requires source lines to be <= 100 characters.

## Fix Focus Areas
- tests/unit/services/apis/provider-registry.test.mjs[134-134]
- tests/unit/services/apis/provider-registry.test.mjs[189-189]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +45 to +77
const legacyKeys = relevantProviderIds.map(
(providerId) => LEGACY_API_KEY_FIELD_BY_PROVIDER_ID[providerId],
)
const stored = await storageArea.get([
'completedBuiltinProviderIdMigrations',
'customOpenAIProviders',
'providerSecrets',
...legacyKeys,
])
const completedMigrations = new Set(
Array.isArray(stored.completedBuiltinProviderIdMigrations)
? stored.completedBuiltinProviderIdMigrations.map(normalizeProviderId).filter(Boolean)
: [],
)
const existingProviderIds = new Set(
(Array.isArray(stored.customOpenAIProviders) ? stored.customOpenAIProviders : [])
.map((provider) => normalizeProviderId(provider?.id))
.filter(Boolean),
)
const existingProviderSecrets =
stored.providerSecrets && typeof stored.providerSecrets === 'object'
? stored.providerSecrets
: {}

for (const providerId of relevantProviderIds) {
const legacyKey = LEGACY_API_KEY_FIELD_BY_PROVIDER_ID[providerId]
if (
completedMigrations.has(providerId) &&
!existingProviderIds.has(providerId) &&
Object.hasOwn(existingProviderSecrets, providerId)
) {
normalizedData[legacyKey] = existingProviderSecrets[providerId]
}

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.

Action required

2. Import overwrites builtin secret 🐞 Bug ≡ Correctness

preserveExistingBuiltinSecrets() only restores stored builtin secrets from
providerSecrets[providerId] and ignores the legacy-key values it already fetches, so a provider-only
import can overwrite a valid legacy-only builtin key with an empty/stale imported legacy field. This
can silently drop API credentials for newly reserved builtin providers after import.
Agent Prompt
### Issue description
`preserveExistingBuiltinSecrets()` fetches legacy builtin key fields (e.g., `xaiApiKey`) from storage but never uses their stored values. If the existing correct secret is only present in the legacy key field (or `providerSecrets[providerId]` is missing/stale) and the import payload contains that legacy key as empty/stale, `importDataIntoStorage()` will overwrite the correct value.

### Issue Context
- This helper runs specifically when importing `customOpenAIProviders` **without** importing `providerSecrets`, which is exactly when protecting existing secrets matters most.
- The code already requests `...legacyKeys` from storage, so the intent appears to be to preserve those values, but the implementation only consults `stored.providerSecrets`.

### Fix Focus Areas
- src/popup/sections/import-data-cleanup.mjs[27-79]

### Suggested fix
- When iterating `relevantProviderIds`, compute the existing builtin secret with a precedence such as:
  1) `stored.providerSecrets[providerId]` if present and non-empty, else
  2) `stored[legacyKey]` (trimmed) if present and non-empty.
- If that existing secret is non-empty and the import would otherwise set `normalizedData[legacyKey]` to empty/stale (at minimum: empty/missing), write `normalizedData[legacyKey] = existingSecret`.
- Add/extend a unit test for the case where:
  - `completedBuiltinProviderIdMigrations` contains the providerId,
  - `stored.providerSecrets` is missing the providerId entry,
  - `stored[legacyKey]` has the real secret,
  - import payload would set `legacyKey` to `''` (or includes it as empty),
  - and verify the stored secret is preserved after import + subsequent `getUserConfig()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment on lines 196 to 202
const signature = JSON.stringify({
groupName: toStringOrEmpty(apiMode.groupName).trim(),
itemName: toStringOrEmpty(apiMode.itemName).trim(),
isCustom: Boolean(apiMode.isCustom),
customName: toStringOrEmpty(apiMode.customName).trim(),
providerId: normalizeProviderId(apiMode.providerId),
providerId: toStringOrEmpty(apiMode.providerId).trim(),
})
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit d76707a

@PeterDaveHello
PeterDaveHello merged commit a930f2d into ChatGPTBox-dev:master Jul 23, 2026
4 checks passed
@PeterDaveHello
PeterDaveHello deleted the harden-provider-model-routing branch July 23, 2026 17:10
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.

2 participants