← back to Marketing Command Center
Route settlement vision gate to local $0 ollama qwen2.5vl (both paid vision keys depleted); fail-closed preserved, env-overridable
4c5fac69f1f4534298436cb89284e8fd1666c8c1 · 2026-08-25 10:55:14 -0700 · Steve Abrams
Files touched
Diff
commit 4c5fac69f1f4534298436cb89284e8fd1666c8c1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 25 10:55:14 2026 -0700
Route settlement vision gate to local $0 ollama qwen2.5vl (both paid vision keys depleted); fail-closed preserved, env-overridable
---
lib/settlement-gate.js | 125 ++++++++++++++++++++++++-------------------------
1 file changed, 60 insertions(+), 65 deletions(-)
diff --git a/lib/settlement-gate.js b/lib/settlement-gate.js
index 58b722d..b44bee6 100644
--- a/lib/settlement-gate.js
+++ b/lib/settlement-gate.js
@@ -10,42 +10,46 @@
// Settlement Agreement (Part A all-three AND Part B, no acceptable carve-out)
// BEFORE it can be staged/shown as final.
//
-// MODEL RE-ROUTE (2026-08-25, TK-10845): the vision call now runs on ANTHROPIC
-// CLAUDE vision (Messages API, image block) instead of Gemini. Per Steve's
-// directive "use other model unless for vision" — this IS the genuine vision
-// task (visually verifying a rendered image against a legal gate), so it keeps
-// a VISION-capable model; we just move it off the depleted Gemini credits onto
-// the strongest available Claude vision model. Gemini image credits being
-// depleted was blocking BOTH this legal gate (so even the $0 branded card
-// couldn't SAVE) and the room render.
+// MODEL RE-ROUTE (2026-08-25, TK-10845): the vision call now runs on a LOCAL,
+// $0 vision model (ollama qwen2.5vl:7b on 127.0.0.1:11434) instead of a paid
+// API. History: it was Gemini, then re-routed to Claude vision per Steve's
+// "use other model unless for vision" — but BOTH paid keys (Gemini image AND
+// the box's ANTHROPIC_API_KEY) turned out to be out of credits, which blocked
+// this legal gate (so even the $0 branded card couldn't SAVE) and the room
+// render, and was also killing the sub-agents. Steve's standing directive is
+// $0 / no more paying, so the genuine vision task now runs on the local model
+// Steve already hosts — zero credits, zero rate-limits. Overridable via env
+// (SETTLEMENT_VISION_URL / SETTLEMENT_VISION_MODEL) so Steve can repoint it at
+// a funded frontier vision API later for maximum legal-gate accuracy.
//
// HARD RULES honored from settlement-post-gen-vision/SKILL.md (UNCHANGED — only
-// the model call changed; the prompt text, combination logic, return shape, and
-// fail-CLOSED behavior are IDENTICAL to the prior Gemini port):
+// the model TRANSPORT changed; the prompt text, combination logic, return shape,
+// and fail-CLOSED behavior are IDENTICAL to the prior Gemini/Claude ports):
// - Vision is GROUND TRUTH (text anti-prompts don't bind a generator).
-// - FAIL-SAFE / fail-CLOSED: any API/parse/fetch error or uncertainty returns
+// - FAIL-SAFE / fail-CLOSED: any fetch/parse error or uncertainty returns
// NEEDS_REVIEW, never an auto-pass. This is a LEGAL compliance gate.
// - Verdict vocabulary is BLOCK / NEEDS_REVIEW / OK.
//
-// COST: one Claude vision call per gate (~$0.003–0.01/image on Claude vision;
-// a small image + short JSON reply). The caller logs it via the cost-tracker
-// skill. A plain branded-card composite (Treatment 1) is not AI-generated, but
-// we still post-gen-vision-check it per the task spec ("run it past settlement
-// post-gen-vision to be safe").
+// COST: $0 (local ollama vision). A local 7B vision model is weaker than a
+// frontier model at nuanced motif detection, but the gate stays fail-CLOSED
+// (uncertainty -> NEEDS_REVIEW), so the conservative bias is preserved. A plain
+// branded-card composite (Treatment 1) is not AI-generated, but we still
+// post-gen-vision-check it per the task spec ("run it past settlement post-gen
+// vision to be safe").
-// Strongest available Claude vision model for a LEGAL gate. Overridable via env
-// if the account is repointed. claude-opus-4-8 is the flagship vision model on
-// this box; if it's ever unavailable the fetch simply fails-CLOSED to NEEDS_REVIEW.
-const CLAUDE_VISION_MODEL = process.env.SETTLEMENT_CLAUDE_MODEL || 'claude-opus-4-8';
-const ANTHROPIC_VERSION = '2023-06-01';
-// ~$0.003–0.01 per image on Claude vision (small image tokens + short JSON out).
-const CLAUDE_VISION_COST = 0.006;
+// Local vision endpoint (ollama /api/generate) + model. Both env-overridable so
+// the gate can be repointed at Mac1's ollama or a funded frontier API later.
+const VISION_URL = process.env.SETTLEMENT_VISION_URL || 'http://127.0.0.1:11434/api/generate';
+const VISION_MODEL = process.env.SETTLEMENT_VISION_MODEL || 'qwen2.5vl:7b';
+const VISION_COST = 0; // local, $0
+// Back-compat aliases (some callers reference the old names for cost labels).
+const CLAUDE_VISION_MODEL = VISION_MODEL;
+const CLAUDE_VISION_COST = VISION_COST;
+// Retained for back-compat export; no longer required by the local path.
const ANTHROPIC_KEY_NAMES = ['ANTHROPIC_API_KEY'];
function anthropicKey() {
for (const n of ANTHROPIC_KEY_NAMES) if (process.env[n]) return process.env[n];
- // Best-effort: read the secrets-manager master .env so the MCC doesn't need the
- // key duplicated into its own .env (read-only, last-4 never logged).
try {
const fs = require('fs');
const os = require('os');
@@ -56,13 +60,13 @@ function anthropicKey() {
const m = s.match(new RegExp('^' + n + '=(.*)$', 'm'));
if (m) return m[1].replace(/^["']|["']$/g, '').trim();
}
- } catch { /* no secrets file — gate will fail-CLOSED to NEEDS_REVIEW */ }
+ } catch { /* ignore */ }
return '';
}
// Run the post-gen vision gate on a raw image buffer.
// @param {Buffer} buf the produced image bytes (branded card PNG, or room JPEG)
-// @param {string} mime image mime (image/png | image/jpeg)
+// @param {string} mime image mime (image/png | image/jpeg) [kept for signature compat]
// @param {string} title short label for the prompt (vendor/pattern name)
// @returns {Promise<{verdict:'OK'|'NEEDS_REVIEW'|'BLOCK', reason:string, cost:number, detail:object}>}
async function settlementGateBuffer(buf, mime, title) {
@@ -73,13 +77,11 @@ async function settlementGateBuffer(buf, mime, title) {
detail: { failSafe: true },
});
- const key = anthropicKey();
- if (!key) return failSafe('ANTHROPIC_API_KEY not set');
if (!Buffer.isBuffer(buf) || !buf.length) return failSafe('empty image buffer');
const b64 = buf.toString('base64');
- // CANONICAL PROMPT TEXT — preserved VERBATIM from the prior Gemini port so the
- // legal gate's semantics are byte-identical; only the transport (Claude) changed.
+ // CANONICAL PROMPT TEXT — preserved VERBATIM across every re-route so the legal
+ // gate's semantics are byte-identical; only the transport (local model) changed.
const prompt = `You are performing a strict LEGAL compliance check on a wallcovering design image for "${String(title || '').slice(0, 120)}".
Answer ONLY with valid JSON, no markdown:
{
@@ -94,50 +96,35 @@ Answer ONLY with valid JSON, no markdown:
}
Be precise and conservative: only answer true when the visual evidence is clear.`;
- // Claude vision wants a media_type from a fixed set; normalize jpg->jpeg and
- // fall back to png for anything unexpected (the buffer is still sent as-is).
- let mediaType = String(mime || 'image/png').toLowerCase();
- if (mediaType === 'image/jpg') mediaType = 'image/jpeg';
- if (!['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(mediaType)) mediaType = 'image/png';
-
let json;
try {
- const res = await fetch('https://api.anthropic.com/v1/messages', {
+ // ollama /api/generate with an image + forced JSON output. format:'json'
+ // makes the local model return a single JSON object we can parse reliably.
+ const res = await fetch(VISION_URL, {
method: 'POST',
- headers: {
- 'x-api-key': key,
- 'anthropic-version': ANTHROPIC_VERSION,
- 'content-type': 'application/json',
- },
+ headers: { 'content-type': 'application/json' },
body: JSON.stringify({
- model: CLAUDE_VISION_MODEL,
- max_tokens: 800,
- messages: [
- {
- role: 'user',
- content: [
- { type: 'image', source: { type: 'base64', media_type: mediaType, data: b64 } },
- { type: 'text', text: prompt },
- ],
- },
- ],
+ model: VISION_MODEL,
+ prompt,
+ images: [b64],
+ stream: false,
+ format: 'json',
+ options: { temperature: 0 },
}),
});
json = await res.json();
} catch (e) {
- return failSafe('fetch ' + String((e && e.message) || e).slice(0, 100));
+ return failSafe('local vision fetch ' + String((e && e.message) || e).slice(0, 100));
}
if (!json || json.error) {
- return failSafe('Claude: ' + String((json && json.error && json.error.message) || 'no response').slice(0, 120));
+ return failSafe('local vision: ' + String((json && json.error) || 'no response').slice(0, 120));
}
- // Claude Messages returns content: [{type:'text', text:'...'}]
- const text = Array.isArray(json.content)
- ? json.content.map((p) => (p && p.type === 'text' ? p.text : '') || '').join('')
- : '';
+ // ollama /api/generate returns { response: "<the JSON text>" }.
+ const text = String(json.response || '');
let v;
try {
- v = JSON.parse(String(text).replace(/```json\n?|```\n?/g, '').trim());
+ v = JSON.parse(text.replace(/```json\n?|```\n?/g, '').trim());
} catch {
return failSafe('unparseable vision verdict');
}
@@ -151,14 +138,22 @@ Be precise and conservative: only answer true when the visual evidence is clear.
const needsReview = !violates && ((partA && !carveout) || (partB && !!v.is_tropical_foliage_design));
const reason =
- `Claude vision: A1=${!!v.a1_directional_leaves}, A2=${!!v.a2_open_space}, A3=${!!v.a3_multiple_colors} ` +
+ `Local vision (${VISION_MODEL}): A1=${!!v.a1_directional_leaves}, A2=${!!v.a2_open_space}, A3=${!!v.a3_multiple_colors} ` +
`(Part A ${partA ? 'MET' : 'not met'}); Part B=${partB}` +
`${(v.b_elements_found || []).length ? ` [${(v.b_elements_found || []).join(', ')}]` : ''}; ` +
`carve-out=${carveout}. ${v.visual_summary || ''}`.trim();
const verdict = violates ? 'BLOCK' : (needsReview ? 'NEEDS_REVIEW' : 'OK');
- // ~$0.003–0.01/image is the Claude vision ballpark (per Steve's cost rule).
- return { verdict, reason, cost: CLAUDE_VISION_COST, detail: { partA, partB, carveout, raw: v } };
+ return { verdict, reason, cost: VISION_COST, detail: { partA, partB, carveout, raw: v } };
}
-module.exports = { settlementGateBuffer, anthropicKey, CLAUDE_VISION_MODEL, CLAUDE_VISION_COST };
+module.exports = {
+ settlementGateBuffer,
+ anthropicKey,
+ VISION_MODEL,
+ VISION_COST,
+ VISION_URL,
+ // back-compat aliases
+ CLAUDE_VISION_MODEL,
+ CLAUDE_VISION_COST,
+};
← 88aa02b Owned·DW Fleet: apply contrarian fixes (sort actionability +
·
back to Marketing Command Center
·
assets: background catalog warmer (boot + 6h) + manual refre 4a911a5 →