diff --git a/src/app/manufacturers/page.tsx b/src/app/manufacturers/page.tsx index d2798c2..7627349 100644 --- a/src/app/manufacturers/page.tsx +++ b/src/app/manufacturers/page.tsx @@ -82,9 +82,9 @@ export default async function ManufacturersIndex() {
Are you a manufacturer and want to update your listing?{" "} - + Contact our editors - + .
diff --git a/src/lib/ai/hybridRouter.ts b/src/lib/ai/hybridRouter.ts index 22ba3b1..8603145 100644 --- a/src/lib/ai/hybridRouter.ts +++ b/src/lib/ai/hybridRouter.ts @@ -197,12 +197,17 @@ export async function hybridAI( const endpoint = getOllamaEndpoint(); const ollamaModel = getOllamaModel(taskType); - // Skip Ollama for low-priority tasks when no endpoint configured - // or when priority is 5-6 (translation/classification) — route direct to Anthropic - // to avoid queuing behind interactive sessions - const skipOllama = !endpoint || !ollamaModel || priority >= 5; + if (!endpoint || !ollamaModel) { + throw new Error('Ollama not configured (OLLAMA_ENDPOINT or model missing) — refusing to fall back to paid API'); + } - if (!skipOllama) { + // Retry-with-backoff against Ollama. Per the no-paid-AI policy we never + // fall back to Anthropic, so the caller gets a clear error and is expected + // to surface its own graceful fallback (e.g. non-AI default). + const RETRY_DELAYS_MS = [0, 3000, 12000]; + let lastErr: any = null; + for (let i = 0; i < RETRY_DELAYS_MS.length; i++) { + if (RETRY_DELAYS_MS[i] > 0) await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[i])); const start = Date.now(); try { const text = await callOllama(messages, ollamaModel, endpoint, options); @@ -210,24 +215,18 @@ export async function hybridAI( logHealthEvent({ provider: 'ollama', success: true, latencyMs, model: ollamaModel, priority }); return { text, provider: 'ollama', model: ollamaModel, latencyMs }; } catch (err: any) { + lastErr = err; const latencyMs = Date.now() - start; logHealthEvent({ provider: 'ollama', success: false, latencyMs, model: ollamaModel, priority, - errorMessage: err?.message, isFailover: true, + errorMessage: err?.message, }); - // Fall through to Anthropic + // Retry-eligible if message looks like 503/busy/timeout; otherwise bail. + if (!/503|busy|pending|queue|timeout|empty|abort/i.test(String(err?.message || ''))) break; } } - // Anthropic fallback - const start = Date.now(); - const { text, model } = await callAnthropic(messages, options); - const latencyMs = Date.now() - start; - logHealthEvent({ - provider: 'anthropic', success: true, latencyMs, model, priority, - isFailover: !skipOllama, - }); - return { text, provider: 'anthropic', model, latencyMs }; + throw lastErr || new Error('Ollama failed after retries'); } // ─── Streaming variant (for AI Console) ────────────────────────────────────── @@ -272,55 +271,9 @@ export async function hybridAIStream( } } - // Anthropic streaming fallback - const apiKey = getAnthropicKey(); - if (!apiKey) throw new Error('No AI provider available'); - - const systemMsg = messages.find(m => m.role === 'system'); - const userMessages = messages.filter(m => m.role !== 'system'); - - const model = ANTHROPIC_PRIMARY_MODEL; - const payload: any = { - model, - max_tokens: options.maxTokens ?? 2000, - stream: true, - messages: userMessages.map(m => ({ role: m.role, content: m.content })), - }; - if (systemMsg) payload.system = systemMsg.content; - - let response = await fetch('https://api.anthropic.com/v1/messages', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01', - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - // Try fallback model - if (response.status === 404) { - const fallbackPayload = { ...payload, model: ANTHROPIC_FALLBACK_MODEL }; - const fallbackResponse = await fetch('https://api.anthropic.com/v1/messages', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01', - }, - body: JSON.stringify(fallbackPayload), - }); - if (!fallbackResponse.ok || !fallbackResponse.body) throw new Error('Anthropic unavailable'); - logHealthEvent({ provider: 'anthropic', success: true, latencyMs: 0, model: ANTHROPIC_FALLBACK_MODEL, priority, isFailover: true }); - return { stream: fallbackResponse.body, provider: 'anthropic', model: ANTHROPIC_FALLBACK_MODEL }; - } - throw new Error(`Anthropic HTTP ${response.status}`); - } - - if (!response.body) throw new Error('No response body from Anthropic'); - logHealthEvent({ provider: 'anthropic', success: true, latencyMs: 0, model, priority, isFailover: !!endpoint }); - return { stream: response.body, provider: 'anthropic', model }; + // No-paid-AI policy: no Anthropic fallback. If Ollama streaming failed, + // surface that clearly so the caller can degrade gracefully. + throw new Error('Ollama streaming unavailable — no paid-AI fallback per policy'); } // ─── Ollama model list ────────────────────────────────────────────────────────