From 3a8e18a2d0331dcf25ddbe21d4025f88e2a6092e Mon Sep 17 00:00:00 2001 From: Phil Hetzel Date: Thu, 10 Jul 2025 20:53:05 -0400 Subject: [PATCH 1/6] Fix hardcoded org ID in TriviaEvaluation.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded 'braintrustdata.com' with project.orgId() to ensure the experiment URL is constructed correctly for all environments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../java/dev/braintrust/examples/TriviaEvaluation.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java b/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java index 73d4524..9f9e3c7 100644 --- a/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java +++ b/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java @@ -65,13 +65,9 @@ public static void main(String[] args) throws Exception { java.net.URLEncoder.encode(project.name(), java.nio.charset.StandardCharsets.UTF_8) .replace("+", "%20"); var experimentUrl = - appUrl.contains("staging") - ? String.format( - "%s/app/%s/p/%s/experiments/%s", - appUrl, project.orgId(), projectNameEncoded, experimentName) - : String.format( - "%s/app/braintrustdata.com/p/%s/experiments/%s", - appUrl, projectNameEncoded, experimentName); + String.format( + "%s/app/%s/p/%s/experiments/%s", + appUrl, project.orgId(), projectNameEncoded, experimentName); System.out.println("\nExperiment " + experimentName + " is running at " + experimentUrl); From 284a177305ef70771b24c15d85c91cfcb06257c4 Mon Sep 17 00:00:00 2001 From: Phil Hetzel Date: Thu, 10 Jul 2025 20:57:11 -0400 Subject: [PATCH 2/6] Use project.name() instead of project object in experiment URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix URL construction to use project.name() for the organization identifier in the experiment URL path. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../src/main/java/dev/braintrust/examples/TriviaEvaluation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java b/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java index 9f9e3c7..c5fd8f9 100644 --- a/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java +++ b/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java @@ -67,7 +67,7 @@ public static void main(String[] args) throws Exception { var experimentUrl = String.format( "%s/app/%s/p/%s/experiments/%s", - appUrl, project.orgId(), projectNameEncoded, experimentName); + appUrl, project.name(), projectNameEncoded, experimentName); System.out.println("\nExperiment " + experimentName + " is running at " + experimentUrl); From d4d8daa2716f5aa536415d1ecea4ff061bd7c7b3 Mon Sep 17 00:00:00 2001 From: Phil Hetzel Date: Fri, 11 Jul 2025 21:06:50 -0400 Subject: [PATCH 3/6] Add automatic organization name retrieval to BraintrustConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements organization name gathering similar to the Python SDK by calling the /api/apikey/login endpoint during configuration initialization. Key changes: - Add login API method to BraintrustApiClient with proper response models - Enhance BraintrustConfig to automatically retrieve org name during build() - Fix experiment URL generation to use proper org name instead of hardcoded values - Add proper URL encoding for organization names in experiment URLs The implementation follows the Python SDK pattern: - Calls /api/apikey/login endpoint to get user's organization info - Uses the first organization by default if multiple exist - Gracefully handles API failures with warnings rather than breaking builds - No breaking changes to existing code - all BraintrustConfig instances automatically get org name functionality Fixes experiment URLs that previously showed "Optional.empty" to now display the correct organization name (e.g., "Braintrust%20Demos"). 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../braintrust/examples/TriviaEvaluation.java | 7 ++- .../braintrust/api/BraintrustApiClient.java | 13 +++++ .../braintrust/config/BraintrustConfig.java | 50 ++++++++++++++++++- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java b/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java index c5fd8f9..f6a3da7 100644 --- a/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java +++ b/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java @@ -61,13 +61,18 @@ public static void main(String[] args) throws Exception { // Show experiment URL var appUrl = config.appUrl().toString().replaceAll("/$", ""); + var orgNameEncoded = + java.net.URLEncoder.encode( + config.orgName().orElse("unknown"), + java.nio.charset.StandardCharsets.UTF_8) + .replace("+", "%20"); var projectNameEncoded = java.net.URLEncoder.encode(project.name(), java.nio.charset.StandardCharsets.UTF_8) .replace("+", "%20"); var experimentUrl = String.format( "%s/app/%s/p/%s/experiments/%s", - appUrl, project.name(), projectNameEncoded, experimentName); + appUrl, orgNameEncoded, projectNameEncoded, experimentName); System.out.println("\nExperiment " + experimentName + " is running at " + experimentUrl); diff --git a/src/main/java/dev/braintrust/api/BraintrustApiClient.java b/src/main/java/dev/braintrust/api/BraintrustApiClient.java index 108a641..581cb46 100644 --- a/src/main/java/dev/braintrust/api/BraintrustApiClient.java +++ b/src/main/java/dev/braintrust/api/BraintrustApiClient.java @@ -109,6 +109,12 @@ public CompletableFuture> listDatasets(String projectId) { .thenApply(DatasetList::datasets); } + /** Login to get user information including organization details. */ + public CompletableFuture login() { + var request = new LoginRequest(config.apiKey()); + return postAsync("/api/apikey/login", request, LoginResponse.class); + } + // Low-level HTTP methods private CompletableFuture getAsync(String path, Class responseType) { @@ -263,4 +269,11 @@ public DatasetEvent(Object input, Object output) { private record InsertEventsRequest(List events) {} public record InsertEventsResponse(int insertedCount) {} + + // User and Organization models for login functionality + public record OrganizationInfo(String id, String name) {} + + private record LoginRequest(String token) {} + + public record LoginResponse(List orgInfo) {} } diff --git a/src/main/java/dev/braintrust/config/BraintrustConfig.java b/src/main/java/dev/braintrust/config/BraintrustConfig.java index 5589412..6057de6 100644 --- a/src/main/java/dev/braintrust/config/BraintrustConfig.java +++ b/src/main/java/dev/braintrust/config/BraintrustConfig.java @@ -1,5 +1,6 @@ package dev.braintrust.config; +import dev.braintrust.api.BraintrustApiClient; import java.net.URI; import java.time.Duration; import java.util.Optional; @@ -18,6 +19,7 @@ public final class BraintrustConfig { private final URI apiUrl; private final URI appUrl; @Nullable private final String defaultProjectId; + @Nullable private final String orgName; private final boolean enableTraceConsoleLog; private final boolean debug; private final Duration requestTimeout; @@ -27,6 +29,7 @@ private BraintrustConfig(Builder builder) { this.apiUrl = builder.apiUrl; this.appUrl = builder.appUrl; this.defaultProjectId = builder.defaultProjectId; + this.orgName = builder.orgName; this.enableTraceConsoleLog = builder.enableTraceConsoleLog; this.debug = builder.debug; this.requestTimeout = builder.requestTimeout; @@ -48,6 +51,10 @@ public Optional defaultProjectId() { return Optional.ofNullable(defaultProjectId); } + public Optional orgName() { + return Optional.ofNullable(orgName); + } + public boolean enableTraceConsoleLog() { return enableTraceConsoleLog; } @@ -65,12 +72,14 @@ public static Builder builder() { return new Builder(); } - /** Creates a config from environment variables only. */ + /** Creates a config from environment variables and retrieves organization name from API. */ public static BraintrustConfig fromEnvironment() { return builder().build(); } - /** Creates a config with a consumer for customization. */ + /** + * Creates a config with a consumer for customization and retrieves organization name from API. + */ public static BraintrustConfig create(Consumer customizer) { var builder = builder(); customizer.accept(builder); @@ -82,6 +91,7 @@ public static final class Builder { private URI apiUrl; private URI appUrl; private String defaultProjectId; + private String orgName; private boolean enableTraceConsoleLog; private boolean debug; private Duration requestTimeout = Duration.ofSeconds(30); @@ -125,6 +135,11 @@ public Builder defaultProjectId(String projectId) { return this; } + public Builder orgName(String orgName) { + this.orgName = orgName; + return this; + } + public Builder enableTraceConsoleLog(boolean enable) { this.enableTraceConsoleLog = enable; return this; @@ -146,9 +161,40 @@ public BraintrustConfig build() { "API key is required. Set BRAINTRUST_API_KEY environment variable or use" + " apiKey() method."); } + + // If orgName is not already set, try to retrieve it from the API + if (orgName == null || orgName.isBlank()) { + try { + retrieveOrgName(); + } catch (Exception e) { + // Log warning but don't fail the build if org name retrieval fails + System.err.println( + "Warning: Failed to retrieve organization name: " + e.getMessage()); + } + } + return new BraintrustConfig(this); } + private void retrieveOrgName() { + // Create a temporary config without orgName to avoid circular dependency + var tempConfig = new BraintrustConfig(this); + + try (var apiClient = new BraintrustApiClient(tempConfig)) { + var loginResponse = apiClient.login().join(); + var orgInfo = loginResponse.orgInfo(); + + if (orgInfo != null && !orgInfo.isEmpty()) { + // Use the first organization by default, similar to Python SDK + this.orgName = orgInfo.get(0).name(); + } else { + throw new RuntimeException("User is not part of any organizations"); + } + } catch (Exception e) { + throw new RuntimeException("Failed to retrieve organization information", e); + } + } + private static String getEnv(String key, String defaultValue) { String value = System.getenv(key); // Trim any whitespace that might have been accidentally included From a682a78338308d73a9a7622c1596bc93fcb8173c Mon Sep 17 00:00:00 2001 From: Phil Hetzel Date: Sat, 12 Jul 2025 14:04:57 -0400 Subject: [PATCH 4/6] Fix Java pattern matching syntax for CI compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace instanceof pattern matching with traditional cast syntax to ensure compatibility with CI environment's Java version and formatter. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/main/java/dev/braintrust/api/BraintrustApiClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/braintrust/api/BraintrustApiClient.java b/src/main/java/dev/braintrust/api/BraintrustApiClient.java index 581cb46..a420dca 100644 --- a/src/main/java/dev/braintrust/api/BraintrustApiClient.java +++ b/src/main/java/dev/braintrust/api/BraintrustApiClient.java @@ -182,8 +182,8 @@ private T handleResponse(HttpResponse response, Class responseTyp } private boolean isNotFound(Throwable error) { - if (error instanceof ApiException apiException) { - return apiException.getMessage().contains("404"); + if (error instanceof ApiException) { + return ((ApiException) error).getMessage().contains("404"); } return false; } From 6465cb4f73ca9efb6b197b38e52663cef3ef144e Mon Sep 17 00:00:00 2001 From: Phil Hetzel Date: Sat, 12 Jul 2025 14:13:25 -0400 Subject: [PATCH 5/6] Add blank line in record to improve CI compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/main/java/dev/braintrust/api/BraintrustApiClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/dev/braintrust/api/BraintrustApiClient.java b/src/main/java/dev/braintrust/api/BraintrustApiClient.java index a420dca..9f2293c 100644 --- a/src/main/java/dev/braintrust/api/BraintrustApiClient.java +++ b/src/main/java/dev/braintrust/api/BraintrustApiClient.java @@ -226,6 +226,7 @@ public record CreateExperimentRequest( String name, Optional description, Optional baseExperimentId) { + public CreateExperimentRequest(String projectId, String name) { this(projectId, name, Optional.empty(), Optional.empty()); } From b7546cf89e8bbecb96844628d952cc1a6591d72e Mon Sep 17 00:00:00 2001 From: Phil Hetzel Date: Sat, 12 Jul 2025 14:16:43 -0400 Subject: [PATCH 6/6] Remove Java 11 from CI matrix to fix record syntax errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The project requires Java 17+ (configured in build.gradle) but CI was testing against Java 11 which doesn't support record types. Records were introduced in Java 14 preview and stabilized in Java 16. This fixes the spotless formatting errors in CI that were caused by Java 11 not understanding record syntax. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea6aa1f..040197e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '11', '17', '21' ] + java: [ '17', '21' ] steps: - uses: actions/checkout@v4