feat: llm prompt caching with cache-usage visibility - #631
Conversation
Pin rig-core to the fork rev that adds prompt-cache support and cache-usage reporting (mezmo/rig#12), and switch rig-bedrock from crates.io =0.3.10 to the fork's vendored copy at the same rev — it carries the cachePoint request support and builds against the fork's rig-core, removing the standing semver hazard with crates.io 0.3.11+. Exclude ./rig from the workspace so a local checkout can be path-pinned during fork work without cargo resolving its crates against this workspace root. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
Opt-in per agent because caching changes billing (cache writes bill at a premium) and, on Bedrock, sending cachePoint blocks to a model without caching support fails the request outright. Workers inherit the flag with [agent.llm] or override it per worker. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
Wire the prompt_caching flag into the Anthropic and Bedrock completion models at all three construction sites (single-agent builder, orchestration coordinator, orchestration workers), and surface the provider-reported cache split end to end: - UsageState accumulates cache read/creation tokens across turns; the streaming hook captures them via the fork's cache_token_usage() and includes them in the usage log line. - StreamItem::TurnUsage carries the per-turn cache split so the orchestration path accumulates it into the shared UsageState via TurnTally::record. - aura.usage gains optional cache_read_input_tokens / cache_creation_input_tokens (omitted when no provider reported cache usage, so existing consumers see an unchanged payload). Both are sub-counts of prompt_tokens: providers that report input exclusive of cached tokens are folded in the fork, and OpenAI already includes them. Verified live against Anthropic-format mocks, an OpenAI-format mock, and real Bedrock (us.anthropic.claude-sonnet-5): a cold request reports the full prefix as cache creation, a warm one reports it as cache read, and prompt_tokens stays consistent with the provider total. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
The stream handler passes the aura.usage cache split into the REPL: the status line's tokens segment renders "in N (M cached) / out K" once any turn reports cache reads, and DisplayEvent::Usage records the split (optional fields, so old event logs still replay) so resumed conversations rebuild the counter. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
Translate the fork's gen_ai.usage.cache_read_input_tokens / cache_creation_input_tokens span attributes to OpenInference llm.token_count.prompt_details.cache_read / cache_write. The spec defines prompt_details as sub-counts already included in llm.token_count.prompt, which matches how the fork folds cache tokens into input_tokens. Phoenix (v13+) renders these in the token breakdown and prices them via its cost models. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
Greptile SummaryAdds opt-in Anthropic and Bedrock prompt caching while propagating provider cache-token usage through runtime events, orchestration, telemetry, persistence, and CLI rendering.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| crates/aura-cli/src/ui/event_replay.rs | Replays persisted display events, preserves live counters during streaming, and replaces reconstructed token totals with authoritative ledger totals while idle. |
| crates/aura-web-server/src/streaming/types.rs | Extends per-turn streaming state with cache-usage data. |
| crates/aura-web-server/src/streaming/handlers.rs | Captures provider cache usage and emits it with cumulative usage responses. |
| crates/aura/src/builder.rs | Applies the opt-in prompt-caching configuration to supported provider builders. |
| crates/aura/src/orchestration/orchestrator.rs | Accumulates cache-token usage across orchestration turns. |
| crates/aura/src/openinference_exporter.rs | Translates cache usage into OpenInference prompt-detail token attributes. |
| crates/aura-events/src/lib.rs | Widens the shared usage event contract with backward-compatible optional cache-token counts. |
| Cargo.toml | Pins Rig core and Bedrock to a common fork revision containing prompt-caching support. |
Sequence Diagram
sequenceDiagram
participant C as Agent config
participant R as Aura runtime
participant P as LLM provider
participant E as aura.usage stream
participant U as CLI and telemetry
C->>R: prompt_caching flag
R->>P: request with provider cache breakpoints
P-->>R: prompt, completion, and cache usage
R->>E: cumulative usage event
E->>U: status line, ledger, and span attributes
Reviews (5): Last reviewed commit: "fix(cli): leave live token counters alon..." | Re-trigger Greptile
The resume path treats the usage JSONL as the authoritative source for conversation totals and seeds the status line from it after replay, but cache-read tokens were rebuilt only from retained display events — a conversation whose older events were discarded resumed with full prompt/completion totals and an under-counted cached share. Store the cache split on each usage entry (absent on pre-existing lines, which count zero) and seed the cached counter from the same ledger. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
The prompt_caching field comments narrated defaults and Bedrock's rejection behavior, and TurnUsage narrated when its cache field is populated — behavior owned by the wiring and mapping code. Trim the type-level comments to what the values are and state the Bedrock opt-in rationale at the builder branch that applies the flag. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
The resume path seeded the status-line counters from the usage ledger, but any later display-only replay (/expand, a style repaint, stream toggles) reset them and rebuilt from the display-event log alone, clobbering the authoritative totals with potentially truncated ones — for prompt and completion as well as the new cached count. Move the ledger seed into replay_event_log_global itself, keyed off the active conversation dir, so the ledger gets the last word on every replay; an empty ledger keeps the replay-derived values. The per-site seeds after resume are gone — replay owns it. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
@teriyakichild - Okay trying something out here - I didn't actually find anything wrong with this by hand but all the layers of token counting gets pretty complex. I DID however find a few things that came up in a multi provider adversarial review that might be worth looking into that seems decently convincing: |
The final aura.usage event mixed populations: prompt/completion came from rig's turn-aggregated Final usage (which includes tool-only turns), while the cache split came from the streaming hook, which rig invokes only on turns that produced assistant text. On a tool loop the cache counts could under-report relative to the totals beside them, breaking the sub-count contract. Carry rig's turn-aggregated cache split through FinalResponseInfo into TurnState, and resolve it in resolve_billed_usage from the same source as the totals: the aggregated Final when present, the hook counters (which see every turn via TurnUsage) on the orchestration fallback. Regression test covers the tool-only-turn population mismatch. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
Add the prompt_caching flag to examples/reference.toml for anthropic (cache_control, billing tradeoff, worker inheritance) and bedrock (cachePoint placement, supported-model constraint), and note that OpenAI caches automatically with reads visible in aura.usage. Also update the resume_conversation doc for the three-element usage tuple and extend the rig-bedrock pin comment to cover the reused 0.3.10 version string and the rig-derive resolution. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
|
Good catches from the adversarial pass — dispositions:
|
A replay triggered while a response is streaming (/expand, /help, /conversations, /model, /style) reset the status-line counters and rebuilt them from the global display log plus the usage ledger. The in-flight turn's usage is buffered per-turn and reaches both of those only at turn end, so a replay landing after the turn's usage event had already been added live would erase that turn's prompt, completion, and cached-token counts — and since the counters accumulate deltas, nothing re-added the lost turn afterward. Skip the counter reset, the per-event rebuild, and the ledger seed whenever a turn is in flight: the live counters are authoritative for the whole PROCESSING window, and mid-stream replays only need to repaint the transcript. Idle replays (resume, prompt-time /expand and style changes) rebuild exactly as before. The same guard covers the context-occupancy and scratchpad counters, which had the same clobbering exposure. Refs: #630 Signed-off-by: Tony Rogers <tony@tonyrogers.me>
|
@Shearerbeard closing out finding 4: going with an accepted, documented 0.2.x break rather than an additive constructor or a history rewrite. The Usage variant's new fields break exhaustive matchers no matter what we do to the |
Summary
Adds opt-in provider prompt caching and end-to-end cache-usage visibility. Depends on the fork changes merged in mezmo/rig#12 (pinned here).
prompt_cachingflag on[agent.llm]for Anthropic (cache_controlbreakpoints on the system prompt + last message, covering tools via render order) and Bedrock (cachePointblocks after system, tools, and last message). Off by default: writes bill at a premium, and Bedrock rejects cachePoints on models without caching support. OpenAI caching is automatic — this PR only surfaces itscached_tokens. Wired at all three construction sites (single-agent, coordinator, workers); workers inherit or override per[orchestration.worker.<name>.llm].aura.usagegains optionalcache_read_input_tokens/cache_creation_input_tokens— sub-counts ofprompt_tokens, omitted entirely when no provider reports cache usage, so existing consumers see byte-identical payloads. Orchestration accumulates per-worker turns into the same totals via the widenedStreamItem::TurnUsage.in N (M cached) / out K; the split lands in the replayable event log (old logs still parse).gen_ai.usage.cache_*span attributes translate tollm.token_count.prompt_details.cache_read/cache_write, which Phoenix (v13+) renders in the token breakdown and prices.=0.3.10to the fork's vendored copy at the same version, retiring the 0.3.11-semver pin hazard.Breaking change (aura-events API)
The wire format is backward compatible (new fields are optional and omitted when absent), but the
aura-eventsRust API breaks for external consumers:AuraStreamEvent::usage()takes a newcache_usage: Option<(u64, u64)>parameter.AuraStreamEvent::Usagevariant gains two fields, which breaks exhaustive struct patterns even if the constructor change were papered over — so no purely additive option exists.aura-cli'sStreamHandler::on_usagegains the same parameter (all in-tree impls updated).Accepted as a 0.2.x break rather than adding a parallel
usage_with_cacheconstructor (which would only cover half the break) or rewriting history for aBREAKING CHANGEfooter. External matchers add../None; this note is the changelog record of the break.Testing
cargo test --workspacegreen (2149); clippy + fmt clean; new unit tests for config parsing, usage accumulation, event serde (including the untagged-enum ordering guard), status-line rendering, and the OpenInference translation.cache_controlonly when the flag is set; Bedrock requests carrycachePoint {"type": "default"}.us.anthropic.claude-sonnet-5): cold run reports the 8.8k-token prefix as cache creation, warm run reads all of it back;aura.usage, the status line, and Phoenix's token breakdown all agree with the provider totals.Refs: #630