Files
command-center/agent/models.yml
T
Frank f3360a28f1 feat(models): reach the fleet from off-network through the OpenWebUI door
Every provider in agent/models.yml is a LAN address, so a workstation that
leaves the house has no model at all. Add `openwebui (franksdank.cloud)`:
the already-public OpenWebUI vhost as an OpenAI-compatible provider that
proxies the same engines, with frank-dragoon and frank-zealot rows and the
WAN row as its own compaction target. LAN rows, modelRoles and the speech
runner stay untouched — the door is purely additive.

The key is command-backed (`apiKey: "!cat ~/.config/secrets/openwebui_api_key"`,
mode 600) instead of inline: a literal sk-... in a tracked file is a key in
every agent context on every box that pulls this repo, and the pre-commit
shape gate would block it anyway. Rotate = rewrite the file + restart omp,
because a `!cmd` value is cached for the process lifetime.

Compat blocks copy their LAN twins because the proxy was probed, not assumed
(2026-10-21): streaming SSE carries reasoning_content and tool_calls deltas
plus final usage, image_url content parts reach both vision towers, and
chat_template_kwargs is forwarded — proven on frank-zealot, where
enable_thinking false yields zero reasoning and true a real block.
frank-archon gets no WAN row: OpenWebUI answers 400 "Model not found"
because its picker never published it, so that path is a fleet change.

Verified end-to-end from a scratch HOME whose LAN baseUrls point at a dead
port, so any answer can only have come through the door (text and a real
tool round-trip); `omp models` still lists all four chat providers and the
tts runner, so the file validates. bats 137/137, kb-lint clean.
Docs: omp-docs/core/wan-model-access.md.
2026-10-21 16:48:29 -07:00

