Skip to content

feat(kha-267): finalize multi-provider tabbed UI and state refactor#40

Open
KHAEntertainment wants to merge 6 commits intomainfrom
feat/kha-267-mixed-providers
Open

feat(kha-267): finalize multi-provider tabbed UI and state refactor#40
KHAEntertainment wants to merge 6 commits intomainfrom
feat/kha-267-mixed-providers

Conversation

@KHAEntertainment
Copy link
Copy Markdown
Owner

@KHAEntertainment KHAEntertainment commented Apr 13, 2026

PR Description: Multi-Provider UI Tabbed Refactor (KHA-267)

Overview

This PR completes the transition from the legacy "Mix Providers" dropdown system to a modern, integrated tabbed interface. The refactor simplifies the configuration flow, reduces UI clutter, and ensures robust state persistence for multi-provider setups.

Key Changes

1. Integrated Tabbed UI

  • Unified Header: The primary "Provider" heading now functions as the first tab.
  • Dynamic Spawning: Secondary provider tabs ("Provider 2", "Provider 3") are spawned inline via a [+] button, replacing the complex manual toggle logic.
  • Auto-Activation: Clicking [+] automatically enables "Mix Providers" and "Three Model Mode" if they aren't already active, streamlining the onboarding to advanced routing.

2. State Management & Implicit Tiering

  • Removed Redundant Dropdowns: Eliminated the per-tab "Assign to Tier" fields.
  • Implicit Mapping: Tier assignment is now handled via the existing "Reasoning", "Coding", and "Value" buttons in the Model Filter. Selection of a model now automatically maps the current tab's provider to that tier in the backend.
  • Robust Persistence: Updated state.mixedTierAssignments to track provider-to-tier bindings independently of tab order, ensuring configuraiton remains stable across reloads.
  • Manual Entry Trigger: Added explicit overrides for Minimax and Kimi to force the Manual Model Entry UI, as these providers lack standard model list endpoints.

3. Layout & UX Fixes

  • Grid Stability: Fixed an issue where the content-grid layout would break (pushing the model list below providers) due to incorrect div nesting.
  • Persistent Header: The tab bar now acts as the primary card title when the feature flag is enabled, maintaining a clean visual hierarchy.
  • Key Badges: Refined the key status badges to indicate whether a provider in a specific tab has a valid API key stored.

Invariants Touched

  • Configuration Persistence: Modified handleSaveMixedProviders to accept the new tier-based payload.
  • Start/Stop Hydration: Updated handleConfigLoaded to recreate secondary tabs and restore provider assignments on startup.
  • Event Listener Discipline: Ensured tab switching doesn't leak listeners or cause redundant model fetches.

Manual Testing Results

  • Switching between tabs updates the Model List for the correct provider.
  • Selecting a model and clicking a tier button correctly updates mixedProviders config in VS Code settings.
  • Restarting the extension restores the mixed provider tabs and selected models correctly.
  • Clicking [+] enables mixed mode and spawns a new tab.
  • Multi-column layout remains stable at various widths.

Version Bump

  • Bumped to 1.5.71 for final layout verification.

Summary by CodeRabbit

  • New Features

    • Mixed Provider Routing: Route different model request types (reasoning, completion, value) to different providers and models within a single session using new configuration UI controls.
  • Updates

    • Updated default Anthropic models to latest versions (Opus 4.6, Sonnet 4.6, Haiku 4.5).
    • Kimi and minimax providers now require manual model entry instead of automatic discovery.
  • Documentation

    • Added comprehensive guide for configuring and managing mixed provider routing with security and isolation details.

KHAEntertainment and others added 6 commits April 12, 2026 16:04
…nthropic defaults

On proxy stop, remove all ThroneKeeper env vars from .claude/settings.json
and let Claude Code use its built-in defaults. This eliminates the
fetchAnthropicDefaults() API call during revert, which failed for OAuth
(Pro/Max) users and returned outdated 4.5-era model names.

Also updates hardcoded fallbacks to current 4.6 -latest aliases.

Closes: KHA-272

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New provider-router.js: ProviderContext (per-tier state) + ProviderRouter
  (model→tier→provider mapping) classes with smart key validation
- key-resolver.js: add resolveApiKeyForProvider() and
  buildUpstreamHeadersForContext() (additive exports, no breaking changes)
- index.js: integrate router from MIXED_PROVIDERS_CONFIG env var; route
  each request to correct upstream provider; use effectiveProvider/BaseUrl/Key
  instead of globals inside /v1/messages handler
- lib/config.js: add mixedProviders default (null = single-provider mode)
- 29 unit tests covering ProviderContext, ProviderRouter, createRouterFromEnv,
  smart key validation, and mixed endpoint kinds (Anthropic + OpenAI in same session)

100% backward compatible: when MIXED_PROVIDERS_CONFIG is absent, all behavior
is identical to single-provider mode.
…xed providers

Phase 2: Extension & Config Schema

- config.ts (v1.1.0): add TierProviderBindingSchema, MixedProviderConfigSchema,
  enableMixedProviders feature flag, mixed-provider invariant checks
- messages.ts (v1.1.0): add SaveMixedProvidersMessage type, mixedProviders in
  ConfigLoaded payload, expandenableMixedProviders in feature flag enum
- ProxyManager.ts: serialize MIXED_PROVIDERS_CONFIG env var with smart key
  resolution (resolves keys from SecretStorage per unique provider)
- extension.ts: pass mixedProviders config when starting proxy
- package.json: add claudeThrone.mixedProviders VS Code configuration property

TypeScript compiles clean (zero errors). All 29 router tests pass.
…andler

Phase 3: Webview UI

- PanelViewProvider.ts: add saveMixedProviders message handler,
  handleSaveMixedProviders method, mixedProviders in config payload,
  enableMixedProviders in KNOWN_FLAGS, HTML toggle UI
- main.js: add mixedProviders state, enableMixedProviders feature flag state,
  checkbox event listener, config hydration, visibility tied to 3-model mode

Toggle appears as sub-option under '3 Different Models', hidden when disabled.
Status indicator shows when mixed mode is active.
…tion invariant

Phase 4: Presets, CLI Stubs & Documentation

- docs/mixed-providers.md: Document mixed-provider architecture, schema, security
- CONSTITUTION.md: Add Invariant 6 (Strict Isolation for Mixed Providers)
- cli/commands/mixed.js: Create Phase 4 stub for CLI functionality
- mixed-presets.json: Create Phase 4 stub for CLI presets
@github-actions
Copy link
Copy Markdown

🤖 Hi @KHAEntertainment, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

Walkthrough

This pull request introduces Mixed Provider Routing (KHA-267), enabling per-tier provider routing within a single session. A request's model name is resolved to a tier (reasoning/completion/value), each routed to a potentially different AI provider with distinct API keys and endpoints. Configuration, UI, routing logic, and validation are added across extension, proxy, and CLI layers.

Changes

Cohort / File(s) Summary
Documentation & Configuration
CONSTITUTION.md, docs/mixed-providers.md, mixed-presets.json
Added new invariant requirements for mixed provider isolation and security (Invariant 6), comprehensive feature documentation with ProviderContext/ProviderRouter concepts and configuration examples, and example presets demonstrating cost-optimized vs. speed-optimized tier assignments.
Core Routing & Key Resolution
provider-router.js, key-resolver.js
Introduced ProviderContext and ProviderRouter classes to encapsulate per-tier provider state, resolve model names to appropriate contexts, construct upstream URLs and headers per endpoint kind, and validate key availability. Extended key resolution to support explicit keys from mixed config while maintaining fallback to environment-based lookup.
Proxy Request Handling
extensions/thronekeeper/bundled/proxy/index.cjs, index.js
Integrated mixed-provider routing into request flow: model resolution now checks router before falling back to single-provider behavior; effective provider/endpoint/headers derived from routed context; model-dependent logic (tool handling, capability checks, streaming behavior) updated to use routed provider. Changed Kimi provider key source from KIMI_API_KEY to ANTHROPIC_API_KEY.
Configuration & Schemas
extensions/thronekeeper/src/schemas/config.ts, extensions/thronekeeper/out/schemas/config.js, extensions/thronekeeper/src/schemas/messages.ts, extensions/thronekeeper/out/schemas/messages.js, extensions/thronekeeper/package.json, lib/config.js
Added TierProviderBindingSchema, MixedProviderConfigSchema with version bump to 1.1.0; extended ClaudeThroneConfigSchema with enableMixedProviders feature flag and nullable mixedProviders field; added saveMixedProviders message type for webview persistence; updated config schema invariants to validate required tier fields and twoModelMode requirement.
Proxy Manager & Services
extensions/thronekeeper/src/services/ProxyManager.ts, extensions/thronekeeper/out/services/ProxyManager.js, extensions/thronekeeper/out/services/Models.js, extensions/thronekeeper/src/services/Models.ts
ProxyManager now serializes MIXED_PROVIDERS_CONFIG environment variable from parsed tier bindings, resolving secrets asynchronously per unique provider. Models service now guards against minimax/kimi by requiring manual model entry (no auto-fetch).
Extension & Proxy Startup
extensions/thronekeeper/src/extension.ts, extensions/thronekeeper/out/extension.js
Updated Anthropic model defaults (Opus/Sonnet/Haiku to -4-6-latest and -4-5-latest variants); simplified revertApply to nullify overrides without cache/fetch logic; added storeKimiKey/storeMinimaxKey subscriptions; proxy startup now reads mixedProviders config and logs mixed-provider activation status.
Panel UI & Webview
extensions/thronekeeper/src/views/PanelViewProvider.ts, extensions/thronekeeper/out/views/PanelViewProvider.js, extensions/thronekeeper/webview/main.js
Added webview message handling for requestModels with explicit provider selection and saveMixedProviders for persisting tier assignments; extended postConfig() to include mixedProviders; webview now tracks mixed-tier state, renders tab UI for secondary providers, implements tier-based model selection/persistence, and includes "Mix Providers" checkbox section with corresponding display logic.
CLI & Tests
cli/commands/mixed.js, tests/provider-router.test.js
Added CLI stub mixed command deferring to Phase 4 implementation; comprehensive Vitest test suite validating ProviderContext initialization/URL/header construction, ProviderRouter model resolution/tier lookup/validation, createRouterFromEnv parsing and logging, and integration scenarios with mixed endpoint kinds.

