feat(opencode): capture cache-diagnostics and usage cache accounting per request - #159
feat(opencode): capture cache-diagnostics and usage cache accounting per request#159iceteaSA wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
Triaged all bot findings against the code; Fixed in
Refuted, with the code path:
Declined:
|
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
Second round was right on both counts —
|
7b45b0f to
fc2073b
Compare
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
3b9f126 to
57171fb
Compare
|
Greptile's summary was right — All four per-chunk consumers now share identical drain-before-cap semantics. If the repetition bothers anyone at merge time, the structural follow-up is one shared bounded-drain helper — happy to do that as a separate PR rather than grow this one. |
57171fb to
a8eea4b
Compare
a8eea4b to
5f3d05e
Compare
5f3d05e to
c1b7101
Compare
…synthetic turn Adds an OpenCode-only /claude-start command: queue one synthetic one-token turn through the current session's normal model, agent, variant, quota, routing, cache, relay, signing, and response pipeline, so a resumed session's prompt cache is re-warmed and its TTL clock refreshed without an operator turn. The synthetic prompt ([lane start] - automated cache warm; no response needed.) is injected via the session SDK with synthetic:true and the session's context resolved from message history; OpenCode assembles the request exactly as a real turn, so the warm is byte-exact by construction. The fetch layer correlates the request by its synthetic message id (exact-match, one-shot, session-scoped, bounded) and shapes it fail-closed: max_tokens 1, thinking stripped, streaming kept. The terminal max_tokens stop is rewritten to end_turn for the correlated turn only, so the session records a clean micro-turn. CacheKeep adopts the session afterward; dumps tag start requests -start- on direct and relay paths; diagnostics records carry source:start with synthetic:true. API-key routes are unreachable by all of this. /claude-start off is persisted (and logged to the command audit trail); automatic is reserved and replies honestly that it is not yet wired. Verified live on a ~460K-token session: start read=459,602 write=419; next real turn read=460,021 = start read + write exactly - shaping does not fork the cache key. ~$0.48 per start vs ~$9.26 for a cold rewrite. Stacked on the cache-diagnostics capture branch (cortexkit#159).
…per request The plugin has sent cache-diagnosis-2026-04-07 in its beta list on every request without enabling the feature: the beta requires a request-body opt-in (diagnostics.previous_message_id), and nothing sent it or read the response. Turn the dormant channel on, measure-only. Request side: eligible OAuth requests gain the diagnostics opt-in inside the fail-closed rewriteRequestBody pipeline. The id sent is only ever one captured from a prior Anthropic response (bounded per-session tracker) - opencode mints its own msg_01-shaped ids for every provider, and a foreign id fails silently as previous_message_not_found. Response side: the existing SSE wrapper exposes message_start.message through a typed callback; per valid eligible response one MC-CACHE-DIAG single-line JSON record (schema v:2) is emitted via the logger with verbatim usage, TTL-bucket accounting, diag_state (absent|server_null|pending|populated, always a string), populated cache_miss_reason.type, and attribution fields: source (open set) + synthetic (closed machinery/traffic split with published mapping), account_id (opaque persisted identifier; consumer timelines key on (account_id, prefix) because sticky routing migrates sessions across account-scoped caches), betas_hash (xxh64 of the sorted sent beta list, resolved by a once-per-hash MC-CACHE-DIAG-BETAS side-channel line), and requested_model present only on request/served divergence. Dumps: responses gain artifacts (status/id/model/usage/diagnostics, never content); CacheKeep prewarms are dumped tagged -prewarm-cachekeep- and emit records through the same chain, since any write resets the server TTL clock. Canary: short-gap previous_message_not_found with a genuinely-sent id logs a warn - the id capture broke, not a fingerprint expiry. Every per-chunk stateful consumer in the response wrapper is bounded at 8 MiB with drain-before-cap semantics; complete frames are processed before overflow passthrough engages. Verified live: null(write=24831) -> null(read=24831, hit) -> system_changed(cache_missed_input_tokens=23963) on a forced system change against a warm prefix. Comparison engages only on cacheable requests; consumers classify hit-first (documented in README, which is the record contract). Closes cortexkit#157
c1b7101 to
b5d77c3
Compare
…synthetic turn Adds an OpenCode-only /claude-start command: queue one synthetic one-token turn through the current session's normal model, agent, variant, quota, routing, cache, relay, signing, and response pipeline, so a resumed session's prompt cache is re-warmed and its TTL clock refreshed without an operator turn. The synthetic prompt ([lane start] - automated cache warm; no response needed.) is injected via the session SDK with synthetic:true and the session's context resolved from message history; OpenCode assembles the request exactly as a real turn, so the warm is byte-exact by construction. The fetch layer correlates the request by its synthetic message id (exact-match, one-shot, session-scoped, bounded) and shapes it fail-closed: max_tokens 1, thinking stripped, streaming kept. The terminal max_tokens stop is rewritten to end_turn for the correlated turn only, so the session records a clean micro-turn. CacheKeep adopts the session afterward; dumps tag start requests -start- on direct and relay paths; diagnostics records carry source:start with synthetic:true. API-key routes are unreachable by all of this. /claude-start off is persisted (and logged to the command audit trail); automatic is reserved and replies honestly that it is not yet wired. Verified live on a ~460K-token session: start read=459,602 write=419; next real turn read=460,021 = start read + write exactly - shaping does not fork the cache key. ~$0.48 per start vs ~$9.26 for a cold rewrite. Stacked on the cache-diagnostics capture branch (cortexkit#159).
Closes #157.
The plugin has sent
cache-diagnosis-2026-04-07in its beta list on every request without ever enabling the feature: the beta requires a request-body opt-in (diagnostics.previous_message_id), and nothing sent it or read the response. This PR turns the dormant channel on, measure-only.What it does
Request side: eligible requests (OAuth routes whose beta set already includes the diagnosis beta) gain
"diagnostics": {"previous_message_id": <captured id | null>}inside the existing fail-closedrewriteRequestBodypipeline. API-key fallback routes are untouched. The id sent is only ever one captured from a prior Anthropic response — a bounded per-session tracker holds it. That distinction matters: opencode mints its ownmsg_01…-shaped message ids, and sending one of those fails silently asprevious_message_not_found, which the API also uses for benign fingerprint expiry. The tracker asserts identity (send-what-was-captured), never format.Response side: the existing SSE wrapper exposes
message_start.messagethrough a new typed callback (same pattern as the current fallback-outcome callback; emitted bytes untouched). Per valid eligible response, one machine-parseable record is emitted through the existing logger:Record schema
v:2(the README table is the contract, settled with its first external consumer):diag_state∈absent | server_null | pending | populated(always a string, never JSON null),miss_reason/cache_missed_input_tokensonly when populated. No derived hit/miss flags, no normalization.source(open set; known valuesturn,prewarm_cachekeep) withsyntheticas the closed-form machinery/traffic split (published mapping, synthetic wins on conflict, mismatch logs a warn),account_id(opaque persisted identifier — cache entries are account-scoped and sticky routing migrates sessions, so consumer timelines key on(account_id, prefix)),betas_hash(xxh64 of the sorted sent beta list —unavailablefires on beta-set changes), andrequested_modelpresent only when the request-body model differs from the served model (labels recovery/fallback switches).betas_hashper process emits oneMC-CACHE-DIAG-BETAS {"hash","betas":[…]}resolution line. The trailing space in theMC-CACHE-DIAGdelimiter is load-bearing (it is what keeps the two line types distinct);grep -c "MC-CACHE-DIAG"without it over-counts.Dumps: when dumping is enabled, responses get an artifact (status, id, model, usage, diagnostics — never content blocks), and CacheKeep prewarm requests are dumped too, tagged
-prewarm-cachekeep-so traffic censuses can exclude keepalive noise. Prewarms also send the opt-in and emit records (source: "prewarm_cachekeep") — the server's TTL clock runs from the last write by any caller, so an unrecorded prewarm would be an invisible clock reset.Canary:
previous_message_not_foundon a short-gap turn where an id was actually sent is logged at warn — at short gaps the fingerprint can't plausibly have expired, so it means the id capture broke. This makes the one silent failure mode of the feature self-detecting.Bounded buffering: every per-chunk stateful consumer in the response wrapper is bounded at 8 MiB with drain-before-cap semantics — complete SSE frames (including
message_startand fallback boundaries) are always processed before overflow passthrough engages, so the cap can't drop an id capture or leak an internalfallbackblock.Verified against the live API
Chained smoke on a real cacheable prefix (~24.8K tokens):
Two behaviors worth knowing that the docs understate: the comparison only engages on cacheable requests (
nullmeans "not compared" unless cache activity is nonzero), and on multi-turn agent traffic a populated*_changedroutinely coexists with a full prefix hit — consumers must classify hit-first (cache_readis a fact;miss_reasonis an interpretation). Both documented in the README.Gates
bun run test1084 pass / 0 fail ·bun run typecheckclean ·bun run lintclean · e2e 26/27 — the one failure is the pre-existingtool-prefix.test.tsstale-Opus-bridge case, which fails identically on a cleanb1d8f8cworktree (verified side-by-side; unrelated to this diff).Pi behavior is unchanged (all new core params optional; call sites read to confirm, not just typechecked).
Relation to #155
Same file (
cachekeep.ts), no overlap: this branch deliberately carries none of #155's error-containment scaffolding, so the two merge independently in either order.Tests were written red-first throughout; the concurrency tests bind different predecessors across interleaved in-flight sessions specifically so a mutable-current-request regression cannot pass.
Greptile Summary
This PR enables per-request Anthropic cache diagnostics and cache-usage accounting while preserving response bytes and bounding response-processing buffers.
Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains.
Important Files Changed
Sequence Diagram
Reviews (17): Last reviewed commit: "feat(opencode): capture cache-diagnostic..." | Re-trigger Greptile