441 lines
25 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OMP agent model registry — single source of truth (OMP is the primary
# harness; there is no second copy to keep in sync).
# Conversion notes from the pi-era import:
# - apiKey "EMPTY" -> auth: none (keyless local endpoint)
# - thinkingLevelMap {max: xhigh} -> compat.reasoningEffortMap
providers:
# Provider NAMES are labels, and every reference to them lives in config.yml
# (modelRoles, agentModelOverrides) or in another model's compactionModel —
# rename one and a missed reference fails at call time. So each label is
# migrated with its model in the same edit. One provider per HOST:PORT, so
# Dankclaw's second endpoint (:7771) is a separate block rather than a second
# model inside the :7770 one.
"llamacpp (dankserver)":
baseUrl: http://192.168.0.235:7770/v1
api: openai-completions
auth: none
models:
- id: frank-archon
# Model id renamed from frank-medium on 2026-10-08 (official
# unit-name scheme; the systemd service, scripts and runbooks keep
# the frank-archon name — service names are unchanged).
# Server-side compaction routes to compaction.remoteEndpoint in
# config.yml (192.168.0.143:7771 — frank-zealot's 3090); the
# compactionModel candidate keeps the wire model id valid there.
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
name: "frank-archon"
# Dankserver: BOTH Radeon AI PRO R9700s (gfx1201, TP=2, wht6 wire) on
# the digest-pinned radiance image, plain `podman run` under a root
# unit. Payload, shape, eras, laws, rollback:
# serverAppConfigs-docs/LLM/frank-archon.md — and
# scripts/frank-archon.env is the authority for every number there.
# contextWindow MUST quote the engine's --max-model-len: radiance
# refuses a longer prompt and never truncates.
contextWindow: 200000
# maxTokens must survive the harness compaction gap AND the chars/4
# token-estimate under-run on code-heavy prompts. The clamp is
# max_tokens = min(maxTokens, contextWindow − estimate − 4096); the
# estimate under-runs by ~3 % on code. Compaction fires at 85 % of
# the window = 170,000 est; worst-case prompt + budget ≈
# 170,000 × 1.03 + 16,384 ≈ 191,500 ≤ 200,000 — the engine
# refuses a longer request, never truncates.
maxTokens: 16384
reasoning: true
# Text + image: the official FP8 checkpoint carries the vision
# tower (vision_config depth 27; the first blocks stay bf16 in
# modules_to_not_convert) and the plugin declares it
# (arch/common/rad_block_vit.h). Video preprocessing configs ship
# too; images are the contract that matters here.
input: [text, image]
compat:
# radiance's --reasoning-format defaults to auto: thinking
# comes back in `reasoning_content` (config.cpp). No parser
# flags exist or are needed.
reasoningContentField: "reasoning_content"
# The froggeric template does not gate reasoning_effort the way
# Qwen's shipped template did, and radiance special-cases only
# none/minimal (core/server/oai.cpp:609). The map stays as the
# zero-cost safety net, same as frank-zealot's.
reasoningEffortMap:
minimal: low
low: low
medium: medium
high: xhigh
max: xhigh
# Qwen recommended sampling. extraBody is merged last and wins.
# Every key is inside radiance's known-parameter list
# (core/server/oai.cpp:194-205); unknown keys are refused 400.
extraBody:
temperature: 1.0
top_p: 0.95
top_k: 20
min_p: 0.0
presence_penalty: 0.0
repetition_penalty: 1.0
chat_template_kwargs:
enable_thinking: true
preserve_thinking: true
"llamacpp (dankclaw)":
baseUrl: http://192.168.0.143:7770/v1
api: openai-completions
auth: none
models:
- id: frank-dragoon
# Model id renamed from frank-large on 2026-10-08 (official
# unit-name scheme; the service/unit/scripts keep the frank-dragoon
# name — service names are unchanged). The pre-rename id is
# `frank-large`, recorded here because the callsign rename overwrote
# its own explanation and left "renamed from frank-dragoon".
name: "frank-dragoon (dankclaw-gpu)"
# Server-side compaction runs on the compaction.remoteEndpoint in
# config.yml (192.168.0.143:7771 — frank-zealot's 3090); this steers the
# compaction model candidate so the wire model id stays valid
# there even from a frank-dragoon session.
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
# halogen flash-server on the Strix Halo iGPU. Payload, shape, eras,
# laws, rollback: serverAppConfigs-docs/LLM/frank-dragoon.md — and
# scripts/frank-dragoon.env is the authority for every number there.
# contextWindow: 200,000 under the native 262,144 engine ceiling
# (HALOGEN_CTX, 2026-10-21) — the 400-zone law: the window MUST sit
# ~45k under the ceiling, never at it.
contextWindow: 200000
# The clamp is max_tokens = min(maxTokens, contextWindow − est −
# 4096); the chars/4 est UNDER-counts the rendered prompt by ~3 %
# on code/agent traffic (measured 2026-10-07: est 152,938 ->
# server 157,388). Worst-case request = (200,000 − 4,096) × 1.03
# ≈ 201.8k; the compaction trigger fires at 95 % (config.yml
# thresholdPercent): 190,000 × 1.03 + 45,056 ≈ 240.8k — both inside
# 262,144 (margin 60k / 21k). Compaction rides frank-zealot:
# input ≈ 190,000 − keepRecent(20k) ≈ 175k ≤ its 200,704 slot.
# Recompute all of it after ANY window or threshold change; tests/
# frank_dragoon_unit.bats pins this arithmetic at 95 %.
# HALOGEN_MAX_TOKENS_CAP in frank-dragoon.env must stay >= this.
maxTokens: 45056
reasoning: true
# VISION ON as of 2026-10-06: the v2 vision tower loads beside the
# checkpoint (HALOGEN_VISION_TOWER; priced at 0.8 GiB in the boot
# ledger, and re-probed 2026-10-21 — a 512 px solid-red PNG comes back
# "Red" at 282 prompt tokens). image_url content parts only — the
# legacy top-level images field is the frank-zealot trap, do not
# repeat it here.
input: [text, image]
compat:
# flash-server emits thinking as `reasoning_content` — same
# field as every engine this endpoint has run, no harness
# change needed.
reasoningContentField: "reasoning_content"
# flash-server validates reasoning_effort against its own list and
# 400s anything else, naming it: "reasoning_effort 'maximum'
# unsupported; use minimal|low|medium|high|xhigh|max|none" (measured
# 2026-10-21). Every omp level is already spelled by the engine, so
# the map is identity — it exists to record that, not to fold. An
# earlier version rewrote high->xhigh and minimal->low to stop
# rejections that do not exist, and the rewrite asked the engine for
# a different thinking budget than the caller named. Levels really do
# differ here: thinking length on one prompt at max_tokens 2048
# measured low 766 tokens, xhigh 614, max 1049 with the block closed
# at 1024 (reasoning_closed_at). The harness passes an unmapped level
# through verbatim (`compat.reasoningEffortMap?.[effort] ?? effort`),
# so removing an entry is a pass-through, never a 400.
#
# `max` still does not reach the engine, and the map is not why: the
# harness clamps the requested effort to the model's own effort ladder
# before the map applies, and its ladder for this model tops out at
# xhigh — captured on the wire 2026-10-21, `:max` left as
# `reasoning_effort: "xhigh"` with this identity map in place. The
# `max` row below is the engine-side half of the contract; it takes
# effect the moment the harness ladder gains `max`, and nothing here
# can force it sooner.
#
# Thinking OFF needs an explicit mode. The default disable mode sends
# the LOWEST effort rather than a switch — `:off` left the wire as
# `reasoning_effort: "minimal"` (captured), and minimal still thinks:
# a canned 26-token block on every "off" request. `none-effort` makes
# the harness send `reasoning_effort: "none"`, which this engine
# honours as a real switch: `reasoning_tokens: 0`, answer only, no
# thinking block in the rendered output (both verified 2026-10-21).
#
# Re-measure before changing any of it. The allow-list, in the
# engine's own words:
# curl -s :7770/v1/chat/completions -H 'content-type: application/json' \
# -d '{"model":"frank-dragoon","messages":[{"role":"user","content":"x"}],
# "reasoning_effort":"maximum"}'
# -> 400 "reasoning_effort 'maximum' unsupported; use
# minimal|low|medium|high|xhigh|max|none"
# and read usage.completion_tokens_details.reasoning_tokens at
# max_tokens 2048 to see the levels separate. The wire itself is
# settled by pointing a scratch HOME at a logging proxy on :7799 that
# forwards to :7770 and running `omp --model …:off` — config claims
# are worth exactly what their last capture says.
reasoningDisableMode: none-effort
reasoningEffortMap:
minimal: minimal
low: low
medium: medium
high: high
xhigh: xhigh
max: max
# The card's thinking recipe, on the wire. The flash-server engine
# HAS a server-side sampler (HALOGEN_TEMPERATURE/TOP_P/TOP_K =
# 1.0 / 0.95 / 20 in frank-dragoon.env), but an omitted parameter is
# the engine's choice, not a pinned one: top_k is sent explicitly so
# the recipe survives someone editing HALOGEN_TOP_K. The engine
# accepts top_k, min_p, presence_penalty and repetition_penalty
# (measured 2026-10-21, all 200) — only the values this endpoint is
# meant to run are pinned here.
extraBody:
temperature: 1.0
top_p: 0.95
top_k: 20
"llamacpp (dankclaw-3090)":
baseUrl: http://192.168.0.143:7771/v1
api: openai-completions
auth: none
models:
- id: frank-zealot
# Model id renamed from frank-small on 2026-10-08 (official
# unit-name scheme; the service/unit/scripts keep the
# frank-zealot name — service names are unchanged).
name: "frank-zealot (dankclaw-3090)"
# Server-side compaction routes to compaction.remoteEndpoint in
# config.yml (192.168.0.143:7771 — this endpoint); the
# compactionModel candidate keeps the wire model id valid there.
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
# Dankclaw's RTX 3090 llama.cpp endpoint (PrismML fork build — the only
# loader for this payload's custom tensor type). Shape, payload, eras,
# laws, rollback: serverAppConfigs-docs/LLM/frank-zealot.md, and
# scripts/frank-zealot.env is the authority for every number there.
# contextWindow is the PER-SLOT window: llama.cpp divides --ctx-size
# across slots and answers a longer prompt with a 400, never a
# truncation, so this value must follow every reslice (live now:
# 3 x 200,704 over f16 K + q8_0 V with the MTP drafter armed, ba69ea5).
contextWindow: 200704
# Prompt + budget must fit inside the slot window; thinking counts
# against the budget. Compaction fires at 85 % of the window =
# 170,598 est; worst-case prompt + budget ≈ 170,598 × 1.03 +
# 16,384 ≈ 192,100 ≤ 200,704. This endpoint stays the fleet's
# compaction hub: still the widest window in this file.
maxTokens: 16384
reasoning: true
input: [text, image]
compat:
# llama.cpp --reasoning-format deepseek emits thinking as
# `reasoning_content` — the same field the MiMo/Ornith eras used;
# harness reads unchanged.
reasoningContentField: "reasoning_content"
# Bonsai-2's template DOES read reasoning_effort (the Qwen3.5-9B
# template did not — the map used to be inert here): it accepts ONLY
# xhigh (default), medium, low and calls raise_exception on anything
# else, so an unmapped harness level is a hard HTTP 500 on the role's
# first request, naming Jinja and not the model. The fold below is
# therefore load-bearing, identical to the frank-archon/dragoon law.
# llama.cpp passes chat_template_kwargs through to Jinja.
reasoningEffortMap:
minimal: low
low: low
medium: medium
high: xhigh
max: xhigh
# The payload's OWN sampling metadata (general.sampling.* in the
# GGUF): temperature 1.0 / top_p 0.95 / top_k 20 — thinking-mode
# recipe, the same trio the other Qwen3.8-family endpoints carry.
# (The old 0.6 was the Qwen3.5-9B publisher's card, not this file.)
# extraBody is merged last and wins over the harness default.
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
# The template DOES read preserve_thinking: when false it drops
# prior assistants' <think> blocks from history. Keep them —
# multi-turn tool traffic on this endpoint is the majority.
preserve_thinking: true
# Speech endpoint — the fleet's TTS, NOT a chat provider. `api:
# openai-speech` is a runner API (catalog kind `tts`), so this row never
# enters the chat picker, the Ctrl+P cycle, or any chat role: it is
# reachable only through the `speech` model role and the tts device.
# frank-tts (host port 7772) is the OpenAI /v1/audio/speech façade that
# resolves a voice BY NAME; its engine (frank-tts-engine, container-internal
# 8880, never published) is arm A qwentts.cpp 5916bf7 — measured 0.55-0.67
# RTF warm and 86-451 ms to the first audio byte on Dankclaw's CPU, with
# mainline llama-tts (TTS_BACKEND=llama-tts-cli) kept as the rollback.
# Transport contract, read out of the harness itself: the request body is
# {model, input, response_format, voice?, speed?}; `voice` is forwarded ONLY
# when explicitly set, and sampleRate/bitRate are REJECTED outright (the
# transport throws "openai-speech does not support sampleRate"), so the
# codec comes from the output_path suffix and the only tunable that matters
# is the voice name — and it comes from ONE place: the tool's `voice_id`
# argument. No setting feeds it (`speech.voice` is the speak-aloud Kokoro enum,
# `tts.localVoice` is the local backend's), so a call that omits voice_id arrives
# at 7772 with no `voice` key and the endpoint answers 400 "unknown voice None;
# available voices: narrator1, narrator2, narrator3". Say the voice in the call.
# WIRED 2026-10-18: `config.yml` `modelRoles.speech` is this row, flipped only
# after the service was deployed and answered real synthesis (two packs,
# different bytes, 24 kHz PCM16, RTF 0.57). Why the gate existed: `auth: none`
# makes this provider read as AVAILABLE even while the container is down, and an
# explicit speech role has NO fallback candidate, so a dead 7772 fails the device
# loudly instead of quietly narrating in Kokoro. Reverting is ONE key:
# `modelRoles.speech: local/kokoro`. `speech.voice` stays a Kokoro name either
# way — it was never this endpoint's voice.
# SCHEMA LAW, learned the hard way (2026-10-18): provider-level `api` must be
# a CHAT transport — the models.yml schema accepts only the chat/judge APIs
# there, and naming a runner API (`openai-speech`) at provider level fails the
# file's validation, which DISABLES EVERY custom provider: all three fleet
# endpoints vanish from the registry, not just this one (`omp models` prints
# "models.yml validation failed — custom providers disabled" and lists only
# local/kokoro). The runner api belongs on the MODEL entry only, which is what
# the block below does — `auth: none` covers the credential requirement, and
# `contextWindow`/`maxTokens` stay unset because the check only fires when they
# are provided (the bundled Kokoro row is null on both). `omp models --kind tts`
# still prints 128K/16K for this row — the registry fills display defaults — and
# the speech transport never reads them: its whole request body is the five
# fields named above. Verified 2026-10-18 with `omp models --kind tts` (row
# present) and `omp models` (frank-tts absent from the chat roster, all three
# fleet providers still listed).
"frank-tts (dankclaw)":
baseUrl: http://192.168.0.143:7772/v1
auth: none
models:
- id: frank-tts
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