Skip to content

fix(routing): survive upstream 410 Gone + retire dead Ollama Cloud model - #8

Merged
aliihsaad merged 5 commits into
mainfrom
fix/ollama-410-fallback
Jul 22, 2026
Merged

fix(routing): survive upstream 410 Gone + retire dead Ollama Cloud model#8
aliihsaad merged 5 commits into
mainfrom
fix/ollama-410-fallback

Conversation

@aliihsaad

@aliihsaad aliihsaad commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Problem

model: "auto" returned 502 on every request (VPS and local): auto-route's top-ranked candidate qwen3-coder:480b (Ollama Cloud, intelligence_rank 2) was retired upstream and now answers 410 Gone. classifyProviderError had no case for 410, so it fell through to other (retryable: false, block: none):

  • the fallback chain aborted with a 502 instead of trying the next model, and
  • no runtime-health block was recorded, so the dead model stayed first in the chain forever.

Other pinned models' fallback chains also terminated on it.

Fix

  1. c545ff9 — classify 410 like 404: model_unavailable, retryable, skipModel. Auto-route now skips the dead model and continues the chain, records the 24h runtime-health block, and pinned requests fail honestly instead of wandering.
  2. 317a135 — V19 migration disables the retired qwen3-coder:480b row (V18 precedent). Probed same-day: gemma4:31b, gpt-oss:120b, gpt-oss:20b still answer 200 and stay enabled.

Also ports the two VPS-local commits upstream so nothing diverges (90a11b9 loopback bind — dist artifacts stripped, 160d60e Google audio transcription fallback; both running in production since Jul 8/12).

Validation (TDD)

  • New failing tests written first: 410 classification (was other/false/false) + V19 migration assertions — then the fix; both now pass.
  • Full server suite: 243/243 ✅ · npm run build clean.
  • Live production evidence in PR context: probes of all 10 Ollama Cloud rows, 7 dead via 410-poisoned chains, 3 alive.

Summary by CodeRabbit

  • New Features

    • Added Google Gemini audio transcription support, including plain-text and JSON response formats.
    • Added configurable server host binding through the HOST environment variable.
    • Added Gemini transcription capability to model routing.
  • Bug Fixes

    • Improved handling of retired or unavailable models so routing can continue appropriately.
    • Added recovery handling for restricted provider access.
  • Maintenance

    • Disabled a retired Ollama Cloud model while preserving its configuration.

root and others added 4 commits July 22, 2026 21:32
Ollama Cloud answers 410 Gone for models it has retired (e.g.
qwen3-coder:480b). The classifier had no case for 410, so it fell through
to 'other' (retryable: false, block: none): the fallback chain aborted with
a 502 instead of trying the next model, and no runtime-health block was
ever recorded, so auto-route kept picking the dead model on every request.

