fixes: dead /contact link + remove paid-AI fallbacks from hybridRouter

- /manufacturers footer "Contact our editors" linked to /contact which
  doesn't exist → 404. Swapped to mailto:editors@broadcastbeat.com.
- hybridAI() was falling through to api.anthropic.com whenever Ollama
  failed, which logs were tripping on after my earlier no-paid-AI swap
  (Article suggestions error: ANTHROPIC_API_KEY not configured).
  Removed both Anthropic call-paths in hybridAI + hybridAIStream.
  hybridAI now retries Ollama with 0/3/12s backoff and throws a clear
  error after exhaustion — callers (article-suggestions) already have
  graceful non-AI fallbacks.
This commit is contained in:
Ryan Salazar
2026-05-27 18:46:10 +00:00
parent 346868fea3
commit 6e3a93f90d
2 changed files with 20 additions and 67 deletions

View File

@@ -82,9 +82,9 @@ export default async function ManufacturersIndex() {
</p>
<p className="mt-2">
Are you a manufacturer and want to update your listing?{" "}
<Link href="/contact" className="text-[#3b82f6] hover:underline">
<a href="mailto:editors@broadcastbeat.com?subject=Manufacturer%20Directory%20Update" className="text-[#3b82f6] hover:underline">
Contact our editors
</Link>
</a>
.
</p>
</section>

View File

@@ -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 ────────────────────────────────────────────────────────