Skip to content

feat(opencode): capture cache-diagnostics and usage cache accounting per request - #159

Open
iceteaSA wants to merge 4 commits into
cortexkit:mainfrom
iceteaSA:feat/cache-diagnostics-capture
Open

feat(opencode): capture cache-diagnostics and usage cache accounting per request#159
iceteaSA wants to merge 4 commits into
cortexkit:mainfrom
iceteaSA:feat/cache-diagnostics-capture

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Closes #157.

The plugin has sent cache-diagnosis-2026-04-07 in 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-closed rewriteRequestBody pipeline. 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 own msg_01…-shaped message ids, and sending one of those fails silently as previous_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.message through a new typed callback (same pattern as the current fallback-outcome callback; emitted bytes untouched). Per valid eligible response, one machine-parseable log line is emitted through the existing logger:

MC-CACHE-DIAG {"v":1,"session_id":"...","ts_ms_received":...,"model":"...","is_subagent":false,"ttl_sent":"1h","cache_read":...,"cache_creation":...,"input_tokens":...,"ephemeral_5m_tokens":0,"ephemeral_1h_tokens":...,"message_id":"msg_...","previous_message_id":"msg_...","diag_state":"populated","miss_reason":"system_changed","cache_missed_input_tokens":...}

Values are verbatim from the response — no derived hit/miss flags, no normalization. The four documented diagnostics states (absent / null / comparison-pending / populated) stay distinct. Malformed or error envelopes never fabricate a record.

Dumps: when dumping is enabled, responses now get an artifact (status, id, model, usage, diagnostics — never content blocks), and CacheKeep prewarm requests are dumped too, tagged -prewarm-cachekeep- in the filename and tag in metadata so traffic censuses can exclude keepalive noise. Prewarm responses also feed the same id chain — the server's TTL clock runs from the last write by any caller, so an unrecorded prewarm would silently reset it.

Canary: previous_message_not_found on a short-gap turn 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.

Verified against the live API

Chained smoke on a real cacheable prefix (~24.8K tokens):

1. opt-in, no predecessor      -> diagnostics: null   (write=24831)
2. identical + captured id     -> diagnostics: null   (read=24831, hit)
3. forced system change + id   -> {"cache_miss_reason":{"type":"system_changed",
                                    "cache_missed_input_tokens":23963}}

One behavior worth knowing that the docs don't state: the comparison only engages on cacheable requests. A request below the model's cacheable minimum returns diagnostics: null even when it genuinely changed — so null means "not compared" unless cache activity is nonzero. Documented in the README limitations.

Gates

bun run test 1069 pass / 0 fail · bun run typecheck clean · bun run lint clean · e2e 26/27 — the one failure is the pre-existing tool-prefix.test.ts stale-Opus-bridge case, which fails identically on a clean b1d8f8c worktree (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 test binds two different predecessors across interleaved in-flight sessions specifically so a mutable-current-request regression cannot pass it.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Capture Anthropic prompt-cache diagnostics end to end and preserve provider response IDs across requests and CacheKeep prewarms without changing response delivery. Previously the beta header was sent but not opted in; eligible OAuth requests now include diagnostics.previous_message_id and we emit versioned MC-CACHE-DIAG v1 records with usage and TTL buckets. Streaming is safer: we drain complete SSE frames, bound the pending buffer before fallback passthrough, and bypass SSE consumers in JSON mode with an 8 MiB diagnostics cap.

  • Request/response: inject diagnostics.previous_message_id only on OAuth routes already sending the cache-diagnosis-2026-04-07 beta; API‑key routes remain off. Track the last provider message ID per session, carry diagnostics across sticky retries, summarize TTL sent (1h/5m/null), and thread is_subagent. Expose message_start.message via the stream wrapper and emit one MC-CACHE-DIAG v1 record per eligible response (session, ts, model, is_subagent, TTL sent, usage, provider message_id, sent previous_message_id, diag_state in {absent,server_null,pending,populated}, optional miss fields). Parse cache_miss_reason precisely, warn on short‑gap previous_message_not_found, ignore malformed envelopes. Bound streaming diagnostics pending, drain complete SSE frames before applying the cap and before fallback passthrough; cap JSON‑mode diagnostics at 8 MiB and bypass all SSE‑stateful consumers in JSON mode.

  • Dumps/CacheKeep: write .response.json artifacts with status/id/model/usage/diagnostics only. dumpDirectRequest/dumpRelayRequest return an optional handle and dumpResponseArtifact writes the response artifact; sendViaRelay exposes onDumpCreated. CacheKeep prewarms are dumped and tagged -prewarm-cachekeep- and feed the same provider‑ID chain. CacheKeepManager adds optional prepareBody/onResponse hooks and tracks isSubagent. Export setBounded from @cortexkit/anthropic-auth-core.

  • Migration

    • Update log parsers to handle diag_state: "server_null" where "null" was expected.
    • When capturing response artifacts, pass the optional handle from dumpDirectRequest/dumpRelayRequest to dumpResponseArtifact; relay callers can subscribe via onDumpCreated. Existing callers continue to work unchanged.

Written for commit 8d599a8. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR enables per-request Anthropic cache diagnostics, preserves provider response IDs across regular and CacheKeep requests, and adds sanitized response dump artifacts.

  • Adds bounded diagnostics tracking and versioned cache-accounting logs.
  • Extends streaming and non-streaming response observation without altering delivered bytes.
  • Tags CacheKeep dump artifacts and feeds prewarm responses into the provider-ID chain.
  • Bounds all accumulators identified in the previous review threads.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/opencode/src/transform.ts Bounds JSON and SSE response-processing accumulators, drains complete frames before overflow cutoff, and preserves response pass-through.
packages/opencode/src/index.ts Integrates diagnostics request context, response observation, provider-ID tracking, and dump artifact creation across direct, relay, retry, and CacheKeep paths.
packages/opencode/src/cache-diagnostics.ts Implements bounded per-session provider-ID tracking and strict construction of versioned cache-diagnostics records.
packages/core/src/cachekeep.ts Adds optional request preparation and response observation hooks while recording tagged prewarm dumps.
packages/core/src/dump.ts Adds sanitized response artifacts, dump handles, CacheKeep tags, and response-file sweep support.
packages/opencode/src/tests/transform.test.ts Covers split JSON, oversized non-streaming responses, bounded state consumers, and drain-before-overflow streaming behavior.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Plugin
  participant Tracker
  participant Anthropic
  Client->>Plugin: Request
  Plugin->>Tracker: Read previous provider message ID
  Plugin->>Anthropic: Request + diagnostics.previous_message_id
  Anthropic-->>Plugin: Message response / message_start
  Plugin->>Tracker: Capture provider message ID
  Plugin->>Plugin: Emit MC-CACHE-DIAG and response artifact
  Plugin-->>Client: Original response bytes
Loading

Reviews (9): Last reviewed commit: "fix(opencode): drain complete sse frames..." | Re-trigger Greptile

Comment thread packages/opencode/src/transform.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/README.md

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/core/src/cachekeep.ts
Comment thread packages/opencode/src/cache-diagnostics.ts
Comment thread packages/opencode/src/transform.ts
Comment thread packages/opencode/src/index.ts
Comment thread packages/opencode/src/tests/index.test.ts Outdated
Comment thread packages/core/src/dump.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/opencode/src/cache-diagnostics.ts
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Triaged all bot findings against the code; 42ec968 fixes the three that held up.

Fixed in 42ec968:

  • Non-streaming diagnostics buffering is now capped at 8 MiB (NON_STREAMING_DIAGNOSTICS_MAX_BYTES, matching the sanitize-memo cap). On overflow: no record, pass-through untouched. Covers greptile's and cubic's buffer findings.
  • Prewarm test asserted its flag after a 20 ms sleep instead of after the awaited start promise — reordered (cubic, index.test.ts).
  • dumpRequest returned a non-null handle when the dump write failed, letting a later .response.json orphan appear without companion body/meta — now returns null on write failure (cubic, dump.ts).

Refuted, with the code path:

  • "Prewarm header refresh strips the diagnosis beta" (cubic P1): the refresh rebuilds headers through setOAuthHeadersselectClaudeCodeBetas, and all three beta sets (FULL_AGENT/STRUCTURED_OUTPUT/BASE) carry cache-diagnosis-2026-04-07 at base. The merge afterwards only adds the extended-TTL beta. Prewarm requests keep the beta on the wire.
  • "Pending state skips cache_missed_input_tokens validation" (cubic P2): that field lives inside cache_miss_reason; in the pending state the reason is null, so there is no field to validate. The populated path validates it and rejects non-numbers.
  • "README inconsistent on the token source" (cubic P3): README table says diagnostics.cache_miss_reason.cache_missed_input_tokens; the classifier reads reason.cache_missed_input_tokens. Consistent at head.

Declined:

  • Merge user-supplied diagnostics instead of overwriting (cubic P2): overwrite is deliberate. The chain's integrity depends on only response-captured ids entering previous_message_id; merging would let a caller-supplied id through the identity assertion, and the failure mode of a foreign id is silent (previous_message_not_found reads as benign expiry).
  • Bump v for the server_null rename (cubic P3): the rename landed pre-release in the same PR that introduces v:1; no consumer ever pinned the old value. First released contract is v:1 with server_null.

Comment thread packages/opencode/src/transform.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/opencode/src/transform.ts
Comment thread packages/core/src/tests/dump.test.ts Outdated
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Second round was right on both counts — 42ec968 was incomplete, corrected in 7b45b0f.

  • The 8 MiB cap bounded responseText but json-mode chunks still flowed into updateSseDiagnostics, whose line buffer never drains without an SSE boundary — the full body accumulated there anyway. Fixed at the call site: json mode no longer feeds the SSE diagnostics state at all (a JSON body has no SSE events; the json path delivers its message from the end-of-stream parse, which doesn't touch that state). Over-cap test now asserts ssePendingChars: 0.
  • The orphan-dump test was exercising mkdir failure, not write failure, and asserting in a directory where the orphan couldn't appear. Rewritten against a chmod-restricted real directory (skipped when running as root), red-green proven against the dump.ts null-handle fix.

…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 (contract v:1) is emitted via the logger with
verbatim usage, TTL-bucket accounting, diag_state
(absent|server_null|pending|populated), and populated
cache_miss_reason.type. Short-gap previous_message_not_found logs a
warn-level canary: the id capture broke, not a fingerprint expiry.

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.

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 (documented in README limitations).

Closes cortexkit#157
@iceteaSA
iceteaSA force-pushed the feat/cache-diagnostics-capture branch from 7b45b0f to fc2073b Compare August 20, 2026 05:57
Comment thread packages/opencode/src/transform.ts Outdated
Comment thread packages/opencode/src/transform.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/opencode/src/server-fallback.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/opencode/src/transform.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] cache-diagnosis beta is requested on every call but the response is never read

1 participant