Skip to content

(MOT-4412) fix(llm-router): harden provider lifecycle - #812

Merged
ytallo merged 5 commits into
mainfrom
fix/llm-router-provider-lifecycle
Aug 15, 2026
Merged

(MOT-4412) fix(llm-router): harden provider lifecycle#812
ytallo merged 5 commits into
mainfrom
fix/llm-router-provider-lifecycle

Conversation

@ytallo

@ytallo ytallo commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • make provider registry and catalog updates atomic, generation-aware, and safe across persistence failures and re-registration
  • route only to live providers, reserve request IDs atomically, bound provider RPC admission, and preserve exactly one terminal stream frame across failure paths
  • add adversarial live-engine coverage for cancellation, duplicate IDs, stale generations, persistence rollback, missing handlers, and lingering providers
  • add Harness scenario INT-021, which streams partial content and keepalives before a permanent terminal provider error
  • add a dedicated CI job that always supplies the pinned iii engine instead of allowing the integration suite to self-skip

Root cause

Registry/catalog state could be published partially, stale provider responses could overwrite a newer availability generation, and duplicate request IDs could replace active cancellation state. Some pre-stream and bus failures returned without the terminal frame required by the router contract. Provider trigger cleanup also needed bounded admission without cancelling the SDK future before it could release pending state. The regular Rust CI job did not provide an engine, so live lifecycle coverage could silently skip.

Impact

Provider failure and recovery are deterministic: unavailable or stale providers are excluded from routing, registrations do not expose partial state, duplicate requests cannot orphan active streams, and consumers receive a terminal result instead of hanging. The live-engine CI lane now exercises these guarantees on every relevant change.

The Harness adversarial scenario verifies the complete queue, session, context, state, and harness path. It requires the failed run to preserve partial content, expose the exact terminal error, leave no pending function calls, avoid transient resume, and invoke the router exactly once.

Validation

  • python3 -m pytest .github/scripts/tests -q — 192 passed, 3 subtests passed
  • cargo test --locked --manifest-path llm-router/Cargo.toml --lib --all-features -- --test-threads=1 — 127 passed
  • III_ENGINE_BIN=/home/layon/.local/bin/iii cargo test --locked --manifest-path llm-router/Cargo.toml --no-default-features --test integration -- --nocapture --test-threads=1 with iii 0.22.1 — 22 passed
  • cargo test --locked --manifest-path llm-router/Cargo.toml --all-features --bins --test schemas -- --test-threads=1 — 2 binary and 4 schema tests passed
  • cargo clippy --locked --manifest-path llm-router/Cargo.toml --all-targets --all-features -- -D warnings
  • cargo test --locked --manifest-path harness/Cargo.toml -p harness-integration — 87 unit, 2 determinism, 1 compilation, 4 schema, and 5 supervisor tests passed
  • cargo clippy --locked --manifest-path harness/Cargo.toml -p harness-integration --all-targets -- -D warnings
  • make -C harness integration-validate — 19 scenario fixtures valid
  • make -C harness integration-test III_BIN=<pinned-engine> INTEGRATION_SCENARIO=INT-021 with engine.lock revision 15dc993e — passed in 2698 ms
  • cargo fmt --manifest-path llm-router/Cargo.toml --all -- --check
  • cargo fmt --manifest-path harness/Cargo.toml --all -- --check
  • actionlint 1.7.12 -color
  • git diff --check

Refs MOT-4412

Summary by CodeRabbit

  • New Features
    • Improved provider routing to recognize unavailable providers and return clearer errors.
    • Added safer provider registration and catalog updates with rollback protection.
    • Added handling for partial responses followed by terminal errors, preserving received content.
  • Bug Fixes
    • Prevented duplicate request processing and duplicate terminal responses.
    • Improved request cancellation, provider recovery, and restart availability handling.
    • Link checks now accept valid security-challenge responses while continuing to reject denied requests.
  • Tests
    • Expanded integration coverage for routing, failures, persistence, cancellation, and recovery scenarios.

@vercel

vercel Bot commented Aug 15, 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 Aug 15, 2026 9:55am
workers-tech-spec Ready Ready Preview Aug 15, 2026 9:55am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e39b37c9-8124-4d93-8a32-09260fe0f9f4

📥 Commits

Reviewing files that changed from the base of the PR and between ff77d76 and 8604b01.

⛔ Files ignored due to path filters (2)
  • crates/provider-integration-testkit/Cargo.lock is excluded by !**/*.lock
  • llm-router/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • .github/scripts/discover_changed_workers.py
  • .github/scripts/tests/test_check_links.py
  • .github/scripts/tests/test_discover_changed_workers.py
  • .github/scripts/tests/test_rust_ci_workflows.py
  • .github/workflows/ci.yml
  • .github/workflows/rust-security-audit.yml
  • harness/tests/integration/README.md
  • harness/tests/integration/src/fixtures/loading.rs
  • harness/tests/integration/src/fixtures/tests.rs
  • harness/tests/integration/src/scenarios/dsl.rs
  • harness/tests/integration/src/scenarios/mod.rs
  • harness/tests/integration/src/scenarios/router_midstream_terminal_error.rs
  • llm-router/Cargo.toml
  • llm-router/src/catalog/store.rs
  • llm-router/src/chat/abort.rs
  • llm-router/src/chat/chat.rs
  • llm-router/src/chat/inflight.rs
  • llm-router/src/count_tokens.rs
  • llm-router/src/registry/register.rs
  • llm-router/src/registry/store.rs
  • llm-router/src/routing.rs
  • llm-router/src/types/router.rs
  • llm-router/tests/golden/schemas/router.provider.register.json
  • llm-router/tests/integration.rs
  • scripts/check-links.sh

📝 Walkthrough

Walkthrough

Changes

Router reliability and integration coverage

Layer / File(s) Summary
Integration detection and CI gates
.github/scripts/*, .github/workflows/*, scripts/check-links.sh
Change detection now emits llm_router_integration. CI conditionally runs the pinned live-router suite. Link checks accept Vercel challenge responses, and Rust toolchain checks require version 1.97.1.
Durable provider and catalog state
llm-router/src/catalog/store.rs, llm-router/src/registry/*, llm-router/src/types/router.rs, llm-router/tests/golden/*
Provider and catalog changes use staged persistence and commit operations. Registration generations reject stale updates. Registration-token documentation now defines token ownership and hash-only persistence.
Availability-aware routing and chat lifecycle
llm-router/src/routing.rs, llm-router/src/count_tokens.rs, llm-router/src/chat/*, llm-router/tests/integration.rs
Routing separates registered from available providers. Chat requests reserve IDs, use bounded provider admission, preserve terminal outcomes, reap provider tasks, and apply generation-checked availability updates.
Terminal-error integration scenario
harness/tests/integration/src/scenarios/*, harness/tests/integration/src/fixtures/*, harness/tests/integration/README.md
The DSL and INT-021 fixture support partial output, keepalives, terminal error frames, failed durable outcomes, and exactly-once completion checks.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • iii-hq/workers#808: Overlaps provider lifecycle, catalog persistence, registry handling, and routing changes.
  • iii-hq/workers#525: Overlaps chat relay, partial output, and terminal-error behavior.
  • iii-hq/workers#665: Overlaps integration fixture validation and scenario DSL changes.

Suggested reviewers: andersonleal

Poem

I hop through routes where providers align,
Stage every change till commits shine.
Streams keep their crumbs, errors end clear,
CI wakes the router when changes appear.
A rabbit cheers: “The terminal path is fine!”

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/llm-router-provider-lifecycle

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.

@github-actions

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 60 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@ytallo
ytallo marked this pull request as ready for review August 15, 2026 13:04
@ytallo
ytallo merged commit 147d0a4 into main Aug 15, 2026
52 of 54 checks passed
andersonleal added a commit that referenced this pull request Aug 17, 2026
…injection via configuration entries, hot-applied

Move each worker's system-prompt usage guidance behind an inject_guidance
knob (ON by default) in its builtin-configuration entry: flipping it binds
or unbinds the pre-generate hook live, no restart, shrinking agent prompts
when the guidance is not wanted. The non-harness half of the token-usage
work.

The config plumbing all five share lives in the new crates/config-client
(iii-sdk range dep, the console-ui precedent): retry ladder with a
NOT_FOUND fast-path, seed-only-when-nothing-stored (configuration::register
REPLACES the stored value whenever initial_value is supplied, so the
pre-check is load-bearing), case-SENSITIVE NOT_FOUND so an absent config
plane never reads as "nothing stored yet", serialized reloads with the
fetch inside the lock, and a post-bind boot refresh closing the fetch->bind
gap; scrapling mirrors the same semantics in Python. All five treat the
config path as best-effort at boot — warn and run on defaults rather than
taking the worker's real surface off the bus (docs/sops/configuration.md
now documents the cosmetic-knob exception).

Also in this change:

- sandbox-code-runner seeds on-config-change + ui-content into the claims
  registry (seeded_ids, the code-runner pattern), closing a boot window
  where a guest register_function could claim a late-registered worker id
  and abort the process via the SDK's duplicate-id panic.
- sandbox-code-runner's dead custom console form is removed (it predated
  the entry and would have hidden the knob behind stale timeout fields);
  the console's schema-generated form renders the entry, and the README
  documents it.
- fp ships no injected UI for its one boolean; the fp/ui package, build.rs,
  and src/ui.rs are gone and the schema form serves the knob.
- llm-router resolves composite "provider::model" ids (the console display
  form) at the choke points: catalog queries retry an exact miss via the
  split pair with supports delegating to get, and chat/route/count_tokens
  split known-provider composites before dispatch — metadata and routing
  can never disagree about the same id. Rebased over the provider-lifecycle
  hardening (#812): availability checks run against the split pair.
- workflow's stamp-reply and inject-guidance hook responses are typed
  structs (the interface publish gate refuses AnyValue response schemas);
  inject-guidance also adopts fp's rule of preserving the harness prompt on
  an empty/drifted base instead of replacing it with guidance alone, and
  stamp-reply's no-op answers an explicit continue (parsed identically to
  the old null).
- provider-llamacpp, github-copilot, kimi, and openrouter tag their
  router-ready handlers internal, keeping the default
  engine::functions::list free of provider plumbing; the four providers'
  lockfiles are regenerated so the per-worker --locked gates resolve.
- rust-security-audit audits every changed lockfile with a full fetch: the
  old --no-fetch on later iterations made each lockfile after the first
  fail its yanked lookups against a half-warmed index.
- '!<worker>::on-config-change' denies for all five workers (web and
  workflow were missing theirs too) and configuration dependencies in the
  fp / sandbox-code-runner / workflow / scrapling manifests.
andersonleal added a commit that referenced this pull request Aug 17, 2026
…injection via configuration entries, hot-applied

Move each worker's system-prompt usage guidance behind an inject_guidance
knob (ON by default) in its builtin-configuration entry: flipping it binds
or unbinds the pre-generate hook live, no restart, shrinking agent prompts
when the guidance is not wanted. The non-harness half of the token-usage
work.

The config plumbing all five share lives in the new crates/config-client
(iii-sdk range dep, the console-ui precedent): retry ladder with a
NOT_FOUND fast-path, seed-only-when-nothing-stored (configuration::register
REPLACES the stored value whenever initial_value is supplied, so the
pre-check is load-bearing), case-SENSITIVE NOT_FOUND so an absent config
plane never reads as "nothing stored yet", serialized reloads with the
fetch inside the lock, and a post-bind boot refresh closing the fetch->bind
gap; scrapling mirrors the same semantics in Python. All five treat the
config path as best-effort at boot — warn and run on defaults rather than
taking the worker's real surface off the bus (docs/sops/configuration.md
now documents the cosmetic-knob exception).

Also in this change:

- sandbox-code-runner seeds on-config-change + ui-content into the claims
  registry (seeded_ids, the code-runner pattern), closing a boot window
  where a guest register_function could claim a late-registered worker id
  and abort the process via the SDK's duplicate-id panic.
- sandbox-code-runner's dead custom console form is removed (it predated
  the entry and would have hidden the knob behind stale timeout fields);
  the console's schema-generated form renders the entry, and the README
  documents it.
- fp ships no injected UI for its one boolean; the fp/ui package, build.rs,
  and src/ui.rs are gone and the schema form serves the knob.
- llm-router resolves composite "provider::model" ids (the console display
  form) at the choke points: catalog queries retry an exact miss via the
  split pair with supports delegating to get, and chat/route/count_tokens
  split known-provider composites before dispatch — metadata and routing
  can never disagree about the same id. Rebased over the provider-lifecycle
  hardening (#812): availability checks run against the split pair.
- workflow's stamp-reply and inject-guidance hook responses are typed
  structs (the interface publish gate refuses AnyValue response schemas);
  inject-guidance also adopts fp's rule of preserving the harness prompt on
  an empty/drifted base instead of replacing it with guidance alone, and
  stamp-reply's no-op answers an explicit continue (parsed identically to
  the old null).
- provider-llamacpp, github-copilot, kimi, and openrouter tag their
  router-ready handlers internal, keeping the default
  engine::functions::list free of provider plumbing; the four providers'
  lockfiles are regenerated so the per-worker --locked gates resolve.
- rust-security-audit audits every changed lockfile with a full fetch: the
  old --no-fetch on later iterations made each lockfile after the first
  fail its yanked lookups against a half-warmed index; the
  workflow-convention test pinning the old flag is updated, and the audit's
  first real catch on these lockfiles — quinn-proto RUSTSEC-2026-0185 in the
  kimi and web locks — is patched by a lock-only bump.
- '!<worker>::on-config-change' denies for all five workers (web and
  workflow were missing theirs too) and configuration dependencies in the
  fp / sandbox-code-runner / workflow / scrapling manifests.
andersonleal added a commit that referenced this pull request Aug 18, 2026
…injection via configuration entries, hot-applied

Move each worker's system-prompt usage guidance behind an inject_guidance
knob (ON by default) in its builtin-configuration entry: flipping it binds
or unbinds the pre-generate hook live, no restart, shrinking agent prompts
when the guidance is not wanted. The non-harness half of the token-usage
work.

The config plumbing all five share lives in the new crates/config-client
(iii-sdk range dep, the console-ui precedent): retry ladder with a
NOT_FOUND fast-path, seed-only-when-nothing-stored (configuration::register
REPLACES the stored value whenever initial_value is supplied, so the
pre-check is load-bearing), case-SENSITIVE NOT_FOUND so an absent config
plane never reads as "nothing stored yet", serialized reloads with the
fetch inside the lock, and a post-bind boot refresh closing the fetch->bind
gap; scrapling mirrors the same semantics in Python. All five treat the
config path as best-effort at boot — warn and run on defaults rather than
taking the worker's real surface off the bus (docs/sops/configuration.md
now documents the cosmetic-knob exception).

Also in this change:

- sandbox-code-runner seeds on-config-change + ui-content into the claims
  registry (seeded_ids, the code-runner pattern), closing a boot window
  where a guest register_function could claim a late-registered worker id
  and abort the process via the SDK's duplicate-id panic.
- sandbox-code-runner's dead custom console form is removed (it predated
  the entry and would have hidden the knob behind stale timeout fields);
  the console's schema-generated form renders the entry, and the README
  documents it.
- fp ships no injected UI for its one boolean; the fp/ui package, build.rs,
  and src/ui.rs are gone and the schema form serves the knob.
- llm-router resolves composite "provider::model" ids (the console display
  form) at the choke points: catalog queries retry an exact miss via the
  split pair with supports delegating to get, and chat/route/count_tokens
  split known-provider composites before dispatch — metadata and routing
  can never disagree about the same id. Rebased over the provider-lifecycle
  hardening (#812): availability checks run against the split pair.
- workflow's stamp-reply and inject-guidance hook responses are typed
  structs (the interface publish gate refuses AnyValue response schemas);
  inject-guidance also adopts fp's rule of preserving the harness prompt on
  an empty/drifted base instead of replacing it with guidance alone, and
  stamp-reply's no-op answers an explicit continue (parsed identically to
  the old null).
- provider-llamacpp, github-copilot, kimi, and openrouter tag their
  router-ready handlers internal, keeping the default
  engine::functions::list free of provider plumbing; the four providers'
  lockfiles are regenerated so the per-worker --locked gates resolve.
- rust-security-audit audits every changed lockfile with a full fetch: the
  old --no-fetch on later iterations made each lockfile after the first
  fail its yanked lookups against a half-warmed index; the
  workflow-convention test pinning the old flag is updated, and the audit's
  first real catch on these lockfiles — quinn-proto RUSTSEC-2026-0185 in the
  kimi and web locks — is patched by a lock-only bump.
- '!<worker>::on-config-change' denies for all five workers (web and
  workflow were missing theirs too) and configuration dependencies in the
  fp / sandbox-code-runner / workflow / scrapling manifests.
andersonleal added a commit that referenced this pull request Aug 18, 2026
…n guidance injection via configuration entries, hot-applied (#822)

* feat(fp,web,workflow,sandbox-code-runner,scrapling): opt-in guidance injection via configuration entries, hot-applied

Move each worker's system-prompt usage guidance behind an inject_guidance
knob (ON by default) in its builtin-configuration entry: flipping it binds
or unbinds the pre-generate hook live, no restart, shrinking agent prompts
when the guidance is not wanted. The non-harness half of the token-usage
work.

The config plumbing all five share lives in the new crates/config-client
(iii-sdk range dep, the console-ui precedent): retry ladder with a
NOT_FOUND fast-path, seed-only-when-nothing-stored (configuration::register
REPLACES the stored value whenever initial_value is supplied, so the
pre-check is load-bearing), case-SENSITIVE NOT_FOUND so an absent config
plane never reads as "nothing stored yet", serialized reloads with the
fetch inside the lock, and a post-bind boot refresh closing the fetch->bind
gap; scrapling mirrors the same semantics in Python. All five treat the
config path as best-effort at boot — warn and run on defaults rather than
taking the worker's real surface off the bus (docs/sops/configuration.md
now documents the cosmetic-knob exception).

Also in this change:

- sandbox-code-runner seeds on-config-change + ui-content into the claims
  registry (seeded_ids, the code-runner pattern), closing a boot window
  where a guest register_function could claim a late-registered worker id
  and abort the process via the SDK's duplicate-id panic.
- sandbox-code-runner's dead custom console form is removed (it predated
  the entry and would have hidden the knob behind stale timeout fields);
  the console's schema-generated form renders the entry, and the README
  documents it.
- fp ships no injected UI for its one boolean; the fp/ui package, build.rs,
  and src/ui.rs are gone and the schema form serves the knob.
- llm-router resolves composite "provider::model" ids (the console display
  form) at the choke points: catalog queries retry an exact miss via the
  split pair with supports delegating to get, and chat/route/count_tokens
  split known-provider composites before dispatch — metadata and routing
  can never disagree about the same id. Rebased over the provider-lifecycle
  hardening (#812): availability checks run against the split pair.
- workflow's stamp-reply and inject-guidance hook responses are typed
  structs (the interface publish gate refuses AnyValue response schemas);
  inject-guidance also adopts fp's rule of preserving the harness prompt on
  an empty/drifted base instead of replacing it with guidance alone, and
  stamp-reply's no-op answers an explicit continue (parsed identically to
  the old null).
- provider-llamacpp, github-copilot, kimi, and openrouter tag their
  router-ready handlers internal, keeping the default
  engine::functions::list free of provider plumbing; the four providers'
  lockfiles are regenerated so the per-worker --locked gates resolve.
- rust-security-audit audits every changed lockfile with a full fetch: the
  old --no-fetch on later iterations made each lockfile after the first
  fail its yanked lookups against a half-warmed index; the
  workflow-convention test pinning the old flag is updated, and the audit's
  first real catch on these lockfiles — quinn-proto RUSTSEC-2026-0185 in the
  kimi and web locks — is patched by a lock-only bump.
- '!<worker>::on-config-change' denies for all five workers (web and
  workflow were missing theirs too) and configuration dependencies in the
  fp / sandbox-code-runner / workflow / scrapling manifests.

* fix(ci): bump h2 to 0.4.16 (RUSTSEC-2026-0258) and sync provider testkit lockfile with post-release provider versions
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