feat: add claude-code LLM provider + update dashboard to fo-blog-v5
- client.ts: add claude-code provider routing BLOG_LLM_PROVIDER=claude-code to claude-bridge (flat-rate, no API billing via Claude Code subscription) - checkHealth() now pings /health on claude-bridge for real availability check - Default OLLAMA_LLM_MODEL changed from qwen2.5:14b to fo-blog-v5 - Dashboard: add claude-code card (EMPFOHLEN), rename fo-blog-v3 → fo-blog-v5 - loadBlogLLMStatus() handles all 3 providers: claude-code/anthropic/ollama - Grid expanded from 3 to 4 columns to accommodate new card - ecosystem.config.js + .env on Erik: OLLAMA_LLM_MODEL=fo-blog-v5 confirmed
This commit is contained in:
parent
b9bdcd6fc6
commit
2ebba07bb0
@ -3,6 +3,7 @@
|
||||
Format: `{"d":"YYYY-MM-DD","t":"TYPE","m":"Description"}`
|
||||
Types: FEAT · FIX · UI · DATA · AI · INFRA
|
||||
|
||||
{"d":"2026-04-18","t":"AI","m":"Blog LLM: claude-code provider implemented in packages/api/src/llm/client.ts — routes BLOG_LLM_PROVIDER=claude-code to claude-bridge (http://localhost:3250/api/generate) on Erik using Claude Code flat-rate subscription. No API billing. checkHealth() pings /health endpoint. Dashboard updated: added claude-code card (EMPFOHLEN, AKTIV), fo-blog-v3-qwen7b card replaced with fo-blog-v5, loadBlogLLMStatus() now handles claude-code provider with correct badge/border highlighting. ecosystem.config.js + .env updated: OLLAMA_LLM_MODEL=fo-blog-v5, BLOG_LLM_PROVIDER=claude-code confirmed active via pm2 env."}
|
||||
{"d":"2026-04-18","t":"FIX","m":"Cloudflare Tunnel DNS mass-update: after deleting phantom eo-pulse tunnel and creating main-prod (90c22eb0), 31 context-x.org + 7 fichtmueller.org DNS records still pointed to the deleted 641c39a5 tunnel → 530 on all services. Bulk-patched via Cloudflare API: all records now point to main-prod. Created missing admin.magatama.fichtmueller.org CNAME. TIP cloudflared-tip.service restart policy changed to Restart=always (was on-failure, so clean exits caused permanent outage). peercortex.org remains 530 — DNS is in a separate inaccessible Cloudflare account (NS: fattouche/elisabeth.ns.cloudflare.com); needs manual login."}
|
||||
{"d":"2026-04-18","t":"DATA","m":"Image backfill: GBICS og:image + QSFPTEK backfill scripts run on Erik — 226 new images added (671 → 897 total, 17.5% → 23.4% coverage). OSFP form factor: 0 → 68 images. QSFPTEK og:image URL bug fixed (double-hostname prefix stripped). OSFP-DR8-800G manually set to GBICS-compatible image (cdn11.bigcommerce.com DR8 product photo)."}
|
||||
{"d":"2026-04-18","t":"FIX","m":"FS.com scraper: all 247 prices written as €79 (wrong) — root cause: 'Gratis Versand ab 79 € (ohne MwSt.)' free-shipping banner appears on every FS.com product page. PRICE_QUALIFIED bodyText regex matched this banner text before reaching the actual product price. Fix: (1) DOM-based price extraction added to page.evaluate — targets [class*='price-value']/[class*='product-price'] etc., skipping elements inside shipping/banner/footer parents; (2) bodyText qualified patterns now check 200-char context for versand/shipping/gratis keywords and skip matches that appear in shipping context; (3) waitForSelector for price elements added before evaluate; (4) deleted 247 invalid €79 observations from DB."}
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
/**
|
||||
* LLM client for blog generation — supports Ollama (local) and Anthropic Claude (API).
|
||||
* LLM client for blog generation — supports Ollama (local), Anthropic Claude (API),
|
||||
* and Claude-Code (flat-rate via claude-bridge on Erik).
|
||||
*
|
||||
* Provider selection:
|
||||
* BLOG_LLM_PROVIDER=claude-code → Claude via claude-bridge (flat-rate, recommended)
|
||||
* BLOG_LLM_PROVIDER=anthropic → Claude Sonnet/Haiku via Anthropic API
|
||||
* BLOG_LLM_PROVIDER=ollama → qwen2.5 on local Ollama (default)
|
||||
* BLOG_LLM_PROVIDER=ollama → fine-tuned fo-blog-v5 on local Ollama (default)
|
||||
*
|
||||
* Claude is strongly recommended for blog generation — qwen2.5:14b cannot
|
||||
* follow complex multi-constraint prompts (mode collapse).
|
||||
* Claude-code is preferred: uses Claude Code subscription (flat-rate), no API costs.
|
||||
* Ollama fo-blog-v5 is the fallback for offline/local usage.
|
||||
*/
|
||||
|
||||
const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
|
||||
const LLM_MODEL = process.env.OLLAMA_LLM_MODEL || "qwen2.5:14b";
|
||||
const LLM_MODEL = process.env.OLLAMA_LLM_MODEL || "fo-blog-v5";
|
||||
|
||||
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "";
|
||||
const ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
|
||||
const BLOG_LLM_PROVIDER = process.env.BLOG_LLM_PROVIDER || "ollama";
|
||||
const CLAUDE_BRIDGE_URL = process.env.CLAUDE_BRIDGE_URL || "http://localhost:3250";
|
||||
|
||||
interface LlmResponse {
|
||||
text: string;
|
||||
@ -222,6 +225,48 @@ async function generateOllama(
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// CLAUDE-CODE PROVIDER (claude-bridge — flat-rate via Claude Code subscription)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
async function generateClaudeBridge(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
options?: { temperature?: number; maxTokens?: number; timeoutMs?: number },
|
||||
): Promise<LlmResponse> {
|
||||
const startTime = Date.now();
|
||||
// claude-bridge expects combined prompt — system + user joined with double newline
|
||||
const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
|
||||
|
||||
const resp = await fetch(`${CLAUDE_BRIDGE_URL}/api/generate`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ prompt: fullPrompt }),
|
||||
signal: AbortSignal.timeout(options?.timeoutMs ?? 300000),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const errText = await resp.text();
|
||||
throw new Error(`Claude bridge failed: ${resp.status} ${errText.slice(0, 200)}`);
|
||||
}
|
||||
|
||||
const data = await resp.json() as { success: boolean; content?: string; error?: string };
|
||||
|
||||
if (!data.success || !data.content) {
|
||||
throw new Error(`Claude bridge returned empty response: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`[LLM] Claude-bridge: ${data.content.length} chars, ${duration}ms`);
|
||||
|
||||
return {
|
||||
text: data.content,
|
||||
model: "claude-code",
|
||||
totalDuration: duration * 1_000_000, // ns for compat with Ollama callers
|
||||
evalCount: Math.ceil(data.content.length / 4), // approx tokens
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// PUBLIC API — auto-routes to configured provider
|
||||
// ═══════════════════════════════════════════════════════
|
||||
@ -231,6 +276,9 @@ export async function generate(
|
||||
userPrompt: string,
|
||||
options?: { temperature?: number; maxTokens?: number; timeoutMs?: number },
|
||||
): Promise<LlmResponse> {
|
||||
if (BLOG_LLM_PROVIDER === "claude-code") {
|
||||
return generateClaudeBridge(systemPrompt, userPrompt, options);
|
||||
}
|
||||
if (BLOG_LLM_PROVIDER === "anthropic" && ANTHROPIC_API_KEY) {
|
||||
return generateClaude(systemPrompt, userPrompt, options);
|
||||
}
|
||||
@ -281,6 +329,16 @@ export async function chat(
|
||||
|
||||
/** Check if configured LLM provider is available */
|
||||
export async function checkHealth(): Promise<{ ok: boolean; model: string; provider: string; error?: string }> {
|
||||
if (BLOG_LLM_PROVIDER === "claude-code") {
|
||||
try {
|
||||
const resp = await fetch(`${CLAUDE_BRIDGE_URL}/health`, { signal: AbortSignal.timeout(5000) });
|
||||
if (!resp.ok) return { ok: false, model: "claude-code", provider: "claude-code", error: `HTTP ${resp.status}` };
|
||||
return { ok: true, model: "claude-code", provider: "claude-code" };
|
||||
} catch (err) {
|
||||
return { ok: false, model: "claude-code", provider: "claude-code", error: (err as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
if (BLOG_LLM_PROVIDER === "anthropic" && ANTHROPIC_API_KEY) {
|
||||
// Key presence check only — live API call causes 429 when pipeline is running
|
||||
return { ok: true, model: ANTHROPIC_MODEL, provider: "anthropic" };
|
||||
|
||||
@ -1243,23 +1243,48 @@
|
||||
</div>
|
||||
|
||||
<!-- Model cards -->
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.85rem">
|
||||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.85rem">
|
||||
|
||||
<!-- Claude (recommended) -->
|
||||
<!-- Claude-Code (active — flat-rate via claude-bridge) -->
|
||||
<div id="blog-model-card-cc" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
||||
<div>
|
||||
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🤖 claude-code</div>
|
||||
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">claude-bridge / Erik</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:3px">
|
||||
<span style="font-size:0.62rem;padding:2px 6px;border-radius:3px;background:var(--accent);color:#fff;font-weight:700;white-space:nowrap">★ EMPFOHLEN</span>
|
||||
<span id="blog-model-cc-active" style="display:none;font-size:0.62rem;padding:2px 6px;border-radius:3px;background:#1a7a3a;color:#fff;font-weight:700">● AKTIV</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem">
|
||||
<div><span style="color:var(--accent)">★★★★★</span> Blog-Qualität</div>
|
||||
<div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)">★</span> Geschwindigkeit</div>
|
||||
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Kein Mode Collapse</div>
|
||||
<div style="color:#1a7a3a;font-weight:500">✓ Flat-rate (kein API-Billing)</div>
|
||||
<div style="color:#1a7a3a;font-weight:500">✓ Claude Code Subscription</div>
|
||||
</div>
|
||||
<div style="background:#1e1e1e;border-radius:5px;padding:0.5rem 0.65rem;margin-bottom:0.5rem">
|
||||
<code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=claude-code<br>CLAUDE_BRIDGE_URL=http://localhost:3250</code>
|
||||
</div>
|
||||
<div id="blog-model-cc-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude API -->
|
||||
<div id="blog-model-card-claude" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
||||
<div>
|
||||
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🧠 claude-sonnet-4-6</div>
|
||||
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">Anthropic API</div>
|
||||
</div>
|
||||
<span style="font-size:0.62rem;padding:2px 6px;border-radius:3px;background:var(--accent);color:#fff;font-weight:700;white-space:nowrap">★ EMPFOHLEN</span>
|
||||
<span id="blog-model-claude-active" style="display:none;font-size:0.62rem;padding:2px 6px;border-radius:3px;background:#1a7a3a;color:#fff;font-weight:700">● AKTIV</span>
|
||||
</div>
|
||||
<div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem">
|
||||
<div><span style="color:var(--accent)">★★★★★</span> Blog-Qualität</div>
|
||||
<div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)">★</span> Geschwindigkeit</div>
|
||||
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Komplexe Multi-Constraint Prompts</div>
|
||||
<div style="color:#1a7a3a;font-weight:500">✓ Kein Mode Collapse</div>
|
||||
<div style="color:#1a7a3a;font-weight:500">✓ 4096 Token Output</div>
|
||||
<div style="color:#b45309;font-weight:500">⚠ API-Kosten pro Artikel</div>
|
||||
</div>
|
||||
<div style="background:#1e1e1e;border-radius:5px;padding:0.5rem 0.65rem;margin-bottom:0.5rem">
|
||||
<code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=anthropic<br>ANTHROPIC_MODEL=claude-sonnet-4-6</code>
|
||||
@ -1267,11 +1292,11 @@
|
||||
<div id="blog-model-claude-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div>
|
||||
</div>
|
||||
|
||||
<!-- Fine-tuned local -->
|
||||
<!-- Fine-tuned local fo-blog-v5 -->
|
||||
<div id="blog-model-card-fo" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
||||
<div>
|
||||
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🎯 fo-blog-v3-qwen7b</div>
|
||||
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🎯 fo-blog-v5</div>
|
||||
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">Ollama / Mac Studio</div>
|
||||
</div>
|
||||
<span id="blog-model-fo-active" style="display:none;font-size:0.62rem;padding:2px 6px;border-radius:3px;background:#1a7a3a;color:#fff;font-weight:700">● AKTIV</span>
|
||||
@ -1279,12 +1304,12 @@
|
||||
<div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem">
|
||||
<div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)">★</span> Blog-Qualität</div>
|
||||
<div><span style="color:var(--accent)">★★★★★</span> Geschwindigkeit</div>
|
||||
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Fine-tuned auf TIP-Stil</div>
|
||||
<div style="color:#1a7a3a;font-weight:500">✓ Lokal / kein API-Kosten</div>
|
||||
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Fine-tuned auf TIP-Stil (v5)</div>
|
||||
<div style="color:#1a7a3a;font-weight:500">✓ Lokal / keine API-Kosten</div>
|
||||
<div style="color:#b45309;font-weight:500">⚠ Gelegentlicher Mode Collapse</div>
|
||||
</div>
|
||||
<div style="background:#1e1e1e;border-radius:5px;padding:0.5rem 0.65rem;margin-bottom:0.5rem">
|
||||
<code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=ollama<br>OLLAMA_LLM_MODEL=fo-blog-v3-qwen7b</code>
|
||||
<code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=ollama<br>OLLAMA_LLM_MODEL=fo-blog-v5</code>
|
||||
</div>
|
||||
<div id="blog-model-fo-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div>
|
||||
</div>
|
||||
@ -1316,7 +1341,7 @@
|
||||
<div style="margin-top:0.85rem;padding:0.6rem 0.85rem;background:var(--surface2);border-radius:6px;font-size:0.72rem;color:var(--text)">
|
||||
<strong>Modell wechseln:</strong> SSH → Erik →
|
||||
<code style="background:#1e1e1e;color:#f8f8f2;padding:1px 5px;border-radius:3px">nano /opt/tip/ecosystem.config.js</code>
|
||||
→ BLOG_LLM_PROVIDER + ANTHROPIC_API_KEY →
|
||||
→ BLOG_LLM_PROVIDER + CLAUDE_BRIDGE_URL / ANTHROPIC_API_KEY →
|
||||
<code style="background:#1e1e1e;color:#f8f8f2;padding:1px 5px;border-radius:3px">pm2 restart tip-api --update-env</code>
|
||||
</div>
|
||||
</div><!-- end LLM panel -->
|
||||
@ -4934,7 +4959,8 @@ async function loadBlogLLMStatus() {
|
||||
}
|
||||
if (activeModel) activeModel.textContent = llm.model || '—';
|
||||
if (activeProvider) {
|
||||
activeProvider.textContent = llm.provider === 'anthropic' ? 'anthropic' : 'ollama';
|
||||
var provLabel = llm.provider === 'claude-code' ? 'claude-code' : llm.provider === 'anthropic' ? 'anthropic' : 'ollama';
|
||||
activeProvider.textContent = provLabel;
|
||||
activeProvider.style.background = 'var(--accent)';
|
||||
activeProvider.style.color = '#fff';
|
||||
}
|
||||
@ -4943,35 +4969,60 @@ async function loadBlogLLMStatus() {
|
||||
queueEl.textContent = q > 0 ? 'Queue: ' + q + ' Jobs' : 'Queue: idle';
|
||||
}
|
||||
|
||||
// Mark active model card with accent border
|
||||
var foActive = document.getElementById('blog-model-fo-active');
|
||||
var foCard = document.getElementById('blog-model-card-fo');
|
||||
var claudeCard = document.getElementById('blog-model-card-claude');
|
||||
// Reset all card borders + active badges
|
||||
['cc','claude','fo'].forEach(function(k) {
|
||||
var card = document.getElementById('blog-model-card-' + k);
|
||||
if (card) card.style.border = '2px solid var(--border)';
|
||||
var badge2 = document.getElementById('blog-model-' + k + '-active');
|
||||
if (badge2) badge2.style.display = 'none';
|
||||
});
|
||||
|
||||
if (llm.provider === 'anthropic') {
|
||||
if (llm.provider === 'claude-code') {
|
||||
var ccCard = document.getElementById('blog-model-card-cc');
|
||||
if (ccCard) ccCard.style.border = '2px solid var(--accent)';
|
||||
var ccActive = document.getElementById('blog-model-cc-active');
|
||||
if (ccActive) ccActive.style.display = 'inline';
|
||||
var ccSt = document.getElementById('blog-model-cc-status');
|
||||
if (ccSt) {
|
||||
ccSt.textContent = llm.ok ? '● Aktiv — claude-bridge erreichbar' : '⚠ claude-bridge nicht erreichbar: ' + (llm.error || '').slice(0, 60);
|
||||
ccSt.style.color = llm.ok ? '#1a7a3a' : '#b45309';
|
||||
ccSt.style.fontWeight = '600';
|
||||
}
|
||||
var clSt2 = document.getElementById('blog-model-claude-status');
|
||||
if (clSt2) { clSt2.textContent = 'bereit (nicht aktiv)'; clSt2.style.color = 'var(--text-dim)'; clSt2.style.fontWeight = '400'; }
|
||||
var foSt2 = document.getElementById('blog-model-fo-status');
|
||||
if (foSt2) { foSt2.textContent = 'bereit (nicht aktiv)'; foSt2.style.color = 'var(--text-dim)'; foSt2.style.fontWeight = '400'; }
|
||||
} else if (llm.provider === 'anthropic') {
|
||||
var claudeCard = document.getElementById('blog-model-card-claude');
|
||||
if (claudeCard) claudeCard.style.border = '2px solid var(--accent)';
|
||||
var claudeActive = document.getElementById('blog-model-claude-active');
|
||||
if (claudeActive) claudeActive.style.display = 'inline';
|
||||
var claudeStatusEl = document.getElementById('blog-model-claude-status');
|
||||
if (claudeStatusEl) {
|
||||
claudeStatusEl.textContent = '● Aktiv — API-Key konfiguriert';
|
||||
claudeStatusEl.style.color = '#1a7a3a';
|
||||
claudeStatusEl.style.fontWeight = '600';
|
||||
}
|
||||
var foSt = document.getElementById('blog-model-fo-status');
|
||||
if (foSt) { foSt.textContent = 'bereit (nicht aktiv)'; foSt.style.color = 'var(--text-dim)'; }
|
||||
var ccSt2 = document.getElementById('blog-model-cc-status');
|
||||
if (ccSt2) { ccSt2.textContent = 'bereit (nicht aktiv)'; ccSt2.style.color = 'var(--text-dim)'; ccSt2.style.fontWeight = '400'; }
|
||||
var foSt3 = document.getElementById('blog-model-fo-status');
|
||||
if (foSt3) { foSt3.textContent = 'bereit (nicht aktiv)'; foSt3.style.color = 'var(--text-dim)'; foSt3.style.fontWeight = '400'; }
|
||||
} else {
|
||||
if (foActive) foActive.style.display = 'inline';
|
||||
// ollama
|
||||
var foCard = document.getElementById('blog-model-card-fo');
|
||||
if (foCard) foCard.style.border = '2px solid var(--accent)';
|
||||
var foActive = document.getElementById('blog-model-fo-active');
|
||||
if (foActive) foActive.style.display = 'inline';
|
||||
var foStatusEl = document.getElementById('blog-model-fo-status');
|
||||
if (foStatusEl) {
|
||||
foStatusEl.textContent = llm.ok ? '● Aktiv — Ollama erreichbar' : '⚠ Ollama nicht erreichbar: ' + (llm.error || '').slice(0, 60);
|
||||
foStatusEl.style.color = llm.ok ? '#1a7a3a' : '#b45309';
|
||||
foStatusEl.style.fontWeight = '600';
|
||||
}
|
||||
var clSt = document.getElementById('blog-model-claude-status');
|
||||
if (clSt) {
|
||||
clSt.textContent = 'bereit — BLOG_LLM_PROVIDER=anthropic + ANTHROPIC_API_KEY setzen';
|
||||
clSt.style.color = 'var(--text-dim)';
|
||||
}
|
||||
var ccSt3 = document.getElementById('blog-model-cc-status');
|
||||
if (ccSt3) { ccSt3.textContent = 'bereit — BLOG_LLM_PROVIDER=claude-code setzen'; ccSt3.style.color = 'var(--text-dim)'; ccSt3.style.fontWeight = '400'; }
|
||||
var clSt3 = document.getElementById('blog-model-claude-status');
|
||||
if (clSt3) { clSt3.textContent = 'bereit — BLOG_LLM_PROVIDER=anthropic + API-Key setzen'; clSt3.style.color = 'var(--text-dim)'; clSt3.style.fontWeight = '400'; }
|
||||
}
|
||||
} catch(e) {
|
||||
var b = document.getElementById('blog-llm-status-badge');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user