Files
personal-agent-os/memory-systems/agent-os/01_Wiki/omniroute-routing-failover.md
T
2026-09-13 12:07:23 -07:00

125 lines
5.3 KiB
Markdown

# OmniRoute auto-combo failover — the 200-with-empty-body trap
> Failure mode + fix. Diagnosed 2026-09-12 while repairing the AshBot profile
> (see [[04_projects/ashbot/status]]). Applies to **any** Hermes profile whose primary
> model is an OmniRoute `auto/*` combo.
## Symptom
A profile on an `auto/*` combo (e.g. `auto/best-free`) runs for minutes, then the turn ends
with a stub or an error reply — often mid-task, with tool calls already made.
```
agent.stream_diag: Stream exhausted on attempt 3/3 — retrying
error_type=EmptyStreamError http_status=200 bytes=0 chunks=0
agent.conversation_loop: API call failed after 3 retries
provider=custom model=auto/best-free
```
## Root cause
The target pool for the built-in `auto/*` combos is **dynamic** — there is no combo row to
edit. OmniRoute scores a candidate pool from every configured provider each request.
1. The combo's primary is the OpenRouter free pool. It rate-limits constantly
(`429 Provider returned error`), and the connection is marked
`unavailable / rate_limited` in `provider_connections`.
2. The chain then falls through to targets that are **broken but look healthy**:
- `opencode/minimax-m3-free` → `401 Model minimax-m3-free is not supported`
- `auggie/claude-sonnet-4.6`, `auggie/gemini-3.1-pro` → **HTTP 200 with a 0-byte body**,
~1.0 s, repeatedly
3. OmniRoute counts a 200 as success. The provider circuit breaker only trips on
`408/500/502/503/504`, so **a 200-with-empty-body never trips it, never cools down, and
never fails over**. The combo returns the empty stream to the client.
4. Hermes sees an empty SSE → `EmptyStreamError` → retries → turn dies.
**Never assume "200 = healthy" when reading OmniRoute's `call_logs`.**
## Quirk: `model.fallback:` is not the failover chain
A profile can look protected and have zero failover. In `config.yaml`:
- `model.fallback: custom:omniroute/auto/best-free` — ignored as a chain
- top-level `fallback_providers:` — **the real chain**
```bash
hermes -p <name> fallback list # "No fallback providers configured" = no safety net
```
Hermes does have explicit empty-response and retry-exhausted fallback paths
(`agent/turn_empty_response.py`, `agent/turn_api_error.py`) — they just never fire without
a chain.
## Fix A — give the profile its own failover chain
Back up `config.yaml`, then append:
```yaml
fallback_providers:
- provider: custom:omniroute
model: openrouter/deepseek/deepseek-v4.1-flash
- provider: custom:omniroute
model: freetoken-gemma26b/Gemma-4-26B-A4B-NVFP4 # local, free, no rate limits
```
- `custom:<name>` must match a `providers:` entry in that profile's config.
- **No gateway restart needed** — the gateway re-reads `fallback_providers` from disk for the
next agent create/reuse.
- **Prove it fires, don't assume.** Run one-shot with a deliberately bogus primary, then check
which model actually answered:
```bash
hermes -p <name> -m "openrouter/does-not-exist" -z "Reply with exactly: FB-OK"
# then: state.db → session_model_usage → the row names the model that really served it
```
## Fix B — disable the broken no-auth providers
The mechanism is the `blockedProviders` setting (matches provider **id or alias**). For true
no-auth providers (`auggie`, `opencode`, `theoldllm`, …) this is the documented disable switch.
The management API is **not reachable on this Windows install**: `GET /api/settings` returns
401 even from loopback with the CLI machine token, because the server's `node-machine-id`
require fails inside the standalone bundle — its expected token is empty, and `doctor`
reports the health endpoint as "likely requires MANAGEMENT_TOKEN". Write the setting directly
instead:
```python
import sqlite3, json, os
con = sqlite3.connect(os.path.expanduser("~/.omniroute/storage.sqlite"), timeout=20)
con.execute("INSERT OR REPLACE INTO key_value (namespace,key,value) VALUES ('settings','blockedProviders',?)",
(json.dumps(["auggie", "opencode"]),))
con.commit()
```
Settings cache TTL is 5 s, so it applies with no restart. Verify on the unauthenticated
catalog endpoint — the provider's models disappear and the total count drops:
```bash
curl -s localhost:20128/v1/models | tr ',' '\n' | grep -c '"id":"'
```
## Where OmniRoute actually lives
| Thing | Path |
|---|---|
| Running server | global npm package `~/AppData/Roaming/npm/node_modules/omniroute` (`node dist/server-ws.mjs`), v3.8.48 |
| Data dir | `~/.omniroute` — `storage.sqlite` (`call_logs`, `key_value`, `provider_connections`, `combos`), `call_logs/<date>/` |
| Repo checkout | `~/AppData/Local/hermes/OmniRoute` — **not** the running server; source of truth for behavior/config keys |
## Verification of this fix (2026-09-12)
- `fallback list` shows both entries.
- Forced primary failure → reply returned, and `session_model_usage` records
`openrouter/deepseek/deepseek-v4.1-flash` via `custom:omniroute`.
- Catalog after the block: **476 → 453** models; no `aug/*` or `oc/*` entries remain.
- No other profile referenced those two providers.
## Open issues
- OmniRoute's loopback CLI-token management auth (401) — blocks API-driven config on Windows.
- The primary is still a free-tier roulette: a failure now costs the retry window
(~1 min typical, up to ~4.5 min observed) before recovering instead of dying.
See also: [[01_Wiki/hermes-gitea-ecosystem-manual]]