Sequence Diagram(s)

sequenceDiagram
    actor Client as Client<br/>(VS Code)
    participant Webview as Webview UI
    participant Extension as Extension<br/>Service
    participant SecretStorage as VS Code<br/>SecretStorage
    participant Proxy as Proxy<br/>Server
    participant Router as ProviderRouter
    participant Providers as AI Providers<br/>(Multiple)

    Client->>Webview: Select reasoning/completion/value providers & models
    Webview->>Extension: saveMixedProviders {enabled, reasoning, completion, value}
    Extension->>SecretStorage: Retrieve API keys for each provider
    Extension->>Extension: Serialize MIXED_PROVIDERS_CONFIG
    Extension->>Proxy: Start proxy with mixed config

    Client->>Proxy: POST /v1/messages {model: "reasoning-model"}
    Proxy->>Router: resolve("reasoning-model")
    Router->>Router: Lookup tier from model name
    Router-->>Proxy: Return ProviderContext (provider ID, key, baseUrl)
    Proxy->>Proxy: Construct effective headers<br/>(auth per provider type)
    Proxy->>Providers: Forward request to routed provider<br/>with provider-specific headers
    Providers-->>Proxy: Stream/return response
    Proxy-->>Client: Relay response
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~55 minutes


Possibly Related PRs


Suggested Labels

area:proxy, area:extension, area:config, area:webview, feature:mixed-providers


Poem

🎭 The Tale of Many Providers

One model arrives at the gate,
The Router peers in: which tier waits?
Three paths now diverge—reason, speed, worth—
Each bound to providers across the earth. 🌍
Keys kept safe, headers dressed just right,
Mixed providers dance through the night! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.29% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the changeset: finalizing a multi-provider tabbed UI with state refactoring, and references the ticket identifier (KHA-267).

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/kha-267-mixed-providers

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.

@github-actions
Copy link
Copy Markdown

🤖 I'm sorry @KHAEntertainment, but I was unable to process your request. Please see the logs for more details.

Copy link
Copy Markdown

@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: c33b1b147e

ℹ️ 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 +3517 to +3520
let endpointKind = 'openai';
if (providers[pid]) {
if (pid === 'anthropic' || pid === 'bedrock') endpointKind = 'anthropic';
} else if (state.customProviders) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve Anthropic endpoint kinds for mixed built-ins

buildTierPayload defaults every built-in provider tier to endpointKind: 'openai' and only special-cases anthropic/bedrock, so tiers using deepseek, glm, minimax, or kimi are serialized as OpenAI-compatible. In mixed mode this makes the proxy send /v1/chat/completions + Bearer auth instead of Anthropic-native /v1/messages + x-api-key, which breaks routed requests for those providers.

Useful? React with 👍 / 👎.

Comment on lines +3525 to +3528
let baseUrl = '';
if (state.customProviders) {
const custom = state.customProviders.find(p => p.id === pid);
if (custom?.baseUrl) baseUrl = custom.baseUrl;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include base URLs for mixed built-in provider tiers

For non-custom providers, the mixed-tier payload leaves baseUrl empty, so MIXED_PROVIDERS_CONFIG lacks per-tier upstream URLs. The runtime then falls back to the global base URL (routedContext?.baseUrl || normalizedBaseUrl), causing cross-provider mixes to be routed to the wrong backend host instead of each tier’s selected provider.

Useful? React with 👍 / 👎.

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (2)
index.js (1)

954-960: ⚠️ Potential issue | 🟠 Major

Guard uses global endpointKind instead of effectiveEndpointKind — potential routing bug.

This guard at line 956 checks the global endpointKind, but after the mixed-provider changes, effectiveEndpointKind may differ from the global value.

Scenario: If the global provider is Anthropic-native (e.g., glm) but a request routes to an OpenAI-compatible provider (e.g., openrouter), isAnthropicNative is correctly false (from effectiveEndpointKind), so control reaches the OpenAI path. However, the guard still sees the global endpointKind as ANTHROPIC_NATIVE and throws an incorrect "endpoint kind mismatch" error.

🐛 Proposed fix
     // Comment 2: Guard - never run OpenAI↔Anthropic mapping when endpoint-kind is Anthropic
     // At this point, isAnthropicNative is false, so we proceed with OpenAI-compatible flow
-    if (endpointKind === ENDPOINT_KIND.ANTHROPIC_NATIVE) {
+    if (effectiveEndpointKind === ENDPOINT_KIND.ANTHROPIC_NATIVE) {
       console.error('[Guard Violation] Anthropic-native endpoint reached OpenAI conversion path - this should not happen')
       reply.code(500)
       return { error: { message: 'Internal error: endpoint kind mismatch', type: 'internal_error' } }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.js` around lines 954 - 960, The guard is checking the global
endpointKind constant instead of the per-request effectiveEndpointKind, causing
false positives; update the conditional in the guard (the block that currently
tests "if (endpointKind === ENDPOINT_KIND.ANTHROPIC_NATIVE) { ... }") to use
effectiveEndpointKind (or the same computed variable used to set
isAnthropicNative) so the check reads against effectiveEndpointKind ===
ENDPOINT_KIND.ANTHROPIC_NATIVE and returns the error only when the effective
(per-request) endpoint kind actually indicates Anthropic-native.
extensions/thronekeeper/out/extension.js (1)

196-223: ⚠️ Potential issue | 🟠 Major

Remove -latest suffix from hardcoded Claude fallback model IDs.

The fallback aliases use claude-opus-4-6-latest, claude-sonnet-4-6-latest, and claude-haiku-4-5-latest, but Anthropic's current API aliases omit the -latest suffix: claude-opus-4-6, claude-sonnet-4-6, and claude-haiku-4-5. When the /v1/models endpoint is unavailable, these invalid IDs become the cached defaults and will fail when Claude Code tries to use them downstream. (docs.anthropic.com)

Fix
-        const opus = selectBestModel(opusModels, 'claude-opus-4-6-latest');
+        const opus = selectBestModel(opusModels, 'claude-opus-4-6');
-        const sonnet = selectBestModel(sonnetModels, 'claude-sonnet-4-6-latest');
+        const sonnet = selectBestModel(sonnetModels, 'claude-sonnet-4-6');
-        const haiku = selectBestModel(haikuModels, 'claude-haiku-4-5-latest');
+        const haiku = selectBestModel(haikuModels, 'claude-haiku-4-5');
-        return { opus: 'claude-opus-4-6-latest', sonnet: 'claude-sonnet-4-6-latest', haiku: 'claude-haiku-4-5-latest' };
+        return { opus: 'claude-opus-4-6', sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5' };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/thronekeeper/out/extension.js` around lines 196 - 223, The
hardcoded Claude fallback IDs in the catch block of fetchAnthropicDefaults are
using the deprecated "-latest" suffix and must be changed to the API aliases
without it; update the return in the catch of fetchAnthropicDefaults to return
opus: 'claude-opus-4-6', sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5'
(replace the current 'claude-opus-4-6-latest', 'claude-sonnet-4-6-latest',
'claude-haiku-4-5-latest') so cached defaults are valid for downstream Claude
usage.
🟡 Minor comments (4)
docs/mixed-providers.md-61-73 (1)

61-73: ⚠️ Potential issue | 🟡 Minor

This section no longer matches the implementation.

The code now resolves mixed-provider keys during proxy startup and already renders tabbed provider controls in the webview. Describing SecretStorage lookup as “per request” and the UI as “single toggle only” gives readers the wrong mental model right where they’ll copy their setup from.

As per coding guidelines, "Update documentation in docs/ and README.md for user-facing changes".

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

In `@docs/mixed-providers.md` around lines 61 - 73, Update the docs to match
current implementation: change the description of key resolution to state that
mixed-provider keys are resolved at proxy startup (not per-request) and mention
where startup-resolved keys come from (MIXED_PROVIDERS_CONFIG /
mixed-presets.json / SecretStorage integration during startup), update the
Webview UI section to reflect that tabbed provider controls are rendered (not a
single mix-provider toggle), and remove or reword any references to per-request
SecretStorage lookups and Phase 3b UI TODOs; reference ProviderRouter,
effectiveProvider, MIXED_PROVIDERS_CONFIG, saveMixedProviders,
mixed-presets.json and the Webview/tabbed provider controls while keeping the
security/isolation guarantees intact.
extensions/thronekeeper/src/views/PanelViewProvider.ts-2003-2046 (1)

2003-2046: ⚠️ Potential issue | 🟡 Minor

Add input validation before accessing nested properties in handleSaveMixedProviders.

The method accesses nested properties like msg.reasoning.providerId, msg.completion.baseUrl, etc. without first validating that these objects exist. If the webview sends a malformed message (or if there's a state synchronization issue), this could throw an uncaught exception.

Think of it like unpacking a shipping box — you want to check the box actually contains inner boxes before reaching into them.

🛡️ Proposed fix
   private async handleSaveMixedProviders(msg: any) {
+    // Validate required tier objects exist
+    if (!msg.reasoning || !msg.completion || !msg.value) {
+      this.log.appendLine(`[handleSaveMixedProviders] ERROR: Missing tier data in message`)
+      return
+    }
+    
     const cfg = vscode.workspace.getConfiguration('claudeThrone')
     const applyScope = cfg.get<string>('applyScope', 'workspace')
     const target = this.getConfigurationTarget(applyScope)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/thronekeeper/src/views/PanelViewProvider.ts` around lines 2003 -
2046, handleSaveMixedProviders currently assumes msg and its nested objects
(msg.reasoning, msg.completion, msg.value) exist and will throw if malformed;
add input validation at the start of handleSaveMixedProviders to guard against
missing or non-object msg fields (e.g., check msg && typeof msg === 'object' and
each of msg.reasoning, msg.completion, msg.value are objects), log a clear error
and return early if validation fails, and when building mixedConfig use safe
access (optional chaining or defaults) to populate
providerId/baseUrl/model/displayModel/endpointKind so
cfg.update('mixedProviders', mixedConfig, target) and postConfig() are only
called with a well-formed config.
provider-router.js-281-288 (1)

281-288: ⚠️ Potential issue | 🟡 Minor

Use the injected env for debug gating.

Line 281 reads process.env.DEBUG even though this factory accepts an env object. That makes createRouterFromEnv(fakeEnv) awkward to test and harder to reuse outside the main process, because the caller's DEBUG flag is ignored.

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

In `@provider-router.js` around lines 281 - 288, The debug gating currently reads
process.env.DEBUG inside the createRouterFromEnv factory; change that to use the
injected env parameter (e.g. env.DEBUG or env.debug) so callers of
createRouterFromEnv(fakeEnv) control debug output. Update the conditional that
prints router.toDebugObject() to check the env argument (and ensure
createRouterFromEnv accepts/threads the env object through to this scope),
leaving the existing log messages (router.toDebugObject(),
validation.uniqueProviders, router.tierMap) unchanged.
extensions/thronekeeper/out/extension.js-341-355 (1)

341-355: ⚠️ Potential issue | 🟡 Minor

Update the revert copy everywhere this command is surfaced.

This command now deliberately hands control back to Claude Code's built-in defaults, but extensions/thronekeeper/src/views/PanelViewProvider.ts:1522-1530 still logs and shows "Anthropic defaults". That makes the same success path read like two different products depending on where the user clicked.

Also applies to: 439-445

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

In `@extensions/thronekeeper/out/extension.js` around lines 341 - 355, The
UI/logging still says "Anthropic defaults" even though
updateClaudeSettings(settingsDir, ...) now restores Claude Code's built-in
defaults; update the success message(s) in PanelViewProvider (the UI code that
logs/shows the result around the success path) to say "Claude Code defaults" or
"restored to Claude Code defaults" instead of "Anthropic defaults", and likewise
find and replace the other occurrence referenced (the second block around
439-445) so all user-visible messages and logs match the new behavior of the
updateClaudeSettings call.
🧹 Nitpick comments (3)
extensions/thronekeeper/src/services/Models.ts (1)

259-265: Use structured error metadata for manual-entry routing.

Current string-only errors work, but they’re brittle for callers (like matching by prose). A stable error code makes handling deterministic.

♻️ Suggested hardening
-  if (provider === 'minimax') {
-    throw new Error('Minimax models must be entered manually.')
-  }
-  if (provider === 'kimi') {
-    throw new Error('Kimi models must be entered manually.')
-  }
+  if (provider === 'minimax' || provider === 'kimi') {
+    const err = new Error(`${provider} models must be entered manually.`)
+    ;(err as any).code = 'MANUAL_MODEL_ENTRY_REQUIRED'
+    ;(err as any).provider = provider
+    throw err
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/thronekeeper/src/services/Models.ts` around lines 259 - 265,
Replace brittle string-only throws in Models.ts with a structured error type:
create and export a ManualEntryError class (e.g., class ManualEntryError extends
Error) that includes a stable error code like code = 'MANUAL_ENTRY_REQUIRED' and
a provider property, then change the two branches that currently do throw new
Error('Minimax models must be entered manually.') / throw new Error('Kimi models
must be entered manually.') to throw new ManualEntryError('Manual entry required
for provider', { provider: 'minimax' }) / throw new ManualEntryError(..., {
provider: 'kimi' }); ensure callers can use instanceof ManualEntryError or check
error.code/provider to route to the Manual Entry UI.
tests/provider-router.test.js (1)

269-273: Consider adding tests for partially malformed tier configs.

The ProviderRouter constructor validates that all three tiers must be present, but per Context snippet 1 (lines 134-147), the constructor specifically checks config.reasoning, config.completion, and config.value. You've tested {}, null, and single-tier configs — it would be valuable to add tests for configs where a tier exists but has malformed/empty nested data (e.g., { reasoning: {}, completion: {...}, value: {...} }).

This helps verify the router gracefully handles edge cases from potentially corrupted MIXED_PROVIDERS_CONFIG serialization.

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

In `@tests/provider-router.test.js` around lines 269 - 273, Add tests that
exercise partially malformed tier configs for the ProviderRouter constructor:
create cases where config has all three top-level keys but one tier is an empty
object or missing required nested fields (e.g., reasoning: {}, completion: {…},
value: {…}) and assert that new ProviderRouter(...) throws. Target the
ProviderRouter constructor and add assertions similar to the existing tests to
cover combinations like { reasoning: {}, completion: valid, value: valid }, {
reasoning: valid, completion: {}, value: valid } and { reasoning: valid,
completion: valid, value: {} } to ensure malformed nested tier data is rejected.
extensions/thronekeeper/out/extension.js (1)

1034-1034: Don't push the same disposables twice.

These command disposables were already added to context.subscriptions at Line 507. Adding them again here means shutdown walks the same handles twice; probably harmless today, but it's an easy cleanup footgun.

As per coding guidelines "Prevent duplicate event listeners; check cleanup in Start/Stop hydration sequence."

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

In `@extensions/thronekeeper/out/extension.js` at line 1034, The listed
disposables (openPanel, storeOpenRouterKey, storeOpenAIKey, storeTogetherKey,
storeDeepseekKey, storeGlmKey, storeKimiKey, storeMinimaxKey, storeCustomKey,
storeAnyKey, storeAnthropicKey, refreshAnthropicDefaults, startProxy, stopProxy,
status, applyToClaudeCode, revertApply, log, checkConfigHealthCommand) are being
pushed into context.subscriptions a second time; remove this duplicate push and
ensure these disposables are only added once (reuse the initial push that occurs
earlier around the first registration instead of re-pushing them here), or
centralize the array of disposables into a single variable and push that
variable only once during activation to prevent duplicate disposal on shutdown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@extensions/thronekeeper/bundled/proxy/index.cjs`:
- Around line 34022-34043: The constructor that builds tierMap currently
overwrites duplicate model IDs; update the population logic (the for loop that
iterates Object.entries(this.contexts) and calls this.tierMap.set) to detect
duplicates: if ctx.model is already present in this.tierMap (use
this.tierMap.has(ctx.model)) then reject/raise an error or log and abort
initialization (or otherwise surface a config validation error) citing both tier
names and the conflicting model ID; alternatively, if duplicates are
intentionally allowed, change the lookup strategy used by resolve(modelName) to
include the tier signal (e.g., use a composite key or maintain a map of
modelName -> array of {tier,context}) and update resolve() to disambiguate based
on payload-provided tier. Ensure references to tierMap, resolve(), and the
constructor/context population loop are updated consistently.
- Around line 34710-34743: When a router exists and payload.model is provided
but router.resolve(payload.model) returns falsy, fail closed instead of silently
falling back: return a 400 (or appropriate) error and stop processing rather
than using defaults. Ensure the code path uses routedContext as the single
source of truth when it exists (set routedContext from router.resolve and derive
effectiveProvider/effectiveBaseUrl/effectiveKey/effectiveEndpointKind and
headers exclusively from routedContext.getHeaders()), and only use
normalizedBaseUrl/provider/key/buildUpstreamHeaders when there is no router or
no payload.model; update the logic around router.resolve, routedContext,
effective* variables, headers, and the missing-key check to be deterministic and
avoid mixed fallback behavior (functions/vars: router.resolve, routedContext,
effectiveProvider/effectiveBaseUrl/effectiveKey/effectiveEndpointKind, headers,
ensureEndpointKindReady, buildUpstreamHeaders).

In `@extensions/thronekeeper/out/services/ProxyManager.js`:
- Around line 480-488: When mixed mode is enabled in ProxyManager, the startup
must fail fast if any provider key is missing: in the loop that iterates
uniqueProviders (the block using this.secrets.getProviderKey and providerKeys),
if a key lookup returns falsy for any pid, throw a descriptive Error (e.g.
`throw new Error('[ProxyManager] Mixed mode enabled but missing provider key
for: ' + pid)`) instead of only logging a warning; ensure the thrown error
bubbles out of the initialization/startup routine so startup is blocked. Apply
the same change to the other similar block (around the 490-503 area) that checks
provider keys so both code paths enforce the fail-fast behavior.

In `@extensions/thronekeeper/out/views/PanelViewProvider.js`:
- Around line 563-565: The current implementation of handleListModels uses a
single this.currentSequenceToken for all providers, causing cross-provider
race-condition drops; change to track tokens and cached models per provider/tab:
introduce a map like this.sequenceTokens (keyed by requestedProvider or
this.runtimeProvider fallback) and a models cache keyed by provider, increment
the provider-specific token when issuing a request in handleListModels, store
the issued token in a local variable, and before discarding a late response
compare the response token against this.sequenceTokens[provider]; update cache
this.modelsByProvider[provider] on successful responses. Use the symbols
handleListModels, requestedProvider, this.runtimeProvider,
this.currentSequenceToken (replace), and create this.sequenceTokens and
this.modelsByProvider to locate and modify the code.
- Around line 1814-1849: handleStartProxy currently ignores the saved
mixedProviders; update handleStartProxy to read the saved mixedProviders from
the vscode configuration
(vscode.workspace.getConfiguration('claudeThrone').get('mixedProviders')) or use
this.getConfigurationTarget like handleSaveMixedProviders, and pass that config
into this.proxy.start(...) (or into whatever startOptions object the proxy
expects) so the proxy is started in mixed-provider mode when the panel's
Save/Start flow is used; keep references to handleSaveMixedProviders,
mixedProviders, and this.proxy.start when making the change and add a log line
indicating the mixed config being passed.

In `@extensions/thronekeeper/src/extension.ts`:
- Around line 597-611: Validate mixedProviders before calling proxy.start:
import and use MixedProviderConfigSchema.safeParse (from
extensions/claude-throne/src/schemas) to validate the cfg.get result stored in
mixedProviders; if safeParse returns success, pass the parsed value to
proxy.start, otherwise log the validation errors via log.appendLine and throw or
abort startup so the service fails fast instead of silently falling back to
single-provider mode; update the code around the mixedProviders check and the
await proxy!.start(...) call to use the validated value.

In `@extensions/thronekeeper/src/services/ProxyManager.ts`:
- Around line 494-503: The loop in ProxyManager that builds providerKeys uses
this.secrets.getProviderKey(pid) which misses the special Anthropic secret path
used elsewhere, so mixed tiers end up with null keys and only a warning is
logged; update the loop in ProxyManager to resolve Anthropic with the dedicated
secret method used by the codebase (e.g., call the same Anthropic retrieval like
this.secrets.getAnthropicKey(pid) or equivalent when pid === 'anthropic') and,
for any provider where no key is returned, fail fast by throwing an error (or
return a rejected promise) instead of just logging a warning so the proxy does
not start with missing keys.

In `@extensions/thronekeeper/webview/main.js`:
- Around line 3345-3372: The add button click handler in initMixedTabListeners
currently flips threeModelToggle.checked and sets state.twoModelMode directly
without invoking onTwoModelToggle() or persisting the change, so the UI change
isn't saved; update the handler to (1) set threeModelToggle.checked = true only
if needed and then call onTwoModelToggle() to run the normal toggle logic
(instead of directly setting state.twoModelMode and calling updateTwoModelUI()),
(2) after incrementing mixedTabState.tabCount and switching the tab
(switchToTab(tabId)), call saveState() and saveMixedState() so the new
multi-tab/three-model state is persisted. Ensure you still update the
addBtn/tabBtn display logic and only call these functions when the
checkbox/threeToggle transition actually occurs.
- Around line 3551-3552: The mixed-tab event bindings are being initialized at
module load which can run before the DOM exists; move the
initMixedTabListeners() invocation into the DOM-ready initialization path
(inside the existing init() function or the DOMContentLoaded handler that init()
uses) so it runs after markup is available; locate the initMixedTabListeners()
call and remove the top-level module invocation, then call
initMixedTabListeners() from within init() (or the same DOMContentLoaded
callback) to ensure add/close/tab buttons are bound reliably.
- Around line 3391-3432: switchToTab currently changes state.provider and model
selections directly but doesn't run the same sync/update steps as normal
provider changes; call the same routines after you set state.provider and models
so UI and engine stay in sync: invoke updateProviderUI(),
updateSelectedModelsDisplay(), persist by calling saveState(), and send the same
"updateProvider" message (or call the existing updateProvider handler) so the
extension host receives the change; ensure these calls occur right after you set
state.provider and update
state.reasoningModel/state.codingModel/state.valueModel (and after models are
loaded if loadModels is asynchronous).
- Around line 564-580: The mixedProvidersCheckbox change currently never
persists a disabled state because saveMixedState() only emits an enabled config;
update saveMixedState() so it always writes the current enabled value (including
enabled: false) into the outgoing config payload (e.g., emit
config.mixedProviders = { enabled: state.featureFlags.enableMixedProviders }
even when false), and ensure handleConfigLoaded() reads
config.mixedProviders.enabled and applies it to
state.featureFlags.enableMixedProviders and the UI
(mixedProvidersCheckbox.checked and updateMixedProvidersUI()) so unchecking
truly persists and is restored on hydrate/reload; reference the
mixedProvidersCheckbox change handler, saveMixedState(), and
handleConfigLoaded() when making these edits.
- Around line 3515-3538: buildTierPayload currently determines endpointKind from
providers or custom.provider.endpointKind but ignores state.endpointOverrides
and thus mislabels anthopic-compatible providers like "deepseek" and "glm";
update buildTierPayload to first check state.endpointOverrides for an override
entry for the pid and use that endpointKind if present, then fall back to: if
providers[pid] exists use the provider-based logic (treat "anthropic",
"bedrock", and declared anthopic-compatible ids like "deepseek" and "glm" as
'anthropic'), else look at state.customProviders for baseUrl only (do not use
custom.endpointKind) and finally default to 'openai'; ensure the function
returns endpointKind set from endpointOverrides when available and only uses
automatic detection otherwise.

In `@key-resolver.js`:
- Around line 397-405: The branch that sets headers based on ctx.endpointKind
only checks ENDPOINT_KIND.ANTHROPIC_NATIVE so contexts with canonical
'anthropic' get the wrong Authorization header; update the logic in the
headers-setting block (where ctx.endpointKind is inspected and headers are
assigned: headers['x-api-key'], headers['anthropic-version'],
headers['anthropic-beta'], else Authorization) to normalize or accept canonical
values — e.g., compare ctx.endpointKind after lowercasing to both
ENDPOINT_KIND.ANTHROPIC_NATIVE and the canonical string 'anthropic' (or map
canonical names to constants) so requests for Anthropic use x-api-key and
related headers instead of Bearer.

In `@mixed-presets.json`:
- Around line 6-20: The preset entries under "config" (the "reasoning",
"completion", and "value" tier blocks) are missing the required mixed-provider
field `baseUrl`, so they fail mixed-provider schema validation; update each tier
object (reasoning, completion, value) to include the provider's `baseUrl`
alongside `providerId` and `model` (and ensure the same fix is applied to the
other similar preset block noted in the comment), using the actual endpoint for
each provider so the mixedProviders loader passes validation.

In `@provider-router.js`:
- Around line 263-269: Parse MIXED_PROVIDERS_CONFIG as before but validate the
resulting object against the shared mixed-provider schema in
extensions/claude-throne/src/schemas/ before constructing ProviderRouter: run
the parsed config through the schema validator (use the existing
schema/validator helper), convert any deprecated "coding" key to "completion"
(or merge its contents into "completion"), and ensure required fields like
provider.baseUrl and provider.model are present and strings; if validation
fails, log the validation errors and return null instead of calling new
ProviderRouter(config, endpointOverrides), only constructing ProviderRouter
after the schema validator succeeds.
- Around line 167-173: When building the model → { tier, context } lookup (the
this.tierMap population that iterates this.contexts and calls Map.set with
ctx.model), detect if a model key already exists and the existing entry points
at a different upstream context; in that case reject the configuration (throw or
log and exit) instead of overwriting so ambiguous model collisions are not
allowed; implement this check using this.tierMap.has(model) and compare the
existing entry’s context/tier to ctx before deciding to set, and apply the same
guard to the other identical population block around the 190-194 region.

---

Outside diff comments:
In `@extensions/thronekeeper/out/extension.js`:
- Around line 196-223: The hardcoded Claude fallback IDs in the catch block of
fetchAnthropicDefaults are using the deprecated "-latest" suffix and must be
changed to the API aliases without it; update the return in the catch of
fetchAnthropicDefaults to return opus: 'claude-opus-4-6', sonnet:
'claude-sonnet-4-6', haiku: 'claude-haiku-4-5' (replace the current
'claude-opus-4-6-latest', 'claude-sonnet-4-6-latest', 'claude-haiku-4-5-latest')
so cached defaults are valid for downstream Claude usage.

In `@index.js`:
- Around line 954-960: The guard is checking the global endpointKind constant
instead of the per-request effectiveEndpointKind, causing false positives;
update the conditional in the guard (the block that currently tests "if
(endpointKind === ENDPOINT_KIND.ANTHROPIC_NATIVE) { ... }") to use
effectiveEndpointKind (or the same computed variable used to set
isAnthropicNative) so the check reads against effectiveEndpointKind ===
ENDPOINT_KIND.ANTHROPIC_NATIVE and returns the error only when the effective
(per-request) endpoint kind actually indicates Anthropic-native.

---

Minor comments:
In `@docs/mixed-providers.md`:
- Around line 61-73: Update the docs to match current implementation: change the
description of key resolution to state that mixed-provider keys are resolved at
proxy startup (not per-request) and mention where startup-resolved keys come
from (MIXED_PROVIDERS_CONFIG / mixed-presets.json / SecretStorage integration
during startup), update the Webview UI section to reflect that tabbed provider
controls are rendered (not a single mix-provider toggle), and remove or reword
any references to per-request SecretStorage lookups and Phase 3b UI TODOs;
reference ProviderRouter, effectiveProvider, MIXED_PROVIDERS_CONFIG,
saveMixedProviders, mixed-presets.json and the Webview/tabbed provider controls
while keeping the security/isolation guarantees intact.

In `@extensions/thronekeeper/out/extension.js`:
- Around line 341-355: The UI/logging still says "Anthropic defaults" even
though updateClaudeSettings(settingsDir, ...) now restores Claude Code's
built-in defaults; update the success message(s) in PanelViewProvider (the UI
code that logs/shows the result around the success path) to say "Claude Code
defaults" or "restored to Claude Code defaults" instead of "Anthropic defaults",
and likewise find and replace the other occurrence referenced (the second block
around 439-445) so all user-visible messages and logs match the new behavior of
the updateClaudeSettings call.

In `@extensions/thronekeeper/src/views/PanelViewProvider.ts`:
- Around line 2003-2046: handleSaveMixedProviders currently assumes msg and its
nested objects (msg.reasoning, msg.completion, msg.value) exist and will throw
if malformed; add input validation at the start of handleSaveMixedProviders to
guard against missing or non-object msg fields (e.g., check msg && typeof msg
=== 'object' and each of msg.reasoning, msg.completion, msg.value are objects),
log a clear error and return early if validation fails, and when building
mixedConfig use safe access (optional chaining or defaults) to populate
providerId/baseUrl/model/displayModel/endpointKind so
cfg.update('mixedProviders', mixedConfig, target) and postConfig() are only
called with a well-formed config.

In `@provider-router.js`:
- Around line 281-288: The debug gating currently reads process.env.DEBUG inside
the createRouterFromEnv factory; change that to use the injected env parameter
(e.g. env.DEBUG or env.debug) so callers of createRouterFromEnv(fakeEnv) control
debug output. Update the conditional that prints router.toDebugObject() to check
the env argument (and ensure createRouterFromEnv accepts/threads the env object
through to this scope), leaving the existing log messages
(router.toDebugObject(), validation.uniqueProviders, router.tierMap) unchanged.

---

Nitpick comments:
In `@extensions/thronekeeper/out/extension.js`:
- Line 1034: The listed disposables (openPanel, storeOpenRouterKey,
storeOpenAIKey, storeTogetherKey, storeDeepseekKey, storeGlmKey, storeKimiKey,
storeMinimaxKey, storeCustomKey, storeAnyKey, storeAnthropicKey,
refreshAnthropicDefaults, startProxy, stopProxy, status, applyToClaudeCode,
revertApply, log, checkConfigHealthCommand) are being pushed into
context.subscriptions a second time; remove this duplicate push and ensure these
disposables are only added once (reuse the initial push that occurs earlier
around the first registration instead of re-pushing them here), or centralize
the array of disposables into a single variable and push that variable only once
during activation to prevent duplicate disposal on shutdown.

In `@extensions/thronekeeper/src/services/Models.ts`:
- Around line 259-265: Replace brittle string-only throws in Models.ts with a
structured error type: create and export a ManualEntryError class (e.g., class
ManualEntryError extends Error) that includes a stable error code like code =
'MANUAL_ENTRY_REQUIRED' and a provider property, then change the two branches
that currently do throw new Error('Minimax models must be entered manually.') /
throw new Error('Kimi models must be entered manually.') to throw new
ManualEntryError('Manual entry required for provider', { provider: 'minimax' })
/ throw new ManualEntryError(..., { provider: 'kimi' }); ensure callers can use
instanceof ManualEntryError or check error.code/provider to route to the Manual
Entry UI.

In `@tests/provider-router.test.js`:
- Around line 269-273: Add tests that exercise partially malformed tier configs
for the ProviderRouter constructor: create cases where config has all three
top-level keys but one tier is an empty object or missing required nested fields
(e.g., reasoning: {}, completion: {…}, value: {…}) and assert that new
ProviderRouter(...) throws. Target the ProviderRouter constructor and add
assertions similar to the existing tests to cover combinations like { reasoning:
{}, completion: valid, value: valid }, { reasoning: valid, completion: {},
value: valid } and { reasoning: valid, completion: valid, value: {} } to ensure
malformed nested tier data is rejected.
🪄 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: 573648f7-4bc4-43ad-9018-7b4ac03b940e

📥 Commits

Reviewing files that changed from the base of the PR and between fa1f69e and c33b1b1.

⛔ Files ignored due to path filters (8)
  • extensions/thronekeeper/out/extension.js.map is excluded by !**/*.map
  • extensions/thronekeeper/out/schemas/config.js.map is excluded by !**/*.map
  • extensions/thronekeeper/out/schemas/messages.js.map is excluded by !**/*.map
  • extensions/thronekeeper/out/services/Models.js.map is excluded by !**/*.map
  • extensions/thronekeeper/out/services/ProxyManager.js.map is excluded by !**/*.map
  • extensions/thronekeeper/out/views/PanelViewProvider.js.map is excluded by !**/*.map
  • extensions/thronekeeper/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (24)
  • CONSTITUTION.md
  • cli/commands/mixed.js
  • docs/mixed-providers.md
  • extensions/thronekeeper/bundled/proxy/index.cjs
  • extensions/thronekeeper/out/extension.js
  • extensions/thronekeeper/out/schemas/config.js
  • extensions/thronekeeper/out/schemas/messages.js
  • extensions/thronekeeper/out/services/Models.js
  • extensions/thronekeeper/out/services/ProxyManager.js
  • extensions/thronekeeper/out/views/PanelViewProvider.js
  • extensions/thronekeeper/package.json
  • extensions/thronekeeper/src/extension.ts
  • extensions/thronekeeper/src/schemas/config.ts
  • extensions/thronekeeper/src/schemas/messages.ts
  • extensions/thronekeeper/src/services/Models.ts
  • extensions/thronekeeper/src/services/ProxyManager.ts
  • extensions/thronekeeper/src/views/PanelViewProvider.ts
  • extensions/thronekeeper/webview/main.js
  • index.js
  • key-resolver.js
  • lib/config.js
  • mixed-presets.json
  • provider-router.js
  • tests/provider-router.test.js

Comment on lines +34022 to +34043
this.tierMap = /* @__PURE__ */ new Map();
for (const [tier, ctx] of Object.entries(this.contexts)) {
if (ctx.model) {
this.tierMap.set(ctx.model, { tier, context: ctx });
}
}
}
/**
* Given a model name from an incoming request, resolve which ProviderContext to use.
*
* @param {string} modelName - The model name from payload.model
* @returns {{ tier: string, context: ProviderContext } | null} Resolved context, or null if not found
*/
resolve(modelName) {
if (!modelName) return null;
const exact = this.tierMap.get(modelName);
if (exact) return exact;
const lower = modelName.toLowerCase();
for (const [name, entry] of this.tierMap) {
if (name.toLowerCase() === lower) return entry;
}
return null;
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 duplicate model IDs across tiers.

tierMap is a single mailbox per model name. If two tiers reuse the same model ID—especially across custom or OpenAI-compatible providers exposing the same upstream name—the later set() wins and resolve() sends both requests through one context.

Possible guard
     this.tierMap = /* `@__PURE__` */ new Map();
+    const normalizedModels = /* `@__PURE__` */ new Map();
     for (const [tier, ctx] of Object.entries(this.contexts)) {
       if (ctx.model) {
+        const normalized = ctx.model.toLowerCase();
+        const existingTier = normalizedModels.get(normalized);
+        if (existingTier) {
+          throw new Error(
+            `[ProviderRouter] Model "${ctx.model}" is assigned to both "${existingTier}" and "${tier}"`
+          );
+        }
+        normalizedModels.set(normalized, tier);
         this.tierMap.set(ctx.model, { tier, context: ctx });
       }
     }

If duplicate model IDs are meant to be supported, the lookup key needs an explicit tier signal instead of bare payload.model.

📝 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
this.tierMap = /* @__PURE__ */ new Map();
for (const [tier, ctx] of Object.entries(this.contexts)) {
if (ctx.model) {
this.tierMap.set(ctx.model, { tier, context: ctx });
}
}
}
/**
* Given a model name from an incoming request, resolve which ProviderContext to use.
*
* @param {string} modelName - The model name from payload.model
* @returns {{ tier: string, context: ProviderContext } | null} Resolved context, or null if not found
*/
resolve(modelName) {
if (!modelName) return null;
const exact = this.tierMap.get(modelName);
if (exact) return exact;
const lower = modelName.toLowerCase();
for (const [name, entry] of this.tierMap) {
if (name.toLowerCase() === lower) return entry;
}
return null;
this.tierMap = /* `@__PURE__` */ new Map();
const normalizedModels = /* `@__PURE__` */ new Map();
for (const [tier, ctx] of Object.entries(this.contexts)) {
if (ctx.model) {
const normalized = ctx.model.toLowerCase();
const existingTier = normalizedModels.get(normalized);
if (existingTier) {
throw new Error(
`[ProviderRouter] Model "${ctx.model}" is assigned to both "${existingTier}" and "${tier}"`
);
}
normalizedModels.set(normalized, tier);
this.tierMap.set(ctx.model, { tier, context: ctx });
}
}
}
/**
* Given a model name from an incoming request, resolve which ProviderContext to use.
*
* `@param` {string} modelName - The model name from payload.model
* `@returns` {{ tier: string, context: ProviderContext } | null} Resolved context, or null if not found
*/
resolve(modelName) {
if (!modelName) return null;
const exact = this.tierMap.get(modelName);
if (exact) return exact;
const lower = modelName.toLowerCase();
for (const [name, entry] of this.tierMap) {
if (name.toLowerCase() === lower) return entry;
}
return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/thronekeeper/bundled/proxy/index.cjs` around lines 34022 - 34043,
The constructor that builds tierMap currently overwrites duplicate model IDs;
update the population logic (the for loop that iterates
Object.entries(this.contexts) and calls this.tierMap.set) to detect duplicates:
if ctx.model is already present in this.tierMap (use
this.tierMap.has(ctx.model)) then reject/raise an error or log and abort
initialization (or otherwise surface a config validation error) citing both tier
names and the conflicting model ID; alternatively, if duplicates are
intentionally allowed, change the lookup strategy used by resolve(modelName) to
include the tier signal (e.g., use a composite key or maintain a map of
modelName -> array of {tier,context}) and update resolve() to disambiguate based
on payload-provided tier. Ensure references to tierMap, resolve(), and the
constructor/context population loop are updated consistently.

Comment on lines +34710 to +34743
if (router && payload.model) {
const resolved = router.resolve(payload.model);
if (resolved) {
routedContext = resolved.context;
console.log(`[Mixed Router] Model "${payload.model}" \u2192 tier "${resolved.tier}" \u2192 provider "${routedContext.providerId}" (${routedContext.endpointKind})`);
} else {
console.log(`[Mixed Router] Model "${payload.model}" not in tier map, falling back to default provider`);
}
}
const isAnthropicNative = endpointKind === ENDPOINT_KIND.ANTHROPIC_NATIVE;
if (!key) {
const effectiveProvider = routedContext?.providerId || provider;
const effectiveBaseUrl = routedContext?.baseUrl || normalizedBaseUrl;
const effectiveKey = routedContext?.key || key;
const effectiveEndpointKind = routedContext?.endpointKind || endpointKind;
if (!routedContext) {
const negotiationError = await ensureEndpointKindReady();
if (negotiationError) {
reply.code(negotiationError.statusCode);
return negotiationError.body;
}
}
const isAnthropicNative = effectiveEndpointKind === ENDPOINT_KIND.ANTHROPIC_NATIVE;
if (!effectiveKey) {
reply.code(400);
const hint = isAnthropicNative ? `Store the provider API key in the extension (Thronekeeper: Store ${provider === "deepseek" ? "Deepseek" : provider === "glm" ? "GLM" : provider} API Key) or set the correct env var (${provider === "deepseek" ? "DEEPSEEK_API_KEY" : provider === "glm" ? "ZAI_API_KEY or GLM_API_KEY" : "API_KEY"}), and confirm the provider switch in settings.` : `Use Authorization: Bearer <token> header or configure ${provider === "openrouter" ? "OpenRouter" : provider} API key in the extension settings.`;
const hint = isAnthropicNative ? `Store the provider API key in the extension (Thronekeeper: Store ${effectiveProvider === "deepseek" ? "Deepseek" : effectiveProvider === "glm" ? "GLM" : effectiveProvider} API Key) or set the correct env var (${effectiveProvider === "deepseek" ? "DEEPSEEK_API_KEY" : effectiveProvider === "glm" ? "ZAI_API_KEY or GLM_API_KEY" : "API_KEY"}), and confirm the provider switch in settings.` : `Use Authorization: Bearer <token> header or configure ${effectiveProvider === "openrouter" ? "OpenRouter" : effectiveProvider} API key in the extension settings.`;
return {
error: {
message: `No API key found for provider "${provider}". Checked ${KEY_ENV_HINT}.`,
message: `No API key found for provider "${effectiveProvider}". Checked ${KEY_ENV_HINT}.`,
type: "missing_api_key",
hint
}
};
}
const requestUrl = isAnthropicNative ? `${normalizedBaseUrl}/v1/messages` : `${normalizedBaseUrl}/v1/chat/completions`;
const headers = buildUpstreamHeaders({ provider, endpointKind, key });
const requestUrl = isAnthropicNative ? `${effectiveBaseUrl}/v1/messages` : `${effectiveBaseUrl}/v1/chat/completions`;
const headers = routedContext ? routedContext.getHeaders() : buildUpstreamHeaders({ provider, endpointKind, key });
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

Fail closed and keep routed config to one source of truth.

This branch currently mixes two routing sources: unresolved models silently fall back to the default provider, while effective* values can fall back to defaults even though headers still come from routedContext. That can turn a config miss into a request sent to the wrong upstream, like a train switch snapping back to the main line when the destination plate is missing.

One way to make the path deterministic
     if (router && payload.model) {
       const resolved = router.resolve(payload.model);
       if (resolved) {
         routedContext = resolved.context;
         console.log(`[Mixed Router] Model "${payload.model}" → tier "${resolved.tier}" → provider "${routedContext.providerId}" (${routedContext.endpointKind})`);
       } else {
-        console.log(`[Mixed Router] Model "${payload.model}" not in tier map, falling back to default provider`);
+        reply.code(400);
+        return {
+          error: {
+            type: "invalid_model",
+            message: `Model "${payload.model}" is not configured in MIXED_PROVIDERS_CONFIG`
+          }
+        };
       }
     }
-    const effectiveProvider = routedContext?.providerId || provider;
-    const effectiveBaseUrl = routedContext?.baseUrl || normalizedBaseUrl;
-    const effectiveKey = routedContext?.key || key;
-    const effectiveEndpointKind = routedContext?.endpointKind || endpointKind;
+    const effectiveProvider = routedContext ? routedContext.providerId : provider;
+    const effectiveBaseUrl = routedContext ? routedContext.baseUrl : normalizedBaseUrl;
+    const effectiveKey = routedContext ? routedContext.key : key;
+    const effectiveEndpointKind = routedContext ? routedContext.endpointKind : endpointKind;
@@
-    const headers = routedContext ? routedContext.getHeaders() : buildUpstreamHeaders({ provider, endpointKind, key });
+    const headers = buildUpstreamHeaders({
+      provider: effectiveProvider,
+      endpointKind: effectiveEndpointKind,
+      key: effectiveKey
+    });

That makes the missing-key check trustworthy again and avoids silent reroutes in mixed mode.

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

In `@extensions/thronekeeper/bundled/proxy/index.cjs` around lines 34710 - 34743,
When a router exists and payload.model is provided but
router.resolve(payload.model) returns falsy, fail closed instead of silently
falling back: return a 400 (or appropriate) error and stop processing rather
than using defaults. Ensure the code path uses routedContext as the single
source of truth when it exists (set routedContext from router.resolve and derive
effectiveProvider/effectiveBaseUrl/effectiveKey/effectiveEndpointKind and
headers exclusively from routedContext.getHeaders()), and only use
normalizedBaseUrl/provider/key/buildUpstreamHeaders when there is no router or
no payload.model; update the logic around router.resolve, routedContext,
effective* variables, headers, and the missing-key check to be deterministic and
avoid mixed fallback behavior (functions/vars: router.resolve, routedContext,
effectiveProvider/effectiveBaseUrl/effectiveKey/effectiveEndpointKind, headers,
ensureEndpointKindReady, buildUpstreamHeaders).

Comment on lines +468 to +509
// KHA-267: Serialize mixed-provider config if enabled
if (opts.mixedProviders?.enabled) {
try {
const mp = opts.mixedProviders;
const tiers = ['reasoning', 'completion', 'value'];
// Collect unique provider IDs to resolve keys
const uniqueProviders = new Set();
for (const tier of tiers) {
uniqueProviders.add(mp[tier].providerId);
}
// Resolve keys for each unique provider (smart key validation)
const providerKeys = new Map();
for (const pid of uniqueProviders) {
const key = await this.secrets.getProviderKey(pid);
if (key) {
providerKeys.set(pid, key);
this.log.appendLine(`[ProxyManager] Mixed provider key found for: ${pid}`);
}
else {
this.log.appendLine(`[ProxyManager] WARNING: No key found for mixed provider: ${pid}`);
}
}
// Build MIXED_PROVIDERS_CONFIG object
const mixedConfig = {};
for (const tier of tiers) {
const binding = mp[tier];
mixedConfig[tier] = {
providerId: binding.providerId,
baseUrl: binding.baseUrl,
key: providerKeys.get(binding.providerId) || null,
model: binding.model,
endpointKind: binding.endpointKind || undefined,
};
}
base.MIXED_PROVIDERS_CONFIG = JSON.stringify(mixedConfig);
this.log.appendLine(`[ProxyManager] Mixed provider config serialized: ${uniqueProviders.size} unique providers`);
this.log.appendLine(`[ProxyManager] Mixed tiers: reasoning=${mp.reasoning.providerId}/${mp.reasoning.model}, completion=${mp.completion.providerId}/${mp.completion.model}, value=${mp.value.providerId}/${mp.value.model}`);
}
catch (err) {
this.log.appendLine(`[ProxyManager] Failed to serialize mixed provider config: ${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 | 🟠 Major

Clear MIXED_PROVIDERS_CONFIG when mixed mode is disabled.

Because base starts from process.env, a pre-existing MIXED_PROVIDERS_CONFIG can accidentally keep mixed routing active even when the feature is off.

🧹 Suggested fix
         if (opts.mixedProviders?.enabled) {
             try {
               // ...
             }
             catch (err) {
                 this.log.appendLine(`[ProxyManager] Failed to serialize mixed provider config: ${err}`);
             }
+        } else {
+            delete base.MIXED_PROVIDERS_CONFIG;
         }

Comment on lines +480 to +488
for (const pid of uniqueProviders) {
const key = await this.secrets.getProviderKey(pid);
if (key) {
providerKeys.set(pid, key);
this.log.appendLine(`[ProxyManager] Mixed provider key found for: ${pid}`);
}
else {
this.log.appendLine(`[ProxyManager] WARNING: No key found for mixed provider: ${pid}`);
}
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

Fail fast when mixed mode is enabled but provider keys are missing.

Right now this is like launching a 3-engine plane after confirming only 2 engines have fuel: startup succeeds, but failures happen mid-flight (401s). Block startup if any referenced provider key is missing.

🛠️ Suggested fix
-                const providerKeys = new Map();
+                const providerKeys = new Map();
+                const missingProviders = [];
                 for (const pid of uniqueProviders) {
                     const key = await this.secrets.getProviderKey(pid);
                     if (key) {
                         providerKeys.set(pid, key);
                         this.log.appendLine(`[ProxyManager] Mixed provider key found for: ${pid}`);
                     }
                     else {
                         this.log.appendLine(`[ProxyManager] WARNING: No key found for mixed provider: ${pid}`);
+                        missingProviders.push(pid);
                     }
                 }
+                if (missingProviders.length > 0) {
+                    throw new Error(`Mixed provider mode missing API keys for: ${missingProviders.join(', ')}`);
+                }

Also applies to: 490-503

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

In `@extensions/thronekeeper/out/services/ProxyManager.js` around lines 480 - 488,
When mixed mode is enabled in ProxyManager, the startup must fail fast if any
provider key is missing: in the loop that iterates uniqueProviders (the block
using this.secrets.getProviderKey and providerKeys), if a key lookup returns
falsy for any pid, throw a descriptive Error (e.g. `throw new
Error('[ProxyManager] Mixed mode enabled but missing provider key for: ' +
pid)`) instead of only logging a warning; ensure the thrown error bubbles out of
the initialization/startup routine so startup is blocked. Apply the same change
to the other similar block (around the 490-503 area) that checks provider keys
so both code paths enforce the fail-fast behavior.

Comment on lines +563 to +565
async handleListModels(freeOnly, requestToken, requestedProvider) {
// Use requestedProvider (for Phase 3b) or fallback to runtimeProvider
const provider = requestedProvider || this.runtimeProvider || 'openrouter';
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

Track request tokens per provider/tab, not with one global counter.

Adding requestedProvider turns model loading into a multi-lane road, but this.currentSequenceToken is still a single traffic light for every lane. If Provider 2 requests after Provider 1, Provider 1’s slower response gets discarded as “late” even though it belongs to a different tab. Keep the latest token per provider (or per tab) before the late-response check.

Based on learnings, "In provider-aware model loading logic, validate request/response token matching and cache models by provider to prevent race conditions from concurrent provider switches".

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

In `@extensions/thronekeeper/out/views/PanelViewProvider.js` around lines 563 -
565, The current implementation of handleListModels uses a single
this.currentSequenceToken for all providers, causing cross-provider
race-condition drops; change to track tokens and cached models per provider/tab:
introduce a map like this.sequenceTokens (keyed by requestedProvider or
this.runtimeProvider fallback) and a models cache keyed by provider, increment
the provider-specific token when issuing a request in handleListModels, store
the issued token in a local variable, and before discarding a late response
compare the response token against this.sequenceTokens[provider]; update cache
this.modelsByProvider[provider] on successful responses. Use the symbols
handleListModels, requestedProvider, this.runtimeProvider,
this.currentSequenceToken (replace), and create this.sequenceTokens and
this.modelsByProvider to locate and modify the code.

Comment on lines +3551 to +3552
// Initialize on DOM ready
initMixedTabListeners();
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

Bind mixed-tab listeners from the DOM-ready init path.

Every other control is wired inside init(), but this runs at module load. If the script is evaluated before the tab markup exists, initMixedTabListeners() is a silent no-op and the add/close/tab buttons never bind.

Minimal fix
function init() {
  console.log('[init] Initializing Claude Throne webview...');
  setupEventListeners();
+ initMixedTabListeners();
  restoreState();
@@
-// Initialize on DOM ready
-initMixedTabListeners();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/thronekeeper/webview/main.js` around lines 3551 - 3552, The
mixed-tab event bindings are being initialized at module load which can run
before the DOM exists; move the initMixedTabListeners() invocation into the
DOM-ready initialization path (inside the existing init() function or the
DOMContentLoaded handler that init() uses) so it runs after markup is available;
locate the initMixedTabListeners() call and remove the top-level module
invocation, then call initMixedTabListeners() from within init() (or the same
DOMContentLoaded callback) to ensure add/close/tab buttons are bound reliably.

Comment thread key-resolver.js
Comment on lines +397 to +405
if (ctx.endpointKind === ENDPOINT_KIND.ANTHROPIC_NATIVE) {
headers['x-api-key'] = ctx.key
headers['anthropic-version'] = env.ANTHROPIC_VERSION || '2023-06-01'
if (env.ANTHROPIC_BETA) {
headers['anthropic-beta'] = env.ANTHROPIC_BETA
}
} else {
headers['Authorization'] = `Bearer ${ctx.key}`
}
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

Normalize endpoint-kind values before choosing auth headers.

This branch only recognizes anthropic-native. If context carries canonical 'anthropic', it falls through and sends Authorization: Bearer instead of x-api-key, which can break provider auth.

🔧 Suggested fix
-  if (ctx.endpointKind === ENDPOINT_KIND.ANTHROPIC_NATIVE) {
+  const kind = String(ctx.endpointKind || '').toLowerCase()
+  const isAnthropic =
+    kind === 'anthropic' ||
+    kind === 'anthropic-native' ||
+    kind === ENDPOINT_KIND.ANTHROPIC_NATIVE
+
+  if (isAnthropic) {
     headers['x-api-key'] = ctx.key
     headers['anthropic-version'] = env.ANTHROPIC_VERSION || '2023-06-01'
     if (env.ANTHROPIC_BETA) {
       headers['anthropic-beta'] = env.ANTHROPIC_BETA
     }
   } else {
     headers['Authorization'] = `Bearer ${ctx.key}`
   }
As per coding guidelines, endpoint kinds are canonicalized as `'openai'` and `'anthropic'` for contract consistency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@key-resolver.js` around lines 397 - 405, The branch that sets headers based
on ctx.endpointKind only checks ENDPOINT_KIND.ANTHROPIC_NATIVE so contexts with
canonical 'anthropic' get the wrong Authorization header; update the logic in
the headers-setting block (where ctx.endpointKind is inspected and headers are
assigned: headers['x-api-key'], headers['anthropic-version'],
headers['anthropic-beta'], else Authorization) to normalize or accept canonical
values — e.g., compare ctx.endpointKind after lowercasing to both
ENDPOINT_KIND.ANTHROPIC_NATIVE and the canonical string 'anthropic' (or map
canonical names to constants) so requests for Anthropic use x-api-key and
related headers instead of Bearer.

Comment thread mixed-presets.json
Comment on lines +6 to +20
"config": {
"enabled": true,
"reasoning": {
"providerId": "deepseek",
"model": "deepseek-reasoner"
},
"completion": {
"providerId": "anthropic",
"model": "claude-3-5-sonnet-20241022"
},
"value": {
"providerId": "deepseek",
"model": "deepseek-chat"
}
}
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

These presets don’t match the mixed-provider schema.

Every tier binding now requires baseUrl, but both presets only provide providerId and model. Right now this is like shipping a map with the street names removed: anything that tries to load these into mixedProviders will fail validation before routing even starts.

Also applies to: 24-38

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

In `@mixed-presets.json` around lines 6 - 20, The preset entries under "config"
(the "reasoning", "completion", and "value" tier blocks) are missing the
required mixed-provider field `baseUrl`, so they fail mixed-provider schema
validation; update each tier object (reasoning, completion, value) to include
the provider's `baseUrl` alongside `providerId` and `model` (and ensure the same
fix is applied to the other similar preset block noted in the comment), using
the actual endpoint for each provider so the mixedProviders loader passes
validation.

Comment thread provider-router.js
Comment on lines +167 to +173
// Build model name → { tier, context } lookup
// The proxy sets these model names in .claude/settings.json, so they are authoritative
this.tierMap = new Map()
for (const [tier, ctx] of Object.entries(this.contexts)) {
if (ctx.model) {
this.tierMap.set(ctx.model, { tier, context: ctx })
}
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 ambiguous model collisions when building the routing map.

If two tiers reuse the same model string but point at different upstream contexts, the later Map.set() quietly paints over the first one. The proxy only receives payload.model, so every request for that ID will route to whichever tier was inserted last.

Possible guard
     this.tierMap = new Map()
+    this.normalizedTierMap = new Map()
     for (const [tier, ctx] of Object.entries(this.contexts)) {
       if (ctx.model) {
-        this.tierMap.set(ctx.model, { tier, context: ctx })
+        const normalizedModel = ctx.model.toLowerCase()
+        const existing = this.normalizedTierMap.get(normalizedModel)
+        if (
+          existing &&
+          (
+            existing.context.providerId !== ctx.providerId ||
+            existing.context.baseUrl !== ctx.baseUrl ||
+            existing.context.endpointKind !== ctx.endpointKind ||
+            existing.context.key !== ctx.key
+          )
+        ) {
+          throw new Error(
+            `[ProviderRouter] Mixed routing requires unique model IDs across providers: "${ctx.model}"`
+          )
+        }
+
+        const entry = { tier, context: ctx }
+        this.tierMap.set(ctx.model, entry)
+        this.normalizedTierMap.set(normalizedModel, entry)
       }
     }
-    const lower = modelName.toLowerCase()
-    for (const [name, entry] of this.tierMap) {
-      if (name.toLowerCase() === lower) return entry
-    }
-
-    return null
+    return this.normalizedTierMap.get(modelName.toLowerCase()) || null

Also applies to: 190-194

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

In `@provider-router.js` around lines 167 - 173, When building the model → { tier,
context } lookup (the this.tierMap population that iterates this.contexts and
calls Map.set with ctx.model), detect if a model key already exists and the
existing entry points at a different upstream context; in that case reject the
configuration (throw or log and exit) instead of overwriting so ambiguous model
collisions are not allowed; implement this check using this.tierMap.has(model)
and compare the existing entry’s context/tier to ctx before deciding to set, and
apply the same guard to the other identical population block around the 190-194
region.

Comment thread provider-router.js
Comment on lines +263 to +269
export function createRouterFromEnv(env = process.env, endpointOverrides = {}) {
const raw = env.MIXED_PROVIDERS_CONFIG
if (!raw) return null

try {
const config = JSON.parse(raw)
const router = new ProviderRouter(config, endpointOverrides)
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

Validate MIXED_PROVIDERS_CONFIG with the shared schema before constructing the router.

JSON.parse() plus the tier-presence check is still too thin here. A stale coding key, missing baseUrl, or non-string model will fail late and just fall back to single-provider mode. Please run this boundary through the existing mixed-provider schema so the env contract stays in lockstep with the rest of the feature.

As per coding guidelines "Validate message/config contracts against schemas in extensions/claude-throne/src/schemas/." and "Use 'completion' key for storage operations (deprecated: 'coding' was deprecated in favor of 'completion')."

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

In `@provider-router.js` around lines 263 - 269, Parse MIXED_PROVIDERS_CONFIG as
before but validate the resulting object against the shared mixed-provider
schema in extensions/claude-throne/src/schemas/ before constructing
ProviderRouter: run the parsed config through the schema validator (use the
existing schema/validator helper), convert any deprecated "coding" key to
"completion" (or merge its contents into "completion"), and ensure required
fields like provider.baseUrl and provider.model are present and strings; if
validation fails, log the validation errors and return null instead of calling
new ProviderRouter(config, endpointOverrides), only constructing ProviderRouter
after the schema validator succeeds.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant