live: heartbeat sync 2026-10-21T17:49:41-07:00 (Refs #11)
This commit is contained in:
1 parent
c0d840a8bb
commit
017404aa51
30 files changed
+568
-109
No files matched your search
@@ -1,14 +1,15 @@
|
||||
---
|
||||
name: omp-models-yml-runner-api-placement
|
||||
description: "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"
|
||||
description: "Add or edit a row in ~/.omp/agent/models.yml — non-chat (tts/image/embedding) runners and authenticated/remote providers with command-backed apiKey values — wire the speech role, verify with a poisoned scratch HOME, 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.
|
||||
adding an **authenticated or off-network** chat provider without committing its
|
||||
secret, flipping `config.yml` `modelRoles` at it, and diagnosing a speech device
|
||||
that refuses to speak.
|
||||
|
||||
# The landmine
|
||||
|
||||
@@ -51,6 +52,46 @@ which the runner transport never reads).
|
||||
Roles live in `config.yml` (`modelRoles`), not in `models.yml`. Kind roles
|
||||
(`image`, `web`, `speech`, `dictation`, `judge`) take no `:thinking` suffix.
|
||||
|
||||
# Authenticated / remote providers
|
||||
|
||||
Provider schema (`src/config/models-config-schema-bundle.ts`): `baseUrl`,
|
||||
`apiKey`, `api`, `headers`, `compat`, `authHeader`, `auth`
|
||||
(`"apiKey" | "none" | "oauth"`), `discovery`, `models`, `modelOverrides`,
|
||||
`transport`. Defining `models` without `apiKey` and without `auth: none|oauth`
|
||||
is a validation error, so an authenticated endpoint needs a key *shape* even if
|
||||
the value comes from outside the file.
|
||||
|
||||
**Never put the literal secret in `models.yml`.** `apiKey` (and any `headers`
|
||||
value) is resolved by `src/config/resolve-config-value.ts`:
|
||||
|
||||
- leading `!` → command-backed: `/bin/sh -c "<rest>"`, cwd = project dir,
|
||||
10 s timeout, stdout **trimmed**, result cached for the process lifetime
|
||||
(failure backs off 30 s);
|
||||
- otherwise the string is looked up as an **exact env-var name**; the literal is
|
||||
the fallback only when no such env key exists.
|
||||
|
||||
So the secret lives in a 600 file, and the tracked file stays clean:
|
||||
|
||||
```yaml
|
||||
apiKey: "!cat ~/.config/secrets/openwebui_api_key" # QUOTE it: an unquoted
|
||||
# leading ! is a YAML tag
|
||||
```
|
||||
|
||||
Laws: a literal `sk-…` in this repo trips the pre-commit shape gate *and* lands
|
||||
the key in every agent context on every box that pulls; rotation means rewrite
|
||||
the secret file **then restart the harness** (the `!cmd` value is process-cached,
|
||||
so a live session keeps serving the old key and a stale session reads exactly
|
||||
like a revoked one); every workstation needs its own copy of the secret file.
|
||||
|
||||
**A row behind a proxy copies its direct-connected twin's `compat` block**, it
|
||||
does not simplify it. Measured through an OpenAI-compatible gateway on
|
||||
2026-10-21: SSE forwards `reasoning_content` and `tool_calls` deltas plus final
|
||||
`usage` under `stream_options.include_usage`; `image_url` content parts reach
|
||||
the vision tower; `chat_template_kwargs` is forwarded (same prompt:
|
||||
`enable_thinking` false → 0 reasoning tokens, true → a real block). Dropping
|
||||
kwargs/effort maps because "it is only a proxy" silently changes thinking
|
||||
behaviour on that path. Check per gateway, never assume either direction.
|
||||
|
||||
# Verify after every edit (never assume the file loaded)
|
||||
|
||||
```bash
|
||||
@@ -62,6 +103,25 @@ 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.
|
||||
|
||||
**Poisoned-scratch-HOME proof** for a remote/credential row (the only test that
|
||||
shows a success came through the new path and not from a LAN endpoint):
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/ompx/.omp/agent /tmp/ompx/.config
|
||||
cp agent/models.yml agent/config.yml /tmp/ompx/.omp/agent/
|
||||
ln -s ~/.config/secrets /tmp/ompx/.config/secrets # REQUIRED: ~ in a !cmd value
|
||||
# expands to the scratch $HOME
|
||||
sed -i 's#http://192\.168\.0\.[0-9]*:[0-9]*/v1#http://127.0.0.1:9/v1#g' /tmp/ompx/.omp/agent/models.yml
|
||||
HOME=/tmp/ompx omp --model '<provider>/<id>:off' --tools bash --no-session --no-skills \
|
||||
--no-rules --no-extensions --no-pty --no-lsp --no-title --cwd /tmp -p '<ask for a real tool call>'
|
||||
```
|
||||
|
||||
Then `rm -rf /tmp/ompx` — that tree holds endpoint URLs. Cross-check the server
|
||||
side before believing the client's echo (gateway access log / container log line
|
||||
for the request you just made), and confirm the change never hit the live
|
||||
checkout until you mean it to: `git -C <live checkout> status --porcelain agent/models.yml`
|
||||
plus a grep for the new provider name.
|
||||
|
||||
# Speech transport contract (`openai-speech`)
|
||||
|
||||
Body is `{model, input, response_format, voice?, speed?}`; `voice` is forwarded
|
||||
@@ -72,13 +132,17 @@ 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.
|
||||
|
||||
A gateway's own `/api/v1/audio/speech` route is NOT an equivalent: OpenWebUI
|
||||
substitutes its own voice enum before forwarding, so a cloned pack name comes
|
||||
back 400 from the upstream. Keep `modelRoles.speech` on the direct endpoint.
|
||||
|
||||
# Three speech-role laws (bundle-read + measured 2026-10-18)
|
||||
|
||||
1. **No setting carries a voice for `openai-speech`.** The transport builds the
|
||||
body with `voice` from the call's `voice_id` and nothing else.
|
||||
`tts.localVoice` is the *local Kokoro backend's* key; `speech.voice` is 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
|
||||
`tts.localVoice` is the *local Kokoro backend's* key; `speech.voice` is 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 — read `cfgSpeechVoice` in `dist/cli.js` before believing
|
||||
otherwise). So a voice-less call is the harness's **normal** shape: a
|
||||
voice-required endpoint needs a server-side default (fleet answer:
|
||||
|
||||
Reference in new issue
Block a user