Merge pull request 'feat(models): WAN model access through the OpenWebUI door' (#80) from feat/79-wan-model-access into main
This commit was merged in pull request #80.
This commit is contained in:
commit
c0d840a8bb
4 files changed
+290
No files matched your search
@@ -306,3 +306,135 @@ providers:
|
||||
name: "frank-tts (dankclaw) — cloned voices"
|
||||
api: openai-speech
|
||||
input: [text]
|
||||
|
||||
# ─── WAN door ───────────────────────────────────────────────────────────
|
||||
# The only block in this file that answers OFF the home network. Every
|
||||
# provider above is a LAN IP, and `modelRoles` (config.yml) still points at
|
||||
# those, so away from home you must NAME one of the rows below:
|
||||
# omp --model 'openwebui (franksdank.cloud)/frank-dragoon:medium'
|
||||
# The door is the OpenWebUI vhost `ai.franksdank.cloud` — Caddy → OWU on
|
||||
# Dankclaw:3005, Anubis-exempt because OWU enforces its own auth, public A
|
||||
# record present (LAN clients resolve 192.168.0.143 via Pi-hole, measured
|
||||
# 2026-10-21). It exposes an OpenAI-compatible surface at /api/v1 and
|
||||
# proxies to the SAME engines as the LAN rows — no second inference stack,
|
||||
# so every engine-side law below still holds through it.
|
||||
# CREDENTIAL LAW: the key is not in this file. `apiKey` accepts a
|
||||
# command-backed value (`!cmd`, resolved by src/config/resolve-config-value.ts:
|
||||
# stdout trimmed, cached for the process lifetime; anything else is tried as
|
||||
# an env-var name, then taken literally), so the secret lives in
|
||||
# ~/.config/secrets/openwebui_api_key, mode 600, like every other credential
|
||||
# on these boxes. A literal `sk-…` here would trip the pre-commit shape gate
|
||||
# and then live in every agent context on every box that pulls this repo.
|
||||
# Rotate: rewrite the secret file, then RESTART omp — the `!cmd` result is
|
||||
# cached per process, so a live session keeps serving the old key.
|
||||
# WHAT THE DOOR SHOWS (probed 2026-10-21 with this key): /api/v1/models
|
||||
# answers frank-dragoon and frank-zealot only. frank-archon is NOT published
|
||||
# in OpenWebUI's picker and the API answers 400 "Model not found" for it, so
|
||||
# the fleet's biggest endpoint has no WAN path until it is published
|
||||
# server-side on Dankclaw — a fleet change, not a harness change.
|
||||
# CONTRACT PROBED THROUGH THE DOOR (2026-10-21): streaming SSE carries
|
||||
# `reasoning_content` deltas and `tool_calls` deltas, finish_reason
|
||||
# `tool_calls`, and a final `usage` under stream_options.include_usage;
|
||||
# `image_url` content parts reach both vision towers (a 64 px red PNG comes
|
||||
# back "Red" from each); and `chat_template_kwargs` IS forwarded — proven on
|
||||
# frank-zealot, where enable_thinking false gives reasoning 0 chars and true
|
||||
# gives a real thinking block on the same prompt. Every compat block below is
|
||||
# therefore a copy of its LAN twin, never a simplification: dropping the
|
||||
# kwargs here would quietly change thinking behaviour on the WAN path.
|
||||
# Chat completions made with an API key create NO OpenWebUI chat row (the
|
||||
# chat list is unchanged after probing), so an off-network session leaves no
|
||||
# trail in the family's chat history.
|
||||
# NOT BEHIND THIS DOOR: speech. OWU does route /api/v1/audio/speech and it
|
||||
# does reach frank-tts:7772, but OWU substitutes its own voice enum first —
|
||||
# a cloned pack name comes back 400 from the upstream. `modelRoles.speech`
|
||||
# stays on `frank-tts (dankclaw)`; do not repoint it here.
|
||||
"openwebui (franksdank.cloud)":
|
||||
baseUrl: https://ai.franksdank.cloud/api/v1
|
||||
api: openai-completions
|
||||
# Quoted because an unquoted leading `!` is a YAML tag, not a command.
|
||||
apiKey: "!cat ~/.config/secrets/openwebui_api_key"
|
||||
models:
|
||||
- id: frank-dragoon
|
||||
# Same halogen flash-server as `llamacpp (dankclaw)` above, one hop
|
||||
# further out; the picker row and the engine share this id, so there
|
||||
# is no alias to keep in sync.
|
||||
name: "frank-dragoon (WAN)"
|
||||
# Off-network the compaction hub must be reachable too: the LAN
|
||||
# frank-zealot row is a dead address from anywhere but the house.
|
||||
# Same engine, same slot arithmetic, so the keepRecent/threshold
|
||||
# math in the LAN rows transfers untouched.
|
||||
compactionModel: openwebui (franksdank.cloud)/frank-zealot
|
||||
# Inherited from the engine, not invented: 200,000 under the native
|
||||
# 262,144 ceiling (the 400-zone law — never sit at the ceiling). The
|
||||
# proxy neither widens the window nor truncates: an over-ceiling
|
||||
# request is still a 400.
|
||||
contextWindow: 200000
|
||||
# Same clamp arithmetic as the LAN row (see frank_dragoon_unit.bats);
|
||||
# HALOGEN_MAX_TOKENS_CAP on the engine is unchanged.
|
||||
maxTokens: 45056
|
||||
reasoning: true
|
||||
input: [text, image]
|
||||
compat:
|
||||
# Verified through the door: reasoning arrives as
|
||||
# `reasoning_content` in the deltas, not folded into content.
|
||||
reasoningContentField: "reasoning_content"
|
||||
# `:off` must be a real switch, and it is: this engine honours
|
||||
# reasoning_effort "none"; the harness default would only send the
|
||||
# lowest effort, which still thinks.
|
||||
reasoningDisableMode: none-effort
|
||||
# Identity map, same reason as the LAN row — flash-server lists
|
||||
# minimal|low|medium|high|xhigh|max|none and 400s anything else.
|
||||
reasoningEffortMap:
|
||||
minimal: minimal
|
||||
low: low
|
||||
medium: medium
|
||||
high: high
|
||||
xhigh: xhigh
|
||||
max: max
|
||||
# Pinned so a server-side HALOGEN_TOP_K edit cannot change this
|
||||
# session's recipe; the engine accepts all three keys.
|
||||
extraBody:
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
top_k: 20
|
||||
- id: frank-zealot
|
||||
# The 3090 endpoint, same llama.cpp engine behind the proxy. Stays
|
||||
# the widest slot in the file, so it is the WAN compaction target.
|
||||
name: "frank-zealot (WAN)"
|
||||
compactionModel: openwebui (franksdank.cloud)/frank-zealot
|
||||
# PER-SLOT window: llama.cpp divides --ctx-size across slots and
|
||||
# answers a longer prompt with a 400, never a truncation. Follows
|
||||
# every reslice on the engine (3 × 200,704 today).
|
||||
contextWindow: 200704
|
||||
# Prompt + thinking budget inside the slot; compaction fires at 85 %
|
||||
# of the window on this row set (see the LAN twin for the full
|
||||
# worst-case arithmetic).
|
||||
maxTokens: 16384
|
||||
reasoning: true
|
||||
input: [text, image]
|
||||
compat:
|
||||
reasoningContentField: "reasoning_content"
|
||||
# LOAD-BEARING, exactly as on the LAN row: the Bonsai-2 template
|
||||
# accepts ONLY xhigh|medium|low and raises on anything else, so an
|
||||
# unmapped harness level is a hard failure on the role's first
|
||||
# request. Probed 2026-10-21 that the mapped value, not the
|
||||
# harness-level, is what reaches the template through this proxy.
|
||||
reasoningEffortMap:
|
||||
minimal: low
|
||||
low: low
|
||||
medium: medium
|
||||
high: xhigh
|
||||
max: xhigh
|
||||
# The payload's own sampling recipe plus the thinking switches.
|
||||
# chat_template_kwargs survives the proxy (verified), so
|
||||
# preserve_thinking keeps prior assistants' thinking blocks in
|
||||
# history for multi-turn tool traffic — same contract as at home.
|
||||
extraBody:
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
presence_penalty: 0.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: true
|
||||
preserve_thinking: true
|
||||
@@ -15,5 +15,6 @@ These files define the structural conventions that govern the agent system itsel
|
||||
* [dankvoice](dankvoice.md) — Voice output is the `dank/dankvoice` package (`pi install git:`); this page keeps fleet wiring and the cutover procedure.
|
||||
* [OMP Status Line Customization](statusline.md) — How presets, composer shapes, and segment arrays actually interact; the fleet's tuned bar. (moved from /pi/, 2026-10-19)
|
||||
* [OMP Subagent Limits & Fan-out Policy](subagent-limits.md) — Live caps: concurrency 2, no nested spawns (the `maxRecursionDepth: 0` trap), per-endpoint in-flight ceilings, isolation policy. (moved from /pi/, 2026-10-19)
|
||||
* [WAN Model Access](wan-model-access.md) — Driving the fleet's models from outside the home network through the OpenWebUI door: the command, the key file and its rotation law, what the door publishes, and the off-network failure modes.
|
||||
|
||||
**Change log:** the bundle keeps exactly one, one level up — [Bundle Change Log](../log.md). A directory here never has its own.
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
type: reference
|
||||
title: WAN Model Access (ai.franksdank.cloud)
|
||||
description: How to drive the fleet's models from outside the home network — the OpenWebUI door in agent/models.yml, the key file and its rotation, what the door does and does not publish, and the off-network failure modes.
|
||||
tags:
|
||||
- omp
|
||||
- models-yml
|
||||
- openwebui
|
||||
- remote-access
|
||||
- fleet
|
||||
verified: 2026-10-21
|
||||
timestamp: 2026-10-21
|
||||
---
|
||||
|
||||
# WAN Model Access (ai.franksdank.cloud)
|
||||
|
||||
Every provider in `agent/models.yml` except one is a LAN address, so a laptop
|
||||
that leaves the house loses the fleet. The exception is the
|
||||
`openwebui (franksdank.cloud)` block: the fleet's already-public OpenWebUI vhost
|
||||
speaks OpenAI-compatible HTTP at `/api/v1` and proxies to the same engines the
|
||||
LAN rows point at. One hop further out, no second inference stack.
|
||||
|
||||
Read this page when you are off-network and a model call fails, when the key
|
||||
needs rotating, or before adding another row behind the door.
|
||||
|
||||
## Run a session away from home
|
||||
|
||||
The door is added as a provider, never as a role default. `modelRoles` in
|
||||
`agent/config.yml` still names LAN providers, so away from home you must name
|
||||
the model explicitly:
|
||||
|
||||
```bash
|
||||
omp --model 'openwebui (franksdank.cloud)/frank-dragoon:medium'
|
||||
omp --model 'openwebui (franksdank.cloud)/frank-zealot:off'
|
||||
```
|
||||
|
||||
The same two ids work in the `/model` picker inside a running session — the
|
||||
provider label is what disambiguates them from the LAN rows, since the wire ids
|
||||
are identical by design (OpenWebUI's picker row and the engine share the id, so
|
||||
there is no alias to keep in sync).
|
||||
|
||||
## The key
|
||||
|
||||
`apiKey: "!cat ~/.config/secrets/openwebui_api_key"` — a **command-backed**
|
||||
value, resolved by `src/config/resolve-config-value.ts` in the harness: stdout
|
||||
is trimmed, an env-var name is tried next, and only then is the string taken
|
||||
literally. The secret itself is a 600-mode file in `~/.config/secrets/`, the
|
||||
same convention as `gitea` and `kagi_api_key`.
|
||||
|
||||
Laws:
|
||||
|
||||
- **Never inline the `sk-…`.** It is a tracked file; the pre-commit shape gate
|
||||
blocks `sk-…` anywhere in the staged diff, and a key that slipped through
|
||||
would live in every agent's context on every box that pulls this repo.
|
||||
- **Rotation is: rewrite the secret file, then restart omp.** The `!cmd` result
|
||||
is cached for the process lifetime, so a long-lived session keeps serving the
|
||||
old key — a stale-session failure reads exactly like a revoked key.
|
||||
- The key is an **OpenWebUI** key (User menu → Settings → Account → API keys),
|
||||
not a fleet endpoint key. It carries that user's model ACL, so what the door
|
||||
shows is whatever the picker shows that user.
|
||||
- Every other workstation needs its own copy of the file; nothing in the repo
|
||||
carries the value.
|
||||
|
||||
## What the door publishes
|
||||
|
||||
Probed 2026-10-21 with the fleet key.
|
||||
|
||||
|Behind the door|Notes|
|
||||
|---|---|
|
||||
|`frank-dragoon`|halogen flash-server, Dankclaw iGPU — same row as the LAN `llamacpp (dankclaw)`|
|
||||
|`frank-zealot`|llama.cpp on Dankclaw's 3090 — same row as `llamacpp (dankclaw-3090)`; also the WAN compaction target|
|
||||
|`frank-archon`|**not published.** `/api/v1/models` omits it and the API answers `400 "Model not found"`. Dankserver's endpoint has no public face at all. Giving it a WAN path is a fleet change on Dankclaw (publish it in the OpenWebUI picker), not a harness change|
|
||||
|`frank-tts`|route exists, contract does not. OpenWebUI forwards `/api/v1/audio/speech` to `frank-tts:7772` but substitutes its own voice enum first, and a cloned-pack name comes back `400` from the upstream. `modelRoles.speech` stays on the LAN `frank-tts (dankclaw)` row|
|
||||
|
||||
## Contract verified through the proxy
|
||||
|
||||
Each of these was measured end-to-end on 2026-10-21 — they are why the compat
|
||||
blocks in the WAN rows are copies of their LAN twins rather than simplifications:
|
||||
|
||||
- Streaming SSE carries `reasoning_content` **and** `tool_calls` deltas,
|
||||
`finish_reason: tool_calls`, and a final `usage` under
|
||||
`stream_options.include_usage`.
|
||||
- `image_url` content parts reach both vision towers.
|
||||
- `chat_template_kwargs` is forwarded — proven on `frank-zealot`, where
|
||||
`enable_thinking: false` yields zero reasoning and `true` yields a real
|
||||
thinking block on the same prompt. Drop the kwargs from a WAN row and you
|
||||
silently change thinking behaviour on the WAN path.
|
||||
- API-key completions create **no** OpenWebUI chat row: the chat list is
|
||||
unchanged after a probe, so an off-network session leaves nothing in the
|
||||
family's chat history.
|
||||
|
||||
Context windows, `maxTokens`, sampling and effort maps are inherited from the
|
||||
engines and documented on the LAN rows and in the fleet vault
|
||||
(`serverAppConfigs-docs/LLM/`). The proxy neither widens a window nor truncates:
|
||||
an over-ceiling request is still a `400`.
|
||||
|
||||
## Off-network failure modes
|
||||
|
||||
- **`Connection refused` / long timeout on a LAN id.** Not a service outage —
|
||||
you are not home. Switch to the WAN id.
|
||||
- **`Model not found` for `frank-archon`.** Expected; see the table above.
|
||||
- **Compaction wants a LAN address.** Each WAN row sets `compactionModel` to
|
||||
the WAN `frank-zealot`, but `compaction.remoteEndpoint` in `agent/config.yml`
|
||||
is a LAN URL. It is last in `methodOrder`, so the other methods run first and
|
||||
the remote attempt just fails — leave it unless off-network compaction turns
|
||||
out to need it.
|
||||
- **Roles still resolve to LAN.** A full off-network session that must not
|
||||
depend on hand-picked models needs `retry.fallbackChains` entries pointing
|
||||
the LAN defaults at the WAN rows; that is a deliberate `config.yml` change,
|
||||
not part of adding rows here.
|
||||
|
||||
## Verify the door
|
||||
|
||||
```bash
|
||||
# Key + model list (LAN clients resolve 192.168.0.143 via Pi-hole; the WAN
|
||||
# A record is what makes this reachable from elsewhere).
|
||||
curl -s https://ai.franksdank.cloud/api/v1/models \
|
||||
-H "Authorization: Bearer $(cat ~/.config/secrets/openwebui_api_key)" | jq -r '.data[].id'
|
||||
|
||||
# Registry loaded, WAN rows present, no validation failure (a failed load
|
||||
# silently disables EVERY custom provider — see
|
||||
# agent/models.yml's schema-law comment):
|
||||
omp models --no-extensions | grep -E 'franksdank|validation failed'
|
||||
```
|
||||
|
||||
A scratch-HOME run is the only real proof that a harness change reaches the
|
||||
door: copy `models.yml` + `config.yml` to `$TMP/.omp/agent/`, symlink
|
||||
`$TMP/.config/secrets` at the real one (a `~` in a `!cmd` value expands to
|
||||
`$HOME`), rewrite the LAN baseUrls to a dead port so a success cannot be
|
||||
faked, then `HOME=$TMP omp --model 'openwebui (franksdank.cloud)/…' -p …`.
|
||||
Delete the scratch tree afterwards — it holds endpoint URLs.
|
||||
|
||||
## Pointers
|
||||
|
||||
- Door topology: [Caddy](/caddy/index.md), [System hosts](/system/index.md).
|
||||
- Model registry law and per-provider limits: [Core config](index.md),
|
||||
[OMP Subagent Limits](subagent-limits.md).
|
||||
- Server-side record: `serverAppConfigs-docs/Dankclaw/OpenWebUI.md`
|
||||
(that repo's vault, not this bundle).
|
||||
@@ -24,6 +24,24 @@ long form.
|
||||
|
||||
## 2026-10-21
|
||||
|
||||
### The WAN door into the model registry
|
||||
|
||||
`openwebui (franksdank.cloud)` joins `agent/models.yml` — the already-public
|
||||
OpenWebUI vhost as an OpenAI-compatible provider, so a workstation outside the
|
||||
house still reaches `frank-dragoon` and `frank-zealot` while every LAN row stays
|
||||
untouched. The credential is command-backed
|
||||
(`apiKey: "!cat ~/.config/secrets/openwebui_api_key"`, 600-mode file): a literal
|
||||
`sk-…` in a tracked file is a key in every agent context on every box that pulls.
|
||||
|
||||
Laws: **OpenWebUI forwards `chat_template_kwargs`**, so a proxied row copies its
|
||||
LAN twin's compat block instead of simplifying it — proved on `frank-zealot`,
|
||||
`enable_thinking` false giving zero reasoning and true a real block on one
|
||||
prompt; **the picker IS the API's model list**, `frank-archon` answers `400
|
||||
"Model not found"` because it was never published, so a WAN path for it is a
|
||||
fleet change on Dankclaw and not a harness one; and **a `!cmd` value is cached
|
||||
for the process lifetime**, so rotating a key without restarting omp leaves the
|
||||
old key working. Page: [core/wan-model-access.md](core/wan-model-access.md).
|
||||
|
||||
### the audit grows an advisory table, and corrects three of its own numbers
|
||||
|
||||
`omp-docs/reports/fleet-security-audit-2026-10-21.html` folds in the two per-host vulnerability
|
||||
|
||||
Reference in new issue
Block a user