5.7 KiB
name, description
| name | description |
|---|---|
| omp-models-yml-runner-api-placement | Add or edit a non-chat (tts/image/embedding) model row in ~/.omp/agent/models.yml, wire the speech role, and diagnose a failing speech device without blaming a healthy endpoint |
Scope
~/.omp/agent/models.yml (or models.yaml) on a workstation: adding a runner
model row (TTS / image / embedding / rerank / video) alongside chat providers,
flipping config.yml modelRoles at it, and diagnosing a speech device that
refuses to speak.
The landmine
Provider-level api accepts only chat/judge transports:
openai-completions, openai-responses, openai-codex-responses,
azure-openai-responses, anthropic-messages, bedrock-converse-stream,
google-generative-ai, google-gemini-cli, google-vertex,
openrouter-decisions, typesafe.
Naming a runner API (openai-speech, openai-images, openai-embeddings, …)
at provider level fails whole-file validation, and the failure mode is not
scoped to your provider:
Warning: models.yml validation failed — custom providers disabled
Failed to load config file models, Schema error: providers.<id>.api: must be "openai-completions", …
Every custom provider disappears from the registry (fleet chat endpoints stop
resolving) and omp models lists only built-in locals. Measured 2026-10-18.
Correct shape
Runner api goes on the model entry only; auth: none satisfies the
credential requirement; omit contextWindow/maxTokens (the positivity check
fires only when they are provided — omp models still prints filled defaults,
which the runner transport never reads).
"frank-tts (dankclaw)":
baseUrl: http://192.168.0.143:7772/v1 # no `api:` here
auth: none
models:
- id: frank-tts
name: "frank-tts (dankclaw) — cloned voices"
api: openai-speech # runner api on the model
input: [text]
Roles live in config.yml (modelRoles), not in models.yml. Kind roles
(image, web, speech, dictation, judge) take no :thinking suffix.
Verify after every edit (never assume the file loaded)
omp models --kind tts --no-extensions # the new row must appear
omp models --no-extensions | grep -E '<each fleet chat model>' # and still be listed
omp models | grep -c 'validation failed' # must be 0
A run that lists only built-ins means your row broke the file — revert before yielding.
Speech transport contract (openai-speech)
Body is {model, input, response_format, voice?, speed?}; voice is forwarded
only when the caller passed voice_id; sample_rate/bit_rate throw
(openai-speech does not support sampleRate). Codec comes from the
output_path suffix; 15 000-character cap. An unknown voice name is a server
error, not a fallback — and auth: none makes a dead endpoint read as
available, so point modelRoles.speech at such a provider only after it has
answered a real synthesis.
Three speech-role laws (bundle-read + measured 2026-10-18)
- No setting carries a voice for
openai-speech. The transport builds the body withvoicefrom the call'svoice_idand nothing else.tts.localVoiceis the local Kokoro backend's key;speech.voiceis the TUI's speak-aloud voice, an enum of Kokoro names that never reaches an HTTP request (setting it to a pack name breaks the vocalizer and does nothing for synthesis — readcfgSpeechVoiceindist/cli.jsbefore believing otherwise). So a voice-less call is the harness's normal shape: a voice-required endpoint needs a server-side default (fleet answer:DEFAULT_VOICEinscripts/frank-tts.env, journal marks the line(default), unknown names still 400 with the list). - An explicit role has no fallback candidate.
modelRoles.speechset to one model yields exactly one candidate, so a dead endpoint or an unresolvable selector fails with "No available speech model matches the speech role" instead of degrading to Kokoro. Loud-by-design; do not "fix" it by adding a fallback in the client. models.ymlloads at process start;config.ymlis read live. A session older than the new row fails exactly like a dead endpoint while a fresh process succeeds in the same minute.
Diagnosing "speech device won't speak" — in this order
curlthe endpoint directly with a real voice name. 200 + audio ⇒ the service is fine and the fault is harness-side; stop touching the server.- Control test in the same session: point
modelRoles.speechatlocal/kokoroand call the device. It works ⇒config.ymlis live-read and the runner selector or its voice argument is the problem, not the endpoint. Revert the role immediately after. - Distinguish stale registry from dead endpoint with a fresh process:
omp -p --no-session --no-extensions --no-skills --no-rules --tools tts --cwd /tmp 'Call the tts tool exactly once with text="…" output_path=/tmp/x.wav voice_id=<name> — then reply with the tool result line.'Succeeds ⇒ the long-lived session predates themodels.ymlrow; restart it. - Cross-check server-side, never trust the client's echo: the façade/access log
line and the engine's own ledger must carry the same byte count the tool
printed. (
model=…in a tool result is what the client requested; a server line is the fact.) - For harness-behaviour claims, read the bundle rather than reasoning from the
tool description:
/home/blank/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js, e.g.grep -o -E '.{300}response_format:[a-zA-Z$]+.{250}'for the speech body builder,grep -o -E 'cfgSpeechVoice:.{0,400}'for what a settings key feeds.