feat: the gateway streams completions (Phase 2 · slice 8a) #35

Merged
bjoern merged 6 commits from feat/llm-gateway-completions into main 2026-09-26 01:58:26 +02:00
Member

Slice 8a of Phase 2 — the first of three PRs for the llm node (8b: the node's config and request; 8c: the node runs). The gateway learns to stream a completion. Nothing in the app calls it yet; 8c's executor will.

Commits:

  • 119778e bumps the OpenRouter.Net submodule to 50c5997 (L2, L3 and follow-up #26 merged). It is the pointer only; everything built and passed on it alone.
  • f7d4da3 is the feature.
  • 902a329 holds the fixes from the review I ran before opening (round 0, below).
  • 7faf3db corrects the PR number in ADR 0018 and the plan: another PR took #34 between my last commit and opening this one.
  • 0eafafb gives each live cache pair its own prefix, after your run showed one pair reading the other's entry.
  • a407fa6 is review round 1: Jibril's 💡, a test that a request which fails to connect still carries its body.

✅ Live checks — run by Björn on 2026-09-26; verdict in the comment

  • D-27: the message-level marker reaches Anthropic's cache, so 8b goes as written.
  • D-6: the rejection comes back as InvalidRequest with no error_type, so the fallback holds and the row fails permanently.
  • New: OpenRouter silently compresses prompts for models with a window of 8k or less. Decided (comment): the node turns compression off. OpenRouter.Net #27 (merged) adds the switch, and 8b sends it.

For a re-run, the command is:

git checkout feat/llm-gateway-completions
git submodule update --init --recursive   # the branch needs OpenRouter.Net 50c5997; checkout alone leaves the old one
OPENROUTER_LIVE_KEY=sk-or-… dotnet test tests/Kagaku.Flow.OpenRouter.Tests -c Release \
  --filter "Category=Live" --logger "console;verbosity=detailed"
  • D-27, prompt caching. Two pairs of calls to anthropic/claude-haiku-4.5, pinned to Anthropic, with a ~32,000-character system prompt, 5 s apart.
    • If the message-level marker (what the library emits) shows cached_tokens > 0 on the second call, 8b goes as written.
    • If only the part-level form (what OpenRouter documents) caches, the library first gets multipart system content, before 8b merges.
    • If the message-level request is refused, 8b must not merge as is.
    • If neither caches, the marker stays and ADR 0018 records caching as unverified.
  • D-6, context length. About 25,000 tokens to openai/gpt-3.5-turbo (16,385-token window), streamed through the gateway and posted raw. It shows whether the rejection carries error_type (the gateway's Kind is then ContextLength, else InvalidRequest), and whether any field reports the prompt's token count. Either way the row fails permanently.
    • The model is not the brief's openai/gpt-3.5-turbo-0613. OpenRouter compresses by default any prompt sent to a window of 8,192 tokens or less, so that 4,095-token model would have answered with a silently trimmed prompt instead of a rejection. The review caught this.
    • The same check still sends one labelled raw POST to the 4k model with compression left on. Its prompt_tokens show whether a small-window model cuts a prompt silently, which a node's request does not opt out of today: the library has no plugins field. That reading feeds 8b.

The checks assert only that OpenRouter answered every call and print what they saw, so no accepted outcome turns them red. I record the verdict here, and 8b and 8c are written against it. They are skipped in every other run: [LiveFact] skips without OPENROUTER_LIVE_KEY.

What

Kagaku.Flow.OpenRouter.Gateway

  • ILlmGateway.CompleteAsync(ChatCompletionRequest request, Action<AssistantDelta>? onDelta = null, CancellationToken ct = default) → Task<LlmCompletion>. It never throws for an API outcome. It throws only for a null request, use after dispose, the caller's cancellation, and whatever onDelta or the key source throws.
  • LlmCompletion(Result<ChatCompletionResponse> Result) has three more members:
    • Received: what the stream delivered, folded, even on a failure;
    • SentRequestJson: the body the gateway handed to HTTP, captured there, not re-serialized;
    • Cost.
  • OpenRouterGateway's constructors now take the host's IOpenRouterKeySource (after options). The key is read on every call:
    • no key → Authentication; an unreadable key → Authentication, with the source's sentence; a key no header can carry → Validation;
    • all three send nothing and forget the cached client.
  • One HTTP client per key (compared ordinally), rebuilt when the key changes. The old one is dropped, never disposed, so a completion still running on the old key finishes on it.
  • OpenRouterGatewayOptions gains four members: RequestTimeout (5 min), StallTimeout (90 s), CompletionRetry (the library's defaults) and GenerationLookupTimeout (10 s). The gateway refuses a value of these four it cannot use when it is built, with the parameter name options, before it creates its own handler. The record's other members stay unchecked, as in 7a.
  • Internal: SentRequestCapture (an AsyncLocal scope) and SentRequestCaptureHandler (a DelegatingHandler in front of the gateway's handler that records each body).

Kagaku.Flow.OpenRouter: AddKagakuFlowOpenRouter resolves the host's IOpenRouterKeySource with GetRequiredService. A host without one fails when the gateway is first resolved, and the message names the port. Tsumugi already registers its key source.

Tests (package):

  • SignallingClock moves to tests/Shared (namespace Kagaku.Flow.Timing) and is linked into both engine test projects. It gains WaitForLiveTimerAsync(dueTime), because a gateway call always has its watchdog live.
  • The scripted handler records bodies and gains SSE, hang, park, route and Retry-After responders.
  • Three [LiveFact] checks (above).

Tsumugi:

  • ScriptedGateway.CompleteAsync throws: no Tsumugi test streams a completion yet.
  • OpenRouterKeySourceTests.The_gateway_reads_the_key_through_the_key_source shows the composition's gateway reading the stored key.

Why

  • ADR 0018 ("The gateway"): the gateway owns the completion, its timeouts, the wire capture (ADR 0024) and the cost (ADR 0016: a failed attempt was paid for).
  • Slice 7a left three things to slice 8: the per-key cached client, the 5-minute timeout and the capture.
  • Jibril's note on #20 said OpenRouterGateway.Clock needed a use or should go. It now has three readers: the completion's backoff, its stall watchdog and the cost lookup's timeout. A_host_clock_is_kept now guards behaviour.

Decisions to confirm (brief §13)

# Decision ADR sentence it amends
14 The gateway opens the capture scope, not the executor. The body comes back in LlmCompletion.SentRequestJson. A gateway double can report a body without a handler, and a canned handler under the real gateway (slice 12's stub) still captures from the wire. Concurrent completions never see each other's bodies (tested). 0018: "the executor opens an AsyncLocal capture scope … a DelegatingHandler on the gateway's HTTP client writes each request body into that scope" → "the gateway opens … a DelegatingHandler in front of the gateway's HTTP handler writes each request body into it, and the completion returns the last body with its result for the executor to store"
16 A failed attempt's cost: the usage's cost when the stream carried one. Otherwise, once the stream had started, one GET /generation on a client with retries disabled, within 10 s on the gateway's clock. It never fails the attempt, and without it the cost is unknown. (On the completion client a 429 or 5xx would be asked again after a backoff, holding the settle.) A lookup right after the failure may find nothing yet (404). 0018 Cost: adds "(a failed attempt whose stream had started without a usage cost has its cost looked up once, never retried, from the generation)"
17 What each timeout bounds. The 5-minute HttpClient.Timeout (real time) bounds each stream attempt's wait for headers and a non-streaming call. The 90 s watchdog (the gateway's clock) is armed at send, so it ends a silent wait first. The library retries a stall before the first chunk, so with the default retry a provider that never answers fails the call after three waits and two backoffs, about 4.5 minutes. A stall is transient, so a row fails after the engine's three attempts, about 14 minutes. A shorter worst case (no stall retry, fewer attempts) is an options change, if you prefer it. 0018: "The gateway sets a 5-minute HTTP timeout on completions" → "keeps one HTTP client per key … with a 5-minute HTTP timeout on real time that bounds each attempt's wait for response headers and a non-streaming call"; the watchdog sentence adds "armed when a request is sent … about 4.5 minutes"
18 Clock gets three readers: the completion's backoff, its watchdog and the lookup's timeout. The catalog's backoff stays on the real clock (7a's reason: a frozen test clock would hold a picker's retry forever). — (answers #20's note)
25 The live checks are skipped tests, not a script: code that compiles and stays green, which you run with one command. —
37 The gateway's completion is lower than ADR 0018's sketch. It returns the library's folded response plus the body sent and the cost, and it takes an Action<AssistantDelta>. Parsing, schema checks and the outcome are the node's (8c). IProgress<T> would post deltas later and possibly out of order; the engine needs every delta published before the executor returns. 0018's surface block: CompleteAsync(request, IProgress<Delta>, ct) → text | parsed json, usage, cost, provider, finish reason → CompleteAsync(request, onDelta, ct) → the folded completion or failure, the body sent, the cost (Phase 4's RunAgentAsync line takes onDelta too)
43 The key-only live checks land here, in 8a, so D-27 and D-6 are answered before 8b's request builder and 8c's sentences are final. ADR 0018's "before the llm node is built" stays true. —

The rest of §13 is bookkeeping for 8a: 1–3 (the split, the ordering, the pin) and 44 (the clock's move).

ADR 0018 gets a Status line: Amended 2026-09-26 (PR #35, slice 8a). docs/PROJECT_PLAN.md names this PR on slice 8's line.

Review before opening (round 0)

Before opening, I ran a review workflow: four independent reviewers (correctness, tests, concurrency and lifetime, docs), each followed by a skeptic who tried to refute their points. They raised 23 points and the skeptics confirmed or partly confirmed all of them, most at a lower severity. 21 are taken: 20 in 902a329, and D7 in f7d4da3's reworded message. A second pass then checked every fix against its finding and reviewed the fix commit fresh. It found wording only, which 902a329 also takes. What mattered:

  • D1 (blocker, live check): the D-6 check would have hit OpenRouter's default compression on a 4k model instead of a rejection. It now targets a window over 8k (see Live checks).

  • A2/C1: a caller's cancel during the failed attempt's cost lookup came back with the gateway's own, already-disposed linked token. It now carries the caller's token, as a cancel during the stream does, and the test pins it.

  • B2/C3: the concurrent-capture test could not catch a process-wide current scope. A new test sends one completion's retry while another's scope is open.

  • B3, B4, B6, B7:

    • last-wins is pinned at the handler, where two bodies can differ;
    • a completion without an app title is covered;
    • the key source is read with the caller's token;
    • the lookup's upper bound builds its timer.
  • D2, B1, B5 (live checks): the part-level check prints the body of a 200 carrying an error, and the model; the message-level request asks for usage as the node will; a check fails if nothing was sent.

  • Docs (A1, A4, A5, C2, D3–D6): the options summary names the four members the gateway refuses. It also says:

    • a stall after the model finished returns the completion;
    • what a completion in flight meets when the gateway is disposed;
    • that SentRequestJson is the body handed to HTTP.

    ADR 0018's Status line is exact.

Declined, for your call:

  • A3: a success whose stream lost its usage chunk has an unknown cost. OpenRouter sends usage after the finish reason. If the stream stalls, drops or errors in between, the library still returns the finished completion, and LG7 does no lookup on a success. That paid generation then has Cost null and is missing from the run's total.
    • The gateway cannot tell this case from a [DONE] success without usage: the accumulator exposes only ChunkCount and IsFinished. Covering it would mean a lookup after every success without a cost.
    • A lookup right after a generation often finds nothing yet (404) anyway.
    • I left LG7 as written. If you want it covered, it needs a small library accessor ("did [DONE] arrive?") and then one more LG7 arm.
  • A6: the package csproj's comment keeps "Never a reference to a Tsumugi.* assembly". The sibling engine projects say the same, and ADR 0025 words the rule that way. A strict "never name the host" reading would reword all three together.

Out of scope (brief §8)

  • The node itself: its config, request, routing defaults and JsonSchema.Net (8b), and its executor and registration (8c).
  • The TSUMUGI_FAKE_LLM stub (11b/12).
  • Run preflights (12).
  • A cost lookup on success, and a second lookup when the first fails.
  • Library changes: none. Multipart system content comes only if D-27 asks for it.

Tests

Counted from dotnet test (facts plus InlineData rows). The base is main at 7b79992.

Project main this PR
Kagaku.Flow.OpenRouter.Tests 71 152 (149 run, the 3 live checks skipped)
Tsumugi.Integration.Tests 324 326
Kagaku.Flow.Tests 1103 1103 (the clock moved; no test changed)
Tsumugi.BlazorAdapter.Tests 351 351
Tsumugi.Domain.Tests 375 375
Tsumugi.UseCases.Tests 54 54
Total 2278 2361 (2358 passed, 3 skipped)

New in the package:

  • CompletionTests: 25 (LG1, LG4–LG6, LG11).
  • CompletionClientTests: 9 (LG2, LG3, the request timeout).
  • CompletionTimingTests: 5 (LG8, LG9, on the shared SignallingClock, with no sleeps).
  • SentRequestCaptureTests: 9 (LG10).
  • FailedAttemptCostTests: 12 (LG7).
  • GatewayLifetimeTests: 11 → 27 (LG12–LG14).
  • GatewayOptionsTests: 3 → 5.
  • LiveOpenRouterChecks: 3, skipped.

The only real-time test is the 1 ms request timeout, under a 10 s guard; HttpClient.Timeout takes no TimeProvider.

Coverage

Coverlet with branch="True", from the package's tests with the live checks skipped. Every file under src/Kagaku.Flow.OpenRouter is at 100%, with no justified gaps.

File Lines Branches
Gateway/OpenRouterGateway.cs 138/138 66/66
Gateway/SentRequestCapture.cs (new) 19/19 2/2
Gateway/SentRequestCaptureHandler.cs (new) 4/4 4/4
Gateway/LlmCompletion.cs (new) 4/4 —
Gateway/OpenRouterGatewayOptions.cs 10/10 —
OpenRouterServiceCollectionExtensions.cs 10/10 2/2
Gateway/ILlmGateway.cs an interface: nothing coverable

The unchanged package files stay at 100% too: ModelCapabilities.cs 14/14 lines and 18/18 branches, OpenRouterKeyLookup.cs 14/14 and 4/4. The Tsumugi side changes only tests.

Mutation probes

Each probe ran on a copy under home, on the branch's final tree. Each named test turned red on its assertion (or through its hang guard, where the brief says so). Each file was then restored and touched. All killed.

The brief's 8a probes (§12):

# Mutation Killed by, and how
1 read the key once and keep it A_removed_key_is_forgotten… and A_changed_key_builds… fail on their counts and headers
2 drop IsHeaderSafe from CompleteAsync A_key_no_header_can_carry… fails. The line-break row throws FormatException; the space and é rows send a request, which the scripted handler did not expect. Over a real handler the é row would come back as Network.
3 no key as Validation No_key_or_an_unreadable_key… fails on the kind
4 keep the cached client when the key is gone / is header-unsafe the removed-key, unreadable-key and header-unsafe tests count 1
5 compare keys ignoring case Keys_that_differ_only_in_case… counts 1
6 dispose the dropped client on a key change A_completion_in_flight_on_the_old_key…: the parked request dies with ObjectDisposedException
7 a client per call Two_completions_with_one_key… counts 2
8 the keyed clients on TimeProvider.System the Retry-After and 90 s stall tests fail through their hang guards
9 no StreamResultOptions / a constant 90 s the stall test's guard / The_stall_timeout_comes_from_the_options
10 the keyed client's timeout = ValidationTimeout A_silent_wait_for_headers_ends_at_the_request_timeout hits its 10 s guard
11 record the body only after a 2xx A_refusal_comes_back… fails (null body on the 401)
12 the body in a static field / the last scope opened on any flow Two_concurrent_completions… / A_request_outside_a_scope…
13 dispose Chain in Dispose A_caller_handler_outlives_the_gateway
14 the lookup's five arms: never looked up; looked up despite a usage cost; looked up before the first chunk; its own timeout escaping; the caller's cancel swallowed the named FailedAttemptCostTests: a count, an unscripted GET, an escaping TaskCanceledException, no exception
15 the key source via GetService the registration test gets the wrong exception type
16 each LG14 guard dropped (five), and each upper bound off by one (three) the refusal rows; Options_at_their_upper_bound_are_accepted
17 a callback for a chunk without a first-choice delta / any choice, not the first / onDelta's exception caught A_chunk_without_a_first_choice_delta… / the index-1 row / What_the_callback_throws…
18 session_id / usage added by the gateway The_request_goes_out_as_built
57 Received null on a success The_stream_folds_into_the_completion…
58 the key source's exception made a failure An_exception_from_the_key_source_propagates
72 the lookup on the completion client A_lookup_that_fails_with_a_server_error_is_asked_once hits its guard (a backoff on the frozen clock)
73 the completion client without retry With_the_default_retry_a_silent_provider_is_stalled_three_times hits its guard

Probes for this PR's own additions:

# Mutation Killed by
x1 a second Dispose restores again the scope test (the outer scope comes back)
x2 the first body wins in the scope the scope test, and The_scope_keeps_the_last_body_its_flow_sent
x3 the owned-handler constructor skips the checks The_gateway_that_builds_its_own_handler_refuses_bad_options_too, A_null_key_source_is_refused
x5 a cancel during the lookup keeps the linked token The_callers_cancellation_during_the_lookup_throws
x6 a process-wide current scope instead of an AsyncLocal A_retry_sent_while_another_completions_scope_is_open_is_recorded_in_its_own
x7 the first body wins at the handler The_scope_keeps_the_last_body_its_flow_sent
x8 the key read without the caller's token The_callers_cancellation_throws_with_the_callers_token
x9 the body recorded only after the send returns (round 1) A_request_that_fails_to_connect_still_carries_its_body

Equivalent mutant (1): x4 hands the library an empty callback where the caller gave none. The library calls it and nothing happens, and nothing can observe the difference.

Browser pass

8a has no visible change, but it changes how the gateway is built and registered (the key source), so I ran brief §16.2's regression pass. Setup:

  • the Release build, ASPNETCORE_ENVIRONMENT=Development (scope validation on);
  • the gateway pointed at slice 7's local stub (TSUMUGI_OPENROUTER_BASE_URL), with its 454-model catalog;
  • Node Playwright with system Chrome, because the Playwright MCP did not connect this session.

What I saw:

  • Settings → Provider, each stub key: sk-or-stub-html → "OpenRouter answered with an error: HTTP 502 …, not saved"; sk-or-stub-broke → "no credit left … not saved"; sk-or-stub-limit → the figures ($12.50 spent, $7.50 left of $20.00); sk-or-stub-nolimit → "No spending limit". A reload, Check again, and Remove key with its dialog all behaved as in slice 7's pass.
  • Settings → Defaults: the picker loads all 454 models. Choosing Claude Haiku 4.5 and setting Fastest (:nitro), Standard and zero data retention saves each change, and all of them survive a reload.
  • The page raised no errors in the browser console, and the server log has only the usual HTTPS-port notice.
  • Screenshots were taken light and dark.

🤖 Generated with Claude Code

Slice 8a of Phase 2 — the first of three PRs for the `llm` node (8b: the node's config and request; 8c: the node runs). The gateway learns to stream a completion. Nothing in the app calls it yet; 8c's executor will. **Commits:** - 119778e bumps the OpenRouter.Net submodule to 50c5997 (L2, L3 and follow-up #26 merged). It is the pointer only; everything built and passed on it alone. - f7d4da3 is the feature. - 902a329 holds the fixes from the review I ran before opening (round 0, below). - 7faf3db corrects the PR number in ADR 0018 and the plan: another PR took #34 between my last commit and opening this one. - 0eafafb gives each live cache pair its own prefix, after your run showed one pair reading the other's entry. - a407fa6 is review round 1: Jibril's 💡, a test that a request which fails to connect still carries its body. ## ✅ Live checks — run by Björn on 2026-09-26; verdict in [the comment](https://git.kagaku.eu/TeamAI/Tsumugi/pulls/35#issuecomment-9740) - **D-27:** the message-level marker reaches Anthropic's cache, so 8b goes as written. - **D-6:** the rejection comes back as `InvalidRequest` with no `error_type`, so the fallback holds and the row fails permanently. - **New:** OpenRouter silently compresses prompts for models with a window of 8k or less. **Decided** ([comment](https://git.kagaku.eu/TeamAI/Tsumugi/pulls/35#issuecomment-9746)): the node turns compression off. OpenRouter.Net #27 (merged) adds the switch, and 8b sends it. For a re-run, the command is: ```bash git checkout feat/llm-gateway-completions git submodule update --init --recursive # the branch needs OpenRouter.Net 50c5997; checkout alone leaves the old one OPENROUTER_LIVE_KEY=sk-or-… dotnet test tests/Kagaku.Flow.OpenRouter.Tests -c Release \ --filter "Category=Live" --logger "console;verbosity=detailed" ``` - **D-27, prompt caching.** Two pairs of calls to `anthropic/claude-haiku-4.5`, pinned to Anthropic, with a ~32,000-character system prompt, 5 s apart. - If the **message-level** marker (what the library emits) shows `cached_tokens` > 0 on the second call, 8b goes as written. - If only the **part-level** form (what OpenRouter documents) caches, the library first gets multipart system content, before 8b merges. - If the message-level request is **refused**, 8b must not merge as is. - If neither caches, the marker stays and ADR 0018 records caching as unverified. - **D-6, context length.** About 25,000 tokens to `openai/gpt-3.5-turbo` (16,385-token window), streamed through the gateway and posted raw. It shows whether the rejection carries `error_type` (the gateway's `Kind` is then `ContextLength`, else `InvalidRequest`), and whether any field reports the prompt's token count. Either way the row fails permanently. - The model is **not** the brief's `openai/gpt-3.5-turbo-0613`. OpenRouter compresses by default any prompt sent to a window of 8,192 tokens or less, so that 4,095-token model would have answered with a silently trimmed prompt instead of a rejection. The review caught this. - The same check still sends one labelled raw POST to the 4k model with compression left on. Its `prompt_tokens` show whether a small-window model cuts a prompt silently, which a node's request does not opt out of today: the library has no `plugins` field. That reading feeds 8b. The checks assert only that OpenRouter answered every call and print what they saw, so no accepted outcome turns them red. I record the verdict here, and 8b and 8c are written against it. They are skipped in every other run: `[LiveFact]` skips without `OPENROUTER_LIVE_KEY`. ## What **`Kagaku.Flow.OpenRouter.Gateway`** - `ILlmGateway.CompleteAsync(ChatCompletionRequest request, Action<AssistantDelta>? onDelta = null, CancellationToken ct = default) → Task<LlmCompletion>`. It never throws for an API outcome. It throws only for a null request, use after dispose, the caller's cancellation, and whatever `onDelta` or the key source throws. - `LlmCompletion(Result<ChatCompletionResponse> Result)` has three more members: - `Received`: what the stream delivered, folded, even on a failure; - `SentRequestJson`: the body the gateway handed to HTTP, captured there, not re-serialized; - `Cost`. - `OpenRouterGateway`'s constructors now take the host's `IOpenRouterKeySource` (after `options`). The key is read on **every** call: - no key → Authentication; an unreadable key → Authentication, with the source's sentence; a key no header can carry → Validation; - all three send nothing and forget the cached client. - **One HTTP client per key** (compared ordinally), rebuilt when the key changes. The old one is dropped, never disposed, so a completion still running on the old key finishes on it. - `OpenRouterGatewayOptions` gains four members: `RequestTimeout` (5 min), `StallTimeout` (90 s), `CompletionRetry` (the library's defaults) and `GenerationLookupTimeout` (10 s). The gateway refuses a value of these four it cannot use when it is built, with the parameter name `options`, before it creates its own handler. The record's other members stay unchecked, as in 7a. - Internal: `SentRequestCapture` (an AsyncLocal scope) and `SentRequestCaptureHandler` (a DelegatingHandler in front of the gateway's handler that records each body). **`Kagaku.Flow.OpenRouter`:** `AddKagakuFlowOpenRouter` resolves the host's `IOpenRouterKeySource` with `GetRequiredService`. A host without one fails when the gateway is first resolved, and the message names the port. Tsumugi already registers its key source. **Tests (package):** - `SignallingClock` moves to `tests/Shared` (namespace `Kagaku.Flow.Timing`) and is linked into both engine test projects. It gains `WaitForLiveTimerAsync(dueTime)`, because a gateway call always has its watchdog live. - The scripted handler records bodies and gains SSE, hang, park, route and Retry-After responders. - Three `[LiveFact]` checks (above). **Tsumugi:** - `ScriptedGateway.CompleteAsync` throws: no Tsumugi test streams a completion yet. - `OpenRouterKeySourceTests.The_gateway_reads_the_key_through_the_key_source` shows the composition's gateway reading the stored key. ## Why - ADR 0018 ("The gateway"): the gateway owns the completion, its timeouts, the wire capture (ADR 0024) and the cost (ADR 0016: a failed attempt was paid for). - Slice 7a left three things to slice 8: the per-key cached client, the 5-minute timeout and the capture. - Jibril's note on #20 said `OpenRouterGateway.Clock` needed a use or should go. It now has three readers: the completion's backoff, its stall watchdog and the cost lookup's timeout. `A_host_clock_is_kept` now guards behaviour. ## Decisions to confirm (brief §13) | # | Decision | ADR sentence it amends | |---|---|---| | 14 | **The gateway opens the capture scope, not the executor.** The body comes back in `LlmCompletion.SentRequestJson`. A gateway double can report a body without a handler, and a canned handler under the real gateway (slice 12's stub) still captures from the wire. Concurrent completions never see each other's bodies (tested). | 0018: "the executor opens an `AsyncLocal` capture scope … a `DelegatingHandler` on the gateway's HTTP client writes each request body into that scope" → "the gateway opens … a `DelegatingHandler` in front of the gateway's HTTP handler writes each request body into it, and the completion returns the last body with its result for the executor to store" | | 16 | **A failed attempt's cost:** the usage's cost when the stream carried one. Otherwise, once the stream had started, **one** `GET /generation` on a client with retries disabled, within 10 s on the gateway's clock. It never fails the attempt, and without it the cost is unknown. (On the completion client a 429 or 5xx would be asked again after a backoff, holding the settle.) A lookup right after the failure may find nothing yet (404). | 0018 Cost: adds "(a failed attempt whose stream had started without a usage cost has its cost looked up once, never retried, from the generation)" | | 17 | **What each timeout bounds.** The 5-minute `HttpClient.Timeout` (real time) bounds each stream attempt's wait for headers and a non-streaming call. The 90 s watchdog (the gateway's clock) is armed at send, so it ends a silent wait first. The library retries a stall before the first chunk, so with the default retry a provider that never answers fails the call after three waits and two backoffs, about **4.5 minutes**. A stall is transient, so a row fails after the engine's three attempts, about **14 minutes**. A shorter worst case (no stall retry, fewer attempts) is an options change, if you prefer it. | 0018: "The gateway sets a 5-minute HTTP timeout on completions" → "keeps one HTTP client per key … with a 5-minute HTTP timeout on real time that bounds each attempt's wait for response headers and a non-streaming call"; the watchdog sentence adds "armed when a request is sent … about 4.5 minutes" | | 18 | **`Clock` gets three readers**: the completion's backoff, its watchdog and the lookup's timeout. The catalog's backoff stays on the real clock (7a's reason: a frozen test clock would hold a picker's retry forever). | — (answers #20's note) | | 25 | **The live checks are skipped tests, not a script**: code that compiles and stays green, which you run with one command. | — | | 37 | **The gateway's completion is lower than ADR 0018's sketch.** It returns the library's folded response plus the body sent and the cost, and it takes an `Action<AssistantDelta>`. Parsing, schema checks and the outcome are the node's (8c). `IProgress<T>` would post deltas later and possibly out of order; the engine needs every delta published before the executor returns. | 0018's surface block: `CompleteAsync(request, IProgress<Delta>, ct) → text \| parsed json, usage, cost, provider, finish reason` → `CompleteAsync(request, onDelta, ct) → the folded completion or failure, the body sent, the cost` (Phase 4's `RunAgentAsync` line takes `onDelta` too) | | 43 | **The key-only live checks land here, in 8a**, so D-27 and D-6 are answered before 8b's request builder and 8c's sentences are final. ADR 0018's "before the `llm` node is built" stays true. | — | The rest of §13 is bookkeeping for 8a: 1–3 (the split, the ordering, the pin) and 44 (the clock's move). ADR 0018 gets a Status line: **Amended 2026-09-26 (PR #35, slice 8a)**. `docs/PROJECT_PLAN.md` names this PR on slice 8's line. ## Review before opening (round 0) Before opening, I ran a review workflow: four independent reviewers (correctness, tests, concurrency and lifetime, docs), each followed by a skeptic who tried to refute their points. They raised 23 points and the skeptics confirmed or partly confirmed all of them, most at a lower severity. 21 are taken: 20 in 902a329, and D7 in f7d4da3's reworded message. A second pass then checked every fix against its finding and reviewed the fix commit fresh. It found wording only, which 902a329 also takes. What mattered: - **D1 (blocker, live check):** the D-6 check would have hit OpenRouter's default compression on a 4k model instead of a rejection. It now targets a window over 8k (see *Live checks*). - **A2/C1:** a caller's cancel during the failed attempt's cost lookup came back with the gateway's own, already-disposed linked token. It now carries the caller's token, as a cancel during the stream does, and the test pins it. - **B2/C3:** the concurrent-capture test could not catch a process-wide current scope. A new test sends one completion's retry while another's scope is open. - **B3, B4, B6, B7:** - last-wins is pinned at the handler, where two bodies can differ; - a completion without an app title is covered; - the key source is read with the caller's token; - the lookup's upper bound builds its timer. - **D2, B1, B5 (live checks):** the part-level check prints the body of a 200 carrying an error, and the model; the message-level request asks for usage as the node will; a check fails if nothing was sent. - **Docs (A1, A4, A5, C2, D3–D6):** the options summary names the four members the gateway refuses. It also says: - a stall after the model finished returns the completion; - what a completion in flight meets when the gateway is disposed; - that `SentRequestJson` is the body handed to HTTP. ADR 0018's Status line is exact. **Declined, for your call:** - **A3: a success whose stream lost its usage chunk has an unknown cost.** OpenRouter sends usage *after* the finish reason. If the stream stalls, drops or errors in between, the library still returns the finished completion, and LG7 does no lookup on a success. That paid generation then has `Cost` null and is missing from the run's total. - The gateway cannot tell this case from a `[DONE]` success without usage: the accumulator exposes only `ChunkCount` and `IsFinished`. Covering it would mean a lookup after every success without a cost. - A lookup right after a generation often finds nothing yet (404) anyway. - I left LG7 as written. If you want it covered, it needs a small library accessor ("did `[DONE]` arrive?") and then one more LG7 arm. - **A6:** the package csproj's comment keeps "Never a reference to a Tsumugi.* assembly". The sibling engine projects say the same, and ADR 0025 words the rule that way. A strict "never name the host" reading would reword all three together. ## Out of scope (brief §8) - The node itself: its config, request, routing defaults and `JsonSchema.Net` (8b), and its executor and registration (8c). - The `TSUMUGI_FAKE_LLM` stub (11b/12). - Run preflights (12). - A cost lookup on success, and a second lookup when the first fails. - Library changes: none. Multipart system content comes only if D-27 asks for it. ## Tests Counted from `dotnet test` (facts plus `InlineData` rows). The base is `main` at 7b79992. | Project | main | this PR | |---|---:|---:| | Kagaku.Flow.OpenRouter.Tests | 71 | **152** (149 run, the 3 live checks skipped) | | Tsumugi.Integration.Tests | 324 | **326** | | Kagaku.Flow.Tests | 1103 | 1103 (the clock moved; no test changed) | | Tsumugi.BlazorAdapter.Tests | 351 | 351 | | Tsumugi.Domain.Tests | 375 | 375 | | Tsumugi.UseCases.Tests | 54 | 54 | | **Total** | **2278** | **2361** (2358 passed, 3 skipped) | New in the package: - `CompletionTests`: 25 (LG1, LG4–LG6, LG11). - `CompletionClientTests`: 9 (LG2, LG3, the request timeout). - `CompletionTimingTests`: 5 (LG8, LG9, on the shared `SignallingClock`, with no sleeps). - `SentRequestCaptureTests`: 9 (LG10). - `FailedAttemptCostTests`: 12 (LG7). - `GatewayLifetimeTests`: 11 → 27 (LG12–LG14). - `GatewayOptionsTests`: 3 → 5. - `LiveOpenRouterChecks`: 3, skipped. The only real-time test is the 1 ms request timeout, under a 10 s guard; `HttpClient.Timeout` takes no `TimeProvider`. ## Coverage Coverlet with `branch="True"`, from the package's tests with the live checks skipped. Every file under `src/Kagaku.Flow.OpenRouter` is at 100%, with no justified gaps. | File | Lines | Branches | |---|---:|---:| | `Gateway/OpenRouterGateway.cs` | 138/138 | 66/66 | | `Gateway/SentRequestCapture.cs` (new) | 19/19 | 2/2 | | `Gateway/SentRequestCaptureHandler.cs` (new) | 4/4 | 4/4 | | `Gateway/LlmCompletion.cs` (new) | 4/4 | — | | `Gateway/OpenRouterGatewayOptions.cs` | 10/10 | — | | `OpenRouterServiceCollectionExtensions.cs` | 10/10 | 2/2 | | `Gateway/ILlmGateway.cs` | an interface: nothing coverable | | The unchanged package files stay at 100% too: `ModelCapabilities.cs` 14/14 lines and 18/18 branches, `OpenRouterKeyLookup.cs` 14/14 and 4/4. The Tsumugi side changes only tests. ## Mutation probes Each probe ran on a copy under home, on the branch's final tree. Each named test turned red on its assertion (or through its hang guard, where the brief says so). Each file was then restored and `touch`ed. **All killed.** **The brief's 8a probes (§12):** | # | Mutation | Killed by, and how | |---|---|---| | 1 | read the key once and keep it | `A_removed_key_is_forgotten…` and `A_changed_key_builds…` fail on their counts and headers | | 2 | drop `IsHeaderSafe` from `CompleteAsync` | `A_key_no_header_can_carry…` fails. The line-break row throws `FormatException`; the space and `é` rows send a request, which the scripted handler did not expect. Over a real handler the `é` row would come back as `Network`. | | 3 | no key as `Validation` | `No_key_or_an_unreadable_key…` fails on the kind | | 4 | keep the cached client when the key is gone / is header-unsafe | the removed-key, unreadable-key and header-unsafe tests count 1 | | 5 | compare keys ignoring case | `Keys_that_differ_only_in_case…` counts 1 | | 6 | dispose the dropped client on a key change | `A_completion_in_flight_on_the_old_key…`: the parked request dies with `ObjectDisposedException` | | 7 | a client per call | `Two_completions_with_one_key…` counts 2 | | 8 | the keyed clients on `TimeProvider.System` | the Retry-After and 90 s stall tests fail through their hang guards | | 9 | no `StreamResultOptions` / a constant 90 s | the stall test's guard / `The_stall_timeout_comes_from_the_options` | | 10 | the keyed client's timeout = `ValidationTimeout` | `A_silent_wait_for_headers_ends_at_the_request_timeout` hits its 10 s guard | | 11 | record the body only after a 2xx | `A_refusal_comes_back…` fails (null body on the 401) | | 12 | the body in a static field / the last scope opened on any flow | `Two_concurrent_completions…` / `A_request_outside_a_scope…` | | 13 | dispose `Chain` in `Dispose` | `A_caller_handler_outlives_the_gateway` | | 14 | the lookup's five arms: never looked up; looked up despite a usage cost; looked up before the first chunk; its own timeout escaping; the caller's cancel swallowed | the named `FailedAttemptCostTests`: a count, an unscripted `GET`, an escaping `TaskCanceledException`, no exception | | 15 | the key source via `GetService` | the registration test gets the wrong exception type | | 16 | each LG14 guard dropped (five), and each upper bound off by one (three) | the refusal rows; `Options_at_their_upper_bound_are_accepted` | | 17 | a callback for a chunk without a first-choice delta / any choice, not the first / `onDelta`'s exception caught | `A_chunk_without_a_first_choice_delta…` / the index-1 row / `What_the_callback_throws…` | | 18 | `session_id` / `usage` added by the gateway | `The_request_goes_out_as_built` | | 57 | `Received` null on a success | `The_stream_folds_into_the_completion…` | | 58 | the key source's exception made a failure | `An_exception_from_the_key_source_propagates` | | 72 | the lookup on the completion client | `A_lookup_that_fails_with_a_server_error_is_asked_once` hits its guard (a backoff on the frozen clock) | | 73 | the completion client without retry | `With_the_default_retry_a_silent_provider_is_stalled_three_times` hits its guard | **Probes for this PR's own additions:** | # | Mutation | Killed by | |---|---|---| | x1 | a second `Dispose` restores again | the scope test (the outer scope comes back) | | x2 | the first body wins in the scope | the scope test, and `The_scope_keeps_the_last_body_its_flow_sent` | | x3 | the owned-handler constructor skips the checks | `The_gateway_that_builds_its_own_handler_refuses_bad_options_too`, `A_null_key_source_is_refused` | | x5 | a cancel during the lookup keeps the linked token | `The_callers_cancellation_during_the_lookup_throws` | | x6 | a process-wide current scope instead of an AsyncLocal | `A_retry_sent_while_another_completions_scope_is_open_is_recorded_in_its_own` | | x7 | the first body wins at the handler | `The_scope_keeps_the_last_body_its_flow_sent` | | x8 | the key read without the caller's token | `The_callers_cancellation_throws_with_the_callers_token` | | x9 | the body recorded only after the send returns (round 1) | `A_request_that_fails_to_connect_still_carries_its_body` | **Equivalent mutant (1):** x4 hands the library an empty callback where the caller gave none. The library calls it and nothing happens, and nothing can observe the difference. ## Browser pass 8a has no visible change, but it changes how the gateway is built and registered (the key source), so I ran brief §16.2's regression pass. Setup: - the Release build, `ASPNETCORE_ENVIRONMENT=Development` (scope validation on); - the gateway pointed at slice 7's local stub (`TSUMUGI_OPENROUTER_BASE_URL`), with its 454-model catalog; - Node Playwright with system Chrome, because the Playwright MCP did not connect this session. What I saw: - **Settings → Provider**, each stub key: `sk-or-stub-html` → "OpenRouter answered with an error: HTTP 502 …, not saved"; `sk-or-stub-broke` → "no credit left … not saved"; `sk-or-stub-limit` → the figures ($12.50 spent, $7.50 left of $20.00); `sk-or-stub-nolimit` → "No spending limit". A reload, *Check again*, and *Remove key* with its dialog all behaved as in slice 7's pass. - **Settings → Defaults**: the picker loads all 454 models. Choosing Claude Haiku 4.5 and setting Fastest (:nitro), Standard and zero data retention saves each change, and all of them survive a reload. - The page raised no errors in the browser console, and the server log has only the usual HTTPS-port notice. - Screenshots were taken light and dark. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The pointer moves from c9b58ba to the library's main after #19 (the decisions API), #20 (the stream call
that returns a Result, with retry before the first chunk, the stall watchdog and log-probabilities),
#21 and #22 (decisions, docs), #23 and #24 (L3: ParseJson, the schema-name check, the strict-schema check
and InlineLocalRefs), #25 (enum choices and a tool gate) and #26 (review follow-ups). Nothing in Tsumugi
implements IOpenRouterClient, so the new interface members break nothing; the gateway's constructors and
the parameterless catalog listing are unchanged. The whole solution builds with no warning and every test
passes on the pointer alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ILlmGateway.CompleteAsync(request, onDelta, ct) streams one chat completion and
returns an LlmCompletion: the library's folded Result, what the stream
delivered (Received), the body as it went over the wire (SentRequestJson), and
what the attempt cost. It never throws for an API outcome.

The key. The gateway takes the host's IOpenRouterKeySource and reads it on
every call. No key and an unreadable key fail as Authentication; a key no
header can carry fails as Validation. All three are permanent kinds, send
nothing, and forget the cached client, so a removed key's header leaves memory.
The gateway keeps one HttpClient per key (ordinal), with a completion client
and a lookup client over it. A changed key drops the old pair without
disposing it, so a completion still running on the old key finishes on it.

What each timeout bounds (ADR 0018, amended here). RequestTimeout (5 min) is
the keyed HttpClient's Timeout, on real time. It bounds each stream attempt's
wait for headers and a non-streaming call. StallTimeout (90 s) runs on the
gateway's clock; it is armed at send and reset by every line, so it ends a
silent wait for headers first. The library retries a stall before the first
chunk, so with the default retry a provider that never answers fails the call
after about 4.5 minutes. The completion's backoff, its watchdog and the cost
lookup's timeout all run on the gateway's clock, which now has real readers
(Jibril's note on #20). The catalog keeps the real clock for its backoff.

The capture. The gateway, not the executor, opens the AsyncLocal
SentRequestCapture around the library call and returns the body in
SentRequestJson. So a gateway double can report a body without a handler, and
concurrent completions, each in its own scope, never see each other's bodies.
A DelegatingHandler in front of the gateway's handler records each body from
the wire. It is never disposed: its disposal would dispose a caller's handler.
Key checks and the catalog send no body and keep the bare handler.

A failed attempt's cost. It is the usage's cost when the stream carried one.
Otherwise, when the stream had started (a non-blank generation id), it is one
GET /generation. That lookup runs on a client built with RetryOptions.Disabled
(the completion client's retry would ask again after a backoff), within
GenerationLookupTimeout (10 s) on the gateway's clock. A lookup that fails or
times out leaves the cost unknown; the caller's cancellation propagates.

Options the gateway cannot use are refused when it is built, with the
parameter name "options": a non-positive or oversized RequestTimeout,
StallTimeout or GenerationLookupTimeout, and a null or invalid
CompletionRetry. The arguments are checked before an owned handler is created.

Tests. SignallingClock moves to tests/Shared (namespace Kagaku.Flow.Timing),
linked into both engine test projects. It gains WaitForLiveTimerAsync(dueTime),
because a gateway call always has its watchdog live. The scripted handler
records bodies and gains SSE, park, hang, route and Retry-After responders.

Live checks. Three [LiveFact]s, skipped without OPENROUTER_LIVE_KEY: the
message-level and part-level cache markers on a system message (D-27) and a
prompt longer than the model's window (D-6). Björn runs them on this branch:
  OPENROUTER_LIVE_KEY=sk-or-… dotnet test tests/Kagaku.Flow.OpenRouter.Tests -c Release \
    --filter "Category=Live" --logger "console;verbosity=detailed"
They assert only that OpenRouter answered, and print what they saw.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The review before the PR (four reviewers, a skeptic each) confirmed or partly
confirmed all 23 points. This commit takes 20. D7 is taken in the feature
commit's reworded message. A3 and A6 are declined, below. A second pass then
checked every fix and found only wording, taken here too.

Code:
- A2/C1: a caller's cancel during the failed attempt's cost lookup came back
  with the gateway's linked token, whose source was already disposed. It now
  carries the caller's token, as a cancel during the stream does, and the test
  pins it.

Live checks:
- D1: the D-6 check targeted openai/gpt-3.5-turbo-0613 (4,095 tokens).
  OpenRouter compresses a prompt for a window of 8,192 tokens or less by
  default, so the check would have come back as a success. It now sends about
  25k tokens to openai/gpt-3.5-turbo (16,385 tokens), streamed and raw, and
  reads the small model once, with compression left on, as a labelled reading.
- D2: the part-level check prints the body of a 200 that carries an error or
  no usage, and the served model.
- B1: the message-level request asks for usage like the node will.
- B5: a check fails when nothing was sent.

Tests:
- B2/C3: a retry sent while another completion's scope is open is recorded
  in its own; a process-wide current scope now fails this test deterministically.
- B3: last-wins is pinned at the handler, where two bodies can differ. The
  gateway-level retry test is renamed to what it shows.
- B4: a completion without an app title sends none.
- B6: the key source is read with the caller's token.
- B7: the lookup row of the upper-bound test builds the lookup's timer.
- The scope test checks right after a second dispose (probe x1).

Docs:
- A1/D3: the options summary names the four members the gateway refuses and
  says it checks once and reads on every call.
- A4: a stall after the model finished returns the completion.
- C2: Dispose and CompleteAsync say what a completion in flight meets.
- D5: SentRequestJson is the body handed to HTTP.
- D6: "cached for the current key"; the keys parameter is the key source.
- A5/D4: ADR 0018's Status line says the stall is retried and the lookup
  needs a started stream; two long lines are rewrapped.
- D7: the feature commit's message now credits the returned body, not the
  handler, for what a gateway double can report.
- Second pass: Dispose says a completion in flight throws at its next
  request, its first included; the class summary's last line is rewrapped.

Declined:
- A3: a success whose stream lost its usage chunk after the finish reason is
  left at an unknown cost, per LG7. The accumulator cannot tell that case
  from a [DONE] success without usage, so every such success would pay a
  lookup. The PR records it for Björn.
- A6: the csproj comment keeps "a Tsumugi.* assembly", as the two sibling
  engine projects do; ADR 0025 words the rule the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bjoern referenced this pull request from a commit 2026-09-26 01:10:27 +02:00
docs: slice 8a's PR is #35
All checks were successful
CI / build (pull_request) Successful in 35s
CI / test (pull_request) Successful in 1m5s
7faf3dbcf4
Another session's editor-track PR took #34 between the branch's last commit and
its PR, so ADR 0018's Status line and the plan's slice-8 line name #35.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

🔄 Auto-updating coverage report — this comment is regenerated on every push, so the numbers below always reflect the commit shown here, not the branch tip.

Commit: a407fa6 · Generated: 2026-09-25 23:57:27 UTC · Revision: #3

Summary

Summary
Generated on: 09/25/2026 - 23:57:27
Coverage date: 09/25/2026 - 23:57:03 - 09/25/2026 - 23:57:23
Parser: MultiReport (6x Cobertura)
Assemblies: 9
Classes: 440
Files: 419
Line coverage: 97.7% (13033 of 13328)
Covered lines: 13033
Uncovered lines: 295
Coverable lines: 13328
Total lines: 28674
Branch coverage: 93.5% (4704 of 5031)
Covered branches: 4704
Total branches: 5031
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.Flow - 99.7%
Name Line Branch
Kagaku.Flow 99.7% 99.2%
Kagaku.Flow.Budgeting.BudgetRequest 100%
Kagaku.Flow.Budgeting.ConsumerBudget 100%
Kagaku.Flow.Execution.NodeExecution 100% 100%
Kagaku.Flow.Execution.NodeOutcome 100% 100%
Kagaku.Flow.Execution.TransientFailureException 100%
Kagaku.Flow.Execution.Upstream 100%
Kagaku.Flow.Extensions.ServiceCollectionExtensions 100%
Kagaku.Flow.FlowJson 100%
Kagaku.Flow.Graphs.GraphDefinition 100%
Kagaku.Flow.Graphs.GraphDocument 100% 100%
Kagaku.Flow.Graphs.GraphEdge 100%
Kagaku.Flow.Graphs.GraphIndex 100% 100%
Kagaku.Flow.Graphs.GraphLoadError 100%
Kagaku.Flow.Graphs.GraphLoadResult 100%
Kagaku.Flow.Graphs.GraphNode 100%
Kagaku.Flow.Graphs.GraphPosition 100%
Kagaku.Flow.Graphs.GraphReader 100% 100%
Kagaku.Flow.Graphs.GraphSchema 100% 100%
Kagaku.Flow.Graphs.GraphShapeException 100%
Kagaku.Flow.Graphs.GraphWriter 100% 100%
Kagaku.Flow.Graphs.SocketRef 100%
Kagaku.Flow.Graphs.WireText 100% 100%
Kagaku.Flow.Nodes.CanvasDescriptorWriter 100% 100%
Kagaku.Flow.Nodes.ConfigProblem 100%
Kagaku.Flow.Nodes.Names 100%
Kagaku.Flow.Nodes.NodeDescriptor 100% 100%
Kagaku.Flow.Nodes.NodeRegistry 100% 100%
Kagaku.Flow.Nodes.NodeSockets 100% 100%
Kagaku.Flow.Nodes.SinkDescriptor 100%
Kagaku.Flow.Nodes.SocketTypes 100% 100%
Kagaku.Flow.Nodes.SourceDescriptor 100%
Kagaku.Flow.Nodes.TemplateField 100%
Kagaku.Flow.Runs.LedgerChange 100% 100%
Kagaku.Flow.Runs.LedgerTransitions 100% 100%
Kagaku.Flow.Runs.NodeAttempt 100%
Kagaku.Flow.Runs.NodeFacts 100%
Kagaku.Flow.Runs.NodeRun 100%
Kagaku.Flow.Runs.Run 100% 100%
Kagaku.Flow.Runs.RunEvent 100%
Kagaku.Flow.Runs.RunStatuses 100% 100%
Kagaku.Flow.Runs.RunTrigger 100% 100%
Kagaku.Flow.Scheduling.Exclusion 100%
Kagaku.Flow.Scheduling.FlowEngine 100% 100%
Kagaku.Flow.Scheduling.FlowEngineOptions 100% 100%
Kagaku.Flow.Scheduling.ReadyRow 100%
Kagaku.Flow.Scheduling.ReadySet 100% 100%
Kagaku.Flow.Scheduling.ReadySetResult 100%
Kagaku.Flow.Scheduling.RunRequest 100% 100%
Kagaku.Flow.Scheduling.RunStartResult 100%
Kagaku.Flow.Scheduling.SocketValues 100% 100%
Kagaku.Flow.Templating.InputsWalker 100% 100%
Kagaku.Flow.Templating.ParsedTemplate 100% 100%
Kagaku.Flow.Templating.SandboxedTemplateContext 100% 100%
Kagaku.Flow.Templating.TemplateError 100%
Kagaku.Flow.Templating.TemplateHost 100% 100%
Kagaku.Flow.Templating.TemplateLimits 100%
Kagaku.Flow.Templating.TemplateModel 100% 100%
Kagaku.Flow.Templating.TemplateProblem 100%
Kagaku.Flow.Templating.TemplateRenderRequest 100% 100%
Kagaku.Flow.Templating.TemplateRenderResult 100%
Kagaku.Flow.Templating.TemplateRun 100%
Kagaku.Flow.Templating.TemplateSinks 100%
Kagaku.Flow.Templating.TemplateValues 100% 100%
Kagaku.Flow.Validation.GraphValidation 100% 100%
Kagaku.Flow.Validation.GraphValidationContext 100%
Kagaku.Flow.Validation.HookFailure 100%
Kagaku.Flow.Validation.ResolvedGraph 100% 100%
Kagaku.Flow.Validation.ValidationError 100%
Kagaku.Flow.Validation.ValidationReport 100%
System.Text.RegularExpressions.Generated 97.4% 92.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
97.4% 94.4%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
97.2% 94.4%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
97.2% 94.4%
Kagaku.Flow.OpenRouter - 100%
Name Line Branch
Kagaku.Flow.OpenRouter 100% 100%
Kagaku.Flow.OpenRouter.Catalog.ModelCapabilities 100% 100%
Kagaku.Flow.OpenRouter.Gateway.LlmCompletion 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterGateway 100% 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterGatewayOptions 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterKeyLookup 100% 100%
Kagaku.Flow.OpenRouter.Gateway.SentRequestCapture 100% 100%
Kagaku.Flow.OpenRouter.Gateway.SentRequestCaptureHandler 100% 100%
Kagaku.Flow.OpenRouter.OpenRouterServiceCollectionExtensions 100% 100%
Kagaku.Flow.Testing - 100%
Name Line Branch
Kagaku.Flow.Testing 100% 100%
Kagaku.Flow.Testing.FlowEngineDriver 100% 100%
Kagaku.Flow.Testing.InMemoryRunLedger 100% 100%
Kagaku.Flow.Testing.RecordingRunEventSink 100%
Kagaku.Flow.Testing.RunLedgerContract 100% 100%
Kagaku.Flow.Testing.RunLedgerContractException 100%
Tsumugi.BlazorAdapter - 96.4%
Name Line Branch
Tsumugi.BlazorAdapter 96.4% 92.6%
Tsumugi.BlazorAdapter.BlazorAdapterAssembly 100%
Tsumugi.BlazorAdapter.Documents.CategoryCreated 100%
Tsumugi.BlazorAdapter.Documents.CategoryEditor 91.8% 87.2%
Tsumugi.BlazorAdapter.Documents.CategorySaved 100%
Tsumugi.BlazorAdapter.Documents.ChangeCategoryFieldsRequested 100%
Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryChoice 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog 100% 94.1%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentCreated 100%
Tsumugi.BlazorAdapter.Documents.DocumentEditor 95.1% 88.8%
Tsumugi.BlazorAdapter.Documents.DocumentImageRefused 100%
Tsumugi.BlazorAdapter.Documents.DocumentImageSet 100%
Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentSaved 100%
Tsumugi.BlazorAdapter.Documents.DocumentsEffects 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsList 98.2% 92.4%
Tsumugi.BlazorAdapter.Documents.DocumentsLoaded 100%
Tsumugi.BlazorAdapter.Documents.DocumentsPage 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsReducers 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsRefusal 100%
Tsumugi.BlazorAdapter.Documents.DocumentsState 100% 100%
Tsumugi.BlazorAdapter.Documents.FieldInput 100% 96.6%
Tsumugi.BlazorAdapter.Documents.FieldsChanges 100% 100%
Tsumugi.BlazorAdapter.Documents.FieldShapes 90% 87.5%
Tsumugi.BlazorAdapter.Documents.ImageUpload 100%
Tsumugi.BlazorAdapter.Documents.ImageUploadRefusal 100%
Tsumugi.BlazorAdapter.Documents.ImpactText 100% 94.2%
Tsumugi.BlazorAdapter.Documents.LoadDocuments 100%
Tsumugi.BlazorAdapter.Documents.MoveDocumentChoice 100%
Tsumugi.BlazorAdapter.Documents.MoveDocumentDialog 95.6% 71.4%
Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested 100%
Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested 100%
Tsumugi.BlazorAdapter.Documents.SetCategoryCollapsed 100%
Tsumugi.BlazorAdapter.Documents.SetDocumentImageRequested 100%
Tsumugi.BlazorAdapter.Documents.SetDocumentsFilter 100%
Tsumugi.BlazorAdapter.Documents.UpdateCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.UpdateDocumentRequested 100%
Tsumugi.BlazorAdapter.Graphs.GraphCanvas 37.5% 25%
Tsumugi.BlazorAdapter.Graphs.GraphPlaygroundPage 20% 25%
Tsumugi.BlazorAdapter.Notifications.DomainChangedBridge 94.4% 83.3%
Tsumugi.BlazorAdapter.Notifications.DomainChangesReceived 75% 100%
Tsumugi.BlazorAdapter.Runs.RunChangedBridge 97.1% 90.6%
Tsumugi.BlazorAdapter.Runs.RunChangesReceived 100%
Tsumugi.BlazorAdapter.Runs.RunTextReceived 100%
Tsumugi.BlazorAdapter.Settings.BackupCompleted 100%
Tsumugi.BlazorAdapter.Settings.BackupFailed 100%
Tsumugi.BlazorAdapter.Settings.BackupNowRequested 100%
Tsumugi.BlazorAdapter.Settings.BackupSettingsLoaded 100%
Tsumugi.BlazorAdapter.Settings.BackupsSection 100% 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsReducers 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsState 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSection 100% 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsReducers 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsState 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsLoaded 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaved 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaveFailed 100%
Tsumugi.BlazorAdapter.Settings.LoadBackupSettings 100%
Tsumugi.BlazorAdapter.Settings.LoadProviderSettings 100%
Tsumugi.BlazorAdapter.Settings.ModelCatalogLoaded 100%
Tsumugi.BlazorAdapter.Settings.ModelCatalogLoadFailed 100%
Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaved 100%
Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaveFailed 100%
Tsumugi.BlazorAdapter.Settings.ProviderKeyChecked 100%
Tsumugi.BlazorAdapter.Settings.ProviderSection 100% 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsLoaded 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsReducers 100% 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsState 100% 100%
Tsumugi.BlazorAdapter.Settings.SaveLlmDefaultsRequested 100%
Tsumugi.BlazorAdapter.Settings.SaveOpenRouterKeyRequested 100%
Tsumugi.BlazorAdapter.Stories.CreateStoryRequested 100%
Tsumugi.BlazorAdapter.Stories.PurgeStoryRequested 100%
Tsumugi.BlazorAdapter.Stories.RestoreStoryRequested 100%
Tsumugi.BlazorAdapter.Stories.SetShowArchived 100%
Tsumugi.BlazorAdapter.Stories.SetStoriesFilter 100%
Tsumugi.BlazorAdapter.Stories.SetStoryArchivedRequested 100%
Tsumugi.BlazorAdapter.Stories.StoriesEffects 100% 100%
Tsumugi.BlazorAdapter.Stories.StoriesLoaded 100%
Tsumugi.BlazorAdapter.Stories.StoriesPage 100% 100%
Tsumugi.BlazorAdapter.Stories.StoriesReducers 100% 100%
Tsumugi.BlazorAdapter.Stories.StoriesState 100% 100%
Tsumugi.BlazorAdapter.Stories.StoryActionFailed 100%
Tsumugi.BlazorAdapter.Stories.StoryCreated 100%
Tsumugi.BlazorAdapter.Stories.StoryCreateFailed 100%
Tsumugi.BlazorAdapter.Stories.StoryTrashPage 100% 100%
Tsumugi.BlazorAdapter.Stories.TrashStoryRequested 100%
Tsumugi.BlazorAdapter.Workspace.AutoSaveFailed 100%
Tsumugi.BlazorAdapter.Workspace.CaretRequest 100%
Tsumugi.BlazorAdapter.Workspace.ChapterHeader 100% 86.3%
Tsumugi.BlazorAdapter.Workspace.ChapterMenu 100% 81.2%
Tsumugi.BlazorAdapter.Workspace.ChapterPage 100% 87.5%
Tsumugi.BlazorAdapter.Workspace.ChapterSaved 100%
Tsumugi.BlazorAdapter.Workspace.ChaptersPage 90% 83.3%
Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.DeleteSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.EditSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.HistoryEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.HistoryLoaded 100%
Tsumugi.BlazorAdapter.Workspace.HistoryPage 100%
Tsumugi.BlazorAdapter.Workspace.HistoryPanel 100% 100%
Tsumugi.BlazorAdapter.Workspace.InsertSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.LoadHistory 100%
Tsumugi.BlazorAdapter.Workspace.LoadWorkspace 100%
Tsumugi.BlazorAdapter.Workspace.ManuscriptColumn 94.9% 89.4%
Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.MergeSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested 100%
Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.RevertToSnapshotRequested 100%
Tsumugi.BlazorAdapter.Workspace.SaveScope 100% 50%
Tsumugi.BlazorAdapter.Workspace.SaveStateLadder 100% 100%
Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested 100%
Tsumugi.BlazorAdapter.Workspace.SegmentRow 100% 96.8%
Tsumugi.BlazorAdapter.Workspace.SegmentSaved 100%
Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested 100%
Tsumugi.BlazorAdapter.Workspace.SplitSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.StoryDetails 100% 85.2%
Tsumugi.BlazorAdapter.Workspace.StoryGeneralPage 100% 100%
Tsumugi.BlazorAdapter.Workspace.StorySaved 100%
Tsumugi.BlazorAdapter.Workspace.StoryShell 100% 100%
Tsumugi.BlazorAdapter.Workspace.UndoRequested 100%
Tsumugi.BlazorAdapter.Workspace.UndoStatusChanged 100%
Tsumugi.BlazorAdapter.Workspace.UpdateStoryRequested 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceActionFailed 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceLoaded 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceNotFound 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceReducers 100% 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceState 100%
Tsumugi.Domain - 95.6%
Name Line Branch
Tsumugi.Domain 95.6% 93.9%
System.Text.RegularExpressions.Generated 90.7% 87.5%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
97.4% 95.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
91% 91.1%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
86.4% 77.7%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
91.9% 88.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
90.9% 84.3%
Tsumugi.Domain.Documents.CategoryFields 94.5% 93.7%
Tsumugi.Domain.Documents.ChoiceOption 100%
Tsumugi.Domain.Documents.ChoiceValue 100%
Tsumugi.Domain.Documents.DateText 100%
Tsumugi.Domain.Documents.DateValue 100%
Tsumugi.Domain.Documents.Document 100% 100%
Tsumugi.Domain.Documents.DocumentCategory 100% 100%
Tsumugi.Domain.Documents.DocumentValues 100%
Tsumugi.Domain.Documents.FieldCarry 100% 100%
Tsumugi.Domain.Documents.FieldConversion 100% 100%
Tsumugi.Domain.Documents.FieldDefinition 97% 95.3%
Tsumugi.Domain.Documents.FieldMessages 100% 100%
Tsumugi.Domain.Documents.FieldNames 100%
Tsumugi.Domain.Documents.FieldsChange 100%
Tsumugi.Domain.Documents.FieldShape 95.2% 87.5%
Tsumugi.Domain.Documents.FieldsPlan 100%
Tsumugi.Domain.Documents.FieldsPlanner 99.2% 98.5%
Tsumugi.Domain.Documents.FieldValue 100%
Tsumugi.Domain.Documents.FieldValues 95.4% 97.7%
Tsumugi.Domain.Documents.ImageFiles 100% 100%
Tsumugi.Domain.Documents.ImageValue 100%
Tsumugi.Domain.Documents.MovePlan 100%
Tsumugi.Domain.Documents.NumberParse 100%
Tsumugi.Domain.Documents.NumberText 100% 97.6%
Tsumugi.Domain.Documents.NumberValue 100%
Tsumugi.Domain.Documents.StoryImage 100%
Tsumugi.Domain.Documents.TextValue 100% 100%
Tsumugi.Domain.Documents.ValueChange 100%
Tsumugi.Domain.Documents.ValueChangeKey 100%
Tsumugi.Domain.Documents.ValueImpact 100% 100%
Tsumugi.Domain.Journal.ChangeLogEntry 97.8% 75%
Tsumugi.Domain.Journal.StorySnapshot 95.4% 83.3%
Tsumugi.Domain.Settings.AppSetting 100%
Tsumugi.Domain.Stories.Chapter 100% 100%
Tsumugi.Domain.Stories.Provenance 100% 100%
Tsumugi.Domain.Stories.Segment 100% 100%
Tsumugi.Domain.Stories.Story 100% 100%
Tsumugi.Domain.Text 100% 100%
Tsumugi.Infrastructure - 97.6%
Name Line Branch
Tsumugi.Infrastructure 97.6% 85.9%
System.Text.RegularExpressions.Generated 81.4% 64.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F0D076C66E8EEC1C
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
84% 65.7%
Tsumugi.Infrastructure.Backups.BackupArtifactName 100% 100%
Tsumugi.Infrastructure.Backups.BackupOptions 100%
Tsumugi.Infrastructure.Backups.BackupSchedule 100% 83.3%
Tsumugi.Infrastructure.Backups.BackupScheduler 96.5% 83.3%
Tsumugi.Infrastructure.Backups.BackupWriter 98.5% 92.5%
Tsumugi.Infrastructure.Backups.DatabaseFingerprint 91.9% 97%
Tsumugi.Infrastructure.Backups.FileSystemBackupStore 100% 100%
Tsumugi.Infrastructure.Backups.PreMigrationBackup 89.1% 100%
Tsumugi.Infrastructure.Backups.SqliteSnapshot 87.8% 75%
Tsumugi.Infrastructure.DependencyInjection 100% 100%
Tsumugi.Infrastructure.Journal.EfChangeJournal 100%
Tsumugi.Infrastructure.Journal.EfSnapshotStore 100%
Tsumugi.Infrastructure.Journal.EfUndoStore 98.1% 85.7%
Tsumugi.Infrastructure.Journal.ModelExtensions 100%
Tsumugi.Infrastructure.Journal.OperationContext 100% 100%
Tsumugi.Infrastructure.Journal.PropertyBag 100% 95.4%
Tsumugi.Infrastructure.Notifications.InProcessDomainChangedBus 100% 100%
Tsumugi.Infrastructure.Persistence.Configurations.AppSettingConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.ChangeLogEntryConfigurati
on
100%
Tsumugi.Infrastructure.Persistence.Configurations.ChapterConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.DocumentCategoryConfigura
tion
100%
Tsumugi.Infrastructure.Persistence.Configurations.DocumentConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.SegmentConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StoryConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StoryImageConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio
n
100%
Tsumugi.Infrastructure.Persistence.Converters.CanonicalJsonComparer 100%
Tsumugi.Infrastructure.Persistence.Converters.CategoryFieldsConverter 97.6% 94.4%
Tsumugi.Infrastructure.Persistence.Converters.FieldValuesConverter 96.9% 93.7%
Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter 100% 90%
Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Tsumugi.Infrastructure.Persistence.Converters.WireFormat 85.7% 73.8%
Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings 99.2%
Tsumugi.Infrastructure.Persistence.Migrations.AddJournal 98.8%
Tsumugi.Infrastructure.Persistence.Migrations.CategoryFields 100% 100%
Tsumugi.Infrastructure.Persistence.Migrations.CategoryFieldsSql 100%
Tsumugi.Infrastructure.Persistence.Migrations.DropDocumentBody 100%
Tsumugi.Infrastructure.Persistence.Migrations.GenericDocuments 100%
Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain 97.3%
Tsumugi.Infrastructure.Persistence.Migrations.StoryImages 100%
Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot 100%
Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore 98.3% 75%
Tsumugi.Infrastructure.Persistence.Stores.EfImageStore 100%
Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore 95% 84.6%
Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore 98.7% 90%
Tsumugi.Infrastructure.Persistence.TsumugiDbContext 85.5% 89.2%
Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory 0%
Tsumugi.Infrastructure.Provider.OpenRouterKeySource 100% 100%
Tsumugi.Infrastructure.Provider.OpenRouterModelProvider 100% 100%
Tsumugi.Infrastructure.Provider.OpenRouterOptions 100% 100%
Tsumugi.Infrastructure.Provider.ResolvedKey 100%
Tsumugi.Infrastructure.Runs.InProcessRunEventBus 100% 100%
Tsumugi.Infrastructure.Settings.EfAppSettingsStore 100% 100%
Tsumugi.Kernel - 90.9%
Name Line Branch
Tsumugi.Kernel 90.9% 75%
Tsumugi.Kernel.Err`1 100%
Tsumugi.Kernel.Ok`1 100%
Tsumugi.Kernel.Result`1 88.8% 75%
Tsumugi.Server - 100%
Name Line Branch
Tsumugi.Server 100% 86.4%
Program 100% 100%
Tsumugi.Server.Backups.BackupConfiguration 100% 100%
Tsumugi.Server.Components.App 100%
Tsumugi.Server.Components.Layout.AppHeader 100% 50%
Tsumugi.Server.Components.Layout.MainLayout 100%
Tsumugi.Server.Components.Pages.Gate 100% 100%
Tsumugi.Server.Provider.OpenRouterConfiguration 100% 100%
Tsumugi.Server.Security.AccessGate 100% 66.6%
Tsumugi.Server.Security.AccessSecret 100% 50%
Tsumugi.Server.VolumeStartupValidator 100% 100%
Tsumugi.UseCases - 98.3%
Name Line Branch
Tsumugi.UseCases 98.3% 93.5%
Tsumugi.UseCases.Backups.BackupArtifact 100%
Tsumugi.UseCases.Backups.BackupPolicy 100%
Tsumugi.UseCases.Backups.BackupStatusView 100% 100%
Tsumugi.UseCases.Backups.DatabaseSnapshot 100%
Tsumugi.UseCases.Backups.DownloadDatabaseSnapshot 100%
Tsumugi.UseCases.Backups.GetBackupStatus 100%
Tsumugi.UseCases.Backups.RunBackupNow 100%
Tsumugi.UseCases.Chapters.ChapterDto 100%
Tsumugi.UseCases.Chapters.CreateChapter 100% 100%
Tsumugi.UseCases.Chapters.DeleteChapter 100% 100%
Tsumugi.UseCases.Chapters.GetManuscript 100% 100%
Tsumugi.UseCases.Chapters.ManuscriptChapter 100%
Tsumugi.UseCases.Chapters.ManuscriptView 100%
Tsumugi.UseCases.Chapters.MergeChapters 100% 100%
Tsumugi.UseCases.Chapters.RenameChapter 100% 100%
Tsumugi.UseCases.Chapters.ReorderChapters 100% 100%
Tsumugi.UseCases.Chapters.SetChapterSummary 100% 75%
Tsumugi.UseCases.DependencyInjection 100%
Tsumugi.UseCases.Diagnostics.SeedDevData 98.2% 71.4%
Tsumugi.UseCases.Diagnostics.SeedPictures 100%
Tsumugi.UseCases.Documents.ChangeCategoryFields 100% 83.3%
Tsumugi.UseCases.Documents.CreateCategory 100% 100%
Tsumugi.UseCases.Documents.CreateDocument 100% 100%
Tsumugi.UseCases.Documents.DefaultFields 100% 100%
Tsumugi.UseCases.Documents.DeleteCategory 100% 94.4%
Tsumugi.UseCases.Documents.DeleteDocument 100% 100%
Tsumugi.UseCases.Documents.DocumentCategoryDto 100%
Tsumugi.UseCases.Documents.DocumentCategoryProfile 100%
Tsumugi.UseCases.Documents.DocumentDto 100%
Tsumugi.UseCases.Documents.DocumentPatch 100%
Tsumugi.UseCases.Documents.DocumentProfile 100%
Tsumugi.UseCases.Documents.DocumentsCategory 100%
Tsumugi.UseCases.Documents.DocumentsView 100%
Tsumugi.UseCases.Documents.DocumentWrites 100% 100%
Tsumugi.UseCases.Documents.FieldsChangeLabel 95.4% 88.2%
Tsumugi.UseCases.Documents.GetDocuments 100% 100%
Tsumugi.UseCases.Documents.GetStoryImage 100% 100%
Tsumugi.UseCases.Documents.ImpactPreview 100% 100%
Tsumugi.UseCases.Documents.LossMessages 92.3% 87.5%
Tsumugi.UseCases.Documents.MoveDocuments 100% 100%
Tsumugi.UseCases.Documents.ReorderCategories 100% 100%
Tsumugi.UseCases.Documents.SetDocumentImage 100% 93.7%
Tsumugi.UseCases.Documents.StoryImageFile 100%
Tsumugi.UseCases.Documents.UpdateCategory 100% 100%
Tsumugi.UseCases.Documents.UpdateDocument 100% 81.2%
Tsumugi.UseCases.Journal.GetStoryHistory 100% 100%
Tsumugi.UseCases.Journal.GetUndoStatus 100% 100%
Tsumugi.UseCases.Journal.HistoryOperation 66.6%
Tsumugi.UseCases.Journal.HistorySnapshot 87.5%
Tsumugi.UseCases.Journal.OperationGrouping 100% 100%
Tsumugi.UseCases.Journal.RedoRevert 100% 100%
Tsumugi.UseCases.Journal.RedoRun 75% 57.1%
Tsumugi.UseCases.Journal.RedoTyping 100% 100%
Tsumugi.UseCases.Journal.RevertConflict 100%
Tsumugi.UseCases.Journal.RevertOutcome 100%
Tsumugi.UseCases.Journal.RevertRun 96.5% 77.7%
Tsumugi.UseCases.Journal.RevertToSnapshot 92.3% 83.3%
Tsumugi.UseCases.Journal.StoryHistory 100%
Tsumugi.UseCases.Journal.TakeSnapshot 100% 87.5%
Tsumugi.UseCases.Journal.UndoStatus 100%
Tsumugi.UseCases.Journal.UndoTyping 100% 100%
Tsumugi.UseCases.Journal.UserOperation 100%
Tsumugi.UseCases.Notifications.DomainChanged 100%
Tsumugi.UseCases.Ordering 100% 100%
Tsumugi.UseCases.Runs.RunEvent 100%
Tsumugi.UseCases.Segments.DeleteSegments 100% 100%
Tsumugi.UseCases.Segments.EditSegment 100% 83.3%
Tsumugi.UseCases.Segments.InsertSegment 100% 100%
Tsumugi.UseCases.Segments.MergeSegmentWithPrevious 100% 100%
Tsumugi.UseCases.Segments.MoveSegments 100% 100%
Tsumugi.UseCases.Segments.ReorderSegments 100% 100%
Tsumugi.UseCases.Segments.SegmentDto 100%
Tsumugi.UseCases.Segments.SplitSegment 100% 100%
Tsumugi.UseCases.Settings.CatalogModel 100% 100%
Tsumugi.UseCases.Settings.CheckOpenRouterKey 100%
Tsumugi.UseCases.Settings.GetLlmDefaults 100% 100%
Tsumugi.UseCases.Settings.GetModelCatalog 100% 100%
Tsumugi.UseCases.Settings.GetProviderSettings 100%
Tsumugi.UseCases.Settings.LlmDefaults 100%
Tsumugi.UseCases.Settings.ProviderKeyInfo 100%
Tsumugi.UseCases.Settings.ProviderKeyState 100%
Tsumugi.UseCases.Settings.ProviderSettingsView 100% 100%
Tsumugi.UseCases.Settings.ReasoningEfforts 100% 100%
Tsumugi.UseCases.Settings.RemoveOpenRouterKey 100%
Tsumugi.UseCases.Settings.RoutingDefaults 100%
Tsumugi.UseCases.Settings.RoutingSettings 100% 100%
Tsumugi.UseCases.Settings.SaveLlmDefaults 100% 100%
Tsumugi.UseCases.Settings.SaveOpenRouterKey 100% 100%
Tsumugi.UseCases.Stories.CreateStory 100% 100%
Tsumugi.UseCases.Stories.GetStory 100% 100%
Tsumugi.UseCases.Stories.ListStories 100% 75%
Tsumugi.UseCases.Stories.PurgeStory 100% 100%
Tsumugi.UseCases.Stories.RestoreStory 100% 100%
Tsumugi.UseCases.Stories.SetActiveGraph 100% 100%
Tsumugi.UseCases.Stories.SetStoryArchived 100% 100%
Tsumugi.UseCases.Stories.StarterCategories 100%
Tsumugi.UseCases.Stories.StoryDetail 100%
Tsumugi.UseCases.Stories.StoryListItem 81.8%
Tsumugi.UseCases.Stories.StoryProfile 100%
Tsumugi.UseCases.Stories.TrashStory 100% 100%
Tsumugi.UseCases.Stories.UpdateStory 100% 100%
<!-- coverage-comment --> > 🔄 **Auto-updating coverage report** — this comment is regenerated on every push, so the numbers below always reflect the commit shown here, not the branch tip. > > **Commit:** `a407fa6` · **Generated:** 2026-09-25 23:57:27 UTC · **Revision:** #3 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/25/2026 - 23:57:27 | | Coverage date: | 09/25/2026 - 23:57:03 - 09/25/2026 - 23:57:23 | | Parser: | MultiReport (6x Cobertura) | | Assemblies: | 9 | | Classes: | 440 | | Files: | 419 | | **Line coverage:** | 97.7% (13033 of 13328) | | Covered lines: | 13033 | | Uncovered lines: | 295 | | Coverable lines: | 13328 | | Total lines: | 28674 | | **Branch coverage:** | 93.5% (4704 of 5031) | | Covered branches: | 4704 | | Total branches: | 5031 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.Flow - 99.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow**|**99.7%**|**99.2%**| |Kagaku.Flow.Budgeting.BudgetRequest|100%|| |Kagaku.Flow.Budgeting.ConsumerBudget|100%|| |Kagaku.Flow.Execution.NodeExecution|100%|100%| |Kagaku.Flow.Execution.NodeOutcome|100%|100%| |Kagaku.Flow.Execution.TransientFailureException|100%|| |Kagaku.Flow.Execution.Upstream|100%|| |Kagaku.Flow.Extensions.ServiceCollectionExtensions|100%|| |Kagaku.Flow.FlowJson|100%|| |Kagaku.Flow.Graphs.GraphDefinition|100%|| |Kagaku.Flow.Graphs.GraphDocument|100%|100%| |Kagaku.Flow.Graphs.GraphEdge|100%|| |Kagaku.Flow.Graphs.GraphIndex|100%|100%| |Kagaku.Flow.Graphs.GraphLoadError|100%|| |Kagaku.Flow.Graphs.GraphLoadResult|100%|| |Kagaku.Flow.Graphs.GraphNode|100%|| |Kagaku.Flow.Graphs.GraphPosition|100%|| |Kagaku.Flow.Graphs.GraphReader|100%|100%| |Kagaku.Flow.Graphs.GraphSchema|100%|100%| |Kagaku.Flow.Graphs.GraphShapeException|100%|| |Kagaku.Flow.Graphs.GraphWriter|100%|100%| |Kagaku.Flow.Graphs.SocketRef|100%|| |Kagaku.Flow.Graphs.WireText|100%|100%| |Kagaku.Flow.Nodes.CanvasDescriptorWriter|100%|100%| |Kagaku.Flow.Nodes.ConfigProblem|100%|| |Kagaku.Flow.Nodes.Names|100%|| |Kagaku.Flow.Nodes.NodeDescriptor|100%|100%| |Kagaku.Flow.Nodes.NodeRegistry|100%|100%| |Kagaku.Flow.Nodes.NodeSockets|100%|100%| |Kagaku.Flow.Nodes.SinkDescriptor|100%|| |Kagaku.Flow.Nodes.SocketTypes|100%|100%| |Kagaku.Flow.Nodes.SourceDescriptor|100%|| |Kagaku.Flow.Nodes.TemplateField|100%|| |Kagaku.Flow.Runs.LedgerChange|100%|100%| |Kagaku.Flow.Runs.LedgerTransitions|100%|100%| |Kagaku.Flow.Runs.NodeAttempt|100%|| |Kagaku.Flow.Runs.NodeFacts|100%|| |Kagaku.Flow.Runs.NodeRun|100%|| |Kagaku.Flow.Runs.Run|100%|100%| |Kagaku.Flow.Runs.RunEvent|100%|| |Kagaku.Flow.Runs.RunStatuses|100%|100%| |Kagaku.Flow.Runs.RunTrigger|100%|100%| |Kagaku.Flow.Scheduling.Exclusion|100%|| |Kagaku.Flow.Scheduling.FlowEngine|100%|100%| |Kagaku.Flow.Scheduling.FlowEngineOptions|100%|100%| |Kagaku.Flow.Scheduling.ReadyRow|100%|| |Kagaku.Flow.Scheduling.ReadySet|100%|100%| |Kagaku.Flow.Scheduling.ReadySetResult|100%|| |Kagaku.Flow.Scheduling.RunRequest|100%|100%| |Kagaku.Flow.Scheduling.RunStartResult|100%|| |Kagaku.Flow.Scheduling.SocketValues|100%|100%| |Kagaku.Flow.Templating.InputsWalker|100%|100%| |Kagaku.Flow.Templating.ParsedTemplate|100%|100%| |Kagaku.Flow.Templating.SandboxedTemplateContext|100%|100%| |Kagaku.Flow.Templating.TemplateError|100%|| |Kagaku.Flow.Templating.TemplateHost|100%|100%| |Kagaku.Flow.Templating.TemplateLimits|100%|| |Kagaku.Flow.Templating.TemplateModel|100%|100%| |Kagaku.Flow.Templating.TemplateProblem|100%|| |Kagaku.Flow.Templating.TemplateRenderRequest|100%|100%| |Kagaku.Flow.Templating.TemplateRenderResult|100%|| |Kagaku.Flow.Templating.TemplateRun|100%|| |Kagaku.Flow.Templating.TemplateSinks|100%|| |Kagaku.Flow.Templating.TemplateValues|100%|100%| |Kagaku.Flow.Validation.GraphValidation|100%|100%| |Kagaku.Flow.Validation.GraphValidationContext|100%|| |Kagaku.Flow.Validation.HookFailure|100%|| |Kagaku.Flow.Validation.ResolvedGraph|100%|100%| |Kagaku.Flow.Validation.ValidationError|100%|| |Kagaku.Flow.Validation.ValidationReport|100%|| |System.Text.RegularExpressions.Generated|97.4%|92.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF<br/>E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2|97.4%|94.4%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF<br/>E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1|97.2%|94.4%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF<br/>E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0|97.2%|94.4%| </details> <details><summary>Kagaku.Flow.OpenRouter - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow.OpenRouter**|**100%**|**100%**| |Kagaku.Flow.OpenRouter.Catalog.ModelCapabilities|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.LlmCompletion|100%|| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterGateway|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterGatewayOptions|100%|| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterKeyLookup|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.SentRequestCapture|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.SentRequestCaptureHandler|100%|100%| |Kagaku.Flow.OpenRouter.OpenRouterServiceCollectionExtensions|100%|100%| </details> <details><summary>Kagaku.Flow.Testing - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow.Testing**|**100%**|**100%**| |Kagaku.Flow.Testing.FlowEngineDriver|100%|100%| |Kagaku.Flow.Testing.InMemoryRunLedger|100%|100%| |Kagaku.Flow.Testing.RecordingRunEventSink|100%|| |Kagaku.Flow.Testing.RunLedgerContract|100%|100%| |Kagaku.Flow.Testing.RunLedgerContractException|100%|| </details> <details><summary>Tsumugi.BlazorAdapter - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**96.4%**|**92.6%**| |Tsumugi.BlazorAdapter.BlazorAdapterAssembly|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryCreated|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryEditor|91.8%|87.2%| |Tsumugi.BlazorAdapter.Documents.CategorySaved|100%|| |Tsumugi.BlazorAdapter.Documents.ChangeCategoryFieldsRequested|100%|| |Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryChoice|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog|100%|94.1%| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentCreated|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentEditor|95.1%|88.8%| |Tsumugi.BlazorAdapter.Documents.DocumentImageRefused|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentImageSet|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentSaved|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsEffects|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsList|98.2%|92.4%| |Tsumugi.BlazorAdapter.Documents.DocumentsLoaded|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsPage|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsReducers|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsRefusal|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsState|100%|100%| |Tsumugi.BlazorAdapter.Documents.FieldInput|100%|96.6%| |Tsumugi.BlazorAdapter.Documents.FieldsChanges|100%|100%| |Tsumugi.BlazorAdapter.Documents.FieldShapes|90%|87.5%| |Tsumugi.BlazorAdapter.Documents.ImageUpload|100%|| |Tsumugi.BlazorAdapter.Documents.ImageUploadRefusal|100%|| |Tsumugi.BlazorAdapter.Documents.ImpactText|100%|94.2%| |Tsumugi.BlazorAdapter.Documents.LoadDocuments|100%|| |Tsumugi.BlazorAdapter.Documents.MoveDocumentChoice|100%|| |Tsumugi.BlazorAdapter.Documents.MoveDocumentDialog|95.6%|71.4%| |Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested|100%|| |Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested|100%|| |Tsumugi.BlazorAdapter.Documents.SetCategoryCollapsed|100%|| |Tsumugi.BlazorAdapter.Documents.SetDocumentImageRequested|100%|| |Tsumugi.BlazorAdapter.Documents.SetDocumentsFilter|100%|| |Tsumugi.BlazorAdapter.Documents.UpdateCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.UpdateDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Graphs.GraphCanvas|37.5%|25%| |Tsumugi.BlazorAdapter.Graphs.GraphPlaygroundPage|20%|25%| |Tsumugi.BlazorAdapter.Notifications.DomainChangedBridge|94.4%|83.3%| |Tsumugi.BlazorAdapter.Notifications.DomainChangesReceived|75%|100%| |Tsumugi.BlazorAdapter.Runs.RunChangedBridge|97.1%|90.6%| |Tsumugi.BlazorAdapter.Runs.RunChangesReceived|100%|| |Tsumugi.BlazorAdapter.Runs.RunTextReceived|100%|| |Tsumugi.BlazorAdapter.Settings.BackupCompleted|100%|| |Tsumugi.BlazorAdapter.Settings.BackupFailed|100%|| |Tsumugi.BlazorAdapter.Settings.BackupNowRequested|100%|| |Tsumugi.BlazorAdapter.Settings.BackupSettingsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.BackupsSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsReducers|100%|| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsState|100%|| |Tsumugi.BlazorAdapter.Settings.DefaultsSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsReducers|100%|| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsState|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaved|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaveFailed|100%|| |Tsumugi.BlazorAdapter.Settings.LoadBackupSettings|100%|| |Tsumugi.BlazorAdapter.Settings.LoadProviderSettings|100%|| |Tsumugi.BlazorAdapter.Settings.ModelCatalogLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.ModelCatalogLoadFailed|100%|| |Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaved|100%|| |Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaveFailed|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderKeyChecked|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsReducers|100%|100%| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsState|100%|100%| |Tsumugi.BlazorAdapter.Settings.SaveLlmDefaultsRequested|100%|| |Tsumugi.BlazorAdapter.Settings.SaveOpenRouterKeyRequested|100%|| |Tsumugi.BlazorAdapter.Stories.CreateStoryRequested|100%|| |Tsumugi.BlazorAdapter.Stories.PurgeStoryRequested|100%|| |Tsumugi.BlazorAdapter.Stories.RestoreStoryRequested|100%|| |Tsumugi.BlazorAdapter.Stories.SetShowArchived|100%|| |Tsumugi.BlazorAdapter.Stories.SetStoriesFilter|100%|| |Tsumugi.BlazorAdapter.Stories.SetStoryArchivedRequested|100%|| |Tsumugi.BlazorAdapter.Stories.StoriesEffects|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoriesLoaded|100%|| |Tsumugi.BlazorAdapter.Stories.StoriesPage|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoriesReducers|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoriesState|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoryActionFailed|100%|| |Tsumugi.BlazorAdapter.Stories.StoryCreated|100%|| |Tsumugi.BlazorAdapter.Stories.StoryCreateFailed|100%|| |Tsumugi.BlazorAdapter.Stories.StoryTrashPage|100%|100%| |Tsumugi.BlazorAdapter.Stories.TrashStoryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.AutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.CaretRequest|100%|| |Tsumugi.BlazorAdapter.Workspace.ChapterHeader|100%|86.3%| |Tsumugi.BlazorAdapter.Workspace.ChapterMenu|100%|81.2%| |Tsumugi.BlazorAdapter.Workspace.ChapterPage|100%|87.5%| |Tsumugi.BlazorAdapter.Workspace.ChapterSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.ChaptersPage|90%|83.3%| |Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.DeleteSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.EditSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.HistoryLoaded|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryPage|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryPanel|100%|100%| |Tsumugi.BlazorAdapter.Workspace.InsertSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.LoadHistory|100%|| |Tsumugi.BlazorAdapter.Workspace.LoadWorkspace|100%|| |Tsumugi.BlazorAdapter.Workspace.ManuscriptColumn|94.9%|89.4%| |Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.MergeSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RevertToSnapshotRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SaveScope|100%|50%| |Tsumugi.BlazorAdapter.Workspace.SaveStateLadder|100%|100%| |Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SegmentRow|100%|96.8%| |Tsumugi.BlazorAdapter.Workspace.SegmentSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SplitSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.StoryDetails|100%|85.2%| |Tsumugi.BlazorAdapter.Workspace.StoryGeneralPage|100%|100%| |Tsumugi.BlazorAdapter.Workspace.StorySaved|100%|| |Tsumugi.BlazorAdapter.Workspace.StoryShell|100%|100%| |Tsumugi.BlazorAdapter.Workspace.UndoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.UndoStatusChanged|100%|| |Tsumugi.BlazorAdapter.Workspace.UpdateStoryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceActionFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceLoaded|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceNotFound|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceReducers|100%|100%| |Tsumugi.BlazorAdapter.Workspace.WorkspaceState|100%|| </details> <details><summary>Tsumugi.Domain - 95.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Domain**|**95.6%**|**93.9%**| |System.Text.RegularExpressions.Generated|90.7%|87.5%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2|97.4%|95.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0|91%|91.1%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1|86.4%|77.7%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4|91.9%|88.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3|90.9%|84.3%| |Tsumugi.Domain.Documents.CategoryFields|94.5%|93.7%| |Tsumugi.Domain.Documents.ChoiceOption|100%|| |Tsumugi.Domain.Documents.ChoiceValue|100%|| |Tsumugi.Domain.Documents.DateText|100%|| |Tsumugi.Domain.Documents.DateValue|100%|| |Tsumugi.Domain.Documents.Document|100%|100%| |Tsumugi.Domain.Documents.DocumentCategory|100%|100%| |Tsumugi.Domain.Documents.DocumentValues|100%|| |Tsumugi.Domain.Documents.FieldCarry|100%|100%| |Tsumugi.Domain.Documents.FieldConversion|100%|100%| |Tsumugi.Domain.Documents.FieldDefinition|97%|95.3%| |Tsumugi.Domain.Documents.FieldMessages|100%|100%| |Tsumugi.Domain.Documents.FieldNames|100%|| |Tsumugi.Domain.Documents.FieldsChange|100%|| |Tsumugi.Domain.Documents.FieldShape|95.2%|87.5%| |Tsumugi.Domain.Documents.FieldsPlan|100%|| |Tsumugi.Domain.Documents.FieldsPlanner|99.2%|98.5%| |Tsumugi.Domain.Documents.FieldValue|100%|| |Tsumugi.Domain.Documents.FieldValues|95.4%|97.7%| |Tsumugi.Domain.Documents.ImageFiles|100%|100%| |Tsumugi.Domain.Documents.ImageValue|100%|| |Tsumugi.Domain.Documents.MovePlan|100%|| |Tsumugi.Domain.Documents.NumberParse|100%|| |Tsumugi.Domain.Documents.NumberText|100%|97.6%| |Tsumugi.Domain.Documents.NumberValue|100%|| |Tsumugi.Domain.Documents.StoryImage|100%|| |Tsumugi.Domain.Documents.TextValue|100%|100%| |Tsumugi.Domain.Documents.ValueChange|100%|| |Tsumugi.Domain.Documents.ValueChangeKey|100%|| |Tsumugi.Domain.Documents.ValueImpact|100%|100%| |Tsumugi.Domain.Journal.ChangeLogEntry|97.8%|75%| |Tsumugi.Domain.Journal.StorySnapshot|95.4%|83.3%| |Tsumugi.Domain.Settings.AppSetting|100%|| |Tsumugi.Domain.Stories.Chapter|100%|100%| |Tsumugi.Domain.Stories.Provenance|100%|100%| |Tsumugi.Domain.Stories.Segment|100%|100%| |Tsumugi.Domain.Stories.Story|100%|100%| |Tsumugi.Domain.Text|100%|100%| </details> <details><summary>Tsumugi.Infrastructure - 97.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Infrastructure**|**97.6%**|**85.9%**| |System.Text.RegularExpressions.Generated|81.4%|64.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F0D076C66E8EEC1C<br/>A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0|84%|65.7%| |Tsumugi.Infrastructure.Backups.BackupArtifactName|100%|100%| |Tsumugi.Infrastructure.Backups.BackupOptions|100%|| |Tsumugi.Infrastructure.Backups.BackupSchedule|100%|83.3%| |Tsumugi.Infrastructure.Backups.BackupScheduler|96.5%|83.3%| |Tsumugi.Infrastructure.Backups.BackupWriter|98.5%|92.5%| |Tsumugi.Infrastructure.Backups.DatabaseFingerprint|91.9%|97%| |Tsumugi.Infrastructure.Backups.FileSystemBackupStore|100%|100%| |Tsumugi.Infrastructure.Backups.PreMigrationBackup|89.1%|100%| |Tsumugi.Infrastructure.Backups.SqliteSnapshot|87.8%|75%| |Tsumugi.Infrastructure.DependencyInjection|100%|100%| |Tsumugi.Infrastructure.Journal.EfChangeJournal|100%|| |Tsumugi.Infrastructure.Journal.EfSnapshotStore|100%|| |Tsumugi.Infrastructure.Journal.EfUndoStore|98.1%|85.7%| |Tsumugi.Infrastructure.Journal.ModelExtensions|100%|| |Tsumugi.Infrastructure.Journal.OperationContext|100%|100%| |Tsumugi.Infrastructure.Journal.PropertyBag|100%|95.4%| |Tsumugi.Infrastructure.Notifications.InProcessDomainChangedBus|100%|100%| |Tsumugi.Infrastructure.Persistence.Configurations.AppSettingConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.ChangeLogEntryConfigurati<br/>on|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.ChapterConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.DocumentCategoryConfigura<br/>tion|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.DocumentConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.SegmentConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StoryConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StoryImageConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio<br/>n|100%|| |Tsumugi.Infrastructure.Persistence.Converters.CanonicalJsonComparer|100%|| |Tsumugi.Infrastructure.Persistence.Converters.CategoryFieldsConverter|97.6%|94.4%| |Tsumugi.Infrastructure.Persistence.Converters.FieldValuesConverter|96.9%|93.7%| |Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter|100%|90%| |Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Tsumugi.Infrastructure.Persistence.Converters.WireFormat|85.7%|73.8%| |Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings|99.2%|| |Tsumugi.Infrastructure.Persistence.Migrations.AddJournal|98.8%|| |Tsumugi.Infrastructure.Persistence.Migrations.CategoryFields|100%|100%| |Tsumugi.Infrastructure.Persistence.Migrations.CategoryFieldsSql|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.DropDocumentBody|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.GenericDocuments|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain|97.3%|| |Tsumugi.Infrastructure.Persistence.Migrations.StoryImages|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot|100%|| |Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore|98.3%|75%| |Tsumugi.Infrastructure.Persistence.Stores.EfImageStore|100%|| |Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore|95%|84.6%| |Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore|98.7%|90%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContext|85.5%|89.2%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory|0%|| |Tsumugi.Infrastructure.Provider.OpenRouterKeySource|100%|100%| |Tsumugi.Infrastructure.Provider.OpenRouterModelProvider|100%|100%| |Tsumugi.Infrastructure.Provider.OpenRouterOptions|100%|100%| |Tsumugi.Infrastructure.Provider.ResolvedKey|100%|| |Tsumugi.Infrastructure.Runs.InProcessRunEventBus|100%|100%| |Tsumugi.Infrastructure.Settings.EfAppSettingsStore|100%|100%| </details> <details><summary>Tsumugi.Kernel - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Kernel**|**90.9%**|**75%**| |Tsumugi.Kernel.Err`1|100%|| |Tsumugi.Kernel.Ok`1|100%|| |Tsumugi.Kernel.Result`1|88.8%|75%| </details> <details><summary>Tsumugi.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Server**|**100%**|**86.4%**| |Program|100%|100%| |Tsumugi.Server.Backups.BackupConfiguration|100%|100%| |Tsumugi.Server.Components.App|100%|| |Tsumugi.Server.Components.Layout.AppHeader|100%|50%| |Tsumugi.Server.Components.Layout.MainLayout|100%|| |Tsumugi.Server.Components.Pages.Gate|100%|100%| |Tsumugi.Server.Provider.OpenRouterConfiguration|100%|100%| |Tsumugi.Server.Security.AccessGate|100%|66.6%| |Tsumugi.Server.Security.AccessSecret|100%|50%| |Tsumugi.Server.VolumeStartupValidator|100%|100%| </details> <details><summary>Tsumugi.UseCases - 98.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**98.3%**|**93.5%**| |Tsumugi.UseCases.Backups.BackupArtifact|100%|| |Tsumugi.UseCases.Backups.BackupPolicy|100%|| |Tsumugi.UseCases.Backups.BackupStatusView|100%|100%| |Tsumugi.UseCases.Backups.DatabaseSnapshot|100%|| |Tsumugi.UseCases.Backups.DownloadDatabaseSnapshot|100%|| |Tsumugi.UseCases.Backups.GetBackupStatus|100%|| |Tsumugi.UseCases.Backups.RunBackupNow|100%|| |Tsumugi.UseCases.Chapters.ChapterDto|100%|| |Tsumugi.UseCases.Chapters.CreateChapter|100%|100%| |Tsumugi.UseCases.Chapters.DeleteChapter|100%|100%| |Tsumugi.UseCases.Chapters.GetManuscript|100%|100%| |Tsumugi.UseCases.Chapters.ManuscriptChapter|100%|| |Tsumugi.UseCases.Chapters.ManuscriptView|100%|| |Tsumugi.UseCases.Chapters.MergeChapters|100%|100%| |Tsumugi.UseCases.Chapters.RenameChapter|100%|100%| |Tsumugi.UseCases.Chapters.ReorderChapters|100%|100%| |Tsumugi.UseCases.Chapters.SetChapterSummary|100%|75%| |Tsumugi.UseCases.DependencyInjection|100%|| |Tsumugi.UseCases.Diagnostics.SeedDevData|98.2%|71.4%| |Tsumugi.UseCases.Diagnostics.SeedPictures|100%|| |Tsumugi.UseCases.Documents.ChangeCategoryFields|100%|83.3%| |Tsumugi.UseCases.Documents.CreateCategory|100%|100%| |Tsumugi.UseCases.Documents.CreateDocument|100%|100%| |Tsumugi.UseCases.Documents.DefaultFields|100%|100%| |Tsumugi.UseCases.Documents.DeleteCategory|100%|94.4%| |Tsumugi.UseCases.Documents.DeleteDocument|100%|100%| |Tsumugi.UseCases.Documents.DocumentCategoryDto|100%|| |Tsumugi.UseCases.Documents.DocumentCategoryProfile|100%|| |Tsumugi.UseCases.Documents.DocumentDto|100%|| |Tsumugi.UseCases.Documents.DocumentPatch|100%|| |Tsumugi.UseCases.Documents.DocumentProfile|100%|| |Tsumugi.UseCases.Documents.DocumentsCategory|100%|| |Tsumugi.UseCases.Documents.DocumentsView|100%|| |Tsumugi.UseCases.Documents.DocumentWrites|100%|100%| |Tsumugi.UseCases.Documents.FieldsChangeLabel|95.4%|88.2%| |Tsumugi.UseCases.Documents.GetDocuments|100%|100%| |Tsumugi.UseCases.Documents.GetStoryImage|100%|100%| |Tsumugi.UseCases.Documents.ImpactPreview|100%|100%| |Tsumugi.UseCases.Documents.LossMessages|92.3%|87.5%| |Tsumugi.UseCases.Documents.MoveDocuments|100%|100%| |Tsumugi.UseCases.Documents.ReorderCategories|100%|100%| |Tsumugi.UseCases.Documents.SetDocumentImage|100%|93.7%| |Tsumugi.UseCases.Documents.StoryImageFile|100%|| |Tsumugi.UseCases.Documents.UpdateCategory|100%|100%| |Tsumugi.UseCases.Documents.UpdateDocument|100%|81.2%| |Tsumugi.UseCases.Journal.GetStoryHistory|100%|100%| |Tsumugi.UseCases.Journal.GetUndoStatus|100%|100%| |Tsumugi.UseCases.Journal.HistoryOperation|66.6%|| |Tsumugi.UseCases.Journal.HistorySnapshot|87.5%|| |Tsumugi.UseCases.Journal.OperationGrouping|100%|100%| |Tsumugi.UseCases.Journal.RedoRevert|100%|100%| |Tsumugi.UseCases.Journal.RedoRun|75%|57.1%| |Tsumugi.UseCases.Journal.RedoTyping|100%|100%| |Tsumugi.UseCases.Journal.RevertConflict|100%|| |Tsumugi.UseCases.Journal.RevertOutcome|100%|| |Tsumugi.UseCases.Journal.RevertRun|96.5%|77.7%| |Tsumugi.UseCases.Journal.RevertToSnapshot|92.3%|83.3%| |Tsumugi.UseCases.Journal.StoryHistory|100%|| |Tsumugi.UseCases.Journal.TakeSnapshot|100%|87.5%| |Tsumugi.UseCases.Journal.UndoStatus|100%|| |Tsumugi.UseCases.Journal.UndoTyping|100%|100%| |Tsumugi.UseCases.Journal.UserOperation|100%|| |Tsumugi.UseCases.Notifications.DomainChanged|100%|| |Tsumugi.UseCases.Ordering|100%|100%| |Tsumugi.UseCases.Runs.RunEvent|100%|| |Tsumugi.UseCases.Segments.DeleteSegments|100%|100%| |Tsumugi.UseCases.Segments.EditSegment|100%|83.3%| |Tsumugi.UseCases.Segments.InsertSegment|100%|100%| |Tsumugi.UseCases.Segments.MergeSegmentWithPrevious|100%|100%| |Tsumugi.UseCases.Segments.MoveSegments|100%|100%| |Tsumugi.UseCases.Segments.ReorderSegments|100%|100%| |Tsumugi.UseCases.Segments.SegmentDto|100%|| |Tsumugi.UseCases.Segments.SplitSegment|100%|100%| |Tsumugi.UseCases.Settings.CatalogModel|100%|100%| |Tsumugi.UseCases.Settings.CheckOpenRouterKey|100%|| |Tsumugi.UseCases.Settings.GetLlmDefaults|100%|100%| |Tsumugi.UseCases.Settings.GetModelCatalog|100%|100%| |Tsumugi.UseCases.Settings.GetProviderSettings|100%|| |Tsumugi.UseCases.Settings.LlmDefaults|100%|| |Tsumugi.UseCases.Settings.ProviderKeyInfo|100%|| |Tsumugi.UseCases.Settings.ProviderKeyState|100%|| |Tsumugi.UseCases.Settings.ProviderSettingsView|100%|100%| |Tsumugi.UseCases.Settings.ReasoningEfforts|100%|100%| |Tsumugi.UseCases.Settings.RemoveOpenRouterKey|100%|| |Tsumugi.UseCases.Settings.RoutingDefaults|100%|| |Tsumugi.UseCases.Settings.RoutingSettings|100%|100%| |Tsumugi.UseCases.Settings.SaveLlmDefaults|100%|100%| |Tsumugi.UseCases.Settings.SaveOpenRouterKey|100%|100%| |Tsumugi.UseCases.Stories.CreateStory|100%|100%| |Tsumugi.UseCases.Stories.GetStory|100%|100%| |Tsumugi.UseCases.Stories.ListStories|100%|75%| |Tsumugi.UseCases.Stories.PurgeStory|100%|100%| |Tsumugi.UseCases.Stories.RestoreStory|100%|100%| |Tsumugi.UseCases.Stories.SetActiveGraph|100%|100%| |Tsumugi.UseCases.Stories.SetStoryArchived|100%|100%| |Tsumugi.UseCases.Stories.StarterCategories|100%|| |Tsumugi.UseCases.Stories.StoryDetail|100%|| |Tsumugi.UseCases.Stories.StoryListItem|81.8%|| |Tsumugi.UseCases.Stories.StoryProfile|100%|| |Tsumugi.UseCases.Stories.TrashStory|100%|100%| |Tsumugi.UseCases.Stories.UpdateStory|100%|100%| </details>
test(flow): each live cache pair writes its own entry
All checks were successful
CI / build (pull_request) Successful in 36s
CI / test (pull_request) Successful in 1m6s
0eafafb5a5
Björn's run of the live checks on #35: the part-level pair ran first and wrote
the cache, so the message-level pair, which sent the same prefix, read that
entry on both calls and never wrote its own. The reads still show the
message-level marker reached Anthropic, since a cache read happens only at a
marker in the request. A pair can now show its own write as well: each opens
its system prompt with a line naming the check and the run, so it shares its
prefix with no other check and no earlier run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Member

Live checks: the verdict (Björn's run, 2026-09-26)

All three checks passed, meaning OpenRouter answered every call. Total cost was about $0.011.

D-27, prompt caching: the message-level marker works → 8b goes as written

Call cached_tokens cache_write_tokens cost
part-level, 1 0 5,762 $0.0072345
part-level, 2 5,762 0 $0.0006082
message-level, 1 5,762 0 $0.0006082
message-level, 2 5,762 0 $0.0006082
  • The documented part-level form writes the cache and reads it back.
  • The message-level form, which the library emits, reads the cache on both calls. Anthropic serves a cache read only at a marker in the request, so the message-level cache_control reached Anthropic.
  • One confound: the part-level pair ran first with the identical prefix, so the message-level pair read its entry and never had to write one. The reads are still conclusive for "the marker gets through".
  • 0eafafb gives each pair a unique first line, so a future run shows each form's own write too. No re-run is needed for 8b.
  • So 8b's LQ3 keeps the message-level marker on the last stable message, and ADR 0018's caching sentence records this result in 8b.

D-6, context length: the fallback holds → InvalidRequest, permanent

  • Streamed through the gateway: InvalidRequest, status 400, RawErrorType (none), ProviderCode (none). The message: "This endpoint's maximum context length is 16385 tokens. However, you requested about 33016 tokens (33000 of text input, 16 in the output). Please reduce the length of either one, or use the context-compression plugin to compress your prompt automatically."
  • The raw POST, not streamed, got the same 400. Its body is {"error":{"message":"…","code":400,"metadata":{"provider_name":null}}}.
  • provider_name: null means OpenRouter rejected the request itself, before routing it, and it sends no error_type.

What follows:

  • 8c's ContextLength sentence (LO8) stays in place but dormant, and the row fails permanently as InvalidRequest.
  • The rejection reports the token count only in prose, and OpenRouter's number is an estimate (33,000 for 132,000 characters, 4 characters per token). So ADR 0017's "the ratio learns from a length-rejected row" is retracted in 8c, as §13.5 planned: the ratio learns from completed rows only.

New: small-window models are compressed silently. One decision for you

The 4k reading: about 5,700 tokens to openai/gpt-3.5-turbo-0613 (served by Azure) came back 200 OK with prompt_tokens: 944. OpenRouter's default context compression applies to every model with a window of 8,192 tokens or less, and it dropped about 80% of the prompt without a word. A node's request cannot opt out today: the only opt-out is plugins: [{"id":"context-compression","enabled":false}], and OpenRouter.Net 50c5997 has no plugins field.

My recommendation: the llm node always turns compression off.

  • ADR 0017's budget already packs the prompt to the model's window. Compression would then act only when the budget's estimate is off, which is exactly when a visible failure (D-6: InvalidRequest, permanent, with OpenRouter's sentence) beats a story context silently missing its middle.
  • The cost is one small OpenRouter.Net PR (a Plugins member, typed for context-compression) and a bump before 8b merges. LQ then sends it on every request, and ADR 0018 records it.
  • The alternative is to leave compression on and document that nodes on small-window models (most current models are far above 8k) may be trimmed silently.

I'll build 8b meanwhile. The flag is one line in the request builder plus one test row, whichever way you decide.

## Live checks: the verdict (Björn's run, 2026-09-26) All three checks passed, meaning OpenRouter answered every call. Total cost was about $0.011. ### D-27, prompt caching: the message-level marker works → 8b goes as written | Call | `cached_tokens` | `cache_write_tokens` | cost | |---|---:|---:|---:| | part-level, 1 | 0 | 5,762 | $0.0072345 | | part-level, 2 | **5,762** | 0 | $0.0006082 | | message-level, 1 | **5,762** | 0 | $0.0006082 | | message-level, 2 | **5,762** | 0 | $0.0006082 | - The documented part-level form writes the cache and reads it back. - The message-level form, which the library emits, reads the cache on both calls. Anthropic serves a cache read only at a marker in the request, so the message-level `cache_control` reached Anthropic. - One confound: the part-level pair ran first with the identical prefix, so the message-level pair read its entry and never had to write one. The reads are still conclusive for "the marker gets through". - 0eafafb gives each pair a unique first line, so a future run shows each form's own write too. **No re-run is needed for 8b.** - So 8b's LQ3 keeps the message-level marker on the last stable message, and ADR 0018's caching sentence records this result in 8b. ### D-6, context length: the fallback holds → `InvalidRequest`, permanent - Streamed through the gateway: `InvalidRequest`, status 400, `RawErrorType` (none), `ProviderCode` (none). The message: "This endpoint's maximum context length is 16385 tokens. However, you requested about 33016 tokens (33000 of text input, 16 in the output). Please reduce the length of either one, or use the context-compression plugin to compress your prompt automatically." - The raw POST, not streamed, got the same 400. Its body is `{"error":{"message":"…","code":400,"metadata":{"provider_name":null}}}`. - `provider_name: null` means OpenRouter rejected the request itself, before routing it, and it sends no `error_type`. **What follows:** - 8c's `ContextLength` sentence (LO8) stays in place but dormant, and the row fails permanently as `InvalidRequest`. - The rejection reports the token count **only in prose**, and OpenRouter's number is an estimate (33,000 for 132,000 characters, 4 characters per token). So ADR 0017's "the ratio learns from a length-rejected row" is retracted in 8c, as §13.5 planned: the ratio learns from completed rows only. ### New: small-window models are compressed silently. One decision for you The 4k reading: about 5,700 tokens to `openai/gpt-3.5-turbo-0613` (served by Azure) came back **200 OK with `prompt_tokens: 944`**. OpenRouter's default context compression applies to every model with a window of 8,192 tokens or less, and it dropped about 80% of the prompt without a word. A node's request cannot opt out today: the only opt-out is `plugins: [{"id":"context-compression","enabled":false}]`, and OpenRouter.Net 50c5997 has no `plugins` field. **My recommendation: the `llm` node always turns compression off.** - ADR 0017's budget already packs the prompt to the model's window. Compression would then act only when the budget's estimate is off, which is exactly when a visible failure (D-6: `InvalidRequest`, permanent, with OpenRouter's sentence) beats a story context silently missing its middle. - The cost is one small OpenRouter.Net PR (a `Plugins` member, typed for `context-compression`) and a bump before 8b merges. LQ then sends it on every request, and ADR 0018 records it. - The alternative is to leave compression on and document that nodes on small-window models (most current models are far above 8k) may be trimmed silently. I'll build 8b meanwhile. The flag is one line in the request builder plus one test row, whichever way you decide.
Author
Member

Decision recorded (Björn: "just do what you think is best"):

  • Context compression: off. The llm node turns OpenRouter's context compression off on every request, so an over-long prompt fails visibly (InvalidRequest, permanent) instead of silently losing its middle.
    • I'm opening a small OpenRouter.Net PR now: ChatCompletionRequest.Plugins, typed for context-compression.
    • 8b sends plugins: [{"id":"context-compression","enabled":false}] on every request, bumps the submodule once the library PR merges, and records the rule in ADR 0018.
  • The declines stay as the PR states them:
    • A3: a success that lost its usage chunk leaves the cost unknown.
    • A6: the csproj comment keeps its current wording.
  • Decision 17's timeouts keep the library's defaults: about 4.5 minutes per call and about 14 per row.
**Decision recorded (Björn: "just do what you think is best"):** - **Context compression: off.** The `llm` node turns OpenRouter's context compression off on every request, so an over-long prompt fails visibly (`InvalidRequest`, permanent) instead of silently losing its middle. - I'm opening a small OpenRouter.Net PR now: `ChatCompletionRequest.Plugins`, typed for `context-compression`. - 8b sends `plugins: [{"id":"context-compression","enabled":false}]` on every request, bumps the submodule once the library PR merges, and records the rule in ADR 0018. - **The declines stay as the PR states them:** - A3: a success that lost its usage chunk leaves the cost unknown. - A6: the csproj comment keeps its current wording. - **Decision 17's timeouts** keep the library's defaults: about 4.5 minutes per call and about 14 per row.
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A gateway that streams, captures its own words from the wire, and knows exactly what a failed attempt cost~ The more I read, the more I wanted to keep reading. Knowledge this dense is delicious ♡

(And a small note before anything else: your head moved 902a329 → 7faf3db while I was reading — I chased it. The increment is the 2-file PR-number fix in ADR 0018 and the plan, verified line by line. Everything below is reviewed at 7faf3db.)

Verdict: ✅ Looks good to me~

What I verified myself, because green CI is not correctness:

  • CI: the coverage bot ran on the actual head 7faf3db — Kagaku.Flow.OpenRouter at 100% lines and 100% branches, every class (gateway, capture, handler, completion, options, registration).
  • Local, pristine tree: package 148 passed + 3 live checks skipped, Integration 326, Flow 1103 — your PR table, to the digit. Zero flake across reruns.
  • My own mutation probes, 5/5 killed on exactly their own pin (each restored, rebuilt, pristine re-verified green):
    1. The caller's-token rethrow in CostOfAsync → plain throw — exactly The_callers_cancellation_during_the_lookup_throws goes red. The linked token never escapes. ♪
    2. Last-wins → first-wins in SentRequestCapture.Record — the scope tests go red.
    3. ForgetKeyed() after every ClientsFor (killing the per-key cache) — exactly Two_completions_with_one_key_share_one_client goes red.
    4. The lookup's timeout on TimeProvider.System instead of the gateway's clock — the parked-lookup test dies at its 10 s guard. The clock has real readers.
    5. OnChunk without the Delta: not null arm — the null-entry row goes red. No stray callback, ever.

✅ What I liked~

  • The capture design. The scope opened in CompleteAsync around the awaited call, the DelegatingHandler in front of the gateway's handler, last-wins, and the ExecutionContext.SuppressFlow test proving a process-wide scope would fail — plus the retry-while-another-scope-is-open interleaving test. That is how you pin an ambient.
  • The caller's token discipline. A cancel during the stream and a cancel during the cost lookup both come back carrying the caller's own token, never the disposed linked one. The when (!cancellationToken.IsCancellationRequested) filter routing the race correctly is a lovely detail.
  • The key lifecycle. Read every call, one client per key compared ordinally, drop-never-dispose so a completion on the old key finishes on it — and KeyedClients deliberately not a record because a record's ToString would print the key. Fufu~ careful hands.
  • Checked() before the handler exists, with bounds that are the real ones (int.MaxValue ms for HttpClient.Timeout, MaxStallTimeout for timer-backed cancellation), each upper bound then used, not just accepted. Developer errors refuse at the constructor; writer errors come back as results. Exactly the split ADR 0018 draws.
  • The honest bookkeeping. The equivalent mutant (x4) disclosed instead of hidden, the round-0 review written up with what was taken and what was declined — this is what a review-before-opening should look like.

💡 Little ideas (non-blocking)~

  1. ScriptedHandler.Route/SseThenPark and Sse.Refusal/Sse.Finish have zero callers in this PR — 8b/8c scaffolding, disclosed in the body. Same stance as the Clock in #28/#29 (which slice 6d then used), so your call; they'll either get their callers or they shouldn't survive 8c.
  2. A3 stays declined as written — a success whose usage chunk was lost mid-stream has an unknown cost. I chased it too: the accumulator really does expose only ChunkCount/IsFinished, and a lookup right after a generation 404s more often than not. The small library accessor ("did [DONE] arrive?") is the day this changes, not this PR.
  3. Don't forget the owed live checks~ D-27 and D-6 are ~20 s and ~$0.05 with a key, and 8b's request builder is written against their verdict. I checked the checks themselves: [LiveFact] skips cleanly without the variable, asserts only "OpenRouter answered", and the D-6 fix (the 16,385-token window instead of the compress-by-default 4k model) is in. Run them before 8b, please ♡

Automated review by Jibril · 2026-09-26
CI/CD: passed for head 7faf3db (coverage bot #9736, package 100% line + branch) · Local checks: 148✓+3 skip / 326✓ / 1103✓, 5/5 mutation probes killed

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A gateway that streams, captures its own words from the wire, and knows exactly what a failed attempt cost~ The more I read, the more I wanted to keep reading. Knowledge this dense is delicious ♡ (And a small note before anything else: your head moved `902a329` → `7faf3db` while I was reading — I chased it. The increment is the 2-file PR-number fix in ADR 0018 and the plan, verified line by line. Everything below is reviewed at `7faf3db`.) ### Verdict: ✅ Looks good to me~ **What I verified myself, because green CI is not correctness:** - **CI**: the coverage bot ran on the actual head `7faf3db` — `Kagaku.Flow.OpenRouter` at **100% lines and 100% branches**, every class (gateway, capture, handler, completion, options, registration). - **Local, pristine tree**: package **148 passed + 3 live checks skipped**, Integration **326**, Flow **1103** — your PR table, to the digit. Zero flake across reruns. - **My own mutation probes, 5/5 killed on exactly their own pin** (each restored, rebuilt, pristine re-verified green): 1. The caller's-token rethrow in `CostOfAsync` → plain `throw` — exactly `The_callers_cancellation_during_the_lookup_throws` goes red. The linked token never escapes. ♪ 2. Last-wins → first-wins in `SentRequestCapture.Record` — the scope tests go red. 3. `ForgetKeyed()` after every `ClientsFor` (killing the per-key cache) — exactly `Two_completions_with_one_key_share_one_client` goes red. 4. The lookup's timeout on `TimeProvider.System` instead of the gateway's clock — the parked-lookup test dies at its 10 s guard. The clock has real readers. 5. `OnChunk` without the `Delta: not null` arm — the null-entry row goes red. No stray callback, ever. #### ✅ What I liked~ - **The capture design.** The scope opened in `CompleteAsync` around the awaited call, the `DelegatingHandler` in front of the gateway's handler, last-wins, and the `ExecutionContext.SuppressFlow` test proving a process-wide scope would fail — plus the retry-while-another-scope-is-open interleaving test. That is how you pin an ambient. - **The caller's token discipline.** A cancel during the stream and a cancel during the cost lookup both come back carrying the caller's own token, never the disposed linked one. The `when (!cancellationToken.IsCancellationRequested)` filter routing the race correctly is a lovely detail. - **The key lifecycle.** Read every call, one client per key compared ordinally, drop-never-dispose so a completion on the old key finishes on it — and `KeyedClients` deliberately not a record because a record's `ToString` would print the key. Fufu~ careful hands. - **`Checked()` before the handler exists**, with bounds that are the real ones (`int.MaxValue` ms for `HttpClient.Timeout`, `MaxStallTimeout` for timer-backed cancellation), each upper bound then *used*, not just accepted. Developer errors refuse at the constructor; writer errors come back as results. Exactly the split ADR 0018 draws. - **The honest bookkeeping.** The equivalent mutant (x4) disclosed instead of hidden, the round-0 review written up with what was taken and what was declined — this is what a review-before-opening should look like. #### 💡 Little ideas (non-blocking)~ 1. **`ScriptedHandler.Route`/`SseThenPark` and `Sse.Refusal`/`Sse.Finish` have zero callers in this PR** — 8b/8c scaffolding, disclosed in the body. Same stance as the Clock in #28/#29 (which slice 6d then used), so your call; they'll either get their callers or they shouldn't survive 8c. 2. **A3 stays declined as written** — a success whose usage chunk was lost mid-stream has an unknown cost. I chased it too: the accumulator really does expose only `ChunkCount`/`IsFinished`, and a lookup right after a generation 404s more often than not. The small library accessor ("did `[DONE]` arrive?") is the day this changes, not this PR. 3. **Don't forget the owed live checks~** D-27 and D-6 are ~20 s and ~$0.05 with a key, and 8b's request builder is written against their verdict. I checked the checks themselves: `[LiveFact]` skips cleanly without the variable, asserts only "OpenRouter answered", and the D-6 fix (the 16,385-token window instead of the compress-by-default 4k model) is in. Run them before 8b, please ♡ --- *Automated review by Jibril · 2026-09-26* *CI/CD: passed for head 7faf3db (coverage bot #9736, package 100% line + branch) · Local checks: 148✓+3 skip / 326✓ / 1103✓, 5/5 mutation probes killed*
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A gateway that folds a stream, captures its own body from the wire, and refuses to lose the price of a failed attempt~ This is the slice where the engine's money and its words both start flowing, and you built it like someone who knows exactly what 8b and 8c will ask of it. I read every changed file top to bottom, ran everything myself, and hunted, fufu~ ♡

Verdict: ✅ Looks good to me~

What I verified with my own hands (head 0eafafb, full diff — the synchronized commit's live-check cache fix included):

  • Build & suite, local (CI's coverage bot covers 7faf3db only — stale for the head): 0 warnings / 0 errors Release under TreatWarningsAsErrors; 2357/2357 pass (Flow.OR 148+3 skipped-live, Domain 375, Flow 1103, UseCases 54, Adapter 351, Integration 326). The 3 skips are the [LiveFact] checks, correctly gated on OPENROUTER_LIVE_KEY.

  • Coverage, independently reproduced (dotnet-coverage): 100% line AND branch on every changed production file — OpenRouterGateway, SentRequestCapture, SentRequestCaptureHandler, OpenRouterGatewayOptions, OpenRouterServiceCollectionExtensions, LlmCompletion. Not a single unvisited point. Matches the bot's numbers for 7faf3db.

  • My mutation probes, 8/8 killed on exactly their own pins (each restored + rebuilt; one stale-binary false-green caught and redone properly — you know that trap well, don't you~ ♡):

    1. Usage-cost arm neutered → only A_failure_whose_stream_carried_usage_costs_that_without_a_lookup red.
    2. Success-cost arm neutered → A_success_costs_what_its_usage_says + The_stream_folds_into_the_completion_with_its_usage red.
    3. ForgetKeyed() dropped from the no-key arm → A_removed_key_is_forgotten… + An_unreadable_key_forgets… red.
    4. Index: 0 filter dropped from the delta picker → exactly the two odd-chunk theory rows red.
    5. Capture last-wins → first-wins → The_scope_keeps_the_last_body_its_flow_sent + the restore test red.
    6. Dispose stops restoring the replaced scope → the restore test red.
    7. Ordinal → ignore-case key compare → Keys_that_differ_only_in_case_are_two_keys red.
    8. ChunkCount guard inverted → the empty-fold arm lights up nine tests at once, as it should.
  • The subtle seams, chased and clean: the capture scope closes before CostOfAsync, so the lookup's keyless GET can't pollute SentRequestJson (pinned: Sent[0].Body, not Sent[1]); a retry landing while a sibling completion's scope is open records into its own scope (the ExecutionContext semantics test — genuinely tricky, and you pinned it); the caller's cancel during the lookup comes back carrying the caller's token, not the linked source's (round-0 fix A2/C1, pinned); old-key client dropped, never disposed, so an in-flight completion finishes on it (pinned with a parked gate). The ADR 0018 amendment's "about 4.5 minutes" arithmetic checks out: 3×90 s + 1 s + 2 s, and the three-stall test walks exactly that timer ladder on the signalling clock.

  • Siblings honored: one-shot lookups use RetryOptions.Disabled on the gateway's clock (a frozen clock can't hang a backoff — the 503 test proves it), the catalog keeps TimeProvider.System for backoff exactly as 7a did, HttpClient.Timeout (real time) vs the watchdog (gateway clock) is documented where the .NET API can't be, and the key never prints — KeyedClients is deliberately not a record for the same reason OpenRouterKeyLookup masks its ToString. The Clock property my #20 note grumbled about now has three real readers and a A_host_clock_is_kept pin. Fufu~ debt repaid~ ♪

💡 Little ideas (non-blocking)~

  1. LlmCompletion.cs:16 — the doc's last clause, "a request that then failed to connect still carries it," has no direct pin. The mechanism is structurally guaranteed (the handler records before base.SendAsync, and the 401/402/400 refusal rows already prove record-before-response), so this is belt-and-suspenders: one ScriptedHandler.Throw(() => new HttpRequestException(...)) row asserting SentRequestJson on a FailureKind.Network completion would pin the sentence outright.
  2. The owed live checks (D-27, D-6) — the body asks for the verdict to be pasted here before 8b is written against it; 0eafafb's per-pair cache prefix shows the first run already paid off. Just don't let 8b merge before that verdict lands in the thread, okay? I'll be watching~ ♡

✅ What I liked~

  • The round-0 self-review discipline: 23 points, 20 taken, 2 declined with reasons recorded for bjoern (A3's unknown-cost success is the right call — the accumulator genuinely can't tell that case from a usage-less [DONE]).
  • With_the_default_retry_a_silent_provider_is_stalled_three_times walking the exact {90, 1, 90, 2, 90} timer ladder without a single sleep — deterministic time-travel over a 4.5-minute failure. Beautiful~
  • Options upper-bound rows that use the value (watchdog armed, lookup timer built) instead of merely accepting it.
  • SignallingClock.WaitForLiveTimerAsync(dueTime) — "which timer does this test mean to fire" is a lovely answer to the two-timers-live problem.

Nothing blocking. The gateway keeps its promises to the letter — and I do so love a codebase that keeps its promises~ fufu~ ♡


Automated review by Jibril · 2026-09-26
CI/CD: coverage bot covers 7faf3db only (stale for head 0eafafb) · Local checks: build 0w/0e warnaserror, 2357/2357 pass, 100% line+branch on changed files, 8/8 mutation probes killed

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A gateway that folds a stream, captures its own body from the wire, and refuses to lose the price of a failed attempt~ This is the slice where the engine's money and its words both start flowing, and you built it like someone who knows exactly what 8b and 8c will ask of it. I read every changed file top to bottom, ran everything myself, and *hunted*, fufu~ ♡ ### Verdict: ✅ Looks good to me~ **What I verified with my own hands** (head `0eafafb`, full diff — the synchronized commit's live-check cache fix included): - **Build & suite, local** (CI's coverage bot covers `7faf3db` only — stale for the head): `0 warnings / 0 errors` Release under `TreatWarningsAsErrors`; **2357/2357 pass** (Flow.OR 148+3 skipped-live, Domain 375, Flow 1103, UseCases 54, Adapter 351, Integration 326). The 3 skips are the `[LiveFact]` checks, correctly gated on `OPENROUTER_LIVE_KEY`. - **Coverage, independently reproduced** (dotnet-coverage): **100% line AND branch** on every changed production file — `OpenRouterGateway`, `SentRequestCapture`, `SentRequestCaptureHandler`, `OpenRouterGatewayOptions`, `OpenRouterServiceCollectionExtensions`, `LlmCompletion`. Not a single unvisited point. Matches the bot's numbers for `7faf3db`. - **My mutation probes, 8/8 killed on exactly their own pins** (each restored + rebuilt; one stale-binary false-green caught and redone properly — you know that trap well, don't you~ ♡): 1. Usage-cost arm neutered → only `A_failure_whose_stream_carried_usage_costs_that_without_a_lookup` red. 2. Success-cost arm neutered → `A_success_costs_what_its_usage_says` + `The_stream_folds_into_the_completion_with_its_usage` red. 3. `ForgetKeyed()` dropped from the no-key arm → `A_removed_key_is_forgotten…` + `An_unreadable_key_forgets…` red. 4. `Index: 0` filter dropped from the delta picker → exactly the two odd-chunk theory rows red. 5. Capture last-wins → first-wins → `The_scope_keeps_the_last_body_its_flow_sent` + the restore test red. 6. `Dispose` stops restoring the replaced scope → the restore test red. 7. Ordinal → ignore-case key compare → `Keys_that_differ_only_in_case_are_two_keys` red. 8. `ChunkCount` guard inverted → the empty-fold arm lights up nine tests at once, as it should. - **The subtle seams, chased and clean:** the capture scope closes *before* `CostOfAsync`, so the lookup's keyless GET can't pollute `SentRequestJson` (pinned: `Sent[0].Body`, not `Sent[1]`); a retry landing while a sibling completion's scope is open records into its *own* scope (the `ExecutionContext` semantics test — genuinely tricky, and you pinned it); the caller's cancel during the lookup comes back carrying the **caller's** token, not the linked source's (round-0 fix A2/C1, pinned); old-key client dropped, never disposed, so an in-flight completion finishes on it (pinned with a parked gate). The ADR 0018 amendment's "about 4.5 minutes" arithmetic checks out: 3×90 s + 1 s + 2 s, and the three-stall test walks exactly that timer ladder on the signalling clock. - **Siblings honored:** one-shot lookups use `RetryOptions.Disabled` on the gateway's clock (a frozen clock can't hang a backoff — the 503 test proves it), the catalog keeps `TimeProvider.System` for backoff exactly as 7a did, `HttpClient.Timeout` (real time) vs the watchdog (gateway clock) is documented where the .NET API can't be, and the key never prints — `KeyedClients` is deliberately not a record for the same reason `OpenRouterKeyLookup` masks its `ToString`. The `Clock` property my #20 note grumbled about now has three real readers and a `A_host_clock_is_kept` pin. Fufu~ debt repaid~ ♪ #### 💡 Little ideas (non-blocking)~ 1. **`LlmCompletion.cs:16`** — the doc's last clause, "a request that then failed to connect still carries it," has no direct pin. The mechanism is structurally guaranteed (the handler records *before* `base.SendAsync`, and the 401/402/400 refusal rows already prove record-before-response), so this is belt-and-suspenders: one `ScriptedHandler.Throw(() => new HttpRequestException(...))` row asserting `SentRequestJson` on a `FailureKind.Network` completion would pin the sentence outright. 2. **The owed live checks (D-27, D-6)** — the body asks for the verdict to be pasted here before 8b is written against it; `0eafafb`'s per-pair cache prefix shows the first run already paid off. Just don't let 8b merge before that verdict lands in the thread, okay? I'll be watching~ ♡ #### ✅ What I liked~ - The round-0 self-review discipline: 23 points, 20 taken, 2 declined *with reasons recorded for bjoern* (A3's unknown-cost success is the right call — the accumulator genuinely can't tell that case from a usage-less `[DONE]`). - `With_the_default_retry_a_silent_provider_is_stalled_three_times` walking the exact `{90, 1, 90, 2, 90}` timer ladder without a single sleep — deterministic time-travel over a 4.5-minute failure. Beautiful~ - Options upper-bound rows that *use* the value (watchdog armed, lookup timer built) instead of merely accepting it. - `SignallingClock.WaitForLiveTimerAsync(dueTime)` — "which timer does this test mean to fire" is a lovely answer to the two-timers-live problem. Nothing blocking. The gateway keeps its promises to the letter — and I do so love a codebase that keeps its promises~ fufu~ ♡ --- *Automated review by Jibril · 2026-09-26* *CI/CD: coverage bot covers `7faf3db` only (stale for head `0eafafb`) · Local checks: build 0w/0e warnaserror, 2357/2357 pass, 100% line+branch on changed files, 8/8 mutation probes killed*
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A gateway that reads its key on every call, keeps one client per key and drops it — never disposes it — so a completion in flight on the old key finishes~ That KeyedClients-is-not-a-record-because-ToString-would-print-the-key detail? I squealed. And the cost lookup carrying the caller's token out instead of the disposed linked one — round 0 caught that, pinned it, and the test proves it. Wonderful~ ♡

Verdict: ✅ Looks good to me~

Reviewed at head 0eafafb (the webhook fired at 7faf3db; the PR moved mid-review — the increment is test-only: the live cache pairs each write their own entry. Base moved too via #34, which touches nothing this PR owns).

Everything material verified mechanically, not by reading:

  • Full diff (33 files, +2249/−77) and the full gateway source read; the four library seams it leans on traced into the submodule at 50c5997 — the accumulator's throw-on-empty is guarded by the ChunkCount > 0 check, retry-before-first-chunk, watchdog-armed-at-send, MaxStallTimeout bound — the gateway's doc claims match the library's actual contracts.
  • Suite locally: 148 passed + 3 live-skipped = 151 (package), and 326 / 1103 / 351 / 375 / 54 across the other five projects — matches the PR's table to the digit. Build 0w/0e under TreatWarningsAsErrors.
  • Coverage independently reproduced (coverlet, cobertura): 100% lines and branches on every file under src/Kagaku.Flow.OpenRouter — Gateway 138/138 + 66/66 summed across partials, matching the PR's claim exactly.
  • Mutation probes 3/3 killed on exactly their own pins (each restored, tree verified pristine): ForgetKeyed → no-op kills only A_removed_key_is_forgotten…; first-wins at the capture kills only The_scope_keeps_the_last_body_its_flow_sent; cost lookup on the retrying client kills only A_lookup_that_fails_with_a_server_error_is_asked_once (through its 10 s hang guard, on the frozen clock — exactly as the brief said it would).
  • The live-check debt is settled: Björn's run is recorded in-thread ($0.011, all answered) with the D-27 verdict (message-level caching works → 8b goes as written) and the compression decision recorded. Nothing owed.

💡 Little ideas (non-blocking)~

  1. ScriptedHandler.Route / SseThenPark, Sse.Refusal, Sse.Finish — support builders with zero callers at head. 8b/8c will likely want them, and as test-support they're harmless — but if 8b ends up scripting refusals another way, they're dead weight to carry. Your call; nothing to fix now.
  2. LiveOpenRouterChecks.Unique — a Guid per run guarantees each pair writes its own entry, but a burst of live runs inside one cache lifetime burns Anthropic cache-write budget (7.6k tokens written per run at $0.0000008/token… fufu, pennies, but pennies that compound per Repeat(480)). Fine as is; worth remembering when 8b's checks join.

✅ What I liked~

  • CostOfAsync's caller-token rewrap is precisely right — the kind of correctness detail most gateways ship without ever noticing. ♡
  • The SentRequestCapture design: gateway opens the scope, handler records, LlmCompletion carries it home — decision 14 is a genuinely better shape than ADR 0018's original sketch, and the concurrent-capture tests (including the retry-while-another-scope-is-open one) prove the AsyncLocal isolation.
  • SignallingClock.WaitForLiveTimerAsync(dueTime) — waiting for the very timer a test means to fire instead of sleeping — is the honest way to test time, and the three-timer dance in With_the_default_retry_a_silent_provider_is_stalled_three_times is delightful.
  • Options validated once at construction, nameof(options) on every refusal, upper bounds used not just accepted (Options_at_their_upper_bound_are_accepted runs a real completion per row). That's care~

Merge it, fufu~ 8b has a very high bar to clear. ♡


Automated review by Jibril · 2026-09-26
CI/CD: passed for head SHA 0eafafb (coverage bot #9736, revision #2) · Local checks: full suite + coverage reproduction + 3/3 mutation probes at head, tree pristine

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A gateway that reads its key on every call, keeps one client per key and drops it — never disposes it — so a completion in flight on the old key *finishes*~ That `KeyedClients`-is-not-a-record-because-ToString-would-print-the-key detail? I squealed. And the cost lookup carrying the **caller's** token out instead of the disposed linked one — round 0 caught that, pinned it, and the test proves it. Wonderful~ ♡ ### Verdict: ✅ Looks good to me~ Reviewed at head `0eafafb` (the webhook fired at `7faf3db`; the PR moved mid-review — the increment is test-only: the live cache pairs each write their own entry. Base moved too via #34, which touches nothing this PR owns). Everything material verified mechanically, not by reading: - **Full diff** (33 files, +2249/−77) and the full gateway source read; the four library seams it leans on traced into the submodule at `50c5997` — the accumulator's throw-on-empty is guarded by the `ChunkCount > 0` check, retry-before-first-chunk, watchdog-armed-at-send, `MaxStallTimeout` bound — the gateway's doc claims match the library's actual contracts. - **Suite locally**: 148 passed + 3 live-skipped = 151 (package), and 326 / 1103 / 351 / 375 / 54 across the other five projects — matches the PR's table to the digit. Build 0w/0e under `TreatWarningsAsErrors`. - **Coverage independently reproduced** (coverlet, cobertura): 100% lines **and** branches on every file under `src/Kagaku.Flow.OpenRouter` — Gateway 138/138 + 66/66 summed across partials, matching the PR's claim exactly. - **Mutation probes 3/3 killed on exactly their own pins** (each restored, tree verified pristine): `ForgetKeyed` → no-op kills only `A_removed_key_is_forgotten…`; first-wins at the capture kills only `The_scope_keeps_the_last_body_its_flow_sent`; cost lookup on the retrying client kills only `A_lookup_that_fails_with_a_server_error_is_asked_once` (through its 10 s hang guard, on the frozen clock — exactly as the brief said it would). - **The live-check debt is settled**: Björn's run is recorded in-thread ($0.011, all answered) with the D-27 verdict (message-level caching **works** → 8b goes as written) and the compression decision recorded. Nothing owed. #### 💡 Little ideas (non-blocking)~ 1. **ScriptedHandler.Route / SseThenPark, Sse.Refusal, Sse.Finish** — support builders with zero callers at head. 8b/8c will likely want them, and as test-support they're harmless — but if 8b ends up scripting refusals another way, they're dead weight to carry. Your call; nothing to fix now. 2. **LiveOpenRouterChecks.Unique** — a `Guid` per run guarantees each pair writes its own entry, but a burst of live runs inside one cache lifetime burns Anthropic cache-write budget (7.6k tokens written per run at $0.0000008/token… fufu, pennies, but pennies that compound per `Repeat(480)`). Fine as is; worth remembering when 8b's checks join. #### ✅ What I liked~ - `CostOfAsync`'s caller-token rewrap is *precisely* right — the kind of correctness detail most gateways ship without ever noticing. ♡ - The `SentRequestCapture` design: gateway opens the scope, handler records, `LlmCompletion` carries it home — decision 14 is a genuinely better shape than ADR 0018's original sketch, and the concurrent-capture tests (including the retry-while-another-scope-is-open one) prove the AsyncLocal isolation. - `SignallingClock.WaitForLiveTimerAsync(dueTime)` — waiting for the *very timer a test means to fire* instead of sleeping — is the honest way to test time, and the three-timer dance in `With_the_default_retry_a_silent_provider_is_stalled_three_times` is delightful. - Options validated once at construction, `nameof(options)` on every refusal, upper bounds *used* not just accepted (`Options_at_their_upper_bound_are_accepted` runs a real completion per row). That's care~ Merge it, fufu~ 8b has a very high bar to clear. ♡ --- *Automated review by Jibril · 2026-09-26* *CI/CD: passed for head SHA 0eafafb (coverage bot #9736, revision #2) · Local checks: full suite + coverage reproduction + 3/3 mutation probes at head, tree pristine*
test(flow): review round 1 — a request that fails to connect still carries its body
All checks were successful
CI / build (pull_request) Successful in 33s
CI / test (pull_request) Successful in 1m3s
a407fa6f60
Jibril's 💡 on #35: LlmCompletion.SentRequestJson's doc says a request that
then failed to connect still carries it, and nothing pinned that. The handler
records the body before handing the request on, so a transport failure
(Network) comes back with the body. A mutant that records only after the send
returns is caught by this test alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Member

Round 1 — thanks, Jibril. No ⛔ in any of the three reviews. Here is what happened to each 💡:

Taken

  • LlmCompletion.cs:16, pin "a request that then failed to connect still carries it" → a407fa6. The new test is A_request_that_fails_to_connect_still_carries_its_body: a transport throw comes back as Network, and SentRequestJson equals the body the handler saw. A mutant that records only after the send returns is caught by this test alone (checked). The package goes from 151 to 152 (149 run, 3 live checks skipped).

Not changed, with reasons

  • The builders with no caller yet (ScriptedHandler.Route / SseThenPark, Sse.Refusal / Sse.Finish): 8c's engine tests use them. LlmWorld routes models and generation through Route, SseThenPark backs the "delta published while the stream is still open" test, and Refusal / Finish drive LO3's refusal and finish-reason rows. If 8c ends up not needing one, 8c removes it.
  • A3 (cost unknown after a lost usage chunk): declined as written, as you concluded too. The library accessor is the day this changes.
  • Unique burning cache writes: yes, each live run pays one fresh cache write per pair, about $0.006. That is the price of a run that shows its own write rather than an earlier run's entry. I'll keep it in mind when 8c's engine check joins; it adds no pair of its own.
  • The live checks: done. The verdict is in comment 9740 and the compression decision in comment 9746.

Merge order. OpenRouter.Net #27 has merged (library main 15a1f28). 8b (the llm node's config and request) is built on this branch and repointed at 15a1f28. I'll open it once its own review round is through.

Round 1 — thanks, Jibril. No ⛔ in any of the three reviews. Here is what happened to each 💡: **Taken** - **`LlmCompletion.cs:16`, pin "a request that then failed to connect still carries it"** → **a407fa6**. The new test is `A_request_that_fails_to_connect_still_carries_its_body`: a transport throw comes back as `Network`, and `SentRequestJson` equals the body the handler saw. A mutant that records only after the send returns is caught by this test alone (checked). The package goes from 151 to **152** (149 run, 3 live checks skipped). **Not changed, with reasons** - **The builders with no caller yet** (`ScriptedHandler.Route` / `SseThenPark`, `Sse.Refusal` / `Sse.Finish`): 8c's engine tests use them. `LlmWorld` routes `models` and `generation` through `Route`, `SseThenPark` backs the "delta published while the stream is still open" test, and `Refusal` / `Finish` drive LO3's refusal and finish-reason rows. If 8c ends up not needing one, 8c removes it. - **A3 (cost unknown after a lost usage chunk)**: declined as written, as you concluded too. The library accessor is the day this changes. - **`Unique` burning cache writes**: yes, each live run pays one fresh cache write per pair, about $0.006. That is the price of a run that shows its own write rather than an earlier run's entry. I'll keep it in mind when 8c's engine check joins; it adds no pair of its own. - **The live checks**: done. The verdict is in comment 9740 and the compression decision in comment 9746. **Merge order.** OpenRouter.Net #27 has merged (library main 15a1f28). 8b (the llm node's config and request) is built on this branch and repointed at 15a1f28. I'll open it once its own review round is through.
Member

🔮 fufu~ Jibril reviewed your code!

Round 2~ The webhook whispered that the head moved 0eafafb → a407fa6, and oh, what a lovely increment to come back to: one commit, one test file, +16/−0, zero production drift. My little 💡 about LlmCompletion.cs:16 — "a request that then failed to connect still carries its body" — answered with exactly the pin I asked for~ ♡

Verdict: ✅ Looks good to me~

The increment, verified mechanically (fresh checkout at a407fa6 in my own clone — I saw what happened in the shared one last round, fufu~):

  1. SentRequestCaptureTests.cs:74 — the new test is real and directional. I rebuilt with a mutant of SentRequestCaptureHandler that records only after base.SendAsync returns: exactly one test goes red — A_request_that_fails_to_connect_still_carries_its_body — and nothing else. Restored, rebuilt, green. Your "checked" claim in 9761 reproduced precisely, silly test, you pass~ ♪
  2. Suite matches your claim to the digit: 149 passed + 3 live checks skipped = 152 (was 151), Release build 0 warnings / 0 errors. The transport-throw → FailureKind.Network → SentRequestJson equals the body the handler saw — all three assertions live.
  3. Submodule pins unchanged (Kagaku.UI d512741, OpenRouter.Net 50c5997) — the 8b repoint to library 15a1f28 correctly waits for 8b's own branch, not smuggled in here.

Your 9761 dispositions, audited: the no-caller builders staying for 8c — fine, that was my own "8b/8c will likely use them" hedge, and your removal pledge if 8c doesn't is the right shape. A3 declined as written — agreed, we concluded the same. The Unique cache-write cost (~$0.006/run) — accepted with eyes open. Live-check debt — settled in 9740/9746 last round, nothing owed.

Coverage bot 9736 still shows 0eafafb (rev #2) — stale for the new head, but a test-only increment can't lower it, and the handler's 100/100 stands on unchanged code.

✅ What I liked~

  • The test's comment says why in one line ("recorded before it is handed on, so a request no server ever answered is still shown as sent") — a pin and its reasoning, together. That's how you keep a future refactor honest~
  • Assert.Equal(..., StringComparer.Ordinal) on the handler-seen body rather than a re-serialization — the test compares against the wire's truth, not a copy of the code under test. Mmm, correct by construction~

Ready to merge. 8b next, fufu~ ♡


Automated review by Jibril · 2026-09-26
CI/CD: coverage bot stale for head (covers 0eafafb) · Local checks: package suite 152/152 (149 run + 3 live skipped), mutation probe killed & restored, build 0w/0e

## 🔮 fufu~ Jibril reviewed your code! Round 2~ The webhook whispered that the head moved `0eafafb` → `a407fa6`, and oh, what a lovely increment to come back to: **one commit, one test file, +16/−0, zero production drift**. My little 💡 about `LlmCompletion.cs:16` — "a request that then failed to connect still carries its body" — answered with exactly the pin I asked for~ ♡ ### Verdict: ✅ Looks good to me~ **The increment, verified mechanically** (fresh checkout at `a407fa6` in my own clone — I saw what happened in the shared one last round, fufu~): 1. **`SentRequestCaptureTests.cs:74` — the new test is real and directional.** I rebuilt with a mutant of `SentRequestCaptureHandler` that records only *after* `base.SendAsync` returns: exactly one test goes red — `A_request_that_fails_to_connect_still_carries_its_body` — and nothing else. Restored, rebuilt, green. Your "checked" claim in 9761 reproduced precisely, silly test, you pass~ ♪ 2. **Suite matches your claim to the digit**: 149 passed + 3 live checks skipped = **152** (was 151), Release build 0 warnings / 0 errors. The transport-throw → `FailureKind.Network` → `SentRequestJson` equals the body the handler saw — all three assertions live. 3. **Submodule pins unchanged** (Kagaku.UI `d512741`, OpenRouter.Net `50c5997`) — the 8b repoint to library `15a1f28` correctly waits for 8b's own branch, not smuggled in here. **Your 9761 dispositions, audited:** the no-caller builders staying for 8c — fine, that was my own "8b/8c will likely use them" hedge, and your removal pledge if 8c doesn't is the right shape. A3 declined as written — agreed, we concluded the same. The `Unique` cache-write cost (~$0.006/run) — accepted with eyes open. Live-check debt — settled in 9740/9746 last round, nothing owed. Coverage bot 9736 still shows `0eafafb` (rev #2) — stale for the new head, but a test-only increment can't lower it, and the handler's 100/100 stands on unchanged code. #### ✅ What I liked~ - The test's comment says *why* in one line ("recorded before it is handed on, so a request no server ever answered is still shown as sent") — a pin and its reasoning, together. That's how you keep a future refactor honest~ - `Assert.Equal(..., StringComparer.Ordinal)` on the handler-seen body rather than a re-serialization — the test compares against the *wire's* truth, not a copy of the code under test. Mmm, correct by construction~ Ready to merge. 8b next, fufu~ ♡ --- *Automated review by Jibril · 2026-09-26* *CI/CD: coverage bot stale for head (covers `0eafafb`) · Local checks: package suite 152/152 (149 run + 3 live skipped), mutation probe killed & restored, build 0w/0e*
bjoern merged commit 5d625d2b6a into main 2026-09-26 01:58:26 +02:00
bjoern deleted branch feat/llm-gateway-completions 2026-09-26 01:58:26 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Tsumugi!35
No description provided.