Files
command-center/agent/models.yml
Frank d4d6dd0c44 fix(omp): frank-dragoon's effort map folded against rejections that do not exist
The entry rewrote every effort before the wire: `minimal -> low`, `high -> xhigh`,
`max -> xhigh`, under a comment claiming flash-server rejects anything outside
{xhigh, medium, low}. The engine says otherwise, in its own 400:

    reasoning_effort 'maximum' unsupported; use minimal|low|medium|high|xhigh|max|none

and the harness passes an unmapped level through verbatim
(`compat.reasoningEffortMap?.[effort] ?? effort`, read in the pinned `dist/cli.js`). So the
fold protected nothing and silently changed what the caller asked for. The levels are not
cosmetic either — thinking length on one prompt at `max_tokens 2048`: `low` 766 reasoning
tokens, `xhigh` 614, `max` 1049 with the block closed at 1024.

The map is identity. Two things came out of capturing the wire (scratch HOME + a logging
proxy forwarding :7799 to :7770) that reading the config could not have told me:

- `:off` does not switch thinking off. The default disable mode sends the lowest effort,
  so `off` left as `reasoning_effort: "minimal"` — and `minimal` still thinks, a canned
  26-token block on every request. `reasoningDisableMode: none-effort` makes the harness
  send `"none"`, which the engine honours as a real switch: `reasoning_tokens: 0`, answer
  only, no thinking block rendered. Verified after the change, not before it.
- `:max` still arrives as `xhigh`: the harness clamps the requested effort to its own
  ladder for this model before the map applies. Recorded in the comment as the engine-side
  half of the contract rather than papered over — nothing in models.yml forces it.

Also pinned `top_k: 20` (accepted; the comment claimed the wire matched the server sampler
while `top_k` was never sent, leaving the recipe one env edit away from drifting), and
restored the pre-callsign model ids `frank-medium` / `frank-large` / `frank-small` — the
2026-10-08 rename had replaced the old id inside its own explanation, leaving "renamed
from frank-dragoon" three times and destroying the record needed to resolve an old id.

Verified: full suite green (0 failures), `kb-lint` 0 violations, YAML parses, and the four
efforts `off/high/medium/max` captured on the wire as `none/high/medium/xhigh` with
`top_k: 20` on every request. Refs #62
2026-10-21 01:28:37 -07:00

308 lines
17 KiB
YAML
Raw Permalink 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 sits deliberately BELOW the engine ceiling
# (HALOGEN_CTX): the clamp below leaves the gap that absorbs the
# harness's chars/4 under-count, so a window equal to the ceiling
# would make every long request a guaranteed 400.
contextWindow: 155648
# 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 = window − 4096 + 3 % est
# error ≈ 156.2k, inside the engine ceiling with ~44k of margin
# (robust to a ~25 % under-count). Compaction fires at 85 % of the
# window and rides on frank-zealot: input ≈ threshold −
# keepRecent(20k), comfortably inside its per-slot window.
# 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]