feat: OpenRouter as a second provider with admin provider management #39

Merged
alee merged 16 commits from feat/multi-provider-openrouter into main 2026-09-06 14:38:11 +00:00
Owner

feat: OpenRouter as a second provider with admin provider management

Implements the multi-provider plan (.omo/plans/multi-provider-openrouter.md): OpenRouter joins NeuralWatt as the second dispatch provider, with full provider lifecycle management in the admin portal.

What's in it

Config

  • DispatchProvider gains has_energy_telemetry: bool = False and enabled: bool = True
  • config/config.yaml ships neuralwatt (telemetry on) + openrouter (telemetry off, both enabled); .env.example gains OPENROUTER_API_KEY=

Admin portal — Providers page (new)

  • /admin/providers with list/create/edit/delete UI following the existing design system (switches, escapeHtml, restart-required messaging, provenance badges)
  • Provider CRUD API: GET/POST/DELETE /admin/api/providers{,/{name}} persisting to config.local.yaml overlay — malformed URL → 422, deleting the default provider → 422, deleting a base-configured provider → 403
  • "Providers" nav link on all admin pages; topbar no longer overflows at 800px (#generated-at hidden below 900px)

Poller — multi-provider

  • fetch_openrouter() + parse_openrouter_model(): public catalog, virtual routers (openrouter/auto etc.) excluded, variants kept (:batch → latency_class='flex', :free → standard), id → model_id, canonical_slug → base_model_id, per-token prices normalized to per-1M
  • main() iterates enabled providers with failure isolation (one provider's outage/zero-catalog doesn't abort the other), per-provider mark_stale(provider=), and in-process tiering so new rows are routable immediately

Dispatcher

  • Pins now resolve the owning provider from the catalog instead of always using default_provider (fixes llm-router/openai/gpt-6-astra-style pins dispatching to the wrong base URL)
  • Refusal tracking is per-provider (_provider_refusal_since): a NeuralWatt account refusal no longer poisons classification for a router pointed at OpenRouter; cooldown reuses classifier.cooldown_seconds
  • Energy telemetry gracefully absent for has_energy_telemetry=false providers — OpenRouter keepalive comments (: OPENROUTER PROCESSING) pass through to clients, telemetry logs NULL, nothing fabricated

Testing

  • Suite grew 1251 → 1291 passing, 0 failures (offline; new: test_admin_providers, test_openrouter_poller, test_multi_provider_poller, test_multi_provider_dispatch, migrated test_classifier_cascade)
  • Playwright Chromium smoke of /admin/providers at 1400px and 800px: full CRUD round-trip, XSS escape check, zero console errors after idle (screenshots in .omo/evidence/)
  • Scope-fidelity verified: only two providers in base config, no fabricated telemetry, routing/scoring/tiering/proficiency/verification/TUI modules untouched

Operational follow-ups (after merge)

  1. systemctl --user restart llm-router.service — the running instance on 8080 still serves the pre-OpenRouter code (production .env already has OPENROUTER_API_KEY, only in the main checkout)
  2. PYTHONPATH=src python -m poller — first run upserts + tiers the OpenRouter catalog
  3. Known minor follow-up: Providers form sends null api_key_env when the field is left empty (422 until a value is entered) — cosmetic, pre-existing pattern
# feat: OpenRouter as a second provider with admin provider management Implements the multi-provider plan (`.omo/plans/multi-provider-openrouter.md`): OpenRouter joins NeuralWatt as the second dispatch provider, with full provider lifecycle management in the admin portal. ## What's in it **Config** - `DispatchProvider` gains `has_energy_telemetry: bool = False` and `enabled: bool = True` - `config/config.yaml` ships `neuralwatt` (telemetry on) + `openrouter` (telemetry off, both enabled); `.env.example` gains `OPENROUTER_API_KEY=` **Admin portal — Providers page (new)** - `/admin/providers` with list/create/edit/delete UI following the existing design system (switches, `escapeHtml`, restart-required messaging, provenance badges) - Provider CRUD API: `GET/POST/DELETE /admin/api/providers{,/{name}}` persisting to `config.local.yaml` overlay — malformed URL → 422, deleting the default provider → 422, deleting a base-configured provider → 403 - "Providers" nav link on all admin pages; topbar no longer overflows at 800px (`#generated-at` hidden below 900px) **Poller — multi-provider** - `fetch_openrouter()` + `parse_openrouter_model()`: public catalog, virtual routers (`openrouter/auto` etc.) excluded, variants kept (`:batch` → `latency_class='flex'`, `:free` → `standard`), `id` → `model_id`, `canonical_slug` → `base_model_id`, per-token prices normalized to per-1M - `main()` iterates enabled providers with failure isolation (one provider's outage/zero-catalog doesn't abort the other), per-provider `mark_stale(provider=)`, and in-process tiering so new rows are routable immediately **Dispatcher** - Pins now resolve the owning provider from the catalog instead of always using `default_provider` (fixes `llm-router/openai/gpt-6-astra`-style pins dispatching to the wrong base URL) - Refusal tracking is per-provider (`_provider_refusal_since`): a NeuralWatt account refusal no longer poisons classification for a router pointed at OpenRouter; cooldown reuses `classifier.cooldown_seconds` - Energy telemetry gracefully absent for `has_energy_telemetry=false` providers — OpenRouter keepalive comments (`: OPENROUTER PROCESSING`) pass through to clients, telemetry logs NULL, nothing fabricated ## Testing - Suite grew 1251 → **1291 passing, 0 failures** (offline; new: `test_admin_providers`, `test_openrouter_poller`, `test_multi_provider_poller`, `test_multi_provider_dispatch`, migrated `test_classifier_cascade`) - Playwright Chromium smoke of `/admin/providers` at 1400px and 800px: full CRUD round-trip, XSS escape check, zero console errors after idle (screenshots in `.omo/evidence/`) - Scope-fidelity verified: only two providers in base config, no fabricated telemetry, routing/scoring/tiering/proficiency/verification/TUI modules untouched ## Operational follow-ups (after merge) 1. `systemctl --user restart llm-router.service` — the running instance on 8080 still serves the pre-OpenRouter code (production `.env` already has `OPENROUTER_API_KEY`, only in the main checkout) 2. `PYTHONPATH=src python -m poller` — first run upserts + tiers the OpenRouter catalog 3. Known minor follow-up: Providers form sends `null` `api_key_env` when the field is left empty (422 until a value is entered) — cosmetic, pre-existing pattern
alee added 14 commits 2026-09-06 06:35:36 +00:00
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add fetch_openrouter() and parse_openrouter_model() to src/poller.py.

OpenRouter virtual routers are excluded, :batch variants map to latency_class='flex',

and per-token USD prices are normalized to cost per 1M tokens.

Add tests/test_openrouter_poller.py with mocked requests.get coverage.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
alee added 2 commits 2026-09-06 06:43:23 +00:00
alee merged commit d0300d292a into main 2026-09-06 14:38:11 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alee/6krrt#39