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 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-26 03:22:27 -04:00
parent dff014e69a
commit 32563e87f9
5 changed files with 17 additions and 4 deletions

View File

@ -51,8 +51,9 @@ export function checks() {
// AI runs // AI runs
const recent = jobs.listJobs({}).filter((j) => Date.now() - new Date(j.createdAt) < 7 * 24 * HOUR); 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)); const jobFails = recent.filter((j) => ["failed", "interrupted"].includes(j.status));
add("AI runs", process.env.ANTHROPIC_API_KEY ? (jobFails.length ? "warn" : "ok") : "warn", const aiReady = Boolean(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_BASE_URL);
!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)`, 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(" · ")); jobFails.slice(0, 3).map((j) => `${j.type} ${j.slug || ""}: ${j.error || j.status}`).join(" · "));
// Backups and disk // Backups and disk

View File

@ -231,7 +231,7 @@ app.get("/api/status", h(async () => {
return { return {
meta: metaStatus, meta: metaStatus,
odoo: await odoo.status(), 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", publishing: process.env.ALLOW_PUBLISH === "true",
}; };
})); }));

View File

@ -131,6 +131,15 @@ function docStamp(slug) {
return new Map(listDocs(slug).map((d) => [d.path, d.updatedAt])); 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) { async function run(job) {
Object.assign(job, { status: "running", startedAt: new Date().toISOString() }); Object.assign(job, { status: "running", startedAt: new Date().toISOString() });
job.abort = new AbortController(); job.abort = new AbortController();
@ -146,6 +155,7 @@ async function run(job) {
skills: "all", skills: "all",
model: AI.model, model: AI.model,
maxTurns: AI.maxTurns, maxTurns: AI.maxTurns,
env: runEnv(),
permissionMode: "dontAsk", permissionMode: "dontAsk",
allowedTools: READ_TOOLS, allowedTools: READ_TOOLS,
disallowedTools: BLOCKED_TOOLS, disallowedTools: BLOCKED_TOOLS,

View File

@ -28,6 +28,8 @@ const GROUPS = [
{ key: "ANTHROPIC_API_KEY", label: "Anthropic API key", hint: "For audits, weekly plans and reports.", secret: true }, { 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_MODEL", label: "Model", placeholder: "claude-opus-5-5" },
{ key: "DASHBOARD_AI_MAX_TURNS", label: "Max steps per run", type: "number", placeholder: "80" }, { 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: [ { name: "Email (SMTP)", scope: "self", fields: [
{ key: "SMTP_HOST", label: "Host", placeholder: "mail.example.com" }, { key: "SMTP_HOST", label: "Host", placeholder: "mail.example.com" },

View File

@ -418,7 +418,7 @@ export default function Settings() {
: s.odoo.configured ? `Can't connect: ${s.odoo.error}` : "Not connected. Connect it in Connections → Odoo."} : s.odoo.configured ? `Can't connect: ${s.odoo.error}` : "Not connected. Connect it in Connections → Odoo."}
</Row> </Row>
<Row ok warn={!s.ai.apiKey} title="Claude · AI runs"> <Row ok warn={!s.ai.apiKey} title="Claude · AI runs">
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."}
</Row> </Row>
<Row ok={s.publishing} warn={!s.publishing} title="Publishing"> <Row ok={s.publishing} warn={!s.publishing} title="Publishing">
{s.publishing {s.publishing