fix: one provider call gets a generation budget, and every round says what it cost #85
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "worktree-fix-timeout-and-round-usage"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two halves of one failure, from a live run: a refinement page died at round 8 with
Request timed out, and its transcript could say when but not why.The timeout
ClientFornever sethttp.Timeout, so it wasHttpClient's default 100 seconds — a web-request budget, not a generation budget.A vision agent's rounds get slower as its conversation grows: every earlier crop and every earlier reasoning block rides along on each request (
reasoning_detailsis replayed to preserve thinking state). So the loop climbs through 100s partway in. The transcript for p2r16:Round 6 was inside the ceiling by ten seconds. Retrying can't help — the next attempt rebuilds the same conversation and meets the same wall, which is why this presented as three pages failing the same way rather than as a flaky network.
Now five minutes. The comment at the constant records the multiplication, because it isn't obvious:
RetryOptionsallows three attempts per call and the engine three per stage, so this bounds a hung round at 15 minutes rather than 5. That's the price of not failing a page that was merely slow; a genuinely stuck run is stopped by hand from the monitor.The round line
RoundDetail.Usagealready carried per-round tokens and cost —AgentTranscriptrendered only the duration. Duration alone can't separate "this model is slow" from "this conversation got heavy", and those want opposite fixes (a different model vs. fewer tokens). Every round line now carries both:Exact under a thousand, rounded above it — the shape of the growth is the point, and
47238makes the eye do arithmetic. The prompt count is the one that accumulates, so a glance down the column now shows whether a slow round was carrying more than the fast ones.Tests
+3, 767/767 green (Domain 91, UseCases 318, Integration 165, BlazorAdapter 193) — baseline 764 on
cb9ce6c.One_provider_call_gets_a_generation_budget_not_a_web_request_one— asserts the applied timeout, and explicitly asserts it is not 100s, since the bug was a default nobody had chosen. Verified it catches a regression: deleting thehttp.Timeoutline fails it withAssert.Equal() Failure: Values differ.Each_round_carries_its_token_counts_so_a_slow_round_can_be_explained— pins both formatting arms (850 in / 120 out,47.2k in / 1.4k out) and the cost.A_round_whose_provider_reported_no_usage_still_renders— usage is opt-in and providers vary; a missing block costs the round its numbers, never its line.The stub response builder gained an optional usage block, since it couldn't express one before.
Notes
AppliedRequestTimeout, because the configured value is unreachable once the client is built and "we still set it" is exactly what wants pinning. Production logic is otherwise untouched./app/data/transcripts/…).🤖 Generated with Claude Code
Summary
Summary
Coverage
Orihon.BlazorAdapter - 95.7%
Orihon.Domain - 100%
Orihon.Infrastructure - 96.3%
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2
Orihon.Kernel - 90.9%
Orihon.Server - 93.3%
Orihon.UseCases - 97.2%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A timeout that was never chosen, and a transcript that could say when but never why — two halves of one failure, and you've stitched them both shut with surgical precision. This is the kind of diagnosis that makes a Flugel's heart sing~ ♡ The PR body alone is a masterclass in root-cause writing: the round-by-round climb (4.9s → 90.2s → TIMEOUT), the insight that retrying cannot save it because the conversation rebuilds the same wall, the honest "this stops a slow round from being fatal, not from being slow." Chef's kiss. ♪
Verdict: ✅ Looks good to me~
No blockers. Not one. I looked hard — the yandere is pouting because she wanted to find something, but the code is too clean. ♡
What I verified with my own hands:
http.Timeout = RequestTimeout;fromClientFor(), rebuilt, and ranOne_provider_call_gets_a_generation_budget_not_a_web_request_one— it failed withAssert.Equal() Failureexactly as you claimed. Restored, rebuilt, green. That "explicitly assert it is NOT 100s" line is sharp — it pins the regression that was a default nobody chose. Fufu~ you know how to make a test bite. ♡Tokens()both arms exercised.850 in / 120 out(exact, <1000) and47.2k in / 1.4k out(rounded, ≥1000) — theEach_round_carries_its_token_countstest pins both formatting branches AND the cost. Branch coverage on the method is complete.A_round_whose_provider_reported_no_usage_still_rendersfeeds responses with no usage block, asserts the round line still appears (round 1/2) and the token segment is absent (DoesNotContain(" in / ")). Providers vary, usage is opt-in — a missing block costs the round its numbers, never its line. Correct. ♪Pattern consistency (I checked every sibling):
if (round.Usage is { } usage)mirrors the existingif (round.FinishReason is { } finish)two lines down — same shape, same indentation, same null-handling philosophy. ✅if (usage.Cost is { } roundCost)mirrors the top-levelif (result.AggregatedUsage?.Cost is { } cost). ✅RequestTimeoutasprivate static readonly TimeSpanmirrorsDefaultBaseAddressright above it. ✅Tokens()as aprivate statichelper sits besideCap(),FirstLine(),AppendIndented()— same family, same style. ✅AppliedRequestTimeoutas aninternaltest seam is the right call: the value is unreachable once the client is built, and "we still set it" is exactly the invariant that wants pinning. The doc comment says so plainly. ✅HttpWebPageFetcheruses 20s (web fetch budget) vs this 5min (generation budget) — different use cases, different ceilings, both explicit. The distinction is correct, not an inconsistency. ✅✅ What I liked~
RetryOptions(3 attempts/call) × engine (3 attempts/stage) × 5min = 15min worst case for a genuinely stuck round. Recording why the constant is 5 and the real ceiling is 15 in the doc comment is the kind of thing that saves the next reader an hour of arithmetic. That's love for the craft. ♡Tokens()reading-precision philosophy. "47.2k carries the shape where 47238 makes the eye do arithmetic" — yes! The prompt count is the one that accumulates, so a glance down the column shows whether a slow round was carrying more. The format serves the diagnosis, not the other way around.ReasoningToolCallResponsegaining an optionalusageparameter is the minimal, honest extension — it couldn't express usage before, now it can, and every existing caller is unaffected by the default. Clean.Fufu~ this is how you fix a production failure. Two bugs, one PR, three tests that bite, zero scope creep. The Flugel is satisfied. ♡♪ Merge it~
Automated review by Jibril · 2026-07-27
CI/CD: passed for head SHA
938253d(forgejo-actions coverage #1, 96.5% line / 83.2% branch) · Local checks: build 0/0, Integration 165/165 pass, regression-claim verified by mutation