Skip to content

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

Merged
ytallo merged 15 commits into
mainfrom
feat/llm-router-integration
Jun 12, 2026
Merged

feat: wire the llm-router into the harness and retire the legacy routing surfaces#250
ytallo merged 15 commits into
mainfrom
feat/llm-router-integration

Conversation

@ytallo

@ytallo ytallo commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #241 (the llm-router worker). This PR cuts the harness and console over to the router and removes everything provider-shaped from the harness process: the router becomes the single front door for every LLM call (routing, provider registry, credential resolution, model catalog, streaming relay with typed errors/retries/timeouts, abort), and LLM providers are standalone router plugin workers from here on — they self-register over the provider protocol (#247 is the first; the rest follow as their own workers). Until a provider worker is installed, chats fail loudly with router/no_provider_for_model.

Router fixes (first two commits)

  • 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. On the session-manager base (feat: harness with session manager #251), the router channel is the token source and session-manager is the sink: coalesced delta batches land as session::update_message snapshots on the turn's assistant entry (the session::message_updated feed is the live token surface), with a strict final update before completion — thinking blocks ride the same snapshots, so live thought panes work unchanged.
  • Provisioning: one router::route call per turn serves prompt-family selection and model-metadata resolution; the routed provider is persisted on the run request.
  • Thinking: run::start now accepts thinking_level ('off'…'xhigh') — the field was previously stripped at the payload boundary, making the (complete) downstream plumbing unreachable. Verified live: a turn with thinking_level: high on sonnet lands a thinking block in the transcript; an unsupported level degrades with a provider warning. The console gains a thinking-level picker next to the model selector, and thought panes auto-open while streaming so reasoning is visible in real time (collapsing to the duration summary when done).
  • 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

  • The five in-process provider workers (provider-anthropic/openai/kimi/lmstudio/llamacpp) with their worker definitions and test suites.
  • The TS provider-protocol helpers: registration/token handling, models.dev discovery enrichment, the OpenAI-compat URL normalizer, and output-token clamping (the router owns the budget; runtime/catalog.ts keeps the catalog read).
  • 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), and the compaction stream collector.
  • The catalog Model type moves to types/model.ts (wire-aligned with the router) and AgentFunctionSchema to types/function.ts. Agent permission rules deny the router's spend/credential/catalog-write surface and allow the read surface.

