Skip to content

feat: wire the llm-router into the harness and retire the legacy routing surfaces - #248

Closed
ytallo wants to merge 49 commits into
mainfrom
feat/llm-router-integration
Closed

feat: wire the llm-router into the harness and retire the legacy routing surfaces#248
ytallo wants to merge 49 commits into
mainfrom
feat/llm-router-integration

Conversation

@ytallo

@ytallo ytallo commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

The Rust llm-router worker becomes the single front door for every LLM call: routing, provider registry, credential resolution, model catalog, streaming relay with typed errors/retries/timeouts, and abort. This PR lands the worker (supersedes #241), cuts the harness, providers, and console over to it, and removes the legacy surfaces it replaces.

Router (Rust)

  • The per-attempt provider::<id>::stream payload omits absent options instead of serializing them as null (provider-side schemas reject null where a string or array is expected), and always carries resolution_key = request_id so providers can dedupe per-turn credential resolution across retry attempts.
  • New router::route function: a read-only routing preview ({model, provider?} → {provider, candidates}) over the same decide() and error codes as router::chat. Consumers that need the provider before streaming pin the preview as the explicit provider on the chat call, so preview and execution can never diverge.

Harness cutover

  • Streaming: the orchestrator streams turns through router::chat with a deterministic request_id (${session_id}:${started_at_ms}); run::abort fires a best-effort router::abort with the same id, so the upstream actually stops generating. The close-without-terminal synthetic stays as defense-in-depth behind the router's terminal-frame guarantee. Outer trigger budgets are 320s, above the router's 300s stream budget.
  • Provisioning: one router::route call per turn serves prompt-family selection and model-metadata resolution; the routed provider is persisted on the run request.
  • Providers (×5): self-declare via router::provider::register with the registration token persisted in iii-state (re-register without it is rejected), re-declare on the router::ready event, resolve credentials through the token-gated router::provider::resolve, reconcile discovery into the router catalog, and honor the router's resolved max_output_tokens. The Phase-1 provider::<id>::complete functions are gone — router::complete drives the stream function, including the compaction summariser.
  • Config: provider credentials/settings move from the harness configuration entry to the router-owned llm-router entry via an idempotent boot-time migration that also seeds routing parity with the old local decide() (anthropic default; gpt-/o<digit>- → openai, kimi-/moonshot-v1- → kimi). The harness entry is permissions-only now.
  • Console: the model picker reads router::models::list / router::provider::list, provider-credential deep links open the llm-router entry, and the ui::models::changed fanout rebinds to the router::models::changed pubsub topic.

Removed

turn-orchestrator/provider-router.ts (local decide()), the harness::provider::register/resolve/list registry and its refresh-on-config bridge, the models-catalog module (models::list/get/supports/reconcile + state scope models), per-provider complete functions, and the compaction stream collector. The catalog Model type moves to types/model.ts, wire-aligned with the router. Agent permission rules deny the router's spend/credential/catalog-write surface and allow the read surface.

Behavior notes

  • Env-var credential fallback (ANTHROPIC_API_KEY, …) now resolves in the llm-router process, not the harness — launch the router with those variables or paste keys into the llm-router entry (documented in the router README, with the token-loss recovery procedure).
  • Abort now cancels the upstream stream (previously the provider kept generating until terminal/timeout).
  • Mid-turn credential-resolve dedup is keyed by the router's request_id (string) instead of the run start time.

Test plan

  • cargo test (54) and cargo test --test integration (12, engine-backed: relay, cancellation, abort, retry, token gate, paste-a-key, route/chat parity)
  • pnpm vitest run in harness/ — 119 files, 1359 tests, including new coverage for schema null-tolerance, registration-token persistence/retry/terminal rejection, ready-topic re-declare, and config-migration idempotency
  • pnpm vitest run + tsc -b --noEmit in console/web — 654 tests
  • Live engine smoke: register all 5 providers, stream per routing path (anthropic default, gpt-* heuristic, local catalog-owner), mid-stream abort, /compact, paste-a-key, router/harness restarts

ytallo added 30 commits June 11, 2026 09:26
The engine validates the existing entry value against the schema on
re-register; a fresh entry holds null until the operator first writes,
so a strict object type rejected every provider re-registration.
The invocation path reports a missing function as 'function_not_found'
(engine/src/engine/mod.rs); bare NOT_FOUND is the configuration worker's
missing-entry code and must stay Coded, or missing config entries would
flip provider availability.
Removes trait Bus / bus.rs, the SdkBus adapter / bus_sdk.rs, FakeBus, the
scripted provider, and the ChannelFactory dyn seam. Handlers and stores now
take iii_sdk::III directly (thin state.rs wrappers per binary-worker.md §7);
TriggerEmitter implements the SDK TriggerHandler itself; channel plumbing is
plain functions in channels.rs.

Bus-shaped test coverage moves to the engine-backed tests/integration.rs
(8 scenarios, self-skipping when no engine is available, storage-worker
pattern); pure-logic unit tests are unchanged.
The router owned three custom trigger types, tracking subscribers and
fanning out via per-subscriber iii.trigger — a reimplementation of the
engine's built-in iii-pubsub worker (publish function + subscribe
trigger type). Publish to the same three names as pubsub topics
instead; subscribers bind trigger_type "subscribe" with
config.topic. Payloads are delivered verbatim (no envelope) and
failures stay isolated per subscriber, so behavior is unchanged.

Delivery is now concurrent per subscriber (engine-side spawn) instead
of sequential, and publishing no longer waits for subscriber
completion; all emit sites are fire-and-forget so neither is
observable. Subscriptions now live engine-side, removing the
registration-replay dance on router restart. New env-gated
integration test pins raw-payload delivery to a subscribe-bound
probe function.
The local StreamChannelRef/ChannelDirection mirror in types/channel.rs
existed because the old Bus seam barred types/ from importing iii_sdk;
the seam is gone, leaving a duplicate type and a serde round-trip on
every channel mint and open_sink. Use the SDK type everywhere and drop
the conversions. ChatRequest/ProviderStreamInput lose their unused
PartialEq derive (the SDK type does not implement it); wire shape is
identical, as the deleted round-trip itself proved.
ytallo added 19 commits June 11, 2026 10:51
…s absent

The registry-publish flow (and CI's interface-boot smoke) boots the
worker against a bare 'workers: []' engine that has no iii-state
worker, so the boot-time state::get died with function_not_found
before any function registered and interface collection timed out.

Tolerate exactly that error class in the two store loads: warn and
start empty. It is safe to special-case — with no state worker a
later persist can't overwrite the stored snapshot either. Any other
state::get failure still fails the boot. The function_not_found
matcher moves from chat.rs to types/errors.rs for reuse, and a new
env-gated integration test boots the router against a bare engine
and asserts the read surface answers.
… failures

Refactor error handling in the ChatPipeline to guarantee that a terminal error frame is sent to the sink during pre-stream failures. This change addresses issues where consumers may not receive a terminal frame, particularly when routing to an unknown provider or when invalid input is provided. Additionally, introduce a new test to validate that exactly one terminal error frame is emitted in such scenarios.
synthesize_error stamped every terminal it built as transient — a
retryable kind. Correct for the mid-stream no-terminal/idle path, but
the pre-stream failures (invalid request, unrouted model, unknown
provider, structured-output gate) are permanent: a streaming consumer
inspecting error_kind on the frame would retry requests that can never
succeed. error_kind is now a caller choice; pre-stream sites pass
Permanent, the mid-stream synthesis keeps Transient.
…wned provider

Registration flips available back to true, but the register handler
only published op:register — subscribers tracking the
op:available/unavailable transitions stayed stuck on the prior
unavailable. upsert now reports whether the registration recovered a
downed provider (decided under the records lock) and the handler emits
an explicit op:available event on that transition. Fresh registers and
already-up re-registers emit nothing extra.
…rs cannot hang the caller

router::complete drained its internal channel to EOF before consuming
the pipeline result. A pipeline error that never wrote a frame leaves
the channel without an EOF (a zero-write close does not propagate), so
the drain blocked for its full 600s budget — reachable by simply
killing a provider worker: dispatch fails function_not_found, run
returns ProviderUnavailable, the caller times out instead of seeing the
typed error.

Drive the drain and the pipeline concurrently: a pipeline Err
propagates to the caller immediately, for every current and future
error path; on Ok the remaining in-flight frames are drained on a short
budget instead of the streaming one. Engine-backed regression test
registers a provider declaration with no worker behind it and asserts
router::complete answers with router/provider_unavailable fast.
…n_key

Absent options serialized as JSON null, which provider-side schemas reject
where a string or array is expected; every default turn would fail validation.
The payload builder now omits absent keys and always carries
resolution_key = request_id so providers can dedupe per-turn credential
resolution across retry attempts.
Consumers that need the provider before streaming (prompt selection,
provisioning metadata) call router::route and pin the result as the explicit
provider on router::chat, so preview and execution can never diverge. Same
decide(), same inputs, same typed error codes as the chat pipeline.
…, catalog getters

provider-resolve.ts becomes the llm-router provider-protocol client:
token-persisting registerWithRouter (iii-state scope llm-provider-registration,
capped-backoff retry), router::ready re-declare subscription, and token-gated
resolveProviderViaRouter. The provider stream schema tolerates null options,
accepts the router's resolved max_output_tokens, and takes string|number
resolution keys. Catalog reads (getCatalogModel, fetchModelLimit) and the
discovery reconcile move to router::models::get/reconcile.
One-time idempotent boot migration copies the harness entry's providers block
into the router-owned llm-router entry and seeds routing parity with the old
local decide() (anthropic default; gpt-/o<digit>- to openai, kimi-/moonshot-v1-
to kimi). The harness entry is re-registered permissions-only; paste-a-key
reactivity is the router's configuration trigger now.
All five providers self-declare via router::provider::register (re-declaring
on router::ready), resolve credentials via the token-gated resolve, reconcile
discovery into the router catalog, and honor the router's resolved
max_output_tokens as the clamp override. The Phase-1 provider::<id>::complete
functions are gone — router::complete drives the stream function.
Provisioning previews the decision once via router::route, pins the routed
provider on the run request (prompt family becomes a pure provider switch),
and resolves model metadata against the router catalog. The streaming step
calls router::chat with a deterministic request_id
(${session_id}:${started_at_ms}); run::abort fires a best-effort
router::abort with the same id so the upstream actually stops. The
close-without-terminal synthetic stays as defense-in-depth. Compaction
summarises via router::complete. Outer trigger budgets are 320s — they must
exceed the router's 300s stream budget.
The picker lists via router::models::list and router::provider::list, the
provider-credentials deep links open the llm-router configuration entry, and
the ui::models::changed fanout rebinds from the models state-scope trigger to
the router::models::changed pubsub topic.
Deleted: the local provider-router decide() library, the provider registry
(harness::provider::register/resolve/list) and its refresh-on-config bridge,
the models-catalog module (models::list/get/supports/reconcile + scope
'models'), per-provider Phase-1 complete functions, and the compaction
stream collector. The catalog Model type moves to types/model.ts, wire-
aligned with the router (provider-side extras are optional — the router
catalog doesn't persist them). The harness entry is permissions-only; the
harness worker now depends on llm-router instead of models-catalog.
Root permission rules deny the router's spend/credential/catalog-write
surface to agents and allow the read surface (models list/get/supports,
provider list); the dead harness::provider and Phase-1 router::stream_assistant
lines are gone, and the legacy models::* allows are retargeted. The harness
worker manifest depends on llm-router; the router README documents
router::route, the env-fallback process boundary, and token-loss recovery.
Provider, catalog, orchestrator, and compaction suites mock
router::provider::resolve / router::models::get / router::models::reconcile /
router::complete (envelope payloads, token gating, no channel plumbing for
the summariser). New coverage: provider stream schema null tolerance and
router-built payloads, registration-token persistence/retry/terminal
rejection, ready-topic re-declare binding, and config-migration idempotency
and seeding. Suites for the deleted registry, refresh-on-config,
models-catalog, and local decide() are gone with their subjects.
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jun 12, 2026 2:23am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 167 files, which is 17 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a7d5dfe3-b037-4af8-93a2-265de1ad2084

📥 Commits

Reviewing files that changed from the base of the PR and between ab75f28 and daa0305.

⛔ Files ignored due to path filters (1)
  • llm-router/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (167)
  • console/web/src/components/chat/MessageList.tsx
  • console/web/src/components/chat/ModelPicker.tsx
  • console/web/src/hooks/use-model-picker-source.ts
  • console/web/src/lib/harness-config-events.ts
  • console/web/src/lib/models-catalog.ts
  • console/web/src/lib/providers.ts
  • console/web/src/pages/Configuration/tabs/ConsoleSettingsTab.tsx
  • harness/package.json
  • harness/src/context-compaction/model-resolver.ts
  • harness/src/context-compaction/stream-collect.ts
  • harness/src/context-compaction/summarize.ts
  • harness/src/harness/fanout/models-changed.ts
  • harness/src/harness/iii.worker.yaml
  • harness/src/harness/migrate-llm-router-config.ts
  • harness/src/harness/permissions-config.ts
  • harness/src/harness/providers/refresh-on-config.ts
  • harness/src/harness/providers/register.ts
  • harness/src/harness/providers/registry.ts
  • harness/src/harness/register.ts
  • harness/src/index.ts
  • harness/src/models-catalog/handlers/get.ts
  • harness/src/models-catalog/handlers/list.ts
  • harness/src/models-catalog/handlers/reconcile.ts
  • harness/src/models-catalog/handlers/supports.ts
  • harness/src/models-catalog/iii.worker.yaml
  • harness/src/models-catalog/main.ts
  • harness/src/models-catalog/register.ts
  • harness/src/models-catalog/state.ts
  • harness/src/models-catalog/types.ts
  • harness/src/provider-anthropic/auth.ts
  • harness/src/provider-anthropic/complete.ts
  • harness/src/provider-anthropic/discover.ts
  • harness/src/provider-anthropic/iii.worker.yaml
  • harness/src/provider-anthropic/main.ts
  • harness/src/provider-anthropic/register.ts
  • harness/src/provider-anthropic/stream-fn.ts
  • harness/src/provider-anthropic/thinking.ts
  • harness/src/provider-anthropic/types.ts
  • harness/src/provider-kimi/auth.ts
  • harness/src/provider-kimi/complete.ts
  • harness/src/provider-kimi/discover.ts
  • harness/src/provider-kimi/iii.worker.yaml
  • harness/src/provider-kimi/main.ts
  • harness/src/provider-kimi/register.ts
  • harness/src/provider-kimi/stream-fn.ts
  • harness/src/provider-llamacpp/auth.ts
  • harness/src/provider-llamacpp/complete.ts
  • harness/src/provider-llamacpp/discover.ts
  • harness/src/provider-llamacpp/iii.worker.yaml
  • harness/src/provider-llamacpp/main.ts
  • harness/src/provider-llamacpp/register.ts
  • harness/src/provider-llamacpp/stream-fn.ts
  • harness/src/provider-lmstudio/auth.ts
  • harness/src/provider-lmstudio/complete.ts
  • harness/src/provider-lmstudio/discover.ts
  • harness/src/provider-lmstudio/iii.worker.yaml
  • harness/src/provider-lmstudio/main.ts
  • harness/src/provider-lmstudio/register.ts
  • harness/src/provider-lmstudio/stream-fn.ts
  • harness/src/provider-openai/auth.ts
  • harness/src/provider-openai/complete.ts
  • harness/src/provider-openai/discover.ts
  • harness/src/provider-openai/iii.worker.yaml
  • harness/src/provider-openai/main.ts
  • harness/src/provider-openai/register.ts
  • harness/src/provider-openai/stream-fn.ts
  • harness/src/provider-openai/types.ts
  • harness/src/runtime/harness-config.ts
  • harness/src/runtime/models-discovery.ts
  • harness/src/runtime/output-tokens.ts
  • harness/src/runtime/provider-resolve.ts
  • harness/src/turn-orchestrator/assistant-streaming/ports.ts
  • harness/src/turn-orchestrator/assistant-streaming/run.ts
  • harness/src/turn-orchestrator/preflight.ts
  • harness/src/turn-orchestrator/prompt/index.ts
  • harness/src/turn-orchestrator/provider-router.ts
  • harness/src/turn-orchestrator/provider-stream.ts
  • harness/src/turn-orchestrator/provisioning/ports.ts
  • harness/src/turn-orchestrator/provisioning/process.ts
  • harness/src/turn-orchestrator/run-abort.ts
  • harness/src/turn-orchestrator/run-request.ts
  • harness/src/turn-orchestrator/state.ts
  • harness/src/turn-orchestrator/system-prompt.ts
  • harness/src/types/model.ts
  • harness/src/types/provider.ts
  • harness/tests/context-compaction/compact-session-registered.test.ts
  • harness/tests/context-compaction/compact-session.test.ts
  • harness/tests/context-compaction/e2e/full-session.test.ts
  • harness/tests/context-compaction/handler-async.test.ts
  • harness/tests/context-compaction/integration/backward-compat.test.ts
  • harness/tests/context-compaction/integration/flow-async.test.ts
  • harness/tests/context-compaction/integration/flow-sync.test.ts
  • harness/tests/context-compaction/summarize.test.ts
  • harness/tests/harness/migrate-llm-router-config.test.ts
  • harness/tests/harness/policy.test.ts
  • harness/tests/harness/providers/refresh-on-config.test.ts
  • harness/tests/harness/providers/registry.test.ts
  • harness/tests/models-catalog/state.test.ts
  • harness/tests/models-catalog/types.test.ts
  • harness/tests/provider-anthropic/auth.test.ts
  • harness/tests/provider-anthropic/discover.test.ts
  • harness/tests/provider-anthropic/thinking.test.ts
  • harness/tests/provider-llamacpp/auth.test.ts
  • harness/tests/provider-llamacpp/discover.test.ts
  • harness/tests/provider-lmstudio/auth.test.ts
  • harness/tests/provider-lmstudio/discover.test.ts
  • harness/tests/provider-openai/stream-request.test.ts
  • harness/tests/runtime/output-tokens.test.ts
  • harness/tests/runtime/provider-resolve.test.ts
  • harness/tests/turn-orchestrator/preflight.test.ts
  • harness/tests/turn-orchestrator/provider-router.test.ts
  • harness/tests/turn-orchestrator/provisioning-layer.test.ts
  • harness/tests/turn-orchestrator/system-prompt.test.ts
  • harness/tests/types/provider.test.ts
  • iii-permissions.yaml
  • llm-router/.gitignore
  • llm-router/Cargo.toml
  • llm-router/README.md
  • llm-router/build.rs
  • llm-router/iii-permissions.yaml
  • llm-router/iii.worker.yaml
  • llm-router/src/catalog/handlers.rs
  • llm-router/src/catalog/mod.rs
  • llm-router/src/catalog/queries.rs
  • llm-router/src/catalog/reconcile.rs
  • llm-router/src/catalog/store.rs
  • llm-router/src/channels.rs
  • llm-router/src/chat/abort.rs
  • llm-router/src/chat/chat.rs
  • llm-router/src/chat/complete.rs
  • llm-router/src/chat/inflight.rs
  • llm-router/src/chat/mod.rs
  • llm-router/src/chat/output_tokens.rs
  • llm-router/src/chat/pricing.rs
  • llm-router/src/chat/relay.rs
  • llm-router/src/chat/retry.rs
  • llm-router/src/chat/synthesize.rs
  • llm-router/src/config/entry.rs
  • llm-router/src/config/fingerprint.rs
  • llm-router/src/config/mod.rs
  • llm-router/src/config/on_changed.rs
  • llm-router/src/config/schema.rs
  • llm-router/src/lib.rs
  • llm-router/src/main.rs
  • llm-router/src/manifest.rs
  • llm-router/src/register.rs
  • llm-router/src/registry/availability.rs
  • llm-router/src/registry/mod.rs
  • llm-router/src/registry/register.rs
  • llm-router/src/registry/resolve.rs
  • llm-router/src/registry/store.rs
  • llm-router/src/routing.rs
  • llm-router/src/settings.rs
  • llm-router/src/state.rs
  • llm-router/src/testkit/fake_channels.rs
  • llm-router/src/testkit/mod.rs
  • llm-router/src/triggers.rs
  • llm-router/src/types/content.rs
  • llm-router/src/types/credential.rs
  • llm-router/src/types/errors.rs
  • llm-router/src/types/events.rs
  • llm-router/src/types/messages.rs
  • llm-router/src/types/mod.rs
  • llm-router/src/types/model.rs
  • llm-router/src/types/router.rs
  • llm-router/tests/integration.rs
  • tech-specs/2026-06-agentic/llm-router.md

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/llm-router-integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 16 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant