From 32563e87f989c8fd1bca9fa97340a6748d65e7fa Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Mon, 26 Oct 2026 03:22:27 -0400 Subject: [PATCH] AI runs on our own model server (Ollama): address and token in Server settings With a model server address set, runs send every model slot (opus/sonnet/haiku/subagent) to the chosen model and never send the Anthropic key there. Tested locally with qwen3.6:35b-a3b through the Agent SDK. Co-Authored-By: Claude Opus 5.5 --- dashboard/server/health.js | 5 +++-- dashboard/server/index.js | 2 +- dashboard/server/jobs.js | 10 ++++++++++ dashboard/server/settings.js | 2 ++ dashboard/web/src/pages/Settings.jsx | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/dashboard/server/health.js b/dashboard/server/health.js index d40d79c..e3b6e3f 100644 --- a/dashboard/server/health.js +++ b/dashboard/server/health.js @@ -51,8 +51,9 @@ export function checks() { // AI runs const recent = jobs.listJobs({}).filter((j) => Date.now() - new Date(j.createdAt) < 7 * 24 * HOUR); const jobFails = recent.filter((j) => ["failed", "interrupted"].includes(j.status)); - add("AI runs", process.env.ANTHROPIC_API_KEY ? (jobFails.length ? "warn" : "ok") : "warn", - !process.env.ANTHROPIC_API_KEY ? "No Anthropic API key: AI runs (audits, weekly plans) can't start" : jobFails.length ? `${jobFails.length} AI run${jobFails.length === 1 ? "" : "s"} failed this week` : `AI runs working (${recent.length} this week)`, + const aiReady = Boolean(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_BASE_URL); + add("AI runs", aiReady ? (jobFails.length ? "warn" : "ok") : "warn", + !aiReady ? "No Anthropic API key: AI runs (audits, weekly plans) can't start" : jobFails.length ? `${jobFails.length} AI run${jobFails.length === 1 ? "" : "s"} failed this week` : `AI runs working (${recent.length} this week)`, jobFails.slice(0, 3).map((j) => `${j.type} ${j.slug || ""}: ${j.error || j.status}`).join(" · ")); // Backups and disk diff --git a/dashboard/server/index.js b/dashboard/server/index.js index 577f7ef..9b62931 100644 --- a/dashboard/server/index.js +++ b/dashboard/server/index.js @@ -231,7 +231,7 @@ app.get("/api/status", h(async () => { return { meta: metaStatus, odoo: await odoo.status(), - ai: { model: process.env.DASHBOARD_AI_MODEL || "claude-opus-5-5", apiKey: Boolean(process.env.ANTHROPIC_API_KEY) }, + ai: { model: process.env.DASHBOARD_AI_MODEL || "claude-opus-5-5", apiKey: Boolean(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_BASE_URL), server: process.env.ANTHROPIC_BASE_URL || null }, publishing: process.env.ALLOW_PUBLISH === "true", }; })); diff --git a/dashboard/server/jobs.js b/dashboard/server/jobs.js index c8544ca..d2c6606 100644 --- a/dashboard/server/jobs.js +++ b/dashboard/server/jobs.js @@ -131,6 +131,15 @@ function docStamp(slug) { return new Map(listDocs(slug).map((d) => [d.path, d.updatedAt])); } +/** Runs on our own model server (Ollama etc.): every model slot points at the chosen model, and the Anthropic key stays home. */ +function runEnv() { + if (!process.env.ANTHROPIC_BASE_URL) return undefined; + const env = { ...process.env, ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN || "ollama", CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" }; + delete env.ANTHROPIC_API_KEY; + for (const k of ["ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "CLAUDE_CODE_SUBAGENT_MODEL"]) env[k] = AI.model; + return env; +} + async function run(job) { Object.assign(job, { status: "running", startedAt: new Date().toISOString() }); job.abort = new AbortController(); @@ -146,6 +155,7 @@ async function run(job) { skills: "all", model: AI.model, maxTurns: AI.maxTurns, + env: runEnv(), permissionMode: "dontAsk", allowedTools: READ_TOOLS, disallowedTools: BLOCKED_TOOLS, diff --git a/dashboard/server/settings.js b/dashboard/server/settings.js index 28f0dbc..0de74a5 100644 --- a/dashboard/server/settings.js +++ b/dashboard/server/settings.js @@ -28,6 +28,8 @@ const GROUPS = [ { key: "ANTHROPIC_API_KEY", label: "Anthropic API key", hint: "For audits, weekly plans and reports.", secret: true }, { key: "DASHBOARD_AI_MODEL", label: "Model", placeholder: "claude-opus-5-5" }, { key: "DASHBOARD_AI_MAX_TURNS", label: "Max steps per run", type: "number", placeholder: "80" }, + { key: "ANTHROPIC_BASE_URL", label: "Model server address", type: "url", placeholder: "http://172.17.0.1:11434", hint: "Leave empty to use Anthropic. For your own Ollama, enter its address and set Model to the Ollama model name (e.g. qwen3.6:35b-a3b)." }, + { key: "ANTHROPIC_AUTH_TOKEN", label: "Model server token", secret: true, hint: "Only with a model server address. Ollama accepts any text, e.g. ollama." }, ] }, { name: "Email (SMTP)", scope: "self", fields: [ { key: "SMTP_HOST", label: "Host", placeholder: "mail.example.com" }, diff --git a/dashboard/web/src/pages/Settings.jsx b/dashboard/web/src/pages/Settings.jsx index 3a6e215..6a4aca5 100644 --- a/dashboard/web/src/pages/Settings.jsx +++ b/dashboard/web/src/pages/Settings.jsx @@ -418,7 +418,7 @@ export default function Settings() { : s.odoo.configured ? `Can't connect: ${s.odoo.error}` : "Not connected. Connect it in Connections → Odoo."} - Model {s.ai.model}. {s.ai.apiKey ? "Using the ANTHROPIC_API_KEY on this server." : "No ANTHROPIC_API_KEY set: runs use this machine's Claude Code login. A hosted server needs an API key."} + Model {s.ai.model}. {s.ai.server ? `Using the model server at ${s.ai.server}.` : s.ai.apiKey ? "Using the ANTHROPIC_API_KEY on this server." : "No ANTHROPIC_API_KEY set: runs use this machine's Claude Code login. A hosted server needs an API key."} {s.publishing