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
  •  
  •  
  •  
24 changes: 19 additions & 5 deletions braintrust-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

plugins {
id 'java'
id 'org.openapi.generator' version '7.14.0'
id 'org.openapi.generator' version '7.27.0'
}

java {
Expand Down Expand Up @@ -70,11 +70,16 @@ openApiGenerate {
// Custom templates under src/generator/ (not src/main/resources, so they don't
// end up on the runtime classpath or in the published JAR — they're only needed
// at code-generation time). See src/generator/templates for the diffs from upstream.
templateDir = "${projectDir}/src/generator/templates/Java"
templateDir = layout.projectDirectory.dir('src/generator/templates/Java')

inputSpec = normalizedSpecFile.map { it.asFile.absolutePath }
inputSpec = normalizedSpecFile

outputDir = generatedSourcesDir.map { it.asFile.absolutePath }
outputDir = generatedSourcesDir
// Remove models and endpoints no longer present in the pinned spec.
cleanupOutput = true

// Adapt OpenAPI 3.1 schema types to the generator's internal representation.
openapiNormalizer = [NORMALIZE_31SPEC: 'true']

apiPackage = 'dev.braintrust.openapi.api'
modelPackage = 'dev.braintrust.openapi.model'
Expand Down Expand Up @@ -144,7 +149,7 @@ openApiGenerate {
// referenced schema, so a genuinely-narrowing allOf is left alone.

tasks.register('normalizeOpenApiSpec') {
description = 'Collapses redundant allOf-over-$ref-plus-composition nodes before generation.'
description = 'Normalizes license metadata and redundant allOf compositions before generation.'
group = 'Build'

def rawSpec = openApiSpecFile
Expand All @@ -156,6 +161,15 @@ tasks.register('normalizeOpenApiSpec') {
def yaml = new org.yaml.snakeyaml.Yaml()
def spec = yaml.load(rawSpec.get().asFile.text)

// Swagger Parser requires identifier or url for OAS 3.1 licenses, although both are
// optional. Supply the SPDX identifier for the spec's known license without replacing
// upstream metadata or setting identifier alongside its mutually exclusive url field.
def license = spec?.info?.license
if (spec?.openapi?.startsWith('3.1.') && license?.name == 'Apache 2.0'
&& !license.containsKey('identifier') && !license.containsKey('url')) {
license.identifier = 'Apache-2.0'
}

// The branches of a component schema's own anyOf/oneOf, or null if it isn't a composition.
def declaredBranches = { String refName ->
def target = spec?.components?.schemas?.get(refName)
Expand Down

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions braintrust-api/src/generator/templates/PATCHES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Custom Mustache Template Patches

These templates are forked from **openapi-generator v7.14.0** and contain targeted
patches to fix bugs or add functionality not present upstream. They live under
These templates contain targeted patches to fix bugs or add functionality not present
upstream. Their base versions are recorded below; both were compared against v7.27.0.
The generator runs v7.27.0 with Jackson 2. They live under
`src/generator/` (not `src/main/resources`) so they are only available at
code-generation time and do not appear on the runtime classpath or in the published JAR.

Expand All @@ -15,6 +16,10 @@ When upgrading the generator version, diff each file against the new upstream te

**Upstream:** `Java/libraries/native/anyof_model.mustache` @ v7.14.0

Retained for the explicit variant API below. Upstream v7.27.0 includes container
`TypeReference` deserialization, but not our `SchemaType`, explicit constructor, or
named variant getters. Its Jackson 3 branches are not needed by this build.

### Patch 1 — TypeReference for container variants in deserializer
The upstream deserializer calls `readValueAs(List.class)` / `readValueAs(Map.class)` for
container-typed anyOf variants, using the raw erased type. This causes silent
Expand Down Expand Up @@ -64,7 +69,7 @@ parameter names or code that does not compile.

## api.mustache

**Upstream:** `Java/libraries/native/api.mustache` @ v7.14.0
**Upstream:** `Java/libraries/native/api.mustache` @ v7.27.0

### Patch 1 — anyOf/oneOf model query parameters are serialized via `parameterToPairs`
In the `isExplode`/`!hasVars`/`isModel` branch, the upstream template serializes the parameter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,8 @@ void projectScoreCategories_getVariantType_weighted_viaConstructor() {

@Test
void projectScoreCategories_getVariantType_minimum_viaConstructor() {
// Explicitly declare the List<String> variant as Minimum — this is the key fix.
// Without the (SchemaType, Object) constructor, getVariantType() would incorrectly
// return Categorical for both List variants due to erasure.
// Explicit variants distinguish List<String> from List<ProjectScoreCategory>
// despite their identical raw type after erasure.
ProjectScoreCategories c =
new ProjectScoreCategories(
ProjectScoreCategories.SchemaType.Minimum, List.of("pass", "fail"));
Expand All @@ -200,9 +199,7 @@ void projectScoreCategories_getVariantType_minimum_viaConstructor() {

@Test
void projectScoreCategories_getVariantType_viaDeserialization_minimum() throws Exception {
// Uses TypeReference<List<String>> so Jackson fails fast on object elements,
// correctly distinguishing Minimum (List<String>) from Categorical
// (List<ProjectScoreCategory>)
// TypeReference distinguishes string elements from ProjectScoreCategory objects.
ProjectScoreCategories c =
mapper.readValue("[\"pass\",\"fail\"]", ProjectScoreCategories.class);
assertEquals(ProjectScoreCategories.SchemaType.Minimum, c.getVariantType());
Expand Down Expand Up @@ -250,15 +247,4 @@ void projectScoreCategories_typedGetter_weighted() {
new ProjectScoreCategories(ProjectScoreCategories.SchemaType.Weighted, weights);
assertEquals(weights, c.getWeightedInstance());
}

@Test
void projectScoreCategories_schemaType_dataType_field() {
// Each enum constant carries the full generic type as .dataType
assertEquals(
"List<ProjectScoreCategory>",
ProjectScoreCategories.SchemaType.Categorical.dataType);
assertEquals(
"Map<String, BigDecimal>", ProjectScoreCategories.SchemaType.Weighted.dataType);
assertEquals("List<String>", ProjectScoreCategories.SchemaType.Minimum.dataType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,25 @@
* Tests that prompt caching metrics (per-TTL breakdown) are correctly extracted from Anthropic API
* responses and attached to spans.
*
* <p>Uses a cache-buster nonce in the system prompt to guarantee cache misses, ensuring {@code
* cache_creation_input_tokens} is always positive. In VCR replay mode the nonce is a fixed string
* so cassette matching still works.
* <p>Uses a random nonce for live requests and a client-specific stable nonce for recording and
* replay. Recording requires a cold cache; repeated recordings must wait for the cache TTLs.
*/
public class BraintrustAnthropicPromptCachingTest {
private static final String TEST_MODEL = "claude-sonnet-4-5-20250929";
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

/**
* Nonce injected into system prompts to bust Anthropic's server-side prompt cache. Random UUID
* when running live ({@code VCR_MODE=off}), fixed string otherwise so VCR cassettes match.
* Nonce injected into system prompts to isolate each client's Anthropic prompt cache. Random
* UUID in {@code VCR_MODE=off}, otherwise the versioned test class name for cassette matching.
*/
private static final String VCR_NONCE;

static {
String vcrMode = System.getenv().getOrDefault("VCR_MODE", "replay").toUpperCase();
VCR_NONCE = "OFF".equals(vcrMode) ? UUID.randomUUID().toString() : "vcr-mode";
VCR_NONCE =
"OFF".equals(vcrMode)
? UUID.randomUUID().toString()
: BraintrustAnthropicPromptCachingTest.class.getName();
}

@BeforeAll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,25 @@
* Tests that prompt caching metrics (per-TTL breakdown) are correctly extracted from Anthropic API
* responses and attached to spans.
*
* <p>Uses a cache-buster nonce in the system prompt to guarantee cache misses, ensuring {@code
* cache_creation_input_tokens} is always positive. In VCR replay mode the nonce is a fixed string
* so cassette matching still works.
* <p>Uses a random nonce for live requests and a client-specific stable nonce for recording and
* replay. Recording requires a cold cache; repeated recordings must wait for the cache TTLs.
*/
public class BraintrustAnthropicPromptCachingTest {
private static final String TEST_MODEL = "claude-sonnet-4-5-20250929";
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

/**
* Nonce injected into system prompts to bust Anthropic's server-side prompt cache. Random UUID
* when running live ({@code VCR_MODE=off}), fixed string otherwise so VCR cassettes match.
* Nonce injected into system prompts to isolate each client's Anthropic prompt cache. Random
* UUID in {@code VCR_MODE=off}, otherwise the versioned test class name for cassette matching.
*/
private static final String VCR_NONCE;

static {
String vcrMode = System.getenv().getOrDefault("VCR_MODE", "replay").toUpperCase();
VCR_NONCE = "OFF".equals(vcrMode) ? UUID.randomUUID().toString() : "vcr-mode";
VCR_NONCE =
"OFF".equals(vcrMode)
? UUID.randomUUID().toString()
: BraintrustAnthropicPromptCachingTest.class.getName();
}

@BeforeAll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ void testImageGeneration() {
client.images()
.generate(
ImageGenerateParams.builder()
.model(ImageModel.GPT_IMAGE_1)
.model(ImageModel.of("gpt-image-2.5-flare"))
.prompt(
"A small blue circle on a plain white"
+ " background.")
Expand All @@ -148,7 +148,7 @@ void testImageGeneration() {
} finally {
parent.end();
}
assertSingleMediaSpan(parent, "images/generations", "gpt-image-1");
assertSingleMediaSpan(parent, "images/generations", "gpt-image-2.5-flare");
}

@Test
Expand Down Expand Up @@ -491,7 +491,7 @@ void testResponses() {

var request =
ResponseCreateParams.builder()
.model("o4-mini")
.model("gpt-6-luna")
.reasoning(
Reasoning.builder()
.effort(ReasoningEffort.LOW)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,15 @@ public Optional<DatasetCase<INPUT, OUTPUT>> next() {
OUTPUT expected = outputConverter.apply(event.getExpected());

var metadataObj = event.getMetadata();
// InsertProjectLogsEventMetadata extends HashMap<String,Object>. Jackson stores
// unknown fields in the HashMap base (not in additionalProperties) for Map subclasses,
// so copy from both sources defensively.
Map<String, Object> metadata;
if (metadataObj != null) {
metadata = new HashMap<>(metadataObj);
metadata = new HashMap<>();
if (metadataObj.getAdditionalProperties() != null) {
metadata.putAll(metadataObj.getAdditionalProperties());
}
if (metadataObj.getModel_JsonNullable().isPresent()) {
metadata.put("model", metadataObj.getModel());
}
} else {
metadata = Map.of();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ void testMetadataPopulatedFromDatasetRow() {
"input": "test input",
"expected": "test output",
"metadata": {
"model": "gpt-4o",
"performance_strategy": "TURBO",
"userId": "user123"
}
Expand All @@ -260,9 +261,9 @@ void testMetadataPopulatedFromDatasetRow() {

assertEquals(1, cases.size());
Map<String, Object> metadata = cases.get(0).metadata();
assertFalse(metadata.isEmpty(), "metadata should not be empty");
assertEquals("TURBO", metadata.get("performance_strategy"));
assertEquals("user123", metadata.get("userId"));
assertEquals(
Map.of("model", "gpt-4o", "performance_strategy", "TURBO", "userId", "user123"),
metadata);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ private void executeResponses(LlmSpanSpec spec, SpecClientContext ctx) throws Ex
// seed it from the first request.
String defaultModel =
spec.requests().isEmpty()
? "o4-mini"
? "gpt-6-luna"
: String.valueOf(spec.requests().get(0).get("model"));
OpenAiResponsesChatModel model =
BraintrustLangchain.wrap(
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ org.gradle.daemon=true
org.gradle.warning.mode=summary

# braintrust-spec git ref (SHA or tag) used by btx tests
braintrustSpecRef=a751e325b60c1d9e84c5aaaf49f990496d2fe7da
braintrustSpecRef=c3c80aa77cc722164dc6748400853b9a5b2512be

# braintrust-openapi commit SHA used by braintrust-api
braintrustOpenApiRef=8d6e24ce78d8e33cbf9ba5f35d2fbc71749306d7
braintrustOpenApiRef=4cc0615694512d6e77fce6bff2c70d1531208daa

# Let Gradle locate local JDKs and download one if needed
org.gradle.java.installations.auto-detect=true
Expand Down
8 changes: 8 additions & 0 deletions test-harness/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ ext {
}

dependencies {
testImplementation 'org.wiremock:wiremock:3.13.1'
testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}"
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// testFixtures dependencies — everything lives in testFixtures source set
testFixturesImplementation project(":braintrust-api")
testFixturesImplementation project(":braintrust-sdk")
Expand All @@ -48,3 +52,7 @@ dependencies {
testFixturesCompileOnly 'software.amazon.awssdk:bedrockruntime:2.30.0'
testFixturesCompileOnly 'software.amazon.awssdk:netty-nio-client:2.30.0'
}

test {
useJUnitPlatform()
}
Loading
Loading