Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 72 additions & 3 deletions skills/instrumentation-spec/references/features/prompt-cache.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Prompt caching

> **Provider:** Anthropic
> **Upstream reference:** [Anthropic prompt caching docs](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)
> **Providers:** Anthropic, AWS Bedrock
> **Upstream references:** [Anthropic prompt caching docs](https://platform.claude.com/docs/en/build-with-claude/prompt-caching), [Bedrock prompt caching docs](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html)
> **Conformance tests:**
> - [`test/llm_span/anthropic/prompt_caching_5m.yaml`](../../test/llm_span/anthropic/prompt_caching_5m.yaml) — default 5m TTL, no beta header
> - [`test/llm_span/anthropic/prompt_caching_1h.yaml`](../../test/llm_span/anthropic/prompt_caching_1h.yaml) — extended 1h TTL, requires `extended-cache-ttl-2025-04-11` beta
> - [`test/llm_span/bedrock/prompt_caching.yaml`](../../test/llm_span/bedrock/prompt_caching.yaml) — Bedrock `cachePoint`, covering a cache write and a cache read

## Overview

Expand Down Expand Up @@ -37,7 +38,7 @@ SDK implementations **MUST** treat the nested field as optional. A missing `cach

---

## Braintrust metric mapping
## Braintrust metric mapping (Anthropic)

SDKs **MUST** emit the following span metrics. All are optional — omit any metric whose source field is absent from the Anthropic response.

Expand Down Expand Up @@ -101,6 +102,74 @@ Consequences for SDK implementors:

---

## AWS Bedrock

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Disclaimer: this was written with an LLM, but I reviewed it myself and it seems correct (and the spec test also included in this PR gives me more confidence)


Bedrock exposes prompt caching through the Converse API. The mechanics differ from Anthropic's in
three ways that matter to instrumentation, even when the underlying model is a Claude model.

### Request shape

Bedrock marks a cacheable prefix with a standalone `cachePoint` block appended to `system`,
`messages`, or `toolConfig.tools`, rather than attaching `cache_control` to an existing block:

```json
{
"system": [
{ "text": "<long cacheable prefix>" },
{ "cachePoint": { "type": "default" } }
]
}
```

SDKs **MUST** pass `cachePoint` blocks through unchanged, and **MUST NOT** synthesize them.

### No TTL tiers

Bedrock exposes no TTL selection — there is no equivalent of Anthropic's 5m/1h split. Bedrock spans
therefore **MUST** emit the aggregate `prompt_cache_creation_tokens` metric and **MUST NOT** emit
`prompt_cache_creation_5m_tokens` or `prompt_cache_creation_1h_tokens`.

### Braintrust metric mapping

| Braintrust metric | Source on Converse `usage` | Notes |
| ------------------------------ | -------------------------- | ------------------------------------------- |
| `prompt_cached_tokens` | `cacheReadInputTokens` | Cache reads |
| `prompt_cache_creation_tokens` | `cacheWriteInputTokens` | Cache writes; no TTL breakdown exists |

Bedrock also returns `cacheReadInputTokenCount` and `cacheWriteInputTokenCount` as aliases of the
same two values. SDKs **SHOULD** read the `...InputTokens` spelling and **MUST NOT** emit both.

`usage.cacheDetails` (per-checkpoint `{inputTokens, ttl}` entries) has no metric mapping: a metric
must be a single number, and the entries do not distinguish reads from writes. When captured, keep
it as provider metadata.

### Totals

Bedrock reports `inputTokens` **exclusive** of cache reads and writes, but folds both into
`totalTokens`. So `prompt_tokens` **MUST** roll the cache counts back in, while `tokens` **MUST**
preserve the provider's own total rather than recomputing it:

```
prompt_tokens = inputTokens + cacheReadInputTokens + cacheWriteInputTokens
completion_tokens = outputTokens
tokens = totalTokens
```

Because `totalTokens` is reported independently, it doubles as a cross-check on the sum: a correct
mapping always satisfies `tokens == prompt_tokens + completion_tokens`. A conformant implementation
run against the Bedrock conformance test produces, on the cache-write turn:

```
inputTokens 12 + cacheWriteInputTokens 1175 + outputTokens 5 == totalTokens 1192
```

and the identical relationship on the cache-read turn with the 1175 counted as a read. An SDK that
copies `inputTokens` straight into `prompt_tokens` reports 12 instead of 1187 — a ~100x undercount
that flows directly into estimated cost, and which leaves the cache metrics larger than the total
they are defined to be a subset of.

---

## Wire format

### Metrics on events
Expand Down
17 changes: 17 additions & 0 deletions test/llm_span/anthropic/prompt_caching_1h.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,23 @@ expected_brainstore_spans:
version: !fn is_non_empty_string
instrumentation:
name: !fn is_non_empty_string
# The system prompt must survive into the span input. Anthropic requires the
# array-of-content-blocks form of `system` to attach `cache_control`, so this
# assertion is what distinguishes a real prompt-caching request from a plain
# one: an SDK that only understands the plain-string form of `system` will
# drop the prompt here while still passing every other assertion in this file.
# The system entry is appended after the messages, matching `messages.yaml`.
input:
- role: user
content: What is the capital of France?
- role: system
content:
# `text` carries a per-run cache-buster, so only its presence is asserted.
- type: text
text: !fn is_non_empty_string
cache_control:
type: ephemeral
ttl: 1h
output:
content:
- text: !fn is_non_empty_string
Expand Down
17 changes: 17 additions & 0 deletions test/llm_span/anthropic/prompt_caching_5m.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,23 @@ expected_brainstore_spans:
version: !fn is_non_empty_string
instrumentation:
name: !fn is_non_empty_string
# The system prompt must survive into the span input. Anthropic requires the
# array-of-content-blocks form of `system` to attach `cache_control`, so this
# assertion is what distinguishes a real prompt-caching request from a plain
# one: an SDK that only understands the plain-string form of `system` will
# drop the prompt here while still passing every other assertion in this file.
# The system entry is appended after the messages, matching `messages.yaml`.
input:
- role: user
content: What is the capital of France?
- role: system
content:
# `text` carries a per-run cache-buster, so only its presence is asserted.
- type: text
text: !fn is_non_empty_string
cache_control:
type: ephemeral
ttl: 5m
output:
content:
- text: !fn is_non_empty_string
Expand Down
Loading