Skip to content

fix(proxy): let a ModelPool fail over when one member's model is missing - #1274

Merged
murdore merged 1 commit into
releasefrom
fix/model-not-found-retryable
Aug 4, 2026
Merged

murdore merged 1 commit into
releasefrom
fix/model-not-found-retryable

Conversation

@murdore

@murdore murdore commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

A pool member is an explicit {provider, model} pair, and members exist precisely because they differ. "This provider has no such model" is a fact about one member, not about the request — member#2 runs a different model and may well serve it.

The pool nonetheless treated it as non-retryable and stopped dead, which defeats the point of configuring a pool. Observed case: a member naming a retired Anthropic model, so the API answered 404 not_found_error, and the pool gave up instead of failing over to the member that was perfectly healthy.

[ModelPool] non-retryable: [anthropic] Anthropic error: 404
  {"type":"error","error":{"type":"not_found_error",
   "message":"model: claude-definitely-does-not-exist-v99"}}

The change

Adds isNonRetryableForPool, used at the two ModelPool catch sites in neurolink.ts. It differs from isNonRetryableProviderError in exactly one respect: model-not-found does not stop the pool.

The general contract is deliberately left alone. Provider fallback usually carries the same model id from one provider to the next, so retrying a typo across every provider would turn one crisp error into a slow fan-out of identical ones. Only the pool — where members are distinct by construction — gets the looser rule.

looksLikeModelNotFound is deliberately narrow:

  • a 404 alone does not qualify — only one that names a model — so an unrelated missing endpoint still stops the pool;
  • auth-flavoured messages are excluded outright, because PERMISSION_DENIED means the key is not entitled to the model, which every member sharing that key would hit identically.

NON_RETRYABLE_HTTP_STATUS_CODES is untouched, so generic HTTP retry behaviour is unchanged.

Verification

End to end against real credentials. The [LIVE] pool falls back from bogus model to real provider test in continuous-test-suite-model-pool.ts has been failing on release for exactly this reason. With this change:

test:model-pool   56 passed, 0 failed   (with Anthropic credentials)

New suite test:model-not-found-retryable — 21/21, covering:

  • what counts as model-not-found (Anthropic 404, Google NOT_FOUND, unknown model, typed InvalidModelError) and what does not (a bare 404 naming no model, PERMISSION_DENIED, UNAUTHENTICATED, LiteLLM team access-denied, rate limits, non-Error values);
  • that model-not-found is retryable for the pool while staying non-retryable generally — both halves asserted, so the split cannot silently collapse;
  • that 401 / 403 / 400 / typed ModelAccessDeniedError / PERMISSION_DENIED still stop the pool;
  • end-to-end through NeuroLink.generate(): member#1 404s and member#2 serves the request, plus the converse — an auth failure still short-circuits and member#2 is never attempted.

Negative control: reverting isNonRetryableForPool to the general contract fails exactly 2 of the 21 — the two that assert the new behaviour — and nothing else.

One note on the fixtures: the real SDK error carries status: 404, and it is the numeric status the classifier keys on, not the message. My first draft used a bare Error with only the text, which is already retryable today — so it would have passed with or without the fix. The fixture now carries the status, which is what makes the control meaningful.

Summary by CodeRabbit

  • Bug Fixes

    • Improved model pool failover so model-not-found errors can move to another available model.
    • Authentication and access-denied errors continue to stop retries promptly.
    • Preserved existing provider fallback behavior for non-retryable errors.
  • Tests

    • Added coverage for model-not-found detection, retry behavior, failover, and error exclusions.

Copilot AI lite review requested due to automatic review settings August 4, 2026 19:13
@github-actions

github-actions Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

✅ Single Commit Policy - COMPLIANT

Status: Policy requirements met • 1 commit • Valid format • Ready for merge

📊 View validation details

📝 Commit Details

  • Hash: abc18967fc6f71d1e788da51a64b57daebd95b29
  • Message: fix(proxy): let a ModelPool fail over when one member's model is missing
  • Author: Sachin Sharma

✅ Validation Results

  • Single commit requirement met
  • No merge commits in branch
  • Semantic commit message format verified
  • Ready for squash merge to release branch

🤖 Automated validation by NeuroLink Single Commit Enforcement

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@murdore, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 438c5eb3-383c-42cf-ab69-56a577b7072f

📥 Commits

Reviewing files that changed from the base of the PR and between 3c71ef0 and abc1896.

📒 Files selected for processing (4)
  • package.json
  • src/lib/neurolink.ts
  • src/lib/utils/providerErrorClassification.ts
  • test/continuous-test-suite-model-not-found-retryable.ts
📝 Walkthrough

Walkthrough

The change adds pool-specific provider error classification. Model-not-found failures now allow ModelPool failover, while general provider fallback and authentication handling retain their existing behavior. New tests cover classification, failover, and test-suite integration.

Changes

Model-not-found pool failover

Layer / File(s) Summary
Provider error classification
src/lib/utils/providerErrorClassification.ts, test/continuous-test-suite-model-not-found-retryable.ts
Adds model-not-found detection and pool-specific retryability rules. Tests cover recognized errors and excluded authentication, access, and malformed-request errors.
ModelPool failover handling
src/lib/neurolink.ts
Applies pool-specific classification to generation and streaming paths. Model-not-found failures can continue to another pool member.
Failover validation and test wiring
test/continuous-test-suite-model-not-found-retryable.ts, package.json
Adds end-to-end failover and authentication short-circuit tests. Runs the new suite from test:unit.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NeuroLink as NeuroLink.generate()
  participant ModelPool
  participant ProviderMember as Provider member
  NeuroLink->>ModelPool: Submit generation request
  ModelPool->>ProviderMember: Invoke current member
  ProviderMember-->>ModelPool: Return model-not-found error
  ModelPool->>ProviderMember: Invoke next member
  ProviderMember-->>NeuroLink: Return successful response
Loading

Possibly related PRs

Suggested reviewers: copilot, pdogra1299

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes ModelPool failover when a member's model is missing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/model-not-found-retryable

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.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

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 adjusts ModelPool failover behavior so that “model not found” errors are treated as retryable within the pool (allowing the next pool member to be attempted), while preserving the existing “non-retryable” contract for provider fallback outside ModelPool.

Changes:

  • Added looksLikeModelNotFound() + isNonRetryableForPool() to distinguish pool-scoped retryability from the general provider-fallback contract.
  • Updated the two ModelPool error catch sites in src/lib/neurolink.ts to use isNonRetryableForPool() instead of the general isNonRetryableProviderError().
  • Added a dedicated continuous test suite covering classification edge-cases and end-to-end NeuroLink.generate() ModelPool failover, and wired it into package.json.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
test/continuous-test-suite-model-not-found-retryable.ts New regression + classification suite asserting that model-not-found fails over in ModelPool while auth/access failures still short-circuit.
src/lib/utils/providerErrorClassification.ts Adds pool-specific retryability classifier and model-not-found detection helper.
src/lib/neurolink.ts Switches ModelPool short-circuit logic (generate + stream) to use the pool-scoped classifier.
package.json Adds test:model-not-found-retryable script and includes it in test:unit.

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

Comment on lines +72 to +82
const msg = error.message;
if (
msg.includes("PERMISSION_DENIED") ||
msg.includes("UNAUTHENTICATED") ||
looksLikeModelAccessDenied(error)
) {
return false;
}
const lower = msg.toLowerCase();
const namesAModel = lower.includes("model");
return (
Comment on lines +55 to +63
* Detects "the provider does not have this model": HTTP 404 model lookups,
* Google's NOT_FOUND, Anthropic's `not_found_error` naming the model, and the
* typed InvalidModelError providers raise for an unrecognised model id.
*
* Deliberately narrow. A 404 alone is not enough — only a 404 that names a
* model qualifies, so an unrelated missing endpoint stays non-retryable.
* Auth-flavoured messages are excluded outright: PERMISSION_DENIED means the
* key is not entitled to the model, which every member sharing that key would
* hit identically.
@Tara-ag

Tara-ag commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Summary ✅ APPROVED

This PR fixes a critical bug where ModelPool incorrectly treated "model not found" errors as non-retryable, preventing failover to alternative pool members that might have different models configured.

Changes Reviewed

1. src/lib/utils/providerErrorClassification.ts

  • ✅ Added looksLikeModelNotFound() - Detects model-not-found errors with proper text matching
  • ✅ Added isNonRetryableForPool() - ModelPool-specific classification that treats model-not-found as retryable
  • ✅ Excellent JSDoc explaining the rationale: pool members are distinct {provider, model} pairs
  • ✅ No breaking changes - existing functions remain unchanged

2. test/continuous-test-suite-model-not-found-retryable.ts

  • ✅ Comprehensive test coverage for all detection cases
  • ✅ Tests positive matches (Anthropic 404, Google NOT_FOUND, plain "model not found")
  • ✅ Tests negative cases correctly (bare 404, permission errors, rate limits)
  • ✅ End-to-end failover behavior tests through NeuroLink.generate()
  • ✅ Auth failure short-circuit still works correctly

3. package.json

  • ✅ Adds test script entry for new test suite

Impact Analysis (via code knowledge graph)

Functions affected:

  • NeuroLink.directProviderGeneration (line 8370) - Direct provider generation with retries
  • NeuroLink.performMCPGenerationRetries (line 7157) - MCP streaming with retries
  • Pool routing logic in ModelPool

No callers were broken - only new functions added, existing contract preserved

Compliance Check

  • ✅ TypeScript types used (no interfaces)
  • ✅ Types imported from canonical ../types/index.js barrel
  • ✅ No hardcoded secrets or credentials
  • ✅ Proper error handling (errors returned, not thrown)
  • ✅ Comprehensive test coverage added
  • ✅ Backward compatible - no breaking changes
  • ✅ Follows project conventions (CLAUDE.md rules respected)

Recommendation

APPROVE - This is a well-documented, thoroughly tested fix for a real production issue. The change correctly handles the distinction between general fallback (same model ID) vs pool routing (different model IDs).

@murdore
murdore force-pushed the fix/model-not-found-retryable branch from 3c71ef0 to a212133 Compare August 4, 2026 19:31
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

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

Summary

This PR adds a ModelPool-specific retryability rule that allows the pool to fail over when one member's model is missing, while preserving the general contract where provider fallback typically carries the same model ID across providers.

Key Changes:

  1. Added looksLikeModelNotFound() function to detect model-not-found errors (Anthropic 404, Google NOT_FOUND, InvalidModelError)
  2. Added isNonRetryableForPool() which treats model-not-found as retryable (pool members differ by design)
  3. Changed two ModelPool error handlers from isNonRetryableProviderError to isNonRetryableForPool
  4. Added comprehensive test suite (21 tests) covering detection logic and end-to-end failover

Impact: Fixes regression where a pool with one dead member would stop entirely instead of failing over to healthy members. The observed case was an Anthropic member naming a retired model (404), where the pool should have tried the OpenAI member.

Verification: Test suite test:model-not-found-retryable.ts passes 21/21, including end-to-end failover scenarios. Existing test:model-pool continues to pass. No breaking changes to provider fallback behavior.

Decision: APPROVED

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

Summary

This PR adds a ModelPool-specific retryability rule that allows the pool to fail over when one member's model is missing, while preserving the general contract where provider fallback typically carries the same model ID across providers.

Key Changes:

  1. Added looksLikeModelNotFound() function to detect model-not-found errors (Anthropic 404, Google NOT_FOUND, InvalidModelError)
  2. Added isNonRetryableForPool() which treats model-not-found as retryable (pool members differ by design)
  3. Changed two ModelPool error handlers from isNonRetryableProviderError to isNonRetryableForPool
  4. Added comprehensive test suite (21 tests) covering detection logic and end-to-end failover

Impact: Fixes regression where a pool with one dead member would stop entirely instead of failing over to healthy members. The observed case was an Anthropic member naming a retired model (404), where the pool should have tried the OpenAI member.

Verification: Test suite test:model-not-found-retryable.ts passes 21/21, including end-to-end failover scenarios. Existing test:model-pool continues to pass. No breaking changes to provider fallback behavior.

Decision: APPROVED

A pool member is an explicit {provider, model} pair, and members exist
precisely because they differ. "This provider has no such model" is a
fact about ONE member, not about the request — member#2 runs a different
model and may well serve it. The pool nonetheless treated it as
non-retryable and stopped dead, which defeats the point of configuring a
pool: a member naming a retired Anthropic model made the API answer 404
not_found_error, and the pool gave up instead of failing over to the
member that was perfectly healthy.

Adds isNonRetryableForPool, used at the two ModelPool catch sites in
neurolink.ts. It differs from isNonRetryableProviderError in exactly one
respect: model-not-found does not stop the pool.

The general contract is deliberately left alone. Provider fallback
usually carries the same model id from one provider to the next, so
retrying a typo across every provider would turn one crisp error into a
slow fan-out of identical ones. Only the pool, where members are distinct
by construction, gets the looser rule.

looksLikeModelNotFound is deliberately narrow. A 404 alone does not
qualify — only one that names a model — so an unrelated missing endpoint
still stops the pool. Auth-flavoured messages are excluded outright:
PERMISSION_DENIED means the key is not entitled to the model, which every
member sharing that key would hit identically.

Verified end to end: the [LIVE] pool-fallback test in
continuous-test-suite-model-pool.ts has been failing against real
Anthropic credentials for exactly this reason (404 classified
non-retryable, so no fallback). With this change that suite is 56/56.
@murdore
murdore force-pushed the fix/model-not-found-retryable branch from a212133 to abc1896 Compare August 4, 2026 19:40
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore
murdore merged commit 8ae086b into release Aug 4, 2026
15 of 16 checks passed
@murdore
murdore deleted the fix/model-not-found-retryable branch August 4, 2026 19:50
@Tara-ag

Tara-ag commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review summary for PR #1274: fix(proxy): let a ModelPool fail over when one member's model is missing

Decision: APPROVED ✅

Summary:
This PR fixes an important bug where ModelPool incorrectly treated "model not found" errors as non-retryable, preventing failover to alternative providers/models in the pool. The fix correctly distinguishes between:

  • Model-specific errors (e.g., "does not exist", "404 Not Found" for a specific model) → retryable (try next provider in pool)
  • Request-level errors (e.g., auth failures, permission denied, rate limits) → non-retryable (stop entire pool, affects all providers)

Files Reviewed:

  1. package.json - Added test command (no issues)
  2. src/lib/utils/providerErrorClassification.ts - Added looksLikeModelNotFound() and isNonRetryableForPool() functions
  3. src/lib/neurolink.ts - Imported and used isNonRetryableForPool() in model pool logic
  4. test/continuous-test-suite-model-not-found-retryable.ts - New comprehensive test suite

Key Improvements:

  • ✅ Allows automatic failover when a specific model is unavailable
  • ✅ Preserves short-circuit behavior for request-level errors
  • ✅ Comprehensive test coverage (4 test cases covering various scenarios)
  • ✅ No breaking changes to existing functionality
  • ✅ Clean code organization with shared utilities

Impact Analysis:

  • ~500 nodes impacted across the codebase (model pool routing)
  • Affects production flow: handleAuth → useWorkflow
  • Change is self-contained within error classification logic
  • No architectural hotspots touched

Test Results:
All tests pass and cover:

  • Model not found → failover works
  • Auth failure → pool stops immediately
  • Edge cases handled correctly
  • Regression tests prevent weakening the short-circuit

Recommendation:
This is a safe, well-tested fix that improves reliability in multi-provider deployments. Approved for merge.

@Tara-ag

Tara-ag commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Yama Review - APPROVED ✅

No issues found. This is a clean, well-tested fix for model-not-found retryability in ModelPool.

Summary

The PR correctly implements the intended behavior: when a specific {provider, model} pair returns a "model not found" error, the ModelPool should fail over to try other providers/models in the pool, rather than giving up entirely.

Key Changes Verified:

  1. looksLikeModelNotFound(error) - Correctly identifies model-specific 404 errors by checking:

    • Status code 404 with model-specific messages
    • Error messages containing "NOT_FOUND", "does not exist", etc.
    • Properly excludes bare 404s and permission-denied errors
  2. isNonRetryableForPool(error) - Smart retry logic:

    • Returns false (retryable) for model-not-found → failover to next provider
    • Delegates to isNonRetryableProviderError() for all other errors
    • Auth failures, rate limits, permissions still stop the entire pool
  3. Integration in neurolink.ts - Uses new function in model pool retry loop

Test Coverage:

  • ✅ Model not found → member#2 is attempted (failover works)
  • ✅ Auth failure → member#2 never attempted (pool stops)
  • ✅ Various edge cases handled
  • ✅ Regression tests prevent breaking existing behavior

Impact:

  • ~500 nodes affected (model pool routing system)
  • Self-contained change - no breaking API modifications
  • Existing callers benefit from improved reliability automatically

Recommendation:

Safe to merge. Improves production reliability in multi-provider deployments without introducing risks.

@Tara-ag

Tara-ag commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🔍 VERIFYING: Reviewing PR #1274 - Model-Not-Found Retryability Fix

Summary

This PR fixes a regression where ModelPool would stop on encountering a "model not found" error from one member, instead of failing over to another member with a different model. The change makes model-not-found errors retryable within pools while keeping them non-retryable for general provider fallback (since the same typo would fail across all providers).

Impact Analysis

  • Changed files: 4 files (package.json, src/lib/neurolink.ts, src/lib/utils/providerErrorClassification.ts, test/continuous-test-suite-model-not-found-retryable.ts)
  • Risk score: 0.65 (medium)
  • Affected flows: 208 execution flows
  • Test gaps: 3 (NeuroLink class, directProviderGeneration, createMCPStream methods)

The change touches core error handling in NeuroLink's model pool logic. Let me review file-by-file starting with the utility functions.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 10.8.18 🎉

The release is available on:

Your semantic-release bot 📦🚀

@Tara-ag

Tara-ag commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

YAMA Review Summary for PR #1274

Decision: CHANGES_REQUESTED

Summary

This PR fixes a regression where ModelPool would stop on encountering a "model not found" error from one member, instead of failing over to another member with a different model. The change makes model-not-found errors retryable within pools while keeping them non-retryable for general provider fallback.

Findings

🔒 CRITICAL (1)

File: src/lib/utils/providerErrorClassification.ts (line 82)

  • Issue: Auth error detection uses case-sensitive string matching (PERMISSION_DENIED, UNAUTHENTICATED) which allows lowercase auth errors to bypass the exclusion check
  • Impact: If an auth error arrives with lowercase text like "permission denied" or without a numeric status code, it would be treated as retryable, allowing ModelPool to fail over to other members despite having invalid credentials
  • Fix: Change lines 82-83 to use the already-computed lower variable for case-insensitive matching: if (lower.includes('permission denied') || lower.includes('unauthenticated'))

💡 MINOR (1)

File: src/lib/utils/providerErrorClassification.ts (line 63)

  • Issue: JSDoc documentation claims the function detects "HTTP 404 model lookups" but the implementation never checks status codes - it only looks at message content and error types
  • Impact: Creates confusion for developers reading the code who might assume HTTP status validation occurs
  • Fix: Rewrite JSDoc to accurately describe the actual detection logic

Impact on Existing Code

  • Blast radius: 208 execution flows affected (core error handling paths)
  • Changed files: 4 files (package.json, src/lib/neurolink.ts, src/lib/utils/providerErrorClassification.ts, test/continuous-test-suite-model-not-found-retryable.ts)
  • Risk score: 0.65 (medium)
  • Unmodified dependents: None found - changes are self-contained to error classification logic

Review Scope

Reviewed all 4 changed files:

  1. ✅ package.json - Correctly adds test script (no issues)
  2. ✅ src/lib/neurolink.ts - Properly uses new pool-scoped error classification (no issues)
  3. ⚠️ src/lib/utils/providerErrorClassification.ts - Core logic correct but has 2 issues (CRITICAL + MINOR)
  4. ✅ test/continuous-test-suite-model-not-found-retryable.ts - Comprehensive test coverage (no issues)

Conclusion

The core fix is sound and well-tested, but there's a CRITICAL security issue with case-sensitive auth error detection that must be fixed before merging. Once resolved, this would be APPROVED.

@Tara-ag

Tara-ag commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🔒 CRITICAL: Case-sensitive auth error detection creates security vulnerability

Location: src/lib/utils/providerErrorClassification.ts line 82-83

Issue: The function uses case-sensitive string matching:

msg.includes("PERMISSION_DENIED") || msg.includes("UNAUTHENTICATED")

Impact: If an auth error arrives with lowercase text like "permission denied" or without a numeric status code, it bypasses the exclusion check. This means:

  1. ModelPool treats it as retryable
  2. Other members share the same credentials and fail identically
  3. User gets confusing errors after trying multiple providers

Evidence: Test at test/continuous-test-suite-model-not-found-retryable.ts:L237 explicitly expects: "member#2 must NOT be attempted after an auth failure"

Fix: Use the already-computed lower variable for case-insensitive matching:

if (lower.includes('permission denied') || lower.includes('unauthenticated')) {
  return false;
}

⚠️ This blocks the PR until fixed.

@Tara-ag

Tara-ag commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

💡 MINOR: JSDoc documentation doesn't match implementation

Location: src/lib/utils/providerErrorClassification.ts line 53-61

Issue: The JSDoc says:

Detects "the provider does not have this model": HTTP 404 model lookups,
Google's NOT_FOUND, Anthropic's `not_found_error` naming the model...
only a 404 that names a model qualifies

But the implementation (lines 65-90) NEVER checks status codes - it only looks at:

  • Typed error classes (InvalidModelError)
  • Error message content containing "model not found" phrases

Impact: Developers reading the code will assume HTTP status validation occurs when it doesn't. This creates confusion, especially since isNonRetryableProviderError() DOES key off numeric status codes.

Fix: Rewrite JSDoc to accurately describe the actual detection logic:

/**
 * Detects errors indicating "the provider does not have this model": typed
 * InvalidModelError instances, and Error messages containing model-related
 * "not found" phrases (e.g., "model not found", "unknown model").
 * 
 * Deliberately narrow: excludes bare 404s that don't mention models, and
 * excludes auth-flavoured messages (PERMISSION_DENIED, UNAUTHENTICATED).
 * Does not inspect HTTP status codes - relies on message content and error type.
 */

@Tara-ag

Tara-ag commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🛡️ Yama Review Verdict: BLOCKED

Severity counts — 🔒 CRITICAL: 2 · ⚠️ MAJOR: 0 · 💡 MINOR: 2

This PR correctly implements model-not-found retryability within ModelPool to allow failover when a provider member lacks the requested model. However, there is a CRITICAL security issue: auth error detection uses case-sensitive string matching which allows lowercase auth errors to bypass the exclusion check. This could cause ModelPool to fail over to other members despite invalid credentials, confusing users who would get errors after trying multiple providers. The fix requires changing line 82-83 to use the already-computed lower variable for case-insensitive matching. Additionally, there's a MINOR documentation issue where JSDoc claims 'HTTP 404' detection but implementation never checks status codes.

Findings behind this verdict

  • 🔒 CRITICAL: Case-sensitive auth error detection
    ⚠️ No evidence provided (no file or description) — verify before acting on this finding.
  • 🔒 CRITICAL: Case-sensitive auth error detection creates vulnerability — src/lib/utils/providerErrorClassification.ts:82
    Auth error detection uses case-sensitive string matching (PERMISSION_DENIED, UNAUTHENTICATED) which allows lowercase auth errors like 'permission denied' to bypass the exclusion check. If an auth error arrives without numeric status code or with lowercase text, it would be treated as retryable, allowing ModelPool to fail over to other members sharing the same credentials - all failing identically …
  • 💡 MINOR: JSDoc documentation doesn't match implementation
    ⚠️ No evidence provided (no file or description) — verify before acting on this finding.
  • 💡 MINOR: JSDoc documentation doesn't match implementation — src/lib/utils/providerErrorClassification.ts:63
    The JSDoc comment claims the function detects 'HTTP 404 model lookups' and mentions 'only a 404 that names a model qualifies', but the actual implementation (lines 65-90) never checks HTTP status codes - it only examines error message content and typed error classes.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants