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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ void beforeEach() {

static Stream<Arguments> modelProvider() {
return Stream.of(
Arguments.of("us.anthropic.claude-3-haiku-20240307-v1:0"),
Arguments.of("us.anthropic.claude-haiku-4-5-20251001-v1:0"),
Arguments.of("us.amazon.nova-lite-v1:0"));
}

Expand Down Expand Up @@ -94,7 +94,7 @@ void converseProducesLlmSpan(String modelId) {
@Test
@SneakyThrows
void converseStreamProducesLlmSpan() {
String modelId = "us.anthropic.claude-3-haiku-20240307-v1:0";
String modelId = "us.anthropic.claude-haiku-4-5-20251001-v1:0";

try (var client = bedrockUtils.asyncClientBuilder().build()) {
var accumulatedText = new AtomicReference<>(new StringBuilder());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import org.junit.jupiter.api.Test;

public class BraintrustGenAITest {
private static final String MODEL_ID = "gemini-3.1-flash-lite-preview";
private static final String MODEL_ID = "gemini-3.1-flash-lite";
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

@BeforeAll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ private static void tagOpenAIResponse(
if (usage.has("completion_tokens"))
metrics.put("completion_tokens", usage.get("completion_tokens"));
if (usage.has("total_tokens")) metrics.put("tokens", usage.get("total_tokens"));
if (usage.has("prompt_tokens_details")) {
JsonNode details = usage.get("prompt_tokens_details");
if (details.has("cached_tokens")) {
metrics.put("prompt_cached_tokens", details.get("cached_tokens"));
}
}
// Responses API field names
if (usage.has("input_tokens")) metrics.put("prompt_tokens", usage.get("input_tokens"));
if (usage.has("output_tokens"))
Expand All @@ -174,6 +180,12 @@ private static void tagOpenAIResponse(
"tokens",
usage.get("input_tokens").asLong() + usage.get("output_tokens").asLong());
}
if (usage.has("input_tokens_details")) {
JsonNode details = usage.get("input_tokens_details");
if (details.has("cached_tokens")) {
metrics.put("prompt_cached_tokens", details.get("cached_tokens"));
}
}
// Reasoning tokens (Responses API)
if (usage.has("output_tokens_details")) {
JsonNode details = usage.get("output_tokens_details");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ class DevserverTest {
private static String remoteScorerFunctionId;
// Resolved scorer name (set in setUp once the project name is known).
private static String REMOTE_SCORER_NAME;
// The name embedded in the remote scorer's return payload (see REMOTE_SCORER_CODE). When the
// remote invoke actually executes (VCR record/off), the score is keyed by this name. In replay
// mode the invoke request cannot be matched by a cassette (its body embeds live span IDs via
// `parent`), so the scorer falls back to scoreForScorerException and the score is keyed by
// REMOTE_SCORER_NAME instead. Assertions must accept both.
private static final String REMOTE_SCORER_PAYLOAD_NAME = "typescript exact match";

@BeforeAll
static void setUp() throws Exception {
Expand Down Expand Up @@ -336,10 +342,18 @@ void testStreamingEval() throws Exception {
assertEquals(0.7, simpleScorer.get("score").asDouble(), 0.001);

// Verify remote scorer (returns 0.0 because output "java-fruit" != expected
// "fruit"/"vegetable")
assertTrue(scores.has(REMOTE_SCORER_NAME), "Summary should have remote scorer");
JsonNode remoteScorerResult = scores.get(REMOTE_SCORER_NAME);
assertEquals(REMOTE_SCORER_NAME, remoteScorerResult.get("name").asText());
// "fruit"/"vegetable"). Keyed by the payload name on a real invoke (record/off) or
// by the resolved scorer name when replay falls back on an unmatched invoke request.
String remoteScorerKey =
scores.has(REMOTE_SCORER_PAYLOAD_NAME)
? REMOTE_SCORER_PAYLOAD_NAME
: REMOTE_SCORER_NAME;
assertTrue(
scores.has(remoteScorerKey),
"Summary should have remote scorer under '%s' or '%s' -- got: %s"
.formatted(REMOTE_SCORER_PAYLOAD_NAME, REMOTE_SCORER_NAME, scores));
JsonNode remoteScorerResult = scores.get(remoteScorerKey);
assertEquals(remoteScorerKey, remoteScorerResult.get("name").asText());
assertEquals(0.0, remoteScorerResult.get("score").asDouble(), 0.001);
}

Expand Down Expand Up @@ -515,10 +529,17 @@ void testStreamingEval() throws Exception {
output.has("simple_scorer"), "Output should contain simple_scorer results");
assertEquals(0.7, output.get("simple_scorer").asDouble(), 0.001);
} else {
// Keyed by the payload name on a real invoke (record/off) or by the resolved
// scorer name when replay falls back on an unmatched invoke request.
String remoteScorerKey =
output.has(REMOTE_SCORER_PAYLOAD_NAME)
? REMOTE_SCORER_PAYLOAD_NAME
: REMOTE_SCORER_NAME;
assertTrue(
output.has(REMOTE_SCORER_NAME),
"Output should contain remote scorer results");
assertEquals(0.0, output.get(REMOTE_SCORER_NAME).asDouble(), 0.001);
output.has(remoteScorerKey),
"Output should contain remote scorer results under '%s' or '%s' -- got: %s"
.formatted(REMOTE_SCORER_PAYLOAD_NAME, REMOTE_SCORER_NAME, output));
assertEquals(0.0, output.get(remoteScorerKey).asDouble(), 0.001);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ static Stream<Arguments> specs() throws Exception {
}

final AtomicInteger totalExpectedSpans = new AtomicInteger(0);
var pool = new ForkJoinPool(3);
// In replay mode, execution must be sequential: cassettes for identical request bodies
// (e.g. the same spec executed for multiple clients) are recorded as stateful WireMock
// scenarios,
// and concurrent requests race on the scenario state, causing intermittent
// "Request was not matched" failures.
boolean isReplay = TestHarness.getVcrMode().equals(dev.braintrust.VCR.VcrMode.REPLAY);
var pool = new ForkJoinPool(isReplay ? 1 : 3);
var results =
pool.submit(
() ->
Expand Down
13 changes: 13 additions & 0 deletions btx/src/test/java/dev/braintrust/sdkspecimpl/SpanValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,24 @@ public static void validate(
}
}

/**
* Top-level expected-span fields that are not validated (yet).
*
* <p>{@code context} (span-origin provenance, added in spec v0.0.8) is skipped because the Java
* SDK does not emit {@code context.span_origin} yet, and the backend's OTLP ingestion does not
* extract it from {@code braintrust.context_json} / {@code braintrust.sdk.*} attributes. TODO:
* remove this skip once span-origin support is implemented.
*/
private static final java.util.Set<String> SKIPPED_FIELDS = java.util.Set.of("context");

@SuppressWarnings("unchecked")
private static void validateSpan(
Map<String, Object> actual, Map<String, Object> expected, String context) {
for (Map.Entry<String, Object> entry : expected.entrySet()) {
String field = entry.getKey();
if (SKIPPED_FIELDS.contains(field)) {
continue;
}
Object expectedValue = entry.getValue();
Object actualValue = actual.get(field);
validateValue(actualValue, expectedValue, context + "." + field);
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ org.gradle.daemon=true
org.gradle.warning.mode=summary

# braintrust-spec git ref (SHA or tag) used by btx tests
braintrustSpecRef=v0.0.7
braintrustSpecRef=v0.0.9

# braintrust-openapi commit SHA used by braintrust-api
braintrustOpenApiRef=64b79cb9122f50a74eac98ea86c3ec1858c0cdd1
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
event: message_start
data: {"type":"message_start","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01UvZSEM5WDpRsZ9ziBHPQyU","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"}} }
data: {"type":"message_start","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011Cco5NcpU3LywLYSgUff2e","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"}} }

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }

event: ping
data: {"type": "ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The capital of France is **"} }
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The capital of France is **"} }

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Paris**. It is located in the north-central part of the country along the Seine River and is the largest city in France."} }
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Paris**. It is located in the north-central part of the country along the Seine River and is the largest city in France."} }

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris is known for its iconic landmarks, including the Eiffel Tower, Notre"} }

event: content_block_stop
data: {"type":"content_block_stop","index":0}
data: {"type":"content_block_stop","index":0 }

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null,"stop_details":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":36} }
data: {"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null,"stop_details":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":50} }

event: message_stop
data: {"type":"message_stop" }
data: {"type":"message_stop" }

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"model":"claude-haiku-4-5-20251001","id":"msg_011Cco5GN8m7j8M4CbaGkgWt","type":"message","role":"assistant","content":[{"type":"text","text":"I can see that you've shared a PDF attachment, but the document appears to be blank or contains no visible text or images on the page shown. \n\nTo help you better, could you please:\n1. Verify that the file uploaded correctly\n2. Check if there's content on other pages of the PDF\n3. Re-upload the document if it may have been corrupted\n\nOnce you share a document with visible content, I'll be happy to provide a brief description of it."}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1592,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":105,"service_tier":"standard","inference_geo":"not_available"}}
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
event: message_start
data: {"type":"message_start","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01PR2PH2X2CucudJwVnFKKSV","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"}} }
data: {"type":"message_start","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011Cco5Nzt21bPoAtzB4CYuT","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"}} }

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }

event: ping
data: {"type": "ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The capital of France is **"} }
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The capital of France is **"} }

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Paris**. It is located in the north-central part of the country along the Seine River and is the largest city in France."} }
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Paris**. It is located in the north-central part of the country along the Seine River and is the largest city in France."} }

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris is known for its iconic landmarks, including the Eiffel Tower, Notre"} }

event: content_block_stop
data: {"type":"content_block_stop","index":0 }
data: {"type":"content_block_stop","index":0 }

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null,"stop_details":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":36} }
data: {"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null,"stop_details":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":50} }

event: message_stop
data: {"type":"message_stop" }
data: {"type":"message_stop" }

Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"model":"claude-haiku-4-5-20251001","id":"msg_014PvFAwDsbxxvvp84aVydvV","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is **Paris**. It is located in the north-central part of the country along the Seine River and is the largest city in France."}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":36,"service_tier":"standard","inference_geo":"not_available"}}
{"model":"claude-haiku-4-5-20251001","id":"msg_011Cco5NVZQjwwP4nbaFcJfg","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is **Paris**. It is located in the north-central part of the country along the Seine River and is the largest city in France. Paris is known for its iconic landmarks, including the Eiffel Tower, Notre"}],"stop_reason":"max_tokens","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":50,"service_tier":"standard","inference_geo":"not_available"}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"model":"claude-sonnet-4-5-20250929","id":"msg_01GQgHcLysRpHj8cZnp9StUW","type":"message","role":"assistant","content":[{"type":"text","text":"Paris."}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":12,"cache_creation_input_tokens":1365,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":1365,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"not_available"}}
{"model":"claude-sonnet-4-5-20250929","id":"msg_011Cco5FsFw8io86juqvnUwx","type":"message","role":"assistant","content":[{"type":"text","text":"Paris."}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":12,"cache_creation_input_tokens":1365,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":1365,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"not_available"}}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
event: message_start
data: {"type":"message_start","message":{"model":"claude-haiku-4-5-20251001","id":"msg_019GGEJxTUz2mgwRJHze2pCp","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":22,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}} }
data: {"type":"message_start","message":{"model":"claude-haiku-4-5-20251001","id":"msg_011Cco5FLwJ7B22J5PWp29Jh","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":22,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}} }

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }

event: ping
data: {"type": "ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"1\n2\n3\n4\n5"} }
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"1\n2\n3\n4\n5"} }

event: content_block_stop
data: {"type":"content_block_stop","index":0 }
data: {"type":"content_block_stop","index":0 }

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null,"stop_details":null},"usage":{"input_tokens":22,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":13} }
Expand Down
Loading
Loading