-
Notifications
You must be signed in to change notification settings - Fork 0
fix(routing): survive upstream 410 Gone + retire dead Ollama Cloud model #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
90a11b9
160d60e
c545ff9
317a135
ee5a94d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { describe, it, expect, beforeAll } from 'vitest'; | ||
| import { initDb, getDb } from '../../db/index.js'; | ||
|
|
||
| // Live-verified 2026-07-22: Ollama Cloud answers `410 Gone` for | ||
| // qwen3-coder:480b — the hosted model was removed upstream. With | ||
| // intelligence_rank 2 the row sat at the top of the auto-route chain, so | ||
| // every `model: "auto"` request died on it before the 410 classifier fix. | ||
| const RETIRED_OLLAMA_MODELS = ['qwen3-coder:480b']; | ||
|
|
||
| // Probed the same day through /v1/chat/completions: these still answer 200. | ||
| const LIVE_OLLAMA_MODELS = ['gemma4:31b', 'gpt-oss:120b', 'gpt-oss:20b']; | ||
|
|
||
| describe('V19 Ollama Cloud retirement', () => { | ||
| beforeAll(() => { | ||
| process.env.ENCRYPTION_KEY = '0'.repeat(64); | ||
| initDb(':memory:'); | ||
| }); | ||
|
|
||
| it('disables the Ollama Cloud rows the upstream deleted (410 Gone)', () => { | ||
| const db = getDb(); | ||
| for (const modelId of RETIRED_OLLAMA_MODELS) { | ||
| const row = db | ||
| .prepare("SELECT enabled FROM models WHERE platform = 'ollama' AND model_id = ?") | ||
| .get(modelId) as { enabled: number } | undefined; | ||
| expect(row, `${modelId} should exist in the catalog`).toBeDefined(); | ||
| expect(row!.enabled, `${modelId} must be disabled — upstream returns 410`).toBe(0); | ||
| } | ||
| }); | ||
|
|
||
| it('keeps the still-hosted Ollama Cloud models enabled', () => { | ||
| const db = getDb(); | ||
| for (const modelId of LIVE_OLLAMA_MODELS) { | ||
| const row = db | ||
| .prepare("SELECT enabled FROM models WHERE platform = 'ollama' AND model_id = ?") | ||
| .get(modelId) as { enabled: number } | undefined; | ||
| expect(row, `${modelId} should exist in the catalog`).toBeDefined(); | ||
| expect(row!.enabled, `${modelId} should stay enabled`).toBe(1); | ||
| } | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,14 +5,15 @@ import { startHealthChecker } from './services/health.js'; | |
| import { startModelScout } from './services/model-scout.js'; | ||
|
|
||
| const PORT = process.env.PORT ?? 3001; | ||
| const HOST = process.env.HOST ?? '127.0.0.1'; | ||
|
|
||
| async function main() { | ||
| await initDb(); | ||
| const app = createApp(); | ||
|
|
||
| app.listen(Number(PORT), '0.0.0.0', () => { | ||
| console.log(`Server running on http://0.0.0.0:${PORT}`); | ||
| console.log(`Proxy endpoint: http://0.0.0.0:${PORT}/v1/chat/completions`); | ||
| app.listen(Number(PORT), HOST, () => { | ||
| console.log(`Server running on http://${HOST}:${PORT}`); | ||
| console.log(`Proxy endpoint: http://${HOST}:${PORT}/v1/chat/completions`); | ||
|
Comment on lines
+15
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🤖 Prompt for AI Agents |
||
| startHealthChecker(); | ||
| startModelScout(); | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,8 @@ import type { | |||||||||||||||||||||||||||||||||||||||||||||||||
| ChatToolCall, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ChatToolChoice, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ChatToolDefinition, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioTextResult, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioTranscriptionRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ImageEditRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ImageFileUpload, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ImageGenerationRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -715,6 +717,62 @@ export class GoogleProvider extends BaseProvider { | |||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| async transcribeAudio( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apiKey: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| request: AudioTranscriptionRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| modelId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<AudioTextResult> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!request.file) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Google transcription requires an uploaded audio file'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const instructions = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Transcribe the supplied audio exactly.', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Return only the transcript without commentary, labels, quotation marks, or Markdown.', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| request.language ? `The expected language code is ${request.language}.` : '', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| request.prompt ? `Context that may help disambiguate names: ${request.prompt}` : '', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ].filter(Boolean).join(' '); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| contents: [{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| parts: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { text: instructions }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inlineData: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| mimeType: request.file.contentType || 'audio/wav', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data: Buffer.from(request.file.data).toString('base64'), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| generationConfig: { temperature: 0 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = `${API_BASE}/models/${modelId}:generateContent?key=${apiKey}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const res = await this.fetchWithTimeout(url, { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { 'Content-Type': 'application/json' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| body: JSON.stringify(body), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 120000); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!res.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const err = await res.json().catch(() => ({})); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Google API error ${res.status}: ${(err as any).error?.message ?? res.statusText}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| 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', | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+762
to
+771
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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
Suggested change
🤖 Prompt for AI AgentsSource: MCP tools |
||||||||||||||||||||||||||||||||||||||||||||||||||
| _routed_via: { platform: 'google', model: modelId }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| async createSpeech( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apiKey: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| request: SpeechRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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:
Repository: aliihsaad/LLM_HUB
Length of output: 376
🏁 Script executed:
Repository: aliihsaad/LLM_HUB
Length of output: 19783
Await the asynchronous database initialization.
initDbreturns a promise, but this synchronousbeforeAllhook 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
📝 Committable suggestion
🤖 Prompt for AI Agents