410 now classifies like 404 (model_unavailable, retryable, skipModel),
which makes auto-route skip it and continue the chain, records the 24h
runtime-health block, and lets pinned requests fail honestly.
Live-verified 2026-07-22: upstream returns 410 Gone for this model. With
intelligence_rank 2 it sat at the top of the auto-route chain, so every
'model: auto' request died on it before the classifier fix. Disabled, not
deleted, per V18 precedent. gemma4:31b, gpt-oss:120b and gpt-oss:20b were
probed the same day and still answer 200, so they stay enabled.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (24)
  • client/dist/assets/LLM-HUB (1)-BEcCqMjq.svg is excluded by !**/dist/**, !**/*.svg
  • client/dist/assets/index-BC7vwQDA.js is excluded by !**/dist/**
  • client/dist/assets/index-gKqPbOvw.css is excluded by !**/dist/**
  • client/dist/llm-hub-logo.svg is excluded by !**/dist/**, !**/*.svg
  • server/dist/db/index.d.ts.map is excluded by !**/dist/**, !**/*.map
  • server/dist/db/index.js is excluded by !**/dist/**
  • server/dist/db/index.js.map is excluded by !**/dist/**, !**/*.map
  • server/dist/index.js is excluded by !**/dist/**
  • server/dist/index.js.map is excluded by !**/dist/**, !**/*.map
  • server/dist/lib/app-settings.d.ts is excluded by !**/dist/**
  • server/dist/lib/app-settings.d.ts.map is excluded by !**/dist/**, !**/*.map
  • server/dist/lib/app-settings.js is excluded by !**/dist/**
  • server/dist/lib/app-settings.js.map is excluded by !**/dist/**, !**/*.map
  • server/dist/lib/resolve-model.d.ts is excluded by !**/dist/**
  • server/dist/lib/resolve-model.d.ts.map is excluded by !**/dist/**, !**/*.map
  • server/dist/lib/resolve-model.js is excluded by !**/dist/**
  • server/dist/lib/resolve-model.js.map is excluded by !**/dist/**, !**/*.map
  • server/dist/providers/google.d.ts is excluded by !**/dist/**
  • server/dist/providers/google.d.ts.map is excluded by !**/dist/**, !**/*.map
  • server/dist/providers/google.js is excluded by !**/dist/**
  • server/dist/providers/google.js.map is excluded by !**/dist/**, !**/*.map
  • server/dist/services/provider-errors.d.ts.map is excluded by !**/dist/**, !**/*.map
  • server/dist/services/provider-errors.js is excluded by !**/dist/**
  • server/dist/services/provider-errors.js.map is excluded by !**/dist/**, !**/*.map

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3175e82b-3df4-4e00-b138-7e0441ff0eb3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds Google Gemini audio transcription, expands provider error classification, disables a retired Ollama model through migration V19, seeds Google transcription routing, and makes server host binding configurable.

Changes

Google audio transcription

Layer / File(s) Summary
Google transcription flow
server/src/providers/google.ts, server/src/db/index.ts, server/src/__tests__/providers/google.test.ts
GoogleProvider.transcribeAudio sends inline audio to Gemini, extracts transcript text, supports text or JSON responses, and is registered for transcription routing with coverage tests.

Provider errors and model retirement

Layer / File(s) Summary
Provider failure classification
server/src/services/provider-errors.ts, server/src/__tests__/services/provider-errors.test.ts
Restricted-organization errors are retryable authentication failures, while HTTP 410 errors are retryable model-unavailable failures that skip models for routing.
Ollama retirement migration
server/src/db/index.ts, server/src/__tests__/db/ollama-410-v19.test.ts
Migration V19 disables ollama/qwen3-coder:480b while retaining its database row, and tests verify retired and active model states.

Configurable server host binding

Layer / File(s) Summary
HOST-based server binding
server/src/index.ts
Server listening and startup URLs now use HOST, defaulting to 127.0.0.1.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GoogleProvider
  participant GeminiAPI
  Client->>GoogleProvider: Submit audio transcription request
  GoogleProvider->>GeminiAPI: Send inlineData audio to generateContent
  GeminiAPI-->>GoogleProvider: Return candidate transcript
  GoogleProvider-->>Client: Return text or JSON result
Loading

Possibly related PRs

🚥 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 reflects the main routing fix for 410 Gone responses and the Ollama Cloud model retirement.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ollama-410-fallback

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
server/src/__tests__/services/provider-errors.test.ts (1)

5-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the 24-hour cooldown and cover both message variants.

This test only checks that keyCooldownMs is positive and exercises “organization has been restricted”; the new branch also supports “organization is restricted” and promises a 24-hour cooldown. Assert 24 * 60 * 60 * 1000 and add coverage for the second form.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/__tests__/services/provider-errors.test.ts` around lines 5 - 17,
Update the provider-error tests around classifyProviderError to assert
keyCooldownMs equals 24 * 60 * 60 * 1000 instead of only being positive, and add
a case covering the “organization is restricted” message variant. Preserve the
existing auth, retryable, skipModel, and canRetryProviderFailure expectations
for both variants.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/__tests__/db/ollama-410-v19.test.ts`:
- Around line 14-17: Update the beforeAll hook around initDb to be asynchronous
and await its returned promise, ensuring database initialization and any
failures complete within Vitest’s setup lifecycle before tests run.

In `@server/src/index.ts`:
- Around line 15-16: Update the startup logging around the server URL messages
to derive a normalized display host separate from the bind HOST: bracket IPv6
literals when they are usable destinations, and for wildcard binds such as
0.0.0.0 or :: avoid emitting a clickable proxy endpoint URL. Preserve the
server-running message while ensuring it does not advertise wildcard addresses
as client destinations.

In `@server/src/providers/google.ts`:
- Around line 762-771: Update the transcription response handling around
GeminiResponse and the extracted transcript to inspect
candidates[0].finishReason before returning success. Treat MAX_TOKENS as an
error by throwing instead of returning the partial transcript, while preserving
the existing no-text validation and successful response formatting for other
finish reasons.

---

Nitpick comments:
In `@server/src/__tests__/services/provider-errors.test.ts`:
- Around line 5-17: Update the provider-error tests around classifyProviderError
to assert keyCooldownMs equals 24 * 60 * 60 * 1000 instead of only being
positive, and add a case covering the “organization is restricted” message
variant. Preserve the existing auth, retryable, skipModel, and
canRetryProviderFailure expectations for both variants.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 324b5b1f-5512-4dc8-a08a-0ee0a135328b

📥 Commits

Reviewing files that changed from the base of the PR and between efb8546 and 317a135.

📒 Files selected for processing (7)
  • server/src/__tests__/db/ollama-410-v19.test.ts
  • server/src/__tests__/providers/google.test.ts
  • server/src/__tests__/services/provider-errors.test.ts
  • server/src/db/index.ts
  • server/src/index.ts
  • server/src/providers/google.ts
  • server/src/services/provider-errors.ts

Comment on lines +14 to +17
beforeAll(() => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching ollama test / db init:"
fd -i 'ollama-410-v19\.test\.ts|index\.ts$' server/src 2>/dev/null || true

echo
echo "Target test excerpt:"
if [ -f server/src/__tests__/db/ollama-410-v19.test.ts ]; then
  nl -ba server/src/__tests__/db/ollama-410-v19.test.ts | sed -n '1,80p'
fi

echo
echo "Find initDb declarations/usages:"
rg -n "function initDb|const initDb|initDb\(" server/src --glob '*.ts' || true

echo
echo "Relevant db index excerpts:"
for f in $(fd -i 'index\.ts$' server/src/db 2>/dev/null); do
  echo "--- $f"
  nl -ba "$f" | sed -n '1,220p'
done

echo
echo "Git status/diff stat:"
git diff --stat || true
git diff -- server/src/__tests__/db/ollama-410-v19.test.ts server/src/db/index.ts | sed -n '1,220p' || true

Repository: aliihsaad/LLM_HUB

Length of output: 376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Target test excerpt:"
if [ -f server/src/__tests__/db/ollama-410-v19.test.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' server/src/__tests__/db/ollama-19.test.ts 2>/dev/null | sed -n '1,120p' || true
  awk '{printf "%6d\t%s\n", NR, $0}' server/src/__tests__/db/ollama-410-v19.test.ts 2>/dev/null | sed -n '1,140p' || true
fi

echo
echo "Find initDb declarations/usages:"
rg -n "function initDb|const initDb|initDb\(" server/src --glob '*.ts' || true

echo
echo "Relevant db index excerpts:"
if [ -f server/src/db/index.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' server/src/db/index.ts | sed -n '1,260p'
fi

echo
echo "Relevant server index excerpts around db initialization imports:"
if [ -f server/src/index.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' server/src/index.ts | sed -n '1,220p'
fi

echo
echo "Git diff stat:"
git diff --stat || true
git diff -- server/src/__tests__/db/ollama-410-v19.test.ts server/src/db/index.ts server/src/index.ts | sed -n '1,260p' || true

Repository: aliihsaad/LLM_HUB

Length of output: 19783


Await the asynchronous database initialization.

initDb returns a promise, but this synchronous beforeAll hook does not wait for it. The current setup may run the synchronous seed/migrate phase before the first async import, while later initialization and failures can still occur outside Vitest’s setup lifecycle.

Proposed fix
-  beforeAll(() => {
+  beforeAll(async () => {
     process.env.ENCRYPTION_KEY = '0'.repeat(64);
-    initDb(':memory:');
+    await initDb(':memory:');
   });
📝 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
beforeAll(() => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
});
beforeAll(async () => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
await initDb(':memory:');
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/__tests__/db/ollama-410-v19.test.ts` around lines 14 - 17, Update
the beforeAll hook around initDb to be asynchronous and await its returned
promise, ensuring database initialization and any failures complete within
Vitest’s setup lifecycle before tests run.

Comment thread server/src/index.ts
Comment on lines +15 to +16
console.log(`Server running on http://${HOST}:${PORT}`);
console.log(`Proxy endpoint: http://${HOST}:${PORT}/v1/chat/completions`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not advertise every bind host as a directly usable URL.

For HOST=0.0.0.0 or HOST=::, the logged URL is not a reliable client destination; IPv6 literals also require brackets. Use a separate, normalized display host and avoid emitting a clickable endpoint for wildcard binds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/index.ts` around lines 15 - 16, Update the startup logging around
the server URL messages to derive a normalized display host separate from the
bind HOST: bracket IPv6 literals when they are usable destinations, and for
wildcard binds such as 0.0.0.0 or :: avoid emitting a clickable proxy endpoint
URL. Preserve the server-running message while ensuring it does not advertise
wildcard addresses as client destinations.

Comment on lines +762 to +771
const data = await res.json() as GeminiResponse;
const transcript = extractText(data.candidates?.[0]?.content?.parts)?.trim();
if (!transcript) {
throw new Error('Google transcription returned no text');
}

const wantsText = request.response_format === 'text';
return {
body: wantsText ? transcript : { text: transcript },
contentType: wantsText ? 'text/plain; charset=utf-8' : 'application/json',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not return truncated Gemini output as a successful transcript.

finishReason can be MAX_TOKENS, but this method returns any extracted text as success. Long audio can therefore produce a partial transcript with HTTP 200. Check candidate.finishReason before returning; the API documents MAX_TOKENS as output truncation. (ai.google.dev)

Proposed guard
     const data = await res.json() as GeminiResponse;
-    const transcript = extractText(data.candidates?.[0]?.content?.parts)?.trim();
+    const candidate = data.candidates?.[0];
+    if (candidate?.finishReason && candidate.finishReason !== 'STOP') {
+      throw new Error(`Google transcription stopped with ${candidate.finishReason}`);
+    }
+    const transcript = extractText(candidate?.content?.parts)?.trim();
📝 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
const data = await res.json() as GeminiResponse;
const transcript = extractText(data.candidates?.[0]?.content?.parts)?.trim();
if (!transcript) {
throw new Error('Google transcription returned no text');
}
const wantsText = request.response_format === 'text';
return {
body: wantsText ? transcript : { text: transcript },
contentType: wantsText ? 'text/plain; charset=utf-8' : 'application/json',
const data = await res.json() as GeminiResponse;
const candidate = data.candidates?.[0];
if (candidate?.finishReason && candidate.finishReason !== 'STOP') {
throw new Error(`Google transcription stopped with ${candidate.finishReason}`);
}
const transcript = extractText(candidate?.content?.parts)?.trim();
if (!transcript) {
throw new Error('Google transcription returned no text');
}
const wantsText = request.response_format === 'text';
return {
body: wantsText ? transcript : { text: transcript },
contentType: wantsText ? 'text/plain; charset=utf-8' : 'application/json',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/providers/google.ts` around lines 762 - 771, Update the
transcription response handling around GeminiResponse and the extracted
transcript to inspect candidates[0].finishReason before returning success. Treat
MAX_TOKENS as an error by throwing instead of returning the partial transcript,
while preserving the existing no-text validation and successful response
formatting for other finish reasons.

Source: MCP tools

@aliihsaad
aliihsaad merged commit 301e91a into main Jul 22, 2026
2 checks passed
@aliihsaad
aliihsaad deleted the fix/ollama-410-fallback branch July 22, 2026 19:52
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