Commit Graph
2 Commits
Author SHA1 Message Date
Bernd Rodler 2dc224e882 feat(ai): local LLM auto-discovery — find a running Ollama, suggest connecting
New lib/ai/local-discovery.ts: one /api/tags query against the loopback
addresses Ollama binds to (127.0.0.1/localhost), no follow-up /api/show
round trips needed — the tags response already carries capabilities, size,
and parameter_size, enough to recommend a default model. Picks the
smallest non-"thinking" chat-capable model for the fastest first response
("Connect" pre-fills provider+baseUrl+model in one click), and separately
surfaces the largest as a "higher quality" alternative.

New banner in ai-assistant-settings.tsx: fires when Local isn't yet
configured, offers one-click Connect or a persisted "Not now" dismissal.

13 new unit tests using this machine's actual Ollama /api/tags response
(11 real installed models — qwen2.5:32b, llama3.2, deepseek-r1 x2,
gemma4 x3, hermes3, qwen3, qwen3.5, nomic-embed-text) as literal fixtures,
per the explicit instruction to use this machine as the test case:
confirms exactly one query is required, the heuristic recommends
llama3.2:latest (fastest) / qwen2.5:32b (largest) on this real fleet,
never recommends an embedding-only model, and degrades correctly when a
candidate base URL is unreachable.

Full QA gate: tsc clean, eslint clean, 2498/2498 tests passing, build clean.
Also live-verified in a real browser session against this machine's real
Ollama — the banner rendered with exactly these two model names.
2026-08-06 17:41:49 +02:00
Bernd Rodler dda7adf565 feat(ai): multi-key BYOK, real server class, real entitlement enforcement
Three pieces built together tonight since they're naturally linked (the
server-class proxy is the real entitlement enforcement chokepoint):

1. Multi-key BYOK (public class): several named provider profiles
   (name/baseUrl/model), each with its own key in lib/ai/key-store.ts
   (keyed by profile id, not a single fixed 'public' slot). The "Try it"
   pane lets you pick which saved profile answers each question - not one
   fixed default.

2. `server` class, real: app/api/ai/server/{models,chat} proxy through
   this app's own backend to AI_SERVER_BASE_URL - same-origin from the
   browser, no CORS/OLLAMA_ORIGINS story at all, standing in tonight for
   VNC's EU/CH-hosted infra with the real Ollama on this Mac (swapping to
   the real instance tomorrow is a config change).

3. Real entitlement enforcement (lib/ai/entitlement.ts), scoped to `server`
   only (not local/public, per the 2026-08-05 decisions): checkAndAssignSeat()
   re-validates on every /api/ai/server/chat call - first use auto-assigns a
   seat if any remain, further calls from an unlicensed user get a 402 with
   a specific reason. recordUsage() appends to an append-only metering
   ledger (timestamp/user/model/tokens/latency) that IS the billing record.
   Admin data endpoints at /api/admin/ai/entitlement (seat total, revoke) -
   the visual admin console is a separate, not-yet-built task.

Two real bugs found and fixed during verification, not just claimed fixed:
- /api/ai/policy never actually added 'server' to entitlement.classes even
  when AI_SERVER_BASE_URL was set (only the type comment was updated) - the
  Server radio option silently never appeared until this was caught live.
- The new routes used readStalwartAuthContext(0) (hardcoded slot, SSO/reauth-
  specific) instead of getStalwartCredentials() (the general multi-slot
  session resolver every other authenticated route uses) - reachable but
  wrong, and would have hidden a real auth gap behind "works on my slot".

Verified end-to-end for real: built + ran the actual server, logged in via
the real (non-demo) auth flow, selected Server, listed the real Ollama
models through the proxy, asked "Reply with exactly the words: SERVER CLASS
WORKS" and got back exactly that - plus confirmed on disk (not just in the
UI) that data/admin-state/ai-entitlement.json recorded the seat assignment
and ai-metering.jsonl recorded real prompt/completion token counts and
latency from the actual model call. Rejection-path logic (seat limit
reached, zero seats configured, revocation) covered by 5 new unit tests
rather than a second live round trip. Full suite: typecheck clean, lint
clean, translations 48/48, production build succeeds.
2026-08-06 00:07:56 +02:00