From b5a961c3bd9e36eba0a48ba80300e168b893cc5c Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Thu, 4 Jun 2026 18:28:45 +0000 Subject: [PATCH] feat(blog): automatic primary->fallback failover for Ollama endpoint Blog LLM client probes BLOG_OLLAMA_URL (primary, WireGuard tunnel to Mac Studio loopback Ollama) and falls back to BLOG_OLLAMA_URL_FALLBACK (Cloudflare tunnel) when the primary transport is unreachable. Re-probed at startup and every 60s; prefers primary when available. Both tunnels terminate on the Mac loopback over independent transports, so the blog keeps reaching fo-blog regardless of which transport drops. --- packages/api/src/llm/client.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/api/src/llm/client.ts b/packages/api/src/llm/client.ts index 9f98c76..494fc33 100644 --- a/packages/api/src/llm/client.ts +++ b/packages/api/src/llm/client.ts @@ -14,7 +14,27 @@ import { existsSync, readFileSync, writeFileSync } from "fs"; import { join } from "path"; -const OLLAMA_URL = process.env.BLOG_OLLAMA_URL || process.env.OLLAMA_URL || "http://localhost:11434"; +// -- Ollama endpoint with automatic primary->fallback failover -- +// Primary = BLOG_OLLAMA_URL (WireGuard tunnel to Mac Studio loopback Ollama), +// fallback = BLOG_OLLAMA_URL_FALLBACK (Cloudflare tunnel). Independent transports; +// if the primary transport drops, requests move to the backup. Re-probed every 60s. +const OLLAMA_PRIMARY = process.env.BLOG_OLLAMA_URL || process.env.OLLAMA_URL || "http://localhost:11434"; +const OLLAMA_FALLBACK = process.env.BLOG_OLLAMA_URL_FALLBACK || ""; +let OLLAMA_URL = OLLAMA_PRIMARY; +async function pickOllamaUrl(): Promise { + for (const u of [OLLAMA_PRIMARY, OLLAMA_FALLBACK].filter(Boolean)) { + try { + const r = await fetch(`${u}/api/tags`, { signal: AbortSignal.timeout(3000) }); + if (r.ok) { + if (OLLAMA_URL !== u) console.log(`[blog-llm] ollama endpoint -> ${u}`); + OLLAMA_URL = u; + return; + } + } catch { /* try next */ } + } +} +void pickOllamaUrl(); +setInterval(() => { void pickOllamaUrl(); }, 60000).unref(); const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || ""; const ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514"; const CLAUDE_BRIDGE_URL = process.env.CLAUDE_BRIDGE_URL || "http://localhost:3250";