fix(nexus): exclude models with unconfigured provider credentials from routed selection - #1632
Conversation
…m routed selection Root cause of the nondeterministic nexus-chat-pii-passthrough.functional E2E failures (diagnosed 2026-08-10): in AUTO mode the Nexus model router's configured candidate lists can contain OpenAI (or Google/Azure/Latimer) models. On machines where that provider's API key is not configured (local dev: .env.local has no provider keys and the settings table is empty), the router still selected the model, provider creation then threw "OpenAI API key not configured" deep in the streaming path, and POST /api/nexus/chat 500'd with "Failed to process chat request". The router's fallbackModelId cannot catch this because the error occurs after routing, at stream time. The failure was nondeterministic because the nova-micro classifier's tier/intent output varies per run, changing which candidate wins. Fix (option "router excludes unconfigured families", chosen over fallback-on-missing-key — which would need a retry wrapped around the entire stream-setup path — and over pinning a family in the spec, which would mask the same 500 for real local AUTO-mode users): - lib/ai/provider-credentials.ts (new): getConfiguredChatProviders() probes OpenAI/Google/Azure/Latimer credentials through the settings manager (database-first, environment fallback, existing 5-minute cache). amazon-bedrock is always treated as configured because it can authenticate through the ambient AWS credential chain (ECS/Lambda IAM roles), which cannot be probed here. Fails open on probe errors so a settings outage cannot take chat down. - lib/nexus/model-router/router.ts: routeNexusRequest() filters the model pool to configured providers before any routed selection (text, image specialist, and required-tools enforcement paths all inherit the filter), logging the excluded providers. The fallback model — the client's explicitly selected model — is deliberately looked up in the UNFILTERED list, so an explicit selection of an unconfigured provider keeps its honest configuration error instead of being silently rerouted; router-off mode and shadow-mode legacy execution are unchanged. Tests: - lib/ai/__tests__/provider-credentials.test.ts (new): bedrock-only default, full-set when all keys present, whitespace keys rejected, Azure requires key AND resource name, fail-open on probe error. - lib/nexus/model-router/__tests__/router.test.ts: AUTO routing skips an unconfigured-provider candidate; router-off keeps the explicit model without its provider key; Advanced family with unconfigured provider fails with the clear family error; image intent does not route through an unconfigured provider. Existing 19 router tests unchanged (the new credential mock defaults to all-configured). Verification: - bunx jest lib/nexus lib/ai: 35 suites, 298 tests pass. - bun run lint and bun run typecheck: clean over the entire codebase. - Authenticated E2E: nexus-chat-pii-passthrough.functional.spec.ts passed 4/4 with --repeat-each=4 and retries disabled via scripts/test/e2e-local.sh. - Environment probe on the dev machine confirms getConfiguredChatProviders() returns only amazon-bedrock, so local AUTO routing is now deterministic (Bedrock-only) instead of classifier-dependent. docs/features/nexus-model-routing.md: request-flow step 5 now documents the credential filter and the explicit-selection carve-out.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d934a768aa
ℹ️ 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".
| config, | ||
| mode, | ||
| models, | ||
| models: routableModels, |
There was a problem hiding this comment.
Preserve the explicit fallback after routed candidates are exhausted
When active Standard/Auto routing has no accessible model from a configured provider, passing only routableModels means selectRoutedTextModel cannot find the explicitly selected fallback because it searches for that fallback inside its models argument. The request therefore throws No accessible Nexus model is available before provider creation instead of retaining the selected model and surfacing its provider-configuration error as the new comment and routing documentation specify. Keep the unconfigured fallback out of normal candidate selection, but allow it as the final non-routed fallback when no configured alternative exists.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Behavior locked in with tests in 60c8140, but keeping the fail-fast rather than executing the unconfigured explicit fallback: both outcomes are errors, and running a model whose provider creation must throw reproduces the exact stream-time missing-key 500 this PR removes — a misleading mid-stream crash instead of an honest routing error. The docs carve-out applies to the paths that execute the explicit model directly (router-off without required tools, shadow retention), which are unchanged.
Review: #1632 — exclude models with unconfigured provider credentials from routed selectionA well-scoped fix with a clear root-cause writeup and solid test coverage for the core cases (AUTO skip, router-off explicit-selection carve-out, Advanced-family clear error, image intent). A few notes: Code quality / correctness
Maintainability (nitpick)
Performance
Security
Test coverage
Docs
Overall: solid, well-reasoned fix for a real nondeterministic-failure root cause. The one item worth resolving before or after merge is confirming, with a test, that the router-off plus required-tools path degrades the way you intend when the fallback model own provider is not configured. |
… exhaustion behavior Answers the PR #1632 review asks (claude-review + Codex P2) on the credential filter's edge cases without changing behavior: - Router off + required tools + explicitly selected model on an unconfigured provider: routing selects a configured, tool-capable model (required_tools_enforced) instead of executing a model whose provider creation would throw at stream time. - No configured provider has an accessible model: routing fails fast with the clear 'No accessible Nexus model is available' error BEFORE provider creation. The Codex P2 suggestion to execute the unconfigured explicit model as a final fallback is intentionally declined — both outcomes are errors, but executing it reproduces the exact stream-time missing-key 500 this filter exists to prevent, with a misleading mid-stream failure instead of an honest routing error.
|
Review follow-up in 60c8140: added the two tests covering the edge both reviews flagged — (1) router-off + required tools + unconfigured explicit provider re-routes to a configured tool-capable model, (2) full exhaustion fails fast with the clear routing error before provider creation. Declined executing the unconfigured explicit model as a last resort (rationale in the inline reply): it would reproduce the stream-time missing-key 500 this PR eliminates. |
ReviewSolid, well-scoped fix — reviewed the diff, the CLAUDE.md-documented silent-failure patterns, and cross-checked the new probe against Correctness
Minor / non-blocking
Test coverageGood breadth — credential probe (5 cases covering whitespace keys, Azure pairing, fail-open) plus 5 router-level tests covering AUTO exclusion, router-off passthrough, Advanced-family failure, required-tools re-routing, and the image-specialist path. The "fails fast before provider creation" test directly encodes the regression this PR prevents. SecurityNo concerns — this only reads already-server-side settings to build an allow-list for routing; no new attack surface, no secrets logged (only provider names). Nothing blocking. Nice root-cause fix — addresses the actual gap (router's |
Problem
nexus-chat-pii-passthrough.functional.spec.tsfailed nondeterministically in full local E2E walls with "Failed to process chat request" (diagnosed 2026-08-10). In AUTO mode the Nexus model router's candidate lists can contain OpenAI/Google/Azure/Latimer models. On machines where that provider's key is not configured, the router still selected the model; provider creation then threw ("OpenAI API key not configured") deep in the streaming path andPOST /api/nexus/chat500'd. The router'sfallbackModelIdcannot catch this — routing has already returned by the time the provider factory throws. The nova-micro classifier's per-run variance made it intermittent.Fix
Router excludes models whose provider credentials are not configured (chosen over engaging the fallback on missing-key errors, which would need a retry around the whole stream-setup path, and over pinning a family in the spec, which would mask the same 500 for real local AUTO-mode users).
lib/ai/provider-credentials.ts(new):getConfiguredChatProviders()probes OpenAI/Google/Azure/Latimer credentials via the settings manager (DB-first, env fallback, existing 5-min cache).amazon-bedrockalways counts as configured — it authenticates through the ambient AWS credential chain (ECS/Lambda IAM roles), which cannot be probed. Fails open on probe errors so a settings outage cannot take chat down.lib/nexus/model-router/router.ts:routeNexusRequest()filters the model pool to configured providers before all routed selection (text, image-specialist, and required-tools paths), logging excluded providers. The client's explicitly selected model is looked up in the unfiltered list on purpose: explicit selection keeps its honest provider-configuration error instead of being silently rerouted. Router-off and shadow-mode legacy execution unchanged.docs/features/nexus-model-routing.md: request-flow step 5 documents the filter and the explicit-selection carve-out.Verification
bunx jest lib/nexus lib/ai: 35 suites / 298 tests pass. Fulllint+typecheckclean.--repeat-each=4and retries disabled; pre-push wall 319 passed / 0 failed.getConfiguredChatProviders()returns onlyamazon-bedrock(no provider keys in.env.local, empty settings rows), so local AUTO routing is now deterministic instead of classifier-dependent.