Summary
The Anthropic request tagger in InstrumentationSemConv.tagAnthropicRequest() silently drops the system prompt from braintrust.input_json when it is provided in array-of-content-blocks format. This array format is required when using Anthropic prompt caching on system prompts (each block needs a cache_control marker), and is explicitly documented by both Anthropic and Braintrust.
When system is a plain string (e.g. "system": "You are helpful"), it is correctly appended to the input array as a synthetic {role: "system", content: "..."} entry. But when system is an array (e.g. "system": [{"type":"text","text":"You are helpful","cache_control":{"type":"ephemeral"}}]), the guard condition fails and the system prompt is silently omitted.
What is missing
In InstrumentationSemConv.tagAnthropicRequest() (lines 192–199):
if (requestJson.has("system")
&& !requestJson.get("system").isNull()
&& !requestJson.get("system").asText().isEmpty()) {
var systemNode = BraintrustJsonMapper.get().createObjectNode();
systemNode.put("role", "system");
systemNode.set("content", requestJson.get("system"));
inputArray.add(systemNode);
}
The problem is requestJson.get("system").asText() — for a JSON array node, Jackson's asText() returns "" (empty string). The .isEmpty() check then evaluates to true, and the entire block is skipped. The systemNode.set("content", ...) on line 197 would correctly handle both string and array values, but it is never reached.
Fix
Replace the string-based emptiness check with a type-aware check:
if (requestJson.has("system") && !requestJson.get("system").isNull()) {
JsonNode systemValue = requestJson.get("system");
boolean isEmpty = systemValue.isTextual() && systemValue.asText().isEmpty();
if (!isEmpty) {
var systemNode = BraintrustJsonMapper.get().createObjectNode();
systemNode.put("role", "system");
systemNode.set("content", systemValue);
inputArray.add(systemNode);
}
}
Impact
When a user enables Anthropic prompt caching on their system prompt (a key cost-optimization feature), the system prompt disappears from the Braintrust span input. The trace shows the user messages but no system instructions, making it impossible to understand the model's behavior from the trace alone.
This affects:
- Direct Anthropic SDK calls via
BraintrustAnthropic.wrapAnthropic() with system in array format
- Any request using
cache_control on system prompt blocks
Non-streaming and streaming calls are both affected (the request tagging is shared).
Braintrust docs status
Upstream sources
Local files inspected
braintrust-sdk/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java — lines 192–199 (tagAnthropicRequest: asText() on array returns "", causing skip)
braintrust-sdk/instrumentation/anthropic_2_2_0/src/main/java/dev/braintrust/instrumentation/anthropic/v2_2_0/TracingHttpClient.java — delegates to InstrumentationSemConv
braintrust-sdk/instrumentation/anthropic_2_2_0/src/test/java/dev/braintrust/instrumentation/anthropic/v2_2_0/BraintrustAnthropicTest.java — no test uses array-format system prompts
Summary
The Anthropic request tagger in
InstrumentationSemConv.tagAnthropicRequest()silently drops thesystemprompt frombraintrust.input_jsonwhen it is provided in array-of-content-blocks format. This array format is required when using Anthropic prompt caching on system prompts (each block needs acache_controlmarker), and is explicitly documented by both Anthropic and Braintrust.When
systemis a plain string (e.g."system": "You are helpful"), it is correctly appended to the input array as a synthetic{role: "system", content: "..."}entry. But whensystemis an array (e.g."system": [{"type":"text","text":"You are helpful","cache_control":{"type":"ephemeral"}}]), the guard condition fails and the system prompt is silently omitted.What is missing
In
InstrumentationSemConv.tagAnthropicRequest()(lines 192–199):The problem is
requestJson.get("system").asText()— for a JSON array node, Jackson'sasText()returns""(empty string). The.isEmpty()check then evaluates totrue, and the entire block is skipped. ThesystemNode.set("content", ...)on line 197 would correctly handle both string and array values, but it is never reached.Fix
Replace the string-based emptiness check with a type-aware check:
Impact
When a user enables Anthropic prompt caching on their system prompt (a key cost-optimization feature), the system prompt disappears from the Braintrust span input. The trace shows the user messages but no system instructions, making it impossible to understand the model's behavior from the trace alone.
This affects:
BraintrustAnthropic.wrapAnthropic()withsystemin array formatcache_controlon system prompt blocksNon-streaming and streaming calls are both affected (the request tagging is shared).
Braintrust docs status
cache_control: supportedUpstream sources
systemas array of content blocks withcache_controlmarkers as the standard pattern for caching system promptssystemfield accepts both string andArray<ContentBlockParam>formatsMessageCreateParams.system()accepts bothStringandList<TextBlockParam>— both route through the same HTTP clientLocal files inspected
braintrust-sdk/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java— lines 192–199 (tagAnthropicRequest:asText()on array returns"", causing skip)braintrust-sdk/instrumentation/anthropic_2_2_0/src/main/java/dev/braintrust/instrumentation/anthropic/v2_2_0/TracingHttpClient.java— delegates toInstrumentationSemConvbraintrust-sdk/instrumentation/anthropic_2_2_0/src/test/java/dev/braintrust/instrumentation/anthropic/v2_2_0/BraintrustAnthropicTest.java— no test uses array-format system prompts