Behavior notes

  • Provider capability now comes entirely from installed provider workers; a fresh harness has none until e.g. feat(provider-anthropic): Anthropic Messages API provider worker #247's provider-anthropic is added.
  • Env-var credential fallback (ANTHROPIC_API_KEY, …) resolves in the llm-router process — 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).
  • Operational note for environments where the old in-process providers already registered: the router binds each provider id to a registration token, so a standalone worker taking over an id needs the router's registry state key cleared once (providers re-bind on router::ready).

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/ — 895 tests on the rebased (post-feat: harness with session manager #251 session-manager) base
  • pnpm vitest run + tsc --noEmit in console/web — 667 tests
  • Live engine smoke against feat(provider-anthropic): Anthropic Messages API provider worker #247's provider-anthropic (worktree builds): browser chat e2e (multi-turn recall, agent function call verified against the bus, mid-stream abort with upstream cancellation, model + mode switching), explicit thinking via run::start, router restart with router::ready re-declare, harness restart
  • Browser thinking e2e: composer picker → thinking_level on the wire → live-streaming thought panes (content sampled growing mid-stream), combined with an agent function call in the same turn
  • Re-run the browser smoke on the rebased base (live tokens now flow via session::message_updated; the stack needs the session-manager worker running)
  • Remaining smoke: /compact via UI, paste-a-key flow

@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 6:57pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 198 files, which is 48 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: 9f9491f8-bc09-4af7-ab89-26a218798903

📥 Commits

Reviewing files that changed from the base of the PR and between 5e81a6e and 545deb9.

📒 Files selected for processing (198)
  • console/web/src/components/chat/ChatView.tsx
  • console/web/src/components/chat/Composer.stories.tsx
  • console/web/src/components/chat/Composer.tsx
  • console/web/src/components/chat/MessageList.tsx
  • console/web/src/components/chat/ModelPicker.tsx
  • console/web/src/components/chat/ThoughtMessage.tsx
  • console/web/src/hooks/use-model-picker-source.ts
  • console/web/src/lib/backend/real.ts
  • console/web/src/lib/backend/types.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
  • console/web/src/types/chat.ts
  • 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/cache.ts
  • harness/src/provider-anthropic/complete.ts
  • harness/src/provider-anthropic/config.ts
  • harness/src/provider-anthropic/discover.ts
  • harness/src/provider-anthropic/iii.worker.yaml
  • harness/src/provider-anthropic/main.ts
  • harness/src/provider-anthropic/refresh-fn.ts
  • harness/src/provider-anthropic/register.ts
  • harness/src/provider-anthropic/sse.ts
  • harness/src/provider-anthropic/stream-fn.ts
  • harness/src/provider-anthropic/stream.ts
  • harness/src/provider-anthropic/thinking.ts
  • harness/src/provider-anthropic/types.ts
  • harness/src/provider-anthropic/wire-messages.ts
  • harness/src/provider-anthropic/wire-tools.ts
  • harness/src/provider-kimi/auth.ts
  • harness/src/provider-kimi/complete.ts
  • harness/src/provider-kimi/config.ts
  • harness/src/provider-kimi/discover.ts
  • harness/src/provider-kimi/iii.worker.yaml
  • harness/src/provider-kimi/main.ts
  • harness/src/provider-kimi/refresh-fn.ts
  • harness/src/provider-kimi/register.ts
  • harness/src/provider-kimi/sse.ts
  • harness/src/provider-kimi/stream-fn.ts
  • harness/src/provider-kimi/stream.ts
  • harness/src/provider-kimi/types.ts
  • harness/src/provider-kimi/wire-messages.ts
  • harness/src/provider-kimi/wire-tools.ts
  • harness/src/provider-llamacpp/auth.ts
  • harness/src/provider-llamacpp/complete.ts
  • harness/src/provider-llamacpp/config.ts
  • harness/src/provider-llamacpp/discover.ts
  • harness/src/provider-llamacpp/iii.worker.yaml
  • harness/src/provider-llamacpp/main.ts
  • harness/src/provider-llamacpp/refresh-fn.ts
  • harness/src/provider-llamacpp/register.ts
  • harness/src/provider-llamacpp/sse.ts
  • harness/src/provider-llamacpp/stream-fn.ts
  • harness/src/provider-llamacpp/stream.ts
  • harness/src/provider-llamacpp/types.ts
  • harness/src/provider-llamacpp/wire-messages.ts
  • harness/src/provider-llamacpp/wire-tools.ts
  • harness/src/provider-lmstudio/auth.ts
  • harness/src/provider-lmstudio/complete.ts
  • harness/src/provider-lmstudio/config.ts
  • harness/src/provider-lmstudio/discover.ts
  • harness/src/provider-lmstudio/iii.worker.yaml
  • harness/src/provider-lmstudio/load-fn.ts
  • harness/src/provider-lmstudio/load.ts
  • harness/src/provider-lmstudio/main.ts
  • harness/src/provider-lmstudio/refresh-fn.ts
  • harness/src/provider-lmstudio/register.ts
  • harness/src/provider-lmstudio/sse.ts
  • harness/src/provider-lmstudio/stream-fn.ts
  • harness/src/provider-lmstudio/stream.ts
  • harness/src/provider-lmstudio/types.ts
  • harness/src/provider-lmstudio/unload-fn.ts
  • harness/src/provider-lmstudio/wire-messages.ts
  • harness/src/provider-lmstudio/wire-tools.ts
  • harness/src/provider-openai/auth.ts
  • harness/src/provider-openai/complete.ts
  • harness/src/provider-openai/config.ts
  • harness/src/provider-openai/discover.ts
  • harness/src/provider-openai/iii.worker.yaml
  • harness/src/provider-openai/main.ts
  • harness/src/provider-openai/reasoning.ts
  • harness/src/provider-openai/refresh-fn.ts
  • harness/src/provider-openai/register.ts
  • harness/src/provider-openai/sse.ts
  • harness/src/provider-openai/stream-fn.ts
  • harness/src/provider-openai/stream.ts
  • harness/src/provider-openai/types.ts
  • harness/src/provider-openai/wire-messages.ts
  • harness/src/provider-openai/wire-tools.ts
  • harness/src/runtime/catalog.ts
  • harness/src/runtime/harness-config.ts
  • harness/src/runtime/models-discovery.ts
  • harness/src/runtime/modelsdev.ts
  • harness/src/runtime/openai-compat-url.ts
  • harness/src/runtime/output-tokens.ts
  • harness/src/runtime/provider-resolve.ts
  • harness/src/runtime/worker.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/schemas.ts
  • harness/src/turn-orchestrator/state.ts
  • harness/src/turn-orchestrator/system-prompt.ts
  • harness/src/types/function.ts
  • harness/src/types/index.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/cache.test.ts
  • harness/tests/provider-anthropic/discover.test.ts
  • harness/tests/provider-anthropic/sse.test.ts
  • harness/tests/provider-anthropic/stream-request.test.ts
  • harness/tests/provider-anthropic/thinking.test.ts
  • harness/tests/provider-anthropic/wire-messages.test.ts
  • harness/tests/provider-kimi/sse.test.ts
  • harness/tests/provider-kimi/stream.test.ts
  • harness/tests/provider-kimi/wire-messages.test.ts
  • harness/tests/provider-llamacpp/auth.test.ts
  • harness/tests/provider-llamacpp/config.test.ts
  • harness/tests/provider-llamacpp/discover.test.ts
  • harness/tests/provider-llamacpp/sse.test.ts
  • harness/tests/provider-llamacpp/stream.test.ts
  • harness/tests/provider-llamacpp/wire-messages.test.ts
  • harness/tests/provider-llamacpp/wire-tools.test.ts
  • harness/tests/provider-lmstudio/auth.test.ts
  • harness/tests/provider-lmstudio/config.test.ts
  • harness/tests/provider-lmstudio/discover.test.ts
  • harness/tests/provider-lmstudio/load.test.ts
  • harness/tests/provider-lmstudio/sse.test.ts
  • harness/tests/provider-lmstudio/stream.test.ts
  • harness/tests/provider-lmstudio/wire-messages.test.ts
  • harness/tests/provider-lmstudio/wire-tools.test.ts
  • harness/tests/provider-openai/reasoning.test.ts
  • harness/tests/provider-openai/sse.test.ts
  • harness/tests/provider-openai/stream-request.test.ts
  • harness/tests/provider-openai/wire-messages.test.ts
  • harness/tests/runtime/models-discovery.test.ts
  • harness/tests/runtime/modelsdev.test.ts
  • harness/tests/runtime/openai-compat-url.test.ts
  • harness/tests/runtime/output-tokens.test.ts
  • harness/tests/turn-orchestrator/context-view.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/run-start.test.ts
  • harness/tests/turn-orchestrator/system-prompt.test.ts
  • harness/tests/types/provider.test.ts
  • iii-permissions.yaml
  • llm-router/README.md
  • llm-router/iii-permissions.yaml
  • llm-router/src/chat/chat.rs
  • llm-router/src/register.rs
  • llm-router/src/routing.rs
  • llm-router/tests/integration.rs

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.

@ytallo ytallo changed the title Feat/llm router integration feat: wire the llm-router into the harness and retire the legacy routing surfaces Jun 12, 2026
@ytallo
ytallo changed the base branch from feat/llm-router-rs to main June 12, 2026 09:46
@ytallo
ytallo changed the base branch from main to feat/llm-router-rs June 12, 2026 09:51
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 17 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@ytallo
ytallo changed the base branch from feat/llm-router-rs to main June 12, 2026 18:56
ytallo added 12 commits June 12, 2026 15:56
…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.
LLM providers are standalone llm-router plugin workers from here on
(provider-anthropic is the first; the rest follow as their own workers) —
they self-register with the router over the provider protocol, so nothing
provider-specific belongs in the harness process. Removes the five
provider modules with their tests, worker definitions, and the TS
provider-protocol helpers (registration tokens, discovery/models.dev
enrichment, output-token clamping — the router owns the budget now). The
catalog read helper survives as runtime/catalog.ts and AgentFunctionSchema
moves to types/function.ts. Until a provider worker is installed, chats
fail loudly with router/no_provider_for_model.
The downstream plumbing (run request → router::chat → provider thinking
config) was complete but unreachable: the run::start payload schema had no
thinking_level field, so Zod stripped it for every caller — harness::trigger
included. The schema now takes the level enum; invalid levels fail at the
boundary instead of surfacing as provider warnings. Verified live end to
end: a run::start with thinking_level=high on claude-sonnet-4-6 lands an
assistant message with a thinking block in the session tree.
ytallo added 3 commits June 12, 2026 15:56
A thinking select (off…xhigh) joins the composer next to the model picker;
the chosen level rides the run::start payload as thinking_level. Thought
panes now auto-open while their content streams — reasoning is visible in
real time — and collapse back to the duration summary when the block ends.
buildSummaryMessage stamps Date.now() and buildContextView calls it again
internally, so an exact-timestamp deep-equal flakes whenever the two calls
land across a millisecond boundary (as on the CI runner). The summary
expectations now accept any numeric timestamp.
@ytallo
ytallo force-pushed the feat/llm-router-integration branch from e09d581 to 545deb9 Compare June 12, 2026 18:57
@ytallo
ytallo merged commit ad176ca into main Jun 12, 2026
22 checks passed
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.

3 participants