diff --git a/build.gradle b/build.gradle index c82af9f3..b1b7bb09 100644 --- a/build.gradle +++ b/build.gradle @@ -78,7 +78,6 @@ dependencies { // OAI instrumentation compileOnly 'com.openai:openai-java:2.8.1' testImplementation 'com.openai:openai-java:2.8.1' - implementation "io.opentelemetry.instrumentation:opentelemetry-openai-java-1.1:2.25.0-alpha" // Anthropic Instrumentation compileOnly "com.anthropic:anthropic-java:2.8.1" diff --git a/examples/src/main/java/dev/braintrust/examples/GeminiInstrumentationExample.java b/examples/src/main/java/dev/braintrust/examples/GeminiInstrumentationExample.java index da138df2..f6428c33 100644 --- a/examples/src/main/java/dev/braintrust/examples/GeminiInstrumentationExample.java +++ b/examples/src/main/java/dev/braintrust/examples/GeminiInstrumentationExample.java @@ -21,8 +21,6 @@ public static void main(String[] args) throws Exception { Braintrust braintrust = Braintrust.get(); OpenTelemetry openTelemetry = braintrust.openTelemetryCreate(); - // CLAUDE: don't change the type of geminiClient -- sdk users must use the google genai - // client in their signature, not our instrumented client. Client geminiClient = BraintrustGenAI.wrap(openTelemetry, new Client.Builder()); var tracer = openTelemetry.getTracer("my-instrumentation"); diff --git a/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java b/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java new file mode 100644 index 00000000..fa497302 --- /dev/null +++ b/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java @@ -0,0 +1,249 @@ +package dev.braintrust.instrumentation; + +import static dev.braintrust.json.BraintrustJsonMapper.toJson; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import dev.braintrust.json.BraintrustJsonMapper; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.SneakyThrows; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class InstrumentationSemConv { + public static final String PROVIDER_NAME_OPENAI = "openai"; + public static final String PROVIDER_NAME_ANTHROPIC = "anthropic"; + public static final String PROVIDER_NAME_OTHER = "generic-ai-provider"; + public static final String UNSET_LLM_SPAN_NAME = "llm"; + + // ------------------------------------------------------------------------- + // Public API — provider-dispatching entry points + // ------------------------------------------------------------------------- + + @SneakyThrows + public static void tagLLMSpanRequest( + Span span, + @NonNull String providerName, + @NonNull String baseUrl, + @NonNull List pathSegments, + @NonNull String method, + @Nullable String requestBody) { + switch (providerName) { + case PROVIDER_NAME_OPENAI -> + tagOpenAIRequest( + span, providerName, baseUrl, pathSegments, method, requestBody); + case PROVIDER_NAME_ANTHROPIC -> + tagAnthropicRequest( + span, providerName, baseUrl, pathSegments, method, requestBody); + default -> + tagOpenAIRequest( + span, providerName, baseUrl, pathSegments, method, requestBody); + } + } + + public static void tagLLMSpanResponse( + Span span, @NonNull String providerName, @NonNull String responseBody) { + tagLLMSpanResponse(span, providerName, responseBody, null); + } + + @SneakyThrows + public static void tagLLMSpanResponse( + Span span, + @NonNull String providerName, + @NonNull String responseBody, + @Nullable Long timeToFirstTokenNanoseconds) { + switch (providerName) { + case PROVIDER_NAME_OPENAI -> + tagOpenAIResponse(span, responseBody, timeToFirstTokenNanoseconds); + case PROVIDER_NAME_ANTHROPIC -> + tagAnthropicResponse(span, responseBody, timeToFirstTokenNanoseconds); + default -> tagOpenAIResponse(span, responseBody, timeToFirstTokenNanoseconds); + } + } + + public static void tagLLMSpanResponse(Span span, @NonNull Throwable responseError) { + span.setStatus(StatusCode.ERROR, responseError.getMessage()); + span.recordException(responseError); + } + + // ------------------------------------------------------------------------- + // OpenAI provider implementation + // ------------------------------------------------------------------------- + + @SneakyThrows + private static void tagOpenAIRequest( + Span span, + String providerName, + String baseUrl, + List pathSegments, + String method, + @Nullable String requestBody) { + span.updateName(getSpanName(providerName, pathSegments)); + span.setAttribute("braintrust.span_attributes", toJson(Map.of("type", "llm"))); + + Map metadata = new HashMap<>(); + metadata.put("provider", providerName); + metadata.put("request_path", String.join("/", pathSegments)); + metadata.put("request_base_uri", baseUrl); + metadata.put("request_method", method); + + if (requestBody != null) { + JsonNode requestJson = BraintrustJsonMapper.get().readTree(requestBody); + if (requestJson.has("model")) { + metadata.put("model", requestJson.get("model").asText()); + } + // Chat completions API uses "messages"; Responses API uses "input" + if (requestJson.has("messages")) { + span.setAttribute("braintrust.input_json", toJson(requestJson.get("messages"))); + } else if (requestJson.has("input") && requestJson.get("input").isArray()) { + span.setAttribute("braintrust.input_json", toJson(requestJson.get("input"))); + } + } + + span.setAttribute("braintrust.metadata", toJson(metadata)); + } + + @SneakyThrows + private static void tagOpenAIResponse( + Span span, String responseBody, @Nullable Long timeToFirstTokenNanoseconds) { + JsonNode responseJson = BraintrustJsonMapper.get().readTree(responseBody); + + // Output — chat completions API uses "choices"; Responses API uses "output" + if (responseJson.has("choices")) { + span.setAttribute("braintrust.output_json", toJson(responseJson.get("choices"))); + } else if (responseJson.has("output")) { + span.setAttribute("braintrust.output_json", toJson(responseJson.get("output"))); + } + + Map metrics = new HashMap<>(); + if (timeToFirstTokenNanoseconds != null) { + metrics.put("time_to_first_token", timeToFirstTokenNanoseconds / 1_000_000_000.0); + } + + if (responseJson.has("usage")) { + JsonNode usage = responseJson.get("usage"); + // Chat completions API field names + if (usage.has("prompt_tokens")) + metrics.put("prompt_tokens", usage.get("prompt_tokens")); + 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")); + // Responses API field names + if (usage.has("input_tokens")) metrics.put("prompt_tokens", usage.get("input_tokens")); + if (usage.has("output_tokens")) + metrics.put("completion_tokens", usage.get("output_tokens")); + if (usage.has("input_tokens") && usage.has("output_tokens")) { + metrics.put( + "tokens", + usage.get("input_tokens").asLong() + usage.get("output_tokens").asLong()); + } + // Reasoning tokens (Responses API) + if (usage.has("output_tokens_details")) { + JsonNode details = usage.get("output_tokens_details"); + if (details.has("reasoning_tokens")) { + metrics.put("completion_reasoning_tokens", details.get("reasoning_tokens")); + } + } + } + + if (!metrics.isEmpty()) { + span.setAttribute("braintrust.metrics", toJson(metrics)); + } + } + + // ------------------------------------------------------------------------- + // Anthropic provider implementation + // ------------------------------------------------------------------------- + + @SneakyThrows + private static void tagAnthropicRequest( + Span span, + String providerName, + String baseUrl, + List pathSegments, + String method, + @Nullable String requestBody) { + span.updateName(getSpanName(providerName, pathSegments)); + span.setAttribute("braintrust.span_attributes", toJson(Map.of("type", "llm"))); + + Map metadata = new HashMap<>(); + metadata.put("provider", providerName); + metadata.put("request_path", String.join("/", pathSegments)); + metadata.put("request_base_uri", baseUrl); + metadata.put("request_method", method); + + if (requestBody != null) { + JsonNode requestJson = BraintrustJsonMapper.get().readTree(requestBody); + if (requestJson.has("model")) { + metadata.put("model", requestJson.get("model").asText()); + } + // Build input array: messages + system (as a synthetic system-role entry) + if (requestJson.has("messages")) { + ArrayNode inputArray = BraintrustJsonMapper.get().createArrayNode(); + // Append messages first + requestJson.get("messages").forEach(inputArray::add); + // Append system prompt as a {role:"system", content:"..."} entry if present + if (requestJson.has("system")) { + var systemNode = BraintrustJsonMapper.get().createObjectNode(); + systemNode.put("role", "system"); + systemNode.set("content", requestJson.get("system")); + inputArray.add(systemNode); + } + span.setAttribute("braintrust.input_json", toJson(inputArray)); + } + } + + span.setAttribute("braintrust.metadata", toJson(metadata)); + } + + @SneakyThrows + private static void tagAnthropicResponse( + Span span, String responseBody, @Nullable Long timeToFirstTokenNanoseconds) { + JsonNode responseJson = BraintrustJsonMapper.get().readTree(responseBody); + + // Anthropic response is the full Message object — output it whole + span.setAttribute("braintrust.output_json", responseBody); + + Map metrics = new HashMap<>(); + if (timeToFirstTokenNanoseconds != null) { + metrics.put("time_to_first_token", timeToFirstTokenNanoseconds / 1_000_000_000.0); + } + + if (responseJson.has("usage")) { + JsonNode usage = responseJson.get("usage"); + if (usage.has("input_tokens")) metrics.put("prompt_tokens", usage.get("input_tokens")); + if (usage.has("output_tokens")) + metrics.put("completion_tokens", usage.get("output_tokens")); + if (usage.has("input_tokens") && usage.has("output_tokens")) { + metrics.put( + "tokens", + usage.get("input_tokens").asLong() + usage.get("output_tokens").asLong()); + } + } + + if (!metrics.isEmpty()) { + span.setAttribute("braintrust.metrics", toJson(metrics)); + } + } + + // ------------------------------------------------------------------------- + // Shared helpers + // ------------------------------------------------------------------------- + + private static String getSpanName(String providerName, List pathSegments) { + if (pathSegments.isEmpty()) { + return UNSET_LLM_SPAN_NAME; + } + String lastSegment = pathSegments.get(pathSegments.size() - 1); + return switch (providerName + ":" + lastSegment) { + case PROVIDER_NAME_OPENAI + ":completions" -> "Chat Completion"; + case PROVIDER_NAME_OPENAI + ":embeddings" -> "Embeddings"; + case PROVIDER_NAME_ANTHROPIC + ":messages" -> "anthropic.messages.create"; + default -> lastSegment; + }; + } +} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropic.java b/src/main/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropic.java index 8c20a8a8..1d980b20 100644 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropic.java +++ b/src/main/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropic.java @@ -1,14 +1,133 @@ package dev.braintrust.instrumentation.anthropic; import com.anthropic.client.AnthropicClient; -import dev.braintrust.instrumentation.anthropic.otel.AnthropicTelemetry; +import com.anthropic.core.ClientOptions; +import com.anthropic.core.http.HttpClient; import io.opentelemetry.api.OpenTelemetry; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import kotlin.Lazy; +import lombok.extern.slf4j.Slf4j; /** Braintrust Anthropic client instrumentation. */ +@Slf4j public final class BraintrustAnthropic { - /** Instrument Anthropic client with braintrust traces */ - public static AnthropicClient wrap(OpenTelemetry otel, AnthropicClient client) { - return AnthropicTelemetry.builder(otel).setCaptureMessageContent(true).build().wrap(client); + /** Instrument Anthropic client with Braintrust traces. */ + public static AnthropicClient wrap(OpenTelemetry openTelemetry, AnthropicClient client) { + try { + instrumentHttpClient(openTelemetry, client); + return client; + } catch (Exception e) { + log.error("failed to apply anthropic instrumentation", e); + return client; + } + } + + private static void instrumentHttpClient(OpenTelemetry openTelemetry, AnthropicClient client) { + forAllFields( + client, + fieldName -> { + try { + var field = getField(client, fieldName); + if (field instanceof ClientOptions clientOptions) { + instrumentClientOptions( + openTelemetry, clientOptions, "originalHttpClient"); + instrumentClientOptions(openTelemetry, clientOptions, "httpClient"); + } else if (field instanceof Lazy lazyField) { + var resolved = lazyField.getValue(); + forAllFieldsOfType( + resolved, + ClientOptions.class, + (clientOptions, subfieldName) -> + instrumentClientOptions( + openTelemetry, clientOptions, subfieldName)); + } else { + forAllFieldsOfType( + field, + ClientOptions.class, + (clientOptions, subfieldName) -> + instrumentClientOptions( + openTelemetry, clientOptions, subfieldName)); + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + }); + } + + private static void instrumentClientOptions( + OpenTelemetry openTelemetry, ClientOptions clientOptions, String fieldName) { + try { + HttpClient httpClient = getField(clientOptions, fieldName); + if (!(httpClient instanceof TracingHttpClient)) { + setPrivateField( + clientOptions, fieldName, new TracingHttpClient(openTelemetry, httpClient)); + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private static void forAllFields(Object object, Consumer consumer) { + if (object == null || consumer == null) return; + Class clazz = object.getClass(); + while (clazz != null && clazz != Object.class) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isSynthetic()) continue; + if (Modifier.isStatic(field.getModifiers())) continue; + consumer.accept(field.getName()); + } + clazz = clazz.getSuperclass(); + } + } + + private static void forAllFieldsOfType( + Object object, Class targetClazz, BiConsumer consumer) { + forAllFields( + object, + fieldName -> { + try { + if (targetClazz.isAssignableFrom(object.getClass())) { + consumer.accept(getField(object, fieldName), fieldName); + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + }); + } + + @SuppressWarnings("unchecked") + private static T getField(Object obj, String fieldName) + throws ReflectiveOperationException { + Class clazz = obj.getClass(); + while (clazz != null) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(obj); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } + + private static void setPrivateField(Object obj, String fieldName, Object value) + throws ReflectiveOperationException { + Class clazz = obj.getClass(); + while (clazz != null) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(obj, value); + return; + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); } } diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/TracingHttpClient.java b/src/main/java/dev/braintrust/instrumentation/anthropic/TracingHttpClient.java new file mode 100644 index 00000000..ba561c28 --- /dev/null +++ b/src/main/java/dev/braintrust/instrumentation/anthropic/TracingHttpClient.java @@ -0,0 +1,370 @@ +package dev.braintrust.instrumentation.anthropic; + +import com.anthropic.core.RequestOptions; +import com.anthropic.core.http.HttpClient; +import com.anthropic.core.http.HttpRequest; +import com.anthropic.core.http.HttpRequestBody; +import com.anthropic.core.http.HttpResponse; +import com.anthropic.helpers.MessageAccumulator; +import com.anthropic.models.messages.RawMessageStreamEvent; +import dev.braintrust.instrumentation.InstrumentationSemConv; +import dev.braintrust.json.BraintrustJsonMapper; +import dev.braintrust.trace.BraintrustTracing; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; +import org.jspecify.annotations.NonNull; + +@Slf4j +public class TracingHttpClient implements HttpClient { + private final Tracer tracer; + private final HttpClient underlying; + + public TracingHttpClient(OpenTelemetry openTelemetry, HttpClient underlying) { + this.tracer = BraintrustTracing.getTracer(openTelemetry); + this.underlying = underlying; + } + + @Override + public void close() { + underlying.close(); + } + + @Override + public @NonNull HttpResponse execute( + @NonNull HttpRequest httpRequest, @NonNull RequestOptions requestOptions) { + var span = tracer.spanBuilder(InstrumentationSemConv.UNSET_LLM_SPAN_NAME).startSpan(); + try (var ignored = span.makeCurrent()) { + var bufferedRequest = bufferRequestBody(httpRequest); + + String inputJson = + bufferedRequest.body() != null + ? readBodyAsString(bufferedRequest.body()) + : null; + + InstrumentationSemConv.tagLLMSpanRequest( + span, + InstrumentationSemConv.PROVIDER_NAME_ANTHROPIC, + bufferedRequest.baseUrl() != null ? bufferedRequest.baseUrl() : "", + bufferedRequest.pathSegments(), + bufferedRequest.method().name(), + inputJson); + + var response = underlying.execute(bufferedRequest, requestOptions); + return new TeeingStreamHttpResponse(response, span); + } catch (Exception e) { + InstrumentationSemConv.tagLLMSpanResponse(span, e); + span.end(); + throw e; + } + } + + @Override + public @NonNull CompletableFuture executeAsync( + @NonNull HttpRequest httpRequest, @NonNull RequestOptions requestOptions) { + var span = tracer.spanBuilder(InstrumentationSemConv.UNSET_LLM_SPAN_NAME).startSpan(); + try { + var bufferedRequest = bufferRequestBody(httpRequest); + String inputJson = + bufferedRequest.body() != null + ? readBodyAsString(bufferedRequest.body()) + : null; + InstrumentationSemConv.tagLLMSpanRequest( + span, + InstrumentationSemConv.PROVIDER_NAME_ANTHROPIC, + bufferedRequest.baseUrl() != null ? bufferedRequest.baseUrl() : "", + bufferedRequest.pathSegments(), + bufferedRequest.method().name(), + inputJson); + return underlying + .executeAsync(bufferedRequest, requestOptions) + .thenApply( + response -> (HttpResponse) new TeeingStreamHttpResponse(response, span)) + .whenComplete( + (response, t) -> { + if (t != null) { + // this means the future itself failed + InstrumentationSemConv.tagLLMSpanResponse(span, t); + span.end(); + } + }); + } catch (Exception e) { + InstrumentationSemConv.tagLLMSpanResponse(span, e); + span.end(); + throw e; + } + } + + // ------------------------------------------------------------------------- + // Request buffering — identical pattern to OpenAI TracingHttpClient + // ------------------------------------------------------------------------- + + private static HttpRequest bufferRequestBody(HttpRequest request) { + HttpRequestBody originalBody = request.body(); + if (originalBody == null) { + return request; + } + var baos = new ByteArrayOutputStream(); + originalBody.writeTo(baos); + byte[] bytes = baos.toByteArray(); + String contentType = originalBody.contentType(); + + HttpRequestBody bufferedBody = + new HttpRequestBody() { + @Override + public void writeTo(OutputStream outputStream) { + try { + outputStream.write(bytes); + } catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String contentType() { + return contentType; + } + + @Override + public long contentLength() { + return bytes.length; + } + + @Override + public boolean repeatable() { + return true; + } + + @Override + public void close() {} + }; + + return request.toBuilder().body(bufferedBody).build(); + } + + private static String readBodyAsString(HttpRequestBody body) { + var baos = new ByteArrayOutputStream((int) Math.max(body.contentLength(), 0)); + body.writeTo(baos); + return baos.toString(StandardCharsets.UTF_8); + } + + // ------------------------------------------------------------------------- + // Response tee — identical pattern to OpenAI TracingHttpClient + // ------------------------------------------------------------------------- + + /** + * Tees the response body so bytes are accumulated as the caller reads, then on close tags the + * span by auto-detecting SSE vs plain JSON from the first non-empty line. + */ + private static final class TeeingStreamHttpResponse implements HttpResponse { + private final HttpResponse delegate; + private final Span span; + private final long spanStartNanos = System.nanoTime(); + private final AtomicLong timeToFirstTokenNanos = new AtomicLong(); + private final ByteArrayOutputStream teeBuffer = new ByteArrayOutputStream(); + private final InputStream teeStream; + + TeeingStreamHttpResponse(HttpResponse delegate, Span span) { + this.delegate = delegate; + this.span = span; + this.teeStream = + new TeeInputStream( + delegate.body(), teeBuffer, this::onFirstByte, this::onStreamClosed); + } + + private void onFirstByte() { + timeToFirstTokenNanos.set(System.nanoTime() - spanStartNanos); + } + + private void onStreamClosed() { + try { + byte[] bytes; + synchronized (teeBuffer) { + bytes = teeBuffer.toByteArray(); + } + tagSpanFromBuffer(span, bytes, timeToFirstTokenNanos.get()); + } finally { + span.end(); + } + } + + @Override + public int statusCode() { + return delegate.statusCode(); + } + + @Override + public com.anthropic.core.http.Headers headers() { + return delegate.headers(); + } + + @Override + public InputStream body() { + return teeStream; + } + + @Override + public void close() { + try { + teeStream.close(); + } catch (java.io.IOException ignored) { + } + delegate.close(); + } + } + + private static final class TeeInputStream extends InputStream { + private final InputStream source; + private final OutputStream sink; + private final Runnable onFirstByte; + private final Runnable onClose; + private final AtomicBoolean firstByteSeen = new AtomicBoolean(false); + private final AtomicBoolean closed = new AtomicBoolean(false); + + TeeInputStream( + InputStream source, OutputStream sink, Runnable onFirstByte, Runnable onClose) { + this.source = source; + this.sink = sink; + this.onFirstByte = onFirstByte; + this.onClose = onClose; + } + + @Override + public int read() throws java.io.IOException { + int b = source.read(); + if (b == -1) { + notifyClosed(); + } else { + notifyFirstByte(); + sink.write(b); + } + return b; + } + + @Override + public int read(byte[] buf, int off, int len) throws java.io.IOException { + int n = source.read(buf, off, len); + if (n == -1) { + notifyClosed(); + } else { + notifyFirstByte(); + sink.write(buf, off, n); + } + return n; + } + + @Override + public void close() throws java.io.IOException { + notifyClosed(); + source.close(); + } + + private void notifyFirstByte() { + if (!firstByteSeen.compareAndExchange(false, true)) { + onFirstByte.run(); + } + } + + private void notifyClosed() { + if (!closed.compareAndExchange(false, true)) { + onClose.run(); + } + } + } + + // ------------------------------------------------------------------------- + // Span tagging from buffered bytes + // ------------------------------------------------------------------------- + + private static void tagSpanFromBuffer(Span span, byte[] bytes, Long timeToFirstTokenNanos) { + if (bytes.length == 0) return; + try { + String firstLine = firstNonEmptyLine(bytes); + // Anthropic SSE starts with "event: message_start\ndata: ..." so we detect + // either prefix. OpenAI SSE starts directly with "data:". + boolean isSse = + firstLine != null + && (firstLine.startsWith("data:") || firstLine.startsWith("event:")); + if (isSse) { + tagSpanFromSseBytes(span, bytes, timeToFirstTokenNanos); + } else { + // Non-streaming: plain Message JSON — pass it whole, no time_to_first_token + InstrumentationSemConv.tagLLMSpanResponse( + span, + InstrumentationSemConv.PROVIDER_NAME_ANTHROPIC, + new String(bytes, StandardCharsets.UTF_8)); + } + } catch (Exception e) { + log.error("Could not tag span from Anthropic response buffer", e); + } + } + + private static String firstNonEmptyLine(byte[] bytes) { + int start = 0; + for (int i = 0; i <= bytes.length; i++) { + if (i == bytes.length || bytes[i] == '\n') { + String line = new String(bytes, start, i - start, StandardCharsets.UTF_8).strip(); + if (!line.isEmpty()) return line; + start = i + 1; + } + } + return null; + } + + /** + * Anthropic SSE wire format has named events: + * + *
+     * event: message_start
+     * data: {"type":"message_start","message":{...}}
+     *
+     * event: content_block_delta
+     * data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi"}}
+     * 
+ * + * We only need the {@code data:} lines — the event name is redundant with the {@code type} + * field inside the JSON. Feed each data payload to {@link MessageAccumulator} and serialize the + * assembled {@link com.anthropic.models.messages.Message} for the span. + */ + private static void tagSpanFromSseBytes( + Span span, byte[] sseBytes, Long timeToFirstTokenNanos) { + try { + var mapper = BraintrustJsonMapper.get(); + var reader = + new BufferedReader( + new InputStreamReader( + new ByteArrayInputStream(sseBytes), StandardCharsets.UTF_8)); + var accumulator = MessageAccumulator.create(); + String line; + while ((line = reader.readLine()) != null) { + if (!line.startsWith("data:")) continue; + String data = line.substring("data:".length()).strip(); + if (data.isEmpty()) continue; + try { + accumulator.accumulate(mapper.readValue(data, RawMessageStreamEvent.class)); + } catch (Exception ignored) { + // skip unrecognized event types (e.g. ping) + } + } + String assembledMessageJson = BraintrustJsonMapper.toJson(accumulator.message()); + InstrumentationSemConv.tagLLMSpanResponse( + span, + InstrumentationSemConv.PROVIDER_NAME_ANTHROPIC, + assembledMessageJson, + timeToFirstTokenNanos); + } catch (Exception e) { + log.error("Could not parse Anthropic SSE buffer to tag streaming span output", e); + } + } +} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetry.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetry.java deleted file mode 100644 index 12ee6110..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetry.java +++ /dev/null @@ -1,46 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import com.anthropic.client.AnthropicClient; -import com.anthropic.models.beta.messages.BetaMessage; -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageCreateParams; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; - -/** Entrypoint for instrumenting Anthropic clients. */ -public final class AnthropicTelemetry { - /** Returns a new {@link AnthropicTelemetry} configured with the given {@link OpenTelemetry}. */ - public static AnthropicTelemetry create(OpenTelemetry openTelemetry) { - return builder(openTelemetry).build(); - } - - /** - * Returns a new {@link AnthropicTelemetryBuilder} configured with the given {@link - * OpenTelemetry}. - */ - public static AnthropicTelemetryBuilder builder(OpenTelemetry openTelemetry) { - return new AnthropicTelemetryBuilder(openTelemetry); - } - - private final Instrumenter messageInstrumenter; - private final Instrumenter - betaMessageInstrumenter; - private final boolean captureMessageContent; - - AnthropicTelemetry( - Instrumenter messageInstrumenter, - Instrumenter - betaMessageInstrumenter, - boolean captureMessageContent) { - this.messageInstrumenter = messageInstrumenter; - this.betaMessageInstrumenter = betaMessageInstrumenter; - this.captureMessageContent = captureMessageContent; - } - - /** Wraps the provided AnthropicClient, enabling telemetry for it. */ - public AnthropicClient wrap(AnthropicClient client) { - return new InstrumentedAnthropicClient( - client, messageInstrumenter, betaMessageInstrumenter, captureMessageContent) - .createProxy(); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetryBuilder.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetryBuilder.java deleted file mode 100644 index 0934a9fc..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetryBuilder.java +++ /dev/null @@ -1,75 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import com.anthropic.models.beta.messages.BetaMessage; -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageCreateParams; -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor; -import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; - -/** A builder of {@link AnthropicTelemetry}. */ -public final class AnthropicTelemetryBuilder { - static final String INSTRUMENTATION_NAME = "io.opentelemetry.anthropic-java-2.8"; - - private final OpenTelemetry openTelemetry; - - private boolean captureMessageContent; - - AnthropicTelemetryBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; - } - - /** - * Sets whether emitted log events include full content of user and assistant messages. - * - *

Note that full content can have data privacy and size concerns and care should be taken - * when enabling this. - */ - @CanIgnoreReturnValue - public AnthropicTelemetryBuilder setCaptureMessageContent(boolean captureMessageContent) { - this.captureMessageContent = captureMessageContent; - return this; - } - - /** - * Returns a new {@link AnthropicTelemetry} with the settings of this {@link - * AnthropicTelemetryBuilder}. - */ - public AnthropicTelemetry build() { - // Use a custom span name extractor that returns just the operation name - // without appending the model name (unlike the default GenAiSpanNameExtractor) - Instrumenter messageInstrumenter = - Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - request -> - MessageAttributesGetter.INSTANCE.getOperationName(request)) - .addAttributesExtractor( - GenAiAttributesExtractor.create(MessageAttributesGetter.INSTANCE)) - .addOperationMetrics(GenAiClientMetrics.get()) - .buildInstrumenter(SpanKindExtractor.alwaysClient()); - - Instrumenter - betaMessageInstrumenter = - Instrumenter - . - builder( - openTelemetry, - INSTRUMENTATION_NAME, - request -> - BetaMessageAttributesGetter.INSTANCE - .getOperationName(request)) - .addAttributesExtractor( - GenAiAttributesExtractor.create( - BetaMessageAttributesGetter.INSTANCE)) - .addOperationMetrics(GenAiClientMetrics.get()) - .buildInstrumenter(SpanKindExtractor.alwaysClient()); - - return new AnthropicTelemetry( - messageInstrumenter, betaMessageInstrumenter, captureMessageContent); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BetaMessageAttributesGetter.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BetaMessageAttributesGetter.java deleted file mode 100644 index 80cd446e..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BetaMessageAttributesGetter.java +++ /dev/null @@ -1,129 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import static java.util.Collections.emptyList; - -import com.anthropic.models.beta.messages.BetaMessage; -import com.anthropic.models.beta.messages.MessageCreateParams; -import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesGetter; -import java.util.List; -import org.jetbrains.annotations.Nullable; - -enum BetaMessageAttributesGetter - implements GenAiAttributesGetter { - INSTANCE; - - @Override - public String getOperationName(MessageCreateParams request) { - return "anthropic.messages.create"; - } - - @Override - public String getSystem(MessageCreateParams request) { - return BraintrustAnthropicSpanAttributes.SYSTEM_ANTHROPIC; - } - - @Override - public String getRequestModel(MessageCreateParams request) { - return request.model().asString(); - } - - @Nullable - @Override - public Long getRequestSeed(MessageCreateParams request) { - return null; - } - - @Nullable - @Override - public List getRequestEncodingFormats(MessageCreateParams request) { - return null; - } - - @Nullable - @Override - public Double getRequestFrequencyPenalty(MessageCreateParams request) { - return null; - } - - @Nullable - @Override - public Long getRequestMaxTokens(MessageCreateParams request) { - long maxTokens = request.maxTokens(); - return maxTokens > 0 ? maxTokens : null; - } - - @Nullable - @Override - public Double getRequestPresencePenalty(MessageCreateParams request) { - return null; - } - - @Nullable - @Override - public List getRequestStopSequences(MessageCreateParams request) { - return request.stopSequences().orElse(null); - } - - @Nullable - @Override - public Double getRequestTemperature(MessageCreateParams request) { - return request.temperature().orElse(null); - } - - @Nullable - @Override - public Double getRequestTopK(MessageCreateParams request) { - return request.topK().map(Long::doubleValue).orElse(null); - } - - @Nullable - @Override - public Double getRequestTopP(MessageCreateParams request) { - return request.topP().orElse(null); - } - - @Override - public List getResponseFinishReasons( - MessageCreateParams request, @Nullable BetaMessage response) { - if (response == null) { - return emptyList(); - } - return response.stopReason().map(reason -> List.of(reason.asString())).orElse(emptyList()); - } - - @Override - @Nullable - public String getResponseId(MessageCreateParams request, @Nullable BetaMessage response) { - if (response == null) { - return null; - } - return response.id(); - } - - @Override - @Nullable - public String getResponseModel(MessageCreateParams request, @Nullable BetaMessage response) { - if (response == null) { - return null; - } - return response.model().asString(); - } - - @Override - @Nullable - public Long getUsageInputTokens(MessageCreateParams request, @Nullable BetaMessage response) { - if (response == null) { - return null; - } - return response.usage().inputTokens(); - } - - @Override - @Nullable - public Long getUsageOutputTokens(MessageCreateParams request, @Nullable BetaMessage response) { - if (response == null) { - return null; - } - return response.usage().outputTokens(); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BetaStreamListener.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BetaStreamListener.java deleted file mode 100644 index 16046215..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BetaStreamListener.java +++ /dev/null @@ -1,165 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import static dev.braintrust.json.BraintrustJsonMapper.toJson; - -import com.anthropic.models.beta.messages.BetaMessage; -import com.anthropic.models.beta.messages.BetaMessageDeltaUsage; -import com.anthropic.models.beta.messages.BetaRawMessageStreamEvent; -import com.anthropic.models.beta.messages.BetaUsage; -import com.anthropic.models.beta.messages.MessageCreateParams; -import com.anthropic.models.messages.Model; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import dev.braintrust.json.BraintrustJsonMapper; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicBoolean; -import javax.annotation.Nullable; -import lombok.SneakyThrows; - -final class BetaStreamListener { - - private final Context context; - private final MessageCreateParams request; - private final Instrumenter instrumenter; - private final boolean captureMessageContent; - private final boolean newSpan; - private final AtomicBoolean hasEnded; - private final long startTimeNanos; - - private final StringBuilder contentBuilder = new StringBuilder(); - - @Nullable private BetaUsage usage; - @Nullable private BetaMessageDeltaUsage deltaUsage; - @Nullable private Model model; - @Nullable private String responseId; - @Nullable private String stopReason; - @Nullable private Double timeToFirstToken; - - BetaStreamListener( - Context context, - MessageCreateParams request, - Instrumenter instrumenter, - boolean captureMessageContent, - boolean newSpan, - long startTimeNanos) { - this.context = context; - this.request = request; - this.instrumenter = instrumenter; - this.captureMessageContent = captureMessageContent; - this.newSpan = newSpan; - this.startTimeNanos = startTimeNanos; - hasEnded = new AtomicBoolean(); - } - - @SneakyThrows - void onEvent(BetaRawMessageStreamEvent event) { - // Capture time to first token on the first event - if (timeToFirstToken == null) { - long firstEventTimeNanos = System.nanoTime(); - timeToFirstToken = (firstEventTimeNanos - startTimeNanos) / 1_000_000_000.0; - } - - // Handle message_start event - if (event.messageStart().isPresent()) { - var messageStart = event.messageStart().get(); - model = messageStart.message().model(); - responseId = messageStart.message().id(); - if (messageStart.message().usage() != null) { - usage = messageStart.message().usage(); - } - } - - // Handle content_block_delta event - accumulate text - if (event.contentBlockDelta().isPresent()) { - var delta = event.contentBlockDelta().get(); - if (delta.delta().text().isPresent()) { - contentBuilder.append(delta.delta().text().get().text()); - } - } - - // Handle message_delta event - if (event.messageDelta().isPresent()) { - var messageDelta = event.messageDelta().get(); - if (messageDelta.delta().stopReason().isPresent()) { - stopReason = messageDelta.delta().stopReason().get().toString(); - } - if (messageDelta.usage() != null) { - deltaUsage = messageDelta.usage(); - } - } - - // Handle content_block_stop - write output - if (event.contentBlockStop().isPresent()) { - ArrayNode outputArray = BraintrustJsonMapper.get().createArrayNode(); - ObjectNode message = BraintrustJsonMapper.get().createObjectNode(); - message.put("role", "assistant"); - message.put("content", contentBuilder.toString()); - outputArray.add(message); - - BraintrustAnthropicSpanAttributes.setOutputJson( - Span.fromContext(context), toJson(outputArray)); - } - } - - void endSpan(@Nullable Throwable error) { - // Use an atomic operation since close() type of methods are exposed to the user - // and can come from any thread. - if (!hasEnded.compareAndSet(false, true)) { - return; - } - - if (!newSpan) { - return; - } - - if (model == null || responseId == null) { - // Only happens if we got no events, so we have no response. - instrumenter.end(context, request, null, error); - return; - } - - // Set response attributes directly on the span since building a valid BetaMessage is - // complex. The content was already written to braintrust.output_json in onEvent. - Span span = Span.fromContext(context); - - // Set model and response ID - if (model != null) { - span.setAttribute("gen_ai.response.model", model.asString()); - } - if (responseId != null) { - span.setAttribute("gen_ai.response.id", responseId); - } - if (stopReason != null) { - span.setAttribute( - AttributeKey.stringArrayKey("gen_ai.response.finish_reasons"), - Arrays.asList(stopReason)); - } - - // Set usage metrics - combine from both message_start and message_delta - // message_start has input_tokens, message_delta has final output_tokens - if (usage != null) { - span.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens()); - } - if (deltaUsage != null) { - // message_delta may also have input_tokens, prefer it if present - deltaUsage - .inputTokens() - .ifPresent(tokens -> span.setAttribute("gen_ai.usage.input_tokens", tokens)); - span.setAttribute("gen_ai.usage.output_tokens", deltaUsage.outputTokens()); - } else if (usage != null) { - // Fallback to usage from message_start for output_tokens if no delta - span.setAttribute("gen_ai.usage.output_tokens", usage.outputTokens()); - } - - // Set time to first token if captured - if (timeToFirstToken != null) { - span.setAttribute("braintrust.metrics.time_to_first_token", timeToFirstToken); - } - - instrumenter.end(context, request, null, error); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BetaTracingStreamedResponse.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BetaTracingStreamedResponse.java deleted file mode 100644 index a2ee501b..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BetaTracingStreamedResponse.java +++ /dev/null @@ -1,83 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import com.anthropic.core.http.StreamResponse; -import com.anthropic.models.beta.messages.BetaRawMessageStreamEvent; -import java.util.Comparator; -import java.util.Spliterator; -import java.util.function.Consumer; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import javax.annotation.Nullable; - -final class BetaTracingStreamedResponse implements StreamResponse { - - private final StreamResponse delegate; - private final BetaStreamListener listener; - - BetaTracingStreamedResponse( - StreamResponse delegate, BetaStreamListener listener) { - this.delegate = delegate; - this.listener = listener; - } - - @Override - public Stream stream() { - return StreamSupport.stream(new TracingSpliterator(delegate.stream().spliterator()), false); - } - - @Override - public void close() { - listener.endSpan(null); - delegate.close(); - } - - private class TracingSpliterator implements Spliterator { - - private final Spliterator delegateSpliterator; - - private TracingSpliterator(Spliterator delegateSpliterator) { - this.delegateSpliterator = delegateSpliterator; - } - - @Override - public boolean tryAdvance(Consumer action) { - boolean eventReceived = - delegateSpliterator.tryAdvance( - event -> { - listener.onEvent(event); - action.accept(event); - }); - if (!eventReceived) { - listener.endSpan(null); - } - return eventReceived; - } - - @Override - @Nullable - public Spliterator trySplit() { - // do not support parallelism to reliably catch the last event - return null; - } - - @Override - public long estimateSize() { - return delegateSpliterator.estimateSize(); - } - - @Override - public long getExactSizeIfKnown() { - return delegateSpliterator.getExactSizeIfKnown(); - } - - @Override - public int characteristics() { - return delegateSpliterator.characteristics(); - } - - @Override - public Comparator getComparator() { - return delegateSpliterator.getComparator(); - } - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BraintrustAnthropicSpanAttributes.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BraintrustAnthropicSpanAttributes.java deleted file mode 100644 index 2bdfebd2..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BraintrustAnthropicSpanAttributes.java +++ /dev/null @@ -1,51 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import static dev.braintrust.json.BraintrustJsonMapper.toJson; - -import com.anthropic.models.beta.messages.BetaMessage; -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageParam; -import io.opentelemetry.api.trace.Span; -import java.util.List; - -/** Centralized class for setting all Anthropic-related span attributes. */ -final class BraintrustAnthropicSpanAttributes { - - // GenAI semantic convention constants - static final String OPERATION_CHAT = "chat"; - static final String SYSTEM_ANTHROPIC = "anthropic"; - - private BraintrustAnthropicSpanAttributes() {} - - /** - * Sets the braintrust.input_json attribute with the input messages. This captures the user's - * prompt and system messages before sending to Anthropic. - */ - public static void setInputMessages(Span span, List messages) { - span.setAttribute("braintrust.input_json", toJson(messages)); - } - - /** - * Sets the braintrust.output_json attribute with the output message. This captures the - * assistant's response from Anthropic. - */ - public static void setOutputMessage(Span span, Message message) { - span.setAttribute("braintrust.output_json", toJson(message)); - } - - /** - * Sets the braintrust.output_json attribute with a beta output message. This captures the - * assistant's response from Anthropic beta APIs. - */ - public static void setOutputMessage(Span span, BetaMessage message) { - span.setAttribute("braintrust.output_json", toJson(message)); - } - - /** - * Sets the braintrust.output_json attribute with a JSON array. This is used for streaming - * responses where the output is built incrementally. - */ - public static void setOutputJson(Span span, String outputJson) { - span.setAttribute("braintrust.output_json", outputJson); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/DelegatingInvocationHandler.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/DelegatingInvocationHandler.java deleted file mode 100644 index 8d045ee4..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/DelegatingInvocationHandler.java +++ /dev/null @@ -1,37 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; - -abstract class DelegatingInvocationHandler> - implements InvocationHandler { - - private static final ClassLoader CLASS_LOADER = - DelegatingInvocationHandler.class.getClassLoader(); - - protected final T delegate; - - public DelegatingInvocationHandler(T delegate) { - this.delegate = delegate; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - try { - return method.invoke(delegate, args); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - } - - protected abstract Class getProxyType(); - - @SuppressWarnings("rawtypes") - public T createProxy() { - Class proxyType = getProxyType(); - Object proxy = Proxy.newProxyInstance(CLASS_LOADER, new Class[] {proxyType}, this); - return proxyType.cast(proxy); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedAnthropicClient.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedAnthropicClient.java deleted file mode 100644 index ff978262..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedAnthropicClient.java +++ /dev/null @@ -1,51 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import com.anthropic.client.AnthropicClient; -import com.anthropic.models.beta.messages.BetaMessage; -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageCreateParams; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; - -final class InstrumentedAnthropicClient - extends DelegatingInvocationHandler { - - private final Instrumenter messageInstrumenter; - private final Instrumenter - betaMessageInstrumenter; - private final boolean captureMessageContent; - - InstrumentedAnthropicClient( - AnthropicClient delegate, - Instrumenter messageInstrumenter, - Instrumenter - betaMessageInstrumenter, - boolean captureMessageContent) { - super(delegate); - this.messageInstrumenter = messageInstrumenter; - this.betaMessageInstrumenter = betaMessageInstrumenter; - this.captureMessageContent = captureMessageContent; - } - - @Override - protected Class getProxyType() { - return AnthropicClient.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - if (methodName.equals("messages") && parameterTypes.length == 0) { - return new InstrumentedMessageService( - delegate.messages(), messageInstrumenter, captureMessageContent) - .createProxy(); - } - if (methodName.equals("beta") && parameterTypes.length == 0) { - return new InstrumentedBetaService( - delegate.beta(), betaMessageInstrumenter, captureMessageContent) - .createProxy(); - } - return super.invoke(proxy, method, args); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedBetaMessageService.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedBetaMessageService.java deleted file mode 100644 index 39fcdda1..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedBetaMessageService.java +++ /dev/null @@ -1,181 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import static dev.braintrust.json.BraintrustJsonMapper.toJson; - -import com.anthropic.core.RequestOptions; -import com.anthropic.core.http.StreamResponse; -import com.anthropic.models.beta.messages.BetaMessage; -import com.anthropic.models.beta.messages.BetaMessageParam; -import com.anthropic.models.beta.messages.BetaRawMessageStreamEvent; -import com.anthropic.models.beta.messages.BetaTextBlockParam; -import com.anthropic.models.beta.messages.MessageCreateParams; -import com.anthropic.services.blocking.beta.MessageService; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -final class InstrumentedBetaMessageService - extends DelegatingInvocationHandler { - - private final Instrumenter instrumenter; - private final boolean captureMessageContent; - - InstrumentedBetaMessageService( - MessageService delegate, - Instrumenter instrumenter, - boolean captureMessageContent) { - super(delegate); - this.instrumenter = instrumenter; - this.captureMessageContent = captureMessageContent; - } - - @Override - protected Class getProxyType() { - return MessageService.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - - switch (methodName) { - case "create": - if (parameterTypes.length >= 1 && parameterTypes[0] == MessageCreateParams.class) { - if (parameterTypes.length == 1) { - return create((MessageCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 - && parameterTypes[1] == RequestOptions.class) { - return create((MessageCreateParams) args[0], (RequestOptions) args[1]); - } - } - break; - case "createStreaming": - if (parameterTypes.length >= 1 && parameterTypes[0] == MessageCreateParams.class) { - if (parameterTypes.length == 1) { - return createStreaming( - (MessageCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 - && parameterTypes[1] == RequestOptions.class) { - return createStreaming( - (MessageCreateParams) args[0], (RequestOptions) args[1]); - } - } - break; - default: - // fallthrough - } - - return super.invoke(proxy, method, args); - } - - private BetaMessage create(MessageCreateParams inputMessage, RequestOptions requestOptions) { - Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, inputMessage)) { - return delegate.create(inputMessage, requestOptions); - } - - Context context = instrumenter.start(parentContext, inputMessage); - long startTimeNanos = System.nanoTime(); - BetaMessage outputMessage; - try (Scope ignored = context.makeCurrent()) { - Span currentSpan = Span.current(); - // Set provider metadata - currentSpan.setAttribute("provider", "anthropic"); - - List inputMessages = new ArrayList<>(inputMessage.messages()); - // Append system message to the end so the backend will pick it up in the LLM display - if (inputMessage.system().isPresent()) { - inputMessages.add( - BetaMessageParam.builder() - .role(BetaMessageParam.Role.of("system")) - .content( - BetaMessageParam.Content.ofString( - betaSystemToString(inputMessage.system().get()))) - .build()); - } - currentSpan.setAttribute("braintrust.input_json", toJson(inputMessages)); - outputMessage = delegate.create(inputMessage, requestOptions); - long endTimeNanos = System.nanoTime(); - double timeToFirstTokenSeconds = (endTimeNanos - startTimeNanos) / 1_000_000_000.0; - currentSpan.setAttribute( - "braintrust.metrics.time_to_first_token", timeToFirstTokenSeconds); - BraintrustAnthropicSpanAttributes.setOutputMessage(currentSpan, outputMessage); - } catch (Throwable t) { - instrumenter.end(context, inputMessage, null, t); - throw t; - } - - instrumenter.end(context, inputMessage, outputMessage, null); - return outputMessage; - } - - private StreamResponse createStreaming( - MessageCreateParams inputMessage, RequestOptions requestOptions) { - Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, inputMessage)) { - return createStreamingWithAttributes( - parentContext, inputMessage, requestOptions, false); - } - - Context context = instrumenter.start(parentContext, inputMessage); - try (Scope ignored = context.makeCurrent()) { - return createStreamingWithAttributes(context, inputMessage, requestOptions, true); - } catch (Throwable t) { - instrumenter.end(context, inputMessage, null, t); - throw t; - } - } - - private StreamResponse createStreamingWithAttributes( - Context context, - MessageCreateParams inputMessage, - RequestOptions requestOptions, - boolean newSpan) { - Span span = Span.fromContext(context); - // Set provider metadata - span.setAttribute("provider", "anthropic"); - - List inputMessages = new ArrayList<>(inputMessage.messages()); - // Append system message to the end so the backend will pick it up in the LLM display - if (inputMessage.system().isPresent()) { - inputMessages.add( - BetaMessageParam.builder() - .role(BetaMessageParam.Role.of("system")) - .content( - BetaMessageParam.Content.ofString( - betaSystemToString(inputMessage.system().get()))) - .build()); - } - span.setAttribute("braintrust.input_json", toJson(inputMessages)); - - long startTimeNanos = System.nanoTime(); - StreamResponse result = - delegate.createStreaming(inputMessage, requestOptions); - return new BetaTracingStreamedResponse( - result, - new BetaStreamListener( - context, - inputMessage, - instrumenter, - captureMessageContent, - newSpan, - startTimeNanos)); - } - - private static String betaSystemToString(MessageCreateParams.System system) { - if (system.isString()) { - return system.asString(); - } else if (system.isBetaTextBlockParams()) { - return system.asBetaTextBlockParams().stream() - .map(BetaTextBlockParam::text) - .collect(Collectors.joining()); - } - return ""; - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedBetaService.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedBetaService.java deleted file mode 100644 index 77b69f6f..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedBetaService.java +++ /dev/null @@ -1,40 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import com.anthropic.models.beta.messages.BetaMessage; -import com.anthropic.models.beta.messages.MessageCreateParams; -import com.anthropic.services.blocking.BetaService; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; - -final class InstrumentedBetaService - extends DelegatingInvocationHandler { - - private final Instrumenter betaMessageInstrumenter; - private final boolean captureMessageContent; - - InstrumentedBetaService( - BetaService delegate, - Instrumenter betaMessageInstrumenter, - boolean captureMessageContent) { - super(delegate); - this.betaMessageInstrumenter = betaMessageInstrumenter; - this.captureMessageContent = captureMessageContent; - } - - @Override - protected Class getProxyType() { - return BetaService.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - if (methodName.equals("messages") && parameterTypes.length == 0) { - return new InstrumentedBetaMessageService( - delegate.messages(), betaMessageInstrumenter, captureMessageContent) - .createProxy(); - } - return super.invoke(proxy, method, args); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedMessageService.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedMessageService.java deleted file mode 100644 index 69b9c575..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedMessageService.java +++ /dev/null @@ -1,175 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import com.anthropic.core.RequestOptions; -import com.anthropic.core.http.StreamResponse; -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageCreateParams; -import com.anthropic.models.messages.MessageParam; -import com.anthropic.models.messages.RawMessageStreamEvent; -import com.anthropic.models.messages.TextBlockParam; -import com.anthropic.services.blocking.MessageService; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -final class InstrumentedMessageService - extends DelegatingInvocationHandler { - - private final Instrumenter instrumenter; - private final boolean captureMessageContent; - - InstrumentedMessageService( - MessageService delegate, - Instrumenter instrumenter, - boolean captureMessageContent) { - super(delegate); - this.instrumenter = instrumenter; - this.captureMessageContent = captureMessageContent; - } - - @Override - protected Class getProxyType() { - return MessageService.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - - switch (methodName) { - case "create": - if (parameterTypes.length >= 1 && parameterTypes[0] == MessageCreateParams.class) { - if (parameterTypes.length == 1) { - return create((MessageCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 - && parameterTypes[1] == RequestOptions.class) { - return create((MessageCreateParams) args[0], (RequestOptions) args[1]); - } - } - break; - case "createStreaming": - if (parameterTypes.length >= 1 && parameterTypes[0] == MessageCreateParams.class) { - if (parameterTypes.length == 1) { - return createStreaming( - (MessageCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 - && parameterTypes[1] == RequestOptions.class) { - return createStreaming( - (MessageCreateParams) args[0], (RequestOptions) args[1]); - } - } - break; - default: - // fallthrough - } - - return super.invoke(proxy, method, args); - } - - private Message create(MessageCreateParams inputMessage, RequestOptions requestOptions) { - Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, inputMessage)) { - return delegate.create(inputMessage, requestOptions); - } - - Context context = instrumenter.start(parentContext, inputMessage); - long startTimeNanos = System.nanoTime(); - Message outputMessage; - try (Scope ignored = context.makeCurrent()) { - Span currentSpan = Span.current(); - // Set provider metadata - currentSpan.setAttribute("provider", "anthropic"); - - List inputMessages = new ArrayList<>(inputMessage.messages()); - // Append system message to the end so the backend will pick it up in the LLM display - if (inputMessage.system().isPresent()) { - inputMessages.add( - MessageParam.builder() - .role(MessageParam.Role.of("system")) - .content(inputMessage.system().get().asString()) - .build()); - } - BraintrustAnthropicSpanAttributes.setInputMessages(currentSpan, inputMessages); - outputMessage = delegate.create(inputMessage, requestOptions); - long endTimeNanos = System.nanoTime(); - double timeToFirstTokenSeconds = (endTimeNanos - startTimeNanos) / 1_000_000_000.0; - currentSpan.setAttribute( - "braintrust.metrics.time_to_first_token", timeToFirstTokenSeconds); - BraintrustAnthropicSpanAttributes.setOutputMessage(Span.current(), outputMessage); - } catch (Throwable t) { - instrumenter.end(context, inputMessage, null, t); - throw t; - } - - instrumenter.end(context, inputMessage, outputMessage, null); - return outputMessage; - } - - private StreamResponse createStreaming( - MessageCreateParams inputMessage, RequestOptions requestOptions) { - Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, inputMessage)) { - return createStreamingWithAttributes( - parentContext, inputMessage, requestOptions, false); - } - - Context context = instrumenter.start(parentContext, inputMessage); - try (Scope ignored = context.makeCurrent()) { - return createStreamingWithAttributes(context, inputMessage, requestOptions, true); - } catch (Throwable t) { - instrumenter.end(context, inputMessage, null, t); - throw t; - } - } - - private StreamResponse createStreamingWithAttributes( - Context context, - MessageCreateParams inputMessage, - RequestOptions requestOptions, - boolean newSpan) { - Span span = Span.fromContext(context); - // Set provider metadata - span.setAttribute("provider", "anthropic"); - - List inputMessages = new ArrayList<>(inputMessage.messages()); - // Append system message to the end so the backend will pick it up in the LLM display - if (inputMessage.system().isPresent()) { - inputMessages.add( - MessageParam.builder() - .role(MessageParam.Role.of("system")) - .content(inputMessage.system().get().asString()) - .build()); - } - BraintrustAnthropicSpanAttributes.setInputMessages(span, inputMessages); - - long startTimeNanos = System.nanoTime(); - StreamResponse result = - delegate.createStreaming(inputMessage, requestOptions); - return new TracingStreamedResponse( - result, - new StreamListener( - context, - inputMessage, - instrumenter, - captureMessageContent, - newSpan, - startTimeNanos)); - } - - private static String contentToString(MessageCreateParams.System content) { - if (content.isString()) { - return content.asString(); - } else if (content.isTextBlockParams()) { - return content.asTextBlockParams().stream() - .map(TextBlockParam::text) - .collect(Collectors.joining()); - } - return ""; - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/MessageAttributesGetter.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/MessageAttributesGetter.java deleted file mode 100644 index ca54787b..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/MessageAttributesGetter.java +++ /dev/null @@ -1,129 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import static java.util.Collections.emptyList; - -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageCreateParams; -import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesGetter; -import java.util.List; -import org.jetbrains.annotations.Nullable; - -enum MessageAttributesGetter implements GenAiAttributesGetter { - INSTANCE; - - @Override - public String getOperationName(MessageCreateParams request) { - return "anthropic.messages.create"; - } - - @Override - public String getSystem(MessageCreateParams request) { - return BraintrustAnthropicSpanAttributes.SYSTEM_ANTHROPIC; - } - - @Override - public String getRequestModel(MessageCreateParams request) { - return request.model().asString(); - } - - @Nullable - @Override - public Long getRequestSeed(MessageCreateParams request) { - return null; - } - - @Nullable - @Override - public List getRequestEncodingFormats(MessageCreateParams request) { - return null; - } - - @Nullable - @Override - public Double getRequestFrequencyPenalty(MessageCreateParams request) { - return null; - } - - @Nullable - @Override - public Long getRequestMaxTokens(MessageCreateParams request) { - // maxTokens() returns a primitive long, so we convert to Long - long maxTokens = request.maxTokens(); - return maxTokens > 0 ? maxTokens : null; - } - - @Nullable - @Override - public Double getRequestPresencePenalty(MessageCreateParams request) { - return null; - } - - @Nullable - @Override - public List getRequestStopSequences(MessageCreateParams request) { - return request.stopSequences().orElse(null); - } - - @Nullable - @Override - public Double getRequestTemperature(MessageCreateParams request) { - return request.temperature().orElse(null); - } - - @Nullable - @Override - public Double getRequestTopK(MessageCreateParams request) { - return request.topK().map(Long::doubleValue).orElse(null); - } - - @Nullable - @Override - public Double getRequestTopP(MessageCreateParams request) { - return request.topP().orElse(null); - } - - @Override - public List getResponseFinishReasons( - MessageCreateParams request, @Nullable Message response) { - if (response == null) { - return emptyList(); - } - return response.stopReason().map(reason -> List.of(reason.asString())).orElse(emptyList()); - } - - @Override - @Nullable - public String getResponseId(MessageCreateParams request, @Nullable Message response) { - if (response == null) { - return null; - } - return response.id(); - } - - @Override - @Nullable - public String getResponseModel(MessageCreateParams request, @Nullable Message response) { - if (response == null) { - return null; - } - return response.model().asString(); - } - - @Override - @Nullable - public Long getUsageInputTokens(MessageCreateParams request, @Nullable Message response) { - if (response == null) { - return null; - } - return response.usage().inputTokens(); - } - - @Override - @Nullable - public Long getUsageOutputTokens(MessageCreateParams request, @Nullable Message response) { - if (response == null) { - return null; - } - return response.usage().outputTokens(); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/StreamListener.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/StreamListener.java deleted file mode 100644 index b3a3f3c4..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/StreamListener.java +++ /dev/null @@ -1,165 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import static dev.braintrust.json.BraintrustJsonMapper.toJson; - -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageCreateParams; -import com.anthropic.models.messages.MessageDeltaUsage; -import com.anthropic.models.messages.Model; -import com.anthropic.models.messages.RawMessageStreamEvent; -import com.anthropic.models.messages.Usage; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import dev.braintrust.json.BraintrustJsonMapper; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicBoolean; -import javax.annotation.Nullable; -import lombok.SneakyThrows; - -final class StreamListener { - - private final Context context; - private final MessageCreateParams request; - private final Instrumenter instrumenter; - private final boolean captureMessageContent; - private final boolean newSpan; - private final AtomicBoolean hasEnded; - private final long startTimeNanos; - - private final StringBuilder contentBuilder = new StringBuilder(); - - @Nullable private Usage usage; - @Nullable private MessageDeltaUsage deltaUsage; - @Nullable private Model model; - @Nullable private String responseId; - @Nullable private String stopReason; - @Nullable private Double timeToFirstToken; - - StreamListener( - Context context, - MessageCreateParams request, - Instrumenter instrumenter, - boolean captureMessageContent, - boolean newSpan, - long startTimeNanos) { - this.context = context; - this.request = request; - this.instrumenter = instrumenter; - this.captureMessageContent = captureMessageContent; - this.newSpan = newSpan; - this.startTimeNanos = startTimeNanos; - hasEnded = new AtomicBoolean(); - } - - @SneakyThrows - void onEvent(RawMessageStreamEvent event) { - // Capture time to first token on the first event - if (timeToFirstToken == null) { - long firstEventTimeNanos = System.nanoTime(); - timeToFirstToken = (firstEventTimeNanos - startTimeNanos) / 1_000_000_000.0; - } - - // Handle message_start event - if (event.messageStart().isPresent()) { - var messageStart = event.messageStart().get(); - model = messageStart.message().model(); - responseId = messageStart.message().id(); - if (messageStart.message().usage() != null) { - usage = messageStart.message().usage(); - } - } - - // Handle content_block_delta event - accumulate text - if (event.contentBlockDelta().isPresent()) { - var delta = event.contentBlockDelta().get(); - if (delta.delta().text().isPresent()) { - contentBuilder.append(delta.delta().text().get().text()); - } - } - - // Handle message_delta event - if (event.messageDelta().isPresent()) { - var messageDelta = event.messageDelta().get(); - if (messageDelta.delta().stopReason().isPresent()) { - stopReason = messageDelta.delta().stopReason().get().toString(); - } - if (messageDelta.usage() != null) { - deltaUsage = messageDelta.usage(); - } - } - - // Handle content_block_stop - write output - if (event.contentBlockStop().isPresent()) { - ArrayNode outputArray = BraintrustJsonMapper.get().createArrayNode(); - ObjectNode message = BraintrustJsonMapper.get().createObjectNode(); - message.put("role", "assistant"); - message.put("content", contentBuilder.toString()); - outputArray.add(message); - - BraintrustAnthropicSpanAttributes.setOutputJson( - Span.fromContext(context), toJson(outputArray)); - } - } - - void endSpan(@Nullable Throwable error) { - // Use an atomic operation since close() type of methods are exposed to the user - // and can come from any thread. - if (!hasEnded.compareAndSet(false, true)) { - return; - } - - if (!newSpan) { - return; - } - - if (model == null || responseId == null) { - // Only happens if we got no events, so we have no response. - instrumenter.end(context, request, null, error); - return; - } - - // Set response attributes directly on the span since building a valid Message is complex - // The content was already written to braintrust.output_json in onEvent - Span span = Span.fromContext(context); - - // Set model and response ID - if (model != null) { - span.setAttribute("gen_ai.response.model", model.asString()); - } - if (responseId != null) { - span.setAttribute("gen_ai.response.id", responseId); - } - if (stopReason != null) { - span.setAttribute( - AttributeKey.stringArrayKey("gen_ai.response.finish_reasons"), - Arrays.asList(stopReason)); - } - - // Set usage metrics - combine from both message_start and message_delta - // message_start has input_tokens, message_delta has final output_tokens - if (usage != null) { - span.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens()); - } - if (deltaUsage != null) { - // message_delta may also have input_tokens, prefer it if present - deltaUsage - .inputTokens() - .ifPresent(tokens -> span.setAttribute("gen_ai.usage.input_tokens", tokens)); - span.setAttribute("gen_ai.usage.output_tokens", deltaUsage.outputTokens()); - } else if (usage != null) { - // Fallback to usage from message_start for output_tokens if no delta - span.setAttribute("gen_ai.usage.output_tokens", usage.outputTokens()); - } - - // Set time to first token if captured - if (timeToFirstToken != null) { - span.setAttribute("braintrust.metrics.time_to_first_token", timeToFirstToken); - } - - instrumenter.end(context, request, null, error); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/TracingStreamedResponse.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/TracingStreamedResponse.java deleted file mode 100644 index 312fd873..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/TracingStreamedResponse.java +++ /dev/null @@ -1,83 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -import com.anthropic.core.http.StreamResponse; -import com.anthropic.models.messages.RawMessageStreamEvent; -import java.util.Comparator; -import java.util.Spliterator; -import java.util.function.Consumer; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import javax.annotation.Nullable; - -final class TracingStreamedResponse implements StreamResponse { - - private final StreamResponse delegate; - private final StreamListener listener; - - TracingStreamedResponse( - StreamResponse delegate, StreamListener listener) { - this.delegate = delegate; - this.listener = listener; - } - - @Override - public Stream stream() { - return StreamSupport.stream(new TracingSpliterator(delegate.stream().spliterator()), false); - } - - @Override - public void close() { - listener.endSpan(null); - delegate.close(); - } - - private class TracingSpliterator implements Spliterator { - - private final Spliterator delegateSpliterator; - - private TracingSpliterator(Spliterator delegateSpliterator) { - this.delegateSpliterator = delegateSpliterator; - } - - @Override - public boolean tryAdvance(Consumer action) { - boolean eventReceived = - delegateSpliterator.tryAdvance( - event -> { - listener.onEvent(event); - action.accept(event); - }); - if (!eventReceived) { - listener.endSpan(null); - } - return eventReceived; - } - - @Override - @Nullable - public Spliterator trySplit() { - // do not support parallelism to reliably catch the last event - return null; - } - - @Override - public long estimateSize() { - return delegateSpliterator.estimateSize(); - } - - @Override - public long getExactSizeIfKnown() { - return delegateSpliterator.getExactSizeIfKnown(); - } - - @Override - public int characteristics() { - return delegateSpliterator.characteristics(); - } - - @Override - public Comparator getComparator() { - return delegateSpliterator.getComparator(); - } - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/langchain/WrappedHttpClient.java b/src/main/java/dev/braintrust/instrumentation/langchain/WrappedHttpClient.java index e8063fa5..4e73f422 100644 --- a/src/main/java/dev/braintrust/instrumentation/langchain/WrappedHttpClient.java +++ b/src/main/java/dev/braintrust/instrumentation/langchain/WrappedHttpClient.java @@ -3,6 +3,7 @@ import static dev.braintrust.json.BraintrustJsonMapper.toJson; import com.fasterxml.jackson.databind.JsonNode; +import dev.braintrust.instrumentation.InstrumentationSemConv; import dev.braintrust.json.BraintrustJsonMapper; import dev.braintrust.trace.BraintrustTracing; import dev.langchain4j.exception.HttpException; @@ -16,11 +17,12 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; -import java.util.HashMap; -import java.util.Map; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -41,19 +43,19 @@ public WrappedHttpClient( @Override public SuccessfulHttpResponse execute(HttpRequest request) throws HttpException, RuntimeException { - ProviderInfo providerInfo = - new ProviderInfo(options.providerName(), extractEndpoint(request)); - Span span = startNewSpan(getSpanName(providerInfo)); + Span span = + tracer.spanBuilder(InstrumentationSemConv.UNSET_LLM_SPAN_NAME) + .setSpanKind(SpanKind.CLIENT) + .startSpan(); try (Scope scope = span.makeCurrent()) { - tagSpan(span, request, providerInfo); - final long startTime = System.nanoTime(); + tagRequest(span, request); var response = underlying.execute(request); - final long endTime = System.nanoTime(); - double timeToFirstToken = (endTime - startTime) / 1_000_000_000.0; - tagSpan(span, response, providerInfo, timeToFirstToken); + // Non-streaming: time_to_first_token is not meaningful + InstrumentationSemConv.tagLLMSpanResponse( + span, options.providerName(), response.body()); return response; } catch (Throwable t) { - tagSpan(span, t); + InstrumentationSemConv.tagLLMSpanResponse(span, t); throw t; } finally { span.end(); @@ -63,20 +65,21 @@ public SuccessfulHttpResponse execute(HttpRequest request) @Override public void execute(HttpRequest request, ServerSentEventListener listener) { if (listener instanceof WrappedServerSentEventListener) { - // we've already applied instrumentation + // already instrumented underlying.execute(request, listener); return; } - ProviderInfo providerInfo = - new ProviderInfo(options.providerName(), extractEndpoint(request)); - Span span = startNewSpan(getSpanName(providerInfo)); + Span span = + tracer.spanBuilder(InstrumentationSemConv.UNSET_LLM_SPAN_NAME) + .setSpanKind(SpanKind.CLIENT) + .startSpan(); try (Scope ignored = span.makeCurrent()) { - tagSpan(span, request, providerInfo); + tagRequest(span, request); underlying.execute( - request, new WrappedServerSentEventListener(listener, span, providerInfo)); + request, + new WrappedServerSentEventListener(listener, span, options.providerName())); } catch (Throwable t) { - // unlikely to happen, but just in case - tagSpan(span, t); + InstrumentationSemConv.tagLLMSpanResponse(span, t); span.end(); throw t; } @@ -86,157 +89,60 @@ public void execute(HttpRequest request, ServerSentEventListener listener) { public void execute( HttpRequest request, ServerSentEventParser parser, ServerSentEventListener listener) { if (listener instanceof WrappedServerSentEventListener) { - // we've already applied instrumentation + // already instrumented underlying.execute(request, parser, listener); return; } - ProviderInfo providerInfo = - new ProviderInfo(options.providerName(), extractEndpoint(request)); - Span span = startNewSpan(getSpanName(providerInfo)); + Span span = + tracer.spanBuilder(InstrumentationSemConv.UNSET_LLM_SPAN_NAME) + .setSpanKind(SpanKind.CLIENT) + .startSpan(); try (Scope ignored = span.makeCurrent()) { - tagSpan(span, request, providerInfo); + tagRequest(span, request); underlying.execute( request, parser, - new WrappedServerSentEventListener(listener, span, providerInfo)); + new WrappedServerSentEventListener(listener, span, options.providerName())); } catch (Throwable t) { - // unlikely to happen, but just in case - tagSpan(span, t); + InstrumentationSemConv.tagLLMSpanResponse(span, t); span.end(); throw t; } } - /** Extract endpoint path from the request URL. */ - private static String extractEndpoint(HttpRequest request) { - try { - java.net.URI uri = new java.net.URI(request.url()); - return uri.getPath(); - } catch (Exception e) { - log.debug("Failed to parse URL: {}", request.url(), e); - return ""; - } - } - - /** Get span name based on the provider and endpoint. */ - private static String getSpanName(ProviderInfo info) { - if (info.endpoint.contains("/chat/completions") - || info.endpoint.contains("/v1/completions")) { - return "Chat Completion"; - } else if (info.endpoint.contains("/embeddings")) { - return "Embeddings"; - } else if (info.endpoint.contains("/messages")) { - return "Messages"; - } - return info.endpoint(); - } - - private Span startNewSpan(String spanName) { - return tracer.spanBuilder(spanName).setSpanKind(SpanKind.CLIENT).startSpan(); - } - - /** Tag span with request data: input messages, model, provider. */ - private static void tagSpan(Span span, HttpRequest request, ProviderInfo providerInfo) { + private void tagRequest(Span span, HttpRequest request) { try { - span.setAttribute("braintrust.span_attributes", toJson(Map.of("type", "llm"))); - - // Build metadata map - Map metadata = new HashMap<>(); - metadata.put("provider", providerInfo.provider); - - // Parse request body to extract model and messages - String body = request.body(); - if (body != null && !body.isEmpty()) { - JsonNode requestJson = BraintrustJsonMapper.get().readTree(body); - - // Extract model - if (requestJson.has("model")) { - String model = requestJson.get("model").asText(); - metadata.put("model", model); - } - - // Extract messages array for input - if (requestJson.has("messages")) { - String messagesJson = toJson(requestJson.get("messages")); - span.setAttribute("braintrust.input_json", messagesJson); - } - } - - // Serialize metadata as JSON - span.setAttribute("braintrust.metadata", toJson(metadata)); + URI uri = new URI(request.url()); + String baseUrl = uri.getScheme() + "://" + uri.getAuthority(); + List pathSegments = + Arrays.stream(uri.getPath().split("/")).filter(s -> !s.isEmpty()).toList(); + InstrumentationSemConv.tagLLMSpanRequest( + span, options.providerName(), baseUrl, pathSegments, "POST", request.body()); } catch (Exception e) { - log.debug("Failed to parse request for span tagging", e); + log.debug("Failed to tag request span", e); } } - /** Tag span with response data: output messages, usage metrics. */ - private static void tagSpan( - Span span, - SuccessfulHttpResponse response, - ProviderInfo providerInfo, - double timeToFirstToken) { - try { - // Build metrics map - Map metrics = new HashMap<>(); - metrics.put("time_to_first_token", timeToFirstToken); - - String body = response.body(); - if (body != null && !body.isEmpty()) { - JsonNode responseJson = BraintrustJsonMapper.get().readTree(body); - - // Extract choices array for output - if (responseJson.has("choices")) { - String choicesJson = toJson(responseJson.get("choices")); - span.setAttribute("braintrust.output_json", choicesJson); - } - - // Extract usage metrics if present - if (responseJson.has("usage")) { - JsonNode usage = responseJson.get("usage"); - if (usage.has("prompt_tokens")) { - metrics.put("prompt_tokens", usage.get("prompt_tokens").asLong()); - } - if (usage.has("completion_tokens")) { - metrics.put("completion_tokens", usage.get("completion_tokens").asLong()); - } - if (usage.has("total_tokens")) { - metrics.put("tokens", usage.get("total_tokens").asLong()); - } - } - } - - span.setAttribute("braintrust.metrics", toJson(metrics)); - } catch (Exception e) { - log.debug("Failed to parse response for span tagging", e); - } - } - - /** Tag span with error information. */ - private static void tagSpan(Span span, Throwable t) { - span.setStatus(StatusCode.ERROR, t.getMessage()); - span.recordException(t); - } - /** - * Wraps a ServerSentEventListener to properly end the span when streaming completes or errors. - * Also buffers streaming chunks to extract usage data. + * Wraps a {@link ServerSentEventListener} to keep the span open for the duration of the stream, + * accumulate SSE chunks, and finalize span tagging via {@link InstrumentationSemConv} when the + * stream closes or errors. */ private static class WrappedServerSentEventListener implements ServerSentEventListener { private final ServerSentEventListener delegate; private final Span span; - private final ProviderInfo providerInfo; - private final StringBuilder outputBuffer = new StringBuilder(); - private long firstTokenTime = 0; - private final long startTime; - private JsonNode usageData = null; + private final String providerName; + private final long startNanos = System.nanoTime(); + private final AtomicLong timeToFirstTokenNanos = new AtomicLong(); + private final StringBuilder contentBuffer = new StringBuilder(); private String finishReason = null; + private JsonNode usageData = null; WrappedServerSentEventListener( - ServerSentEventListener delegate, Span span, ProviderInfo providerInfo) { + ServerSentEventListener delegate, Span span, String providerName) { this.delegate = delegate; this.span = span; - this.providerInfo = providerInfo; - this.startTime = System.nanoTime(); + this.providerName = providerName; } @Override @@ -249,7 +155,7 @@ public void onOpen(SuccessfulHttpResponse response) { @Override public void onEvent(ServerSentEvent event, ServerSentEventContext context) { try (Scope ignored = span.makeCurrent()) { - instrumentEvent(event); + accumulateChunk(event.data()); delegate.onEvent(event, context); } } @@ -257,59 +163,17 @@ public void onEvent(ServerSentEvent event, ServerSentEventContext context) { @Override public void onEvent(ServerSentEvent event) { try (Scope ignored = span.makeCurrent()) { - instrumentEvent(event); + accumulateChunk(event.data()); delegate.onEvent(event); } } - private void instrumentEvent(ServerSentEvent event) { - String data = event.data(); - if (data == null || data.isEmpty() || "[DONE]".equals(data)) { - return; - } - - // Track time to first token - if (firstTokenTime == 0) { - firstTokenTime = System.nanoTime(); - } - - // Buffer the data for final processing - try { - JsonNode chunk = BraintrustJsonMapper.get().readTree(data); - - // For streaming, we accumulate deltas into the complete message - // Just track if we have any content - if (chunk.has("choices") && chunk.get("choices").size() > 0) { - JsonNode choice = chunk.get("choices").get(0); - if (choice.has("delta")) { - JsonNode delta = choice.get("delta"); - if (delta.has("content")) { - String content = delta.get("content").asText(); - outputBuffer.append(content); - } - } - // Capture finish_reason when present (usually in the last chunk) - if (choice.has("finish_reason") && !choice.get("finish_reason").isNull()) { - finishReason = choice.get("finish_reason").asText(); - } - } - - // Extract usage data if present (usually in the last chunk) - if (chunk.has("usage")) { - usageData = chunk.get("usage"); - } - } catch (Exception e) { - log.debug("Failed to parse streaming event: {}", data, e); - } - } - @Override public void onError(Throwable error) { try (Scope ignored = span.makeCurrent()) { delegate.onError(error); } finally { - tagSpan(span, error); - finalizeSpan(); + InstrumentationSemConv.tagLLMSpanResponse(span, error); span.end(); } } @@ -324,71 +188,62 @@ public void onClose() { } } - private void finalizeSpan() { - // Build metrics map for streaming - Map metrics = new HashMap<>(); - - // Add time to first token if we have it - if (firstTokenTime > 0) { - double timeToFirstToken = (firstTokenTime - startTime) / 1_000_000_000.0; - metrics.put("time_to_first_token", timeToFirstToken); - } - - // Reconstruct output as a choices array for streaming - // Format: [{"index": 0, "finish_reason": "stop", "message": {"role": "assistant", - // "content": "..."}}] - if (outputBuffer.length() > 0 || finishReason != null) { - try { - // Create a proper choice object matching OpenAI API format - var choiceBuilder = BraintrustJsonMapper.get().createObjectNode(); - choiceBuilder.put("index", 0); - if (finishReason != null) { - choiceBuilder.put("finish_reason", finishReason); - } - - var messageNode = BraintrustJsonMapper.get().createObjectNode(); - messageNode.put("role", "assistant"); - messageNode.put("content", outputBuffer.toString()); - - choiceBuilder.set("message", messageNode); - - var choicesArray = BraintrustJsonMapper.get().createArrayNode(); - choicesArray.add(choiceBuilder); - - span.setAttribute("braintrust.output_json", choicesArray.toString()); - } catch (Exception e) { - log.debug("Failed to reconstruct streaming output", e); + private void accumulateChunk(String data) { + if (data == null || data.isEmpty() || "[DONE]".equals(data)) return; + try { + if (timeToFirstTokenNanos.get() == 0L) { + // conditional so we don't make unnecessary calls to nano time + timeToFirstTokenNanos.compareAndExchange(0L, System.nanoTime() - startNanos); } - } - - // Set usage metrics if we collected them - if (usageData != null) { - try { - if (usageData.has("prompt_tokens")) { - metrics.put("prompt_tokens", usageData.get("prompt_tokens").asLong()); - } - if (usageData.has("completion_tokens")) { - metrics.put( - "completion_tokens", usageData.get("completion_tokens").asLong()); + JsonNode chunk = BraintrustJsonMapper.get().readTree(data); + if (chunk.has("choices") && chunk.get("choices").size() > 0) { + JsonNode choice = chunk.get("choices").get(0); + if (choice.has("delta")) { + JsonNode delta = choice.get("delta"); + if (delta.has("content")) { + contentBuffer.append(delta.get("content").asText()); + } } - if (usageData.has("total_tokens")) { - metrics.put("tokens", usageData.get("total_tokens").asLong()); + if (choice.has("finish_reason") && !choice.get("finish_reason").isNull()) { + finishReason = choice.get("finish_reason").asText(); } - } catch (Exception e) { - log.debug("Failed to extract usage metrics from streaming data", e); } + if (chunk.has("usage") && !chunk.get("usage").isNull()) { + usageData = chunk.get("usage"); + } + } catch (Exception e) { + log.debug("Failed to parse SSE chunk: {}", data, e); } + } - // Serialize metrics as JSON + private void finalizeSpan() { try { - if (!metrics.isEmpty()) { - span.setAttribute("braintrust.metrics", toJson(metrics)); + // Reconstruct a minimal response JSON that tagLLMSpanResponse already knows + // how to parse: {"choices":[{"index":0,"finish_reason":"...","message":{...}}], + // "usage":{...}} + var root = BraintrustJsonMapper.get().createObjectNode(); + + var choicesArray = BraintrustJsonMapper.get().createArrayNode(); + var choice = BraintrustJsonMapper.get().createObjectNode(); + choice.put("index", 0); + if (finishReason != null) choice.put("finish_reason", finishReason); + var message = BraintrustJsonMapper.get().createObjectNode(); + message.put("role", "assistant"); + message.put("content", contentBuffer.toString()); + choice.set("message", message); + choicesArray.add(choice); + root.set("choices", choicesArray); + + if (usageData != null) { + root.set("usage", usageData); } + + long ttft = timeToFirstTokenNanos.get(); + InstrumentationSemConv.tagLLMSpanResponse( + span, providerName, toJson(root), ttft == 0L ? null : ttft); } catch (Exception e) { - log.debug("Failed to serialize metrics", e); + log.debug("Failed to finalize streaming span", e); } } } - - private record ProviderInfo(String provider, String endpoint) {} } diff --git a/src/main/java/dev/braintrust/instrumentation/openai/BraintrustOpenAI.java b/src/main/java/dev/braintrust/instrumentation/openai/BraintrustOpenAI.java index e9d4cf16..abee01e3 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/BraintrustOpenAI.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/BraintrustOpenAI.java @@ -1,31 +1,34 @@ package dev.braintrust.instrumentation.openai; import com.openai.client.OpenAIClient; +import com.openai.core.ClientOptions; import com.openai.core.ObjectMappers; +import com.openai.core.http.HttpClient; import com.openai.models.chat.completions.ChatCompletionCreateParams; -import dev.braintrust.instrumentation.openai.otel.OpenAITelemetry; import dev.braintrust.prompt.BraintrustPrompt; import io.opentelemetry.api.OpenTelemetry; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import kotlin.Lazy; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; /** Braintrust OpenAI client instrumentation. */ +@Slf4j public class BraintrustOpenAI { /** Instrument openai client with braintrust traces */ public static OpenAIClient wrapOpenAI(OpenTelemetry openTelemetry, OpenAIClient openAIClient) { - if ("true".equalsIgnoreCase(System.getenv("BRAINTRUST_X_OTEL_LOGS"))) { - return io.opentelemetry.instrumentation.openai.v1_1.OpenAITelemetry.builder( - openTelemetry) - .setCaptureMessageContent(true) - .build() - .wrap(openAIClient); - } else { - return OpenAITelemetry.builder(openTelemetry) - .setCaptureMessageContent(true) - .build() - .wrap(openAIClient); + try { + instrumentHttpClient(openTelemetry, openAIClient); + return openAIClient; + } catch (Exception e) { + log.error("failed to apply openai instrumentation", e); + return openAIClient; } } @@ -45,4 +48,118 @@ public static ChatCompletionCreateParams buildChatCompletionsPrompt( .additionalBodyProperties(Map.of()) .build(); } + + private static void instrumentHttpClient( + OpenTelemetry openTelemetry, OpenAIClient openAIClient) { + forAllFields( + openAIClient, + fieldName -> { + try { + var field = getField(openAIClient, fieldName); + if (field instanceof ClientOptions clientOptions) { + instrumentClientOptions( + openTelemetry, clientOptions, "originalHttpClient"); + instrumentClientOptions(openTelemetry, clientOptions, "httpClient"); + } else { + if (field instanceof Lazy lazyField) { + var resolved = lazyField.getValue(); + forAllFieldsOfType( + resolved, + ClientOptions.class, + (clientOptions, subfieldName) -> + instrumentClientOptions( + openTelemetry, + clientOptions, + subfieldName)); + } else { + forAllFieldsOfType( + field, + ClientOptions.class, + (clientOptions, subfieldName) -> + instrumentClientOptions( + openTelemetry, + clientOptions, + subfieldName)); + } + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + }); + } + + private static void forAllFields(Object object, Consumer consumer) { + if (object == null || consumer == null) return; + + Class clazz = object.getClass(); + while (clazz != null && clazz != Object.class) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isSynthetic()) continue; + if (Modifier.isStatic(field.getModifiers())) continue; + + consumer.accept(field.getName()); + } + clazz = clazz.getSuperclass(); + } + } + + private static void forAllFieldsOfType( + Object object, Class targetClazz, BiConsumer consumer) { + forAllFields( + object, + fieldName -> { + try { + if (targetClazz.isAssignableFrom(object.getClass())) { + consumer.accept(getField(object, fieldName), fieldName); + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + }); + } + + private static void instrumentClientOptions( + OpenTelemetry openTelemetry, ClientOptions clientOptions, String fieldName) { + try { + HttpClient httpClient = getField(clientOptions, fieldName); + if (!(httpClient instanceof TracingHttpClient)) { + setPrivateField( + clientOptions, fieldName, new TracingHttpClient(openTelemetry, httpClient)); + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + private static T getField(Object obj, String fieldName) + throws ReflectiveOperationException { + Class clazz = obj.getClass(); + while (clazz != null) { + try { + java.lang.reflect.Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(obj); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } + + private static void setPrivateField(Object obj, String fieldName, Object value) + throws ReflectiveOperationException { + Class clazz = obj.getClass(); + while (clazz != null) { + try { + java.lang.reflect.Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(obj, value); + return; + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } } diff --git a/src/main/java/dev/braintrust/instrumentation/openai/TracingHttpClient.java b/src/main/java/dev/braintrust/instrumentation/openai/TracingHttpClient.java new file mode 100644 index 00000000..37dc40f7 --- /dev/null +++ b/src/main/java/dev/braintrust/instrumentation/openai/TracingHttpClient.java @@ -0,0 +1,371 @@ +package dev.braintrust.instrumentation.openai; + +import com.openai.core.RequestOptions; +import com.openai.core.http.HttpClient; +import com.openai.core.http.HttpRequest; +import com.openai.core.http.HttpRequestBody; +import com.openai.core.http.HttpResponse; +import com.openai.helpers.ChatCompletionAccumulator; +import com.openai.models.chat.completions.ChatCompletionChunk; +import dev.braintrust.instrumentation.InstrumentationSemConv; +import dev.braintrust.json.BraintrustJsonMapper; +import dev.braintrust.trace.BraintrustTracing; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; +import org.jspecify.annotations.NonNull; + +@Slf4j +public class TracingHttpClient implements HttpClient { + private final Tracer tracer; + private final HttpClient underlying; + + public TracingHttpClient(OpenTelemetry openTelemetry, HttpClient underlying) { + this.tracer = BraintrustTracing.getTracer(openTelemetry); + this.underlying = underlying; + } + + @Override + public void close() { + underlying.close(); + } + + @Override + public @NonNull HttpResponse execute( + @NonNull HttpRequest httpRequest, @NonNull RequestOptions requestOptions) { + var span = tracer.spanBuilder(InstrumentationSemConv.UNSET_LLM_SPAN_NAME).startSpan(); + try (var ignored = span.makeCurrent()) { + // Buffer the request body so we can (a) read its bytes for the span attribute and + // (b) supply a fresh, repeatable body to the underlying client — avoiding any + // one-shot stream consumption issue. + var bufferedRequest = bufferRequestBody(httpRequest); + + String inputJson = + bufferedRequest.body() != null + ? readBodyAsString(bufferedRequest.body()) + : null; + + InstrumentationSemConv.tagLLMSpanRequest( + span, + InstrumentationSemConv.PROVIDER_NAME_OPENAI, + bufferedRequest.baseUrl(), + bufferedRequest.pathSegments(), + bufferedRequest.method().name(), + inputJson); + var response = underlying.execute(bufferedRequest, requestOptions); + // Always tee the response body. onStreamClosed() detects whether the collected + // bytes are SSE or plain JSON and tags the span accordingly. + return new TeeingStreamHttpResponse(response, span); + } catch (Exception e) { + InstrumentationSemConv.tagLLMSpanResponse(span, e); + span.end(); + throw e; + } + } + + @Override + public @NonNull CompletableFuture executeAsync( + @NonNull HttpRequest httpRequest, @NonNull RequestOptions requestOptions) { + var span = tracer.spanBuilder(InstrumentationSemConv.UNSET_LLM_SPAN_NAME).startSpan(); + try { + var bufferedRequest = bufferRequestBody(httpRequest); + String inputJson = + bufferedRequest.body() != null + ? readBodyAsString(bufferedRequest.body()) + : null; + InstrumentationSemConv.tagLLMSpanRequest( + span, + InstrumentationSemConv.PROVIDER_NAME_OPENAI, + bufferedRequest.baseUrl(), + bufferedRequest.pathSegments(), + bufferedRequest.method().name(), + inputJson); + return underlying + .executeAsync(bufferedRequest, requestOptions) + .thenApply( + response -> (HttpResponse) new TeeingStreamHttpResponse(response, span)) + .whenComplete( + (response, t) -> { + if (t != null) { + // this means the future itself failed + InstrumentationSemConv.tagLLMSpanResponse(span, t); + span.end(); + } + }); + } catch (Exception e) { + InstrumentationSemConv.tagLLMSpanResponse(span, e); + span.end(); + throw e; + } + } + + /** + * Captures the request body into an in-memory byte array and returns a new {@link HttpRequest} + * backed by those bytes. The original body stream is consumed exactly once here; the returned + * request uses a {@link HttpRequestBody} that is always {@link HttpRequestBody#repeatable() + * repeatable}, so the underlying client can read it safely (including on retry). + * + *

If the original body is {@code null} or already in-memory (repeatable), the cost is just + * one extra copy of the bytes — acceptable for observability. + */ + private static HttpRequest bufferRequestBody(HttpRequest request) { + HttpRequestBody originalBody = request.body(); + if (originalBody == null) { + return request; + } + var baos = new ByteArrayOutputStream(); + originalBody.writeTo(baos); + byte[] bytes = baos.toByteArray(); + String contentType = originalBody.contentType(); + + HttpRequestBody bufferedBody = + new HttpRequestBody() { + @Override + public void writeTo(OutputStream outputStream) { + try { + outputStream.write(bytes); + } catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String contentType() { + return contentType; + } + + @Override + public long contentLength() { + return bytes.length; + } + + @Override + public boolean repeatable() { + return true; + } + + @Override + public void close() {} + }; + + return request.toBuilder().body(bufferedBody).build(); + } + + private static String readBodyAsString(HttpRequestBody body) { + // Body was already buffered by bufferRequestBody, so writeTo is safe to call again. + var baos = new ByteArrayOutputStream((int) Math.max(body.contentLength(), 0)); + body.writeTo(baos); + return baos.toString(StandardCharsets.UTF_8); + } + + /** + * Tags the span from bytes collected by {@link TeeingStreamHttpResponse}. Auto-detects whether + * the bytes are an SSE stream (first non-empty line starts with {@code "data: "}) or a plain + * JSON response, and parses accordingly. + */ + private static void tagSpanFromBuffer(Span span, byte[] bytes, Long timeToFirstTokenNanos) { + if (bytes.length == 0) return; + try { + String firstLine = firstNonEmptyLine(bytes); + if (firstLine != null && firstLine.startsWith("data:")) { + tagSpanFromSseBytes(span, bytes, timeToFirstTokenNanos); + } else { + InstrumentationSemConv.tagLLMSpanResponse( + span, + InstrumentationSemConv.PROVIDER_NAME_OPENAI, + new String(bytes, StandardCharsets.UTF_8)); + } + } catch (Exception e) { + log.error("Could not tag span from response buffer", e); + } + } + + private static String firstNonEmptyLine(byte[] bytes) { + int start = 0; + for (int i = 0; i <= bytes.length; i++) { + if (i == bytes.length || bytes[i] == '\n') { + String line = new String(bytes, start, i - start, StandardCharsets.UTF_8).strip(); + if (!line.isEmpty()) return line; + start = i + 1; + } + } + return null; + } + + /** + * Parses SSE wire bytes, feeds each {@code data:} chunk through {@link + * ChatCompletionAccumulator}, then tags the span with the reassembled output JSON. + */ + private static void tagSpanFromSseBytes( + Span span, byte[] sseBytes, Long timeToFirstTokenNanos) { + try { + var accumulator = ChatCompletionAccumulator.create(); + var reader = + new BufferedReader( + new InputStreamReader( + new ByteArrayInputStream(sseBytes), StandardCharsets.UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + if (!line.startsWith("data:")) continue; + String data = line.substring("data:".length()).strip(); + if (data.isEmpty() || data.equals("[DONE]")) continue; + ChatCompletionChunk chunk = + BraintrustJsonMapper.get().readValue(data, ChatCompletionChunk.class); + accumulator.accumulate(chunk); + } + var chatCompletion = accumulator.chatCompletion(); + InstrumentationSemConv.tagLLMSpanResponse( + span, + InstrumentationSemConv.PROVIDER_NAME_OPENAI, + BraintrustJsonMapper.toJson(chatCompletion), + timeToFirstTokenNanos); + } catch (Exception e) { + log.error("Could not parse SSE buffer to tag streaming span output", e); + } + } + + /** + * {@link HttpResponse} wrapper for streaming (SSE) responses. Its {@link #body()} returns a tee + * {@link InputStream} that copies every byte the caller reads into an in-memory buffer. When + * the stream is fully consumed and {@link #close()} is called, the accumulated bytes are + * available via {@link #collectedBytes()} for span tagging. + */ + private static final class TeeingStreamHttpResponse implements HttpResponse { + private final HttpResponse delegate; + private final Span span; + private final long spanStartNanos = System.nanoTime(); + private final AtomicLong timeToFirstTokenNanos = new AtomicLong(); + private final ByteArrayOutputStream teeBuffer = new ByteArrayOutputStream(); + private final InputStream teeStream; + + TeeingStreamHttpResponse(HttpResponse delegate, Span span) { + this.delegate = delegate; + this.span = span; + this.teeStream = + new TeeInputStream( + delegate.body(), teeBuffer, this::onFirstByte, this::onStreamClosed); + } + + private void onFirstByte() { + timeToFirstTokenNanos.set(System.nanoTime() - spanStartNanos); + } + + /** Called back by {@link TeeInputStream} when the stream is fully drained or closed. */ + private void onStreamClosed() { + try { + // Synchronize on teeBuffer to ensure any write() that was in-flight on a + // concurrent read thread has fully completed before we snapshot the bytes. + byte[] bytes; + synchronized (teeBuffer) { + bytes = teeBuffer.toByteArray(); + } + tagSpanFromBuffer(span, bytes, timeToFirstTokenNanos.get()); + } finally { + span.end(); + } + } + + byte[] collectedBytes() { + return teeBuffer.toByteArray(); + } + + @Override + public int statusCode() { + return delegate.statusCode(); + } + + @Override + public com.openai.core.http.Headers headers() { + return delegate.headers(); + } + + @Override + public InputStream body() { + return teeStream; + } + + @Override + public void close() { + try { + teeStream.close(); // triggers onStreamClosed if not already fired (e.g. abandoned + // stream) + } catch (java.io.IOException ignored) { + } + delegate.close(); + } + } + + /** + * An {@link InputStream} that copies every byte read from {@code source} into {@code sink}, and + * fires {@code onClose} exactly once when the stream reaches EOF or is explicitly closed. + */ + private static final class TeeInputStream extends InputStream { + private final InputStream source; + private final OutputStream sink; + private final Runnable onFirstByte; + private final Runnable onClose; + private final AtomicBoolean firstByteSeen = new AtomicBoolean(false); + private final AtomicBoolean closed = new AtomicBoolean(false); + + TeeInputStream( + InputStream source, OutputStream sink, Runnable onFirstByte, Runnable onClose) { + this.source = source; + this.sink = sink; + this.onFirstByte = onFirstByte; + this.onClose = onClose; + } + + @Override + public int read() throws java.io.IOException { + int b = source.read(); + if (b == -1) { + notifyClosed(); + } else { + notifyFirstByte(); + sink.write(b); + } + return b; + } + + @Override + public int read(byte[] buf, int off, int len) throws java.io.IOException { + int n = source.read(buf, off, len); + if (n == -1) { + notifyClosed(); + } else { + notifyFirstByte(); + sink.write(buf, off, n); + } + return n; + } + + @Override + public void close() throws java.io.IOException { + notifyClosed(); + source.close(); + } + + private void notifyFirstByte() { + if (!firstByteSeen.compareAndExchange(false, true)) { + onFirstByte.run(); + } + } + + private void notifyClosed() { + if (!closed.compareAndExchange(false, true)) { + onClose.run(); + } + } + } +} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/BraintrustOAISpanAttributes.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/BraintrustOAISpanAttributes.java deleted file mode 100644 index 0eaed174..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/BraintrustOAISpanAttributes.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.models.chat.completions.ChatCompletion; -import io.opentelemetry.api.trace.Span; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; - -/** Centralized class for setting all OpenAI-related span attributes. */ -@Slf4j -final class BraintrustOAISpanAttributes { - - // GenAI semantic convention constants - static final String OPERATION_CHAT = "chat"; - static final String OPERATION_EMBEDDINGS = "embeddings"; - static final String SYSTEM_OPENAI = "openai"; - - private BraintrustOAISpanAttributes() {} - - @SneakyThrows - static void setRequestAttributes( - Span span, com.openai.models.chat.completions.ChatCompletionCreateParams request) { - // Set input messages - String semconvJson = GenAiSemconvSerializer.serializeInputMessages(request.messages()); - span.setAttribute("gen_ai.input.messages", semconvJson); - - // Set Braintrust metadata - span.setAttribute("braintrust.metadata.provider", SYSTEM_OPENAI); - - // Set model in metadata if present - try { - var model = request.model(); - span.setAttribute("braintrust.metadata.model", model.toString()); - } catch (Exception e) { - // If model() throws or returns null, just skip setting it - log.debug("Could not get model from request", e); - } - } - - @SneakyThrows - static void setOutputMessagesFromCompletion(Span span, ChatCompletion completion) { - span.setAttribute( - "gen_ai.output.messages", - GenAiSemconvSerializer.serializeOutputMessages(completion.choices())); - } - - static void setTimeToFirstToken(Span span, double timeInSeconds) { - span.setAttribute("braintrust.metrics.time_to_first_token", timeInSeconds); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatAttributesGetter.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatAttributesGetter.java deleted file mode 100644 index f82e5ace..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatAttributesGetter.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; - -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.models.completions.CompletionUsage; -import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesGetter; -import java.util.List; -import java.util.stream.Collectors; -import org.jetbrains.annotations.Nullable; - -enum ChatAttributesGetter - implements GenAiAttributesGetter { - INSTANCE; - - @Override - public String getOperationName(ChatCompletionCreateParams request) { - return BraintrustOAISpanAttributes.OPERATION_CHAT; - } - - @Override - public String getSystem(ChatCompletionCreateParams request) { - return BraintrustOAISpanAttributes.SYSTEM_OPENAI; - } - - @Override - public String getRequestModel(ChatCompletionCreateParams request) { - return request.model().asString(); - } - - @Nullable - @Override - public Long getRequestSeed(ChatCompletionCreateParams request) { - return request.seed().orElse(null); - } - - @Nullable - @Override - public List getRequestEncodingFormats(ChatCompletionCreateParams request) { - return null; - } - - @Nullable - @Override - public Double getRequestFrequencyPenalty(ChatCompletionCreateParams request) { - return request.frequencyPenalty().orElse(null); - } - - @Nullable - @Override - public Long getRequestMaxTokens(ChatCompletionCreateParams request) { - return request.maxCompletionTokens().orElse(null); - } - - @Nullable - @Override - public Double getRequestPresencePenalty(ChatCompletionCreateParams request) { - return request.presencePenalty().orElse(null); - } - - @Nullable - @Override - public List getRequestStopSequences(ChatCompletionCreateParams request) { - return request.stop() - .map( - s -> { - if (s.isString()) { - return singletonList(s.asString()); - } - if (s.isStrings()) { - return s.asStrings(); - } - return null; - }) - .orElse(null); - } - - @Nullable - @Override - public Double getRequestTemperature(ChatCompletionCreateParams request) { - return request.temperature().orElse(null); - } - - @Nullable - @Override - public Double getRequestTopK(ChatCompletionCreateParams request) { - return null; - } - - @Nullable - @Override - public Double getRequestTopP(ChatCompletionCreateParams request) { - return request.topP().orElse(null); - } - - @Override - public List getResponseFinishReasons( - ChatCompletionCreateParams request, @Nullable ChatCompletion response) { - if (response == null) { - return emptyList(); - } - return response.choices().stream() - .map(choice -> choice.finishReason().asString()) - .collect(Collectors.toList()); - } - - @Override - @Nullable - public String getResponseId( - ChatCompletionCreateParams request, @Nullable ChatCompletion response) { - if (response == null) { - return null; - } - return response.id(); - } - - @Override - @Nullable - public String getResponseModel( - ChatCompletionCreateParams request, @Nullable ChatCompletion response) { - if (response == null) { - return null; - } - return response.model(); - } - - @Override - @Nullable - public Long getUsageInputTokens( - ChatCompletionCreateParams request, @Nullable ChatCompletion response) { - if (response == null) { - return null; - } - return response.usage().map(CompletionUsage::promptTokens).orElse(null); - } - - @Override - @Nullable - public Long getUsageOutputTokens( - ChatCompletionCreateParams request, @Nullable ChatCompletion response) { - if (response == null) { - return null; - } - return response.usage().map(CompletionUsage::completionTokens).orElse(null); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/CompletableFutureWrapper.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/CompletableFutureWrapper.java deleted file mode 100644 index 9dcea970..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/CompletableFutureWrapper.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; -import java.util.concurrent.CompletableFuture; - -final class CompletableFutureWrapper { - private CompletableFutureWrapper() {} - - static CompletableFuture wrap(CompletableFuture future, Context context) { - CompletableFuture result = new CompletableFuture<>(); - future.whenComplete( - (T value, Throwable throwable) -> { - try (Scope ignored = context.makeCurrent()) { - if (throwable != null) { - result.completeExceptionally(throwable); - } else { - result.complete(value); - } - } - }); - - return result; - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/DelegatingInvocationHandler.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/DelegatingInvocationHandler.java deleted file mode 100644 index aba47c88..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/DelegatingInvocationHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; - -abstract class DelegatingInvocationHandler> - implements InvocationHandler { - - private static final ClassLoader CLASS_LOADER = - DelegatingInvocationHandler.class.getClassLoader(); - - protected final T delegate; - - public DelegatingInvocationHandler(T delegate) { - this.delegate = delegate; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - try { - return method.invoke(delegate, args); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - } - - protected abstract Class getProxyType(); - - @SuppressWarnings("rawtypes") - public T createProxy() { - Class proxyType = getProxyType(); - Object proxy = Proxy.newProxyInstance(CLASS_LOADER, new Class[] {proxyType}, this); - return proxyType.cast(proxy); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/EmbeddingAttributesGetter.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/EmbeddingAttributesGetter.java deleted file mode 100644 index d9f33b8e..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/EmbeddingAttributesGetter.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import static java.util.Collections.singletonList; - -import com.openai.models.embeddings.CreateEmbeddingResponse; -import com.openai.models.embeddings.EmbeddingCreateParams; -import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesGetter; -import java.util.Collections; -import java.util.List; -import javax.annotation.Nullable; - -enum EmbeddingAttributesGetter - implements GenAiAttributesGetter { - INSTANCE; - - @Override - public String getOperationName(EmbeddingCreateParams request) { - return BraintrustOAISpanAttributes.OPERATION_EMBEDDINGS; - } - - @Override - public String getSystem(EmbeddingCreateParams request) { - return BraintrustOAISpanAttributes.SYSTEM_OPENAI; - } - - @Override - public String getRequestModel(EmbeddingCreateParams request) { - return request.model().asString(); - } - - @Nullable - @Override - public Long getRequestSeed(EmbeddingCreateParams request) { - return null; - } - - @Nullable - @Override - public List getRequestEncodingFormats(EmbeddingCreateParams request) { - return request.encodingFormat().map(f -> singletonList(f.asString())).orElse(null); - } - - @Nullable - @Override - public Double getRequestFrequencyPenalty(EmbeddingCreateParams request) { - return null; - } - - @Nullable - @Override - public Long getRequestMaxTokens(EmbeddingCreateParams request) { - return null; - } - - @Nullable - @Override - public Double getRequestPresencePenalty(EmbeddingCreateParams request) { - return null; - } - - @Nullable - @Override - public List getRequestStopSequences(EmbeddingCreateParams request) { - return null; - } - - @Nullable - @Override - public Double getRequestTemperature(EmbeddingCreateParams request) { - return null; - } - - @Nullable - @Override - public Double getRequestTopK(EmbeddingCreateParams request) { - return null; - } - - @Nullable - @Override - public Double getRequestTopP(EmbeddingCreateParams request) { - return null; - } - - @Override - public List getResponseFinishReasons( - EmbeddingCreateParams request, @Nullable CreateEmbeddingResponse response) { - return Collections.emptyList(); - } - - @Nullable - @Override - public String getResponseId( - EmbeddingCreateParams request, @Nullable CreateEmbeddingResponse response) { - return null; - } - - @Nullable - @Override - public String getResponseModel( - EmbeddingCreateParams request, @Nullable CreateEmbeddingResponse response) { - if (response == null) { - return null; - } - return response.model(); - } - - @Nullable - @Override - public Long getUsageInputTokens( - EmbeddingCreateParams request, @Nullable CreateEmbeddingResponse response) { - if (response == null) { - return null; - } - return response.usage().promptTokens(); - } - - @Nullable - @Override - public Long getUsageOutputTokens( - EmbeddingCreateParams request, @Nullable CreateEmbeddingResponse response) { - return null; - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/GenAiSemconvSerializer.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/GenAiSemconvSerializer.java deleted file mode 100644 index 3f260150..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/GenAiSemconvSerializer.java +++ /dev/null @@ -1,667 +0,0 @@ -package dev.braintrust.instrumentation.openai.otel; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.openai.models.chat.completions.*; -import dev.braintrust.json.BraintrustJsonMapper; -import dev.braintrust.trace.Base64Attachment; -import java.io.IOException; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; -import javax.annotation.Nullable; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -final class GenAiSemconvSerializer { - - private static final ObjectMapper JSON_MAPPER = createObjectMapper(); - - private static ObjectMapper createObjectMapper() { - final JsonSerializer attachmentSerializer = - Base64Attachment.createSerializer(); - // Start with the base mapper configuration and add OpenAI-specific serializers - ObjectMapper mapper = BraintrustJsonMapper.get().copy(); - SimpleModule module = new SimpleModule(); - module.addSerializer( - ChatCompletionContentPartImage.class, - new JsonSerializer<>() { - @Override - public void serialize( - ChatCompletionContentPartImage value, - JsonGenerator gen, - SerializerProvider serializers) - throws IOException { - try { - var attachment = - Base64Attachment.of( - value.validate().imageUrl().validate().url()); - attachmentSerializer.serialize(attachment, gen, serializers); - } catch (Exception e) { - JsonSerializer defaultSerializer = - serializers.findValueSerializer( - ChatCompletionContentPartImage.class, null); - defaultSerializer.serialize(value, gen, serializers); - } - } - }); - mapper.registerModule(module); - return mapper; - } - - // OTel GenAI Semantic Convention structures - static class SemconvChatMessage { - @JsonProperty("role") - public final String role; - - @JsonProperty("parts") - public final List parts; - - public SemconvChatMessage(String role, List parts) { - this.role = role; - this.parts = parts; - } - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - static class SemconvOutputChatMessage { - @JsonProperty("role") - public final String role; - - @JsonProperty("parts") - public final List parts; - - @JsonProperty("finish_reason") - public final String finishReason; - - public SemconvOutputChatMessage(String role, List parts, String finishReason) { - this.role = role; - this.parts = parts; - this.finishReason = finishReason; - } - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - static class TextPart { - @JsonProperty("type") - public final String type = "text"; - - @JsonProperty("content") - public final String content; - - public TextPart(String content) { - this.content = content; - } - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - static class ToolCallRequestPart { - @JsonProperty("type") - public final String type = "tool_call"; - - @JsonProperty("name") - public final String name; - - @JsonProperty("id") - public final String id; - - @JsonProperty("arguments") - public final String arguments; - - public ToolCallRequestPart(String name, String id, String arguments) { - this.name = name; - this.id = id; - this.arguments = arguments; - } - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - static class ToolCallResponsePart { - @JsonProperty("type") - public final String type = "tool_call_response"; - - @JsonProperty("id") - public final String id; - - @JsonProperty("response") - public final String response; - - public ToolCallResponsePart(String id, String response) { - this.id = id; - this.response = response; - } - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - static class GenericPart { - @JsonProperty("type") - public final String type; - - @JsonProperty("data") - public final Object data; - - public GenericPart(String type, Object data) { - this.type = type; - this.data = data; - } - } - - // Transform OpenAI messages to OTel GenAI semconv format - static List transformToSemconvMessages( - List messages) { - return messages.stream() - .map(GenAiSemconvSerializer::transformMessage) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - @Nullable - private static SemconvChatMessage transformMessage(ChatCompletionMessageParam message) { - // Try each union type accessor - var userOpt = tryGetUser(message); - if (userOpt.isPresent()) { - return transformUserMessage(userOpt.get()); - } - - var systemOpt = tryGetSystem(message); - if (systemOpt.isPresent()) { - return transformSystemMessage(systemOpt.get()); - } - - var assistantOpt = tryGetAssistant(message); - if (assistantOpt.isPresent()) { - return transformAssistantMessage(assistantOpt.get()); - } - - var toolOpt = tryGetTool(message); - if (toolOpt.isPresent()) { - return transformToolMessage(toolOpt.get()); - } - - var developerOpt = tryGetDeveloper(message); - if (developerOpt.isPresent()) { - return transformDeveloperMessage(developerOpt.get()); - } - - return null; - } - - private static Optional tryGetUser( - ChatCompletionMessageParam message) { - try { - var method = message.getClass().getMethod("user"); - @SuppressWarnings("unchecked") - var result = (Optional) method.invoke(message); - return result; - } catch (Exception e) { - return Optional.empty(); - } - } - - private static Optional tryGetSystem( - ChatCompletionMessageParam message) { - try { - var method = message.getClass().getMethod("system"); - @SuppressWarnings("unchecked") - var result = (Optional) method.invoke(message); - return result; - } catch (Exception e) { - return Optional.empty(); - } - } - - private static Optional tryGetAssistant( - ChatCompletionMessageParam message) { - try { - var method = message.getClass().getMethod("assistant"); - @SuppressWarnings("unchecked") - var result = (Optional) method.invoke(message); - return result; - } catch (Exception e) { - return Optional.empty(); - } - } - - private static Optional tryGetTool( - ChatCompletionMessageParam message) { - try { - var method = message.getClass().getMethod("tool"); - @SuppressWarnings("unchecked") - var result = (Optional) method.invoke(message); - return result; - } catch (Exception e) { - return Optional.empty(); - } - } - - private static Optional tryGetDeveloper( - ChatCompletionMessageParam message) { - try { - var method = message.getClass().getMethod("developer"); - @SuppressWarnings("unchecked") - var result = (Optional) method.invoke(message); - return result; - } catch (Exception e) { - return Optional.empty(); - } - } - - private static SemconvChatMessage transformUserMessage(ChatCompletionUserMessageParam message) { - List parts = new ArrayList<>(); - ChatCompletionUserMessageParam.Content content = message.content(); - - if (content.isText()) { - parts.add(new TextPart(content.asText())); - } else if (content.isArrayOfContentParts()) { - for (var part : content.asArrayOfContentParts()) { - if (part.isText()) { - parts.add(new TextPart(part.asText().text())); - } else { - // Try to get image part using union type accessor - var imageOpt = tryGetImagePart(part); - if (imageOpt.isPresent()) { - try { - var imageUrl = imageOpt.get().imageUrl().url(); - var attachment = Base64Attachment.of(imageUrl); - parts.add(attachment); - } catch (Exception e) { - log.debug("Failed to parse image URL", e); - } - } - } - } - } - - return new SemconvChatMessage("user", parts); - } - - private static Optional tryGetImagePart( - com.openai.models.chat.completions.ChatCompletionContentPart part) { - try { - var method = part.getClass().getMethod("imageUrl"); - @SuppressWarnings("unchecked") - var result = (Optional) method.invoke(part); - return result; - } catch (Exception e) { - return Optional.empty(); - } - } - - private static SemconvChatMessage transformSystemMessage( - ChatCompletionSystemMessageParam message) { - List parts = new ArrayList<>(); - ChatCompletionSystemMessageParam.Content content = message.content(); - - if (content.isText()) { - parts.add(new TextPart(content.asText())); - } else if (content.isArrayOfContentParts()) { - for (var part : content.asArrayOfContentParts()) { - parts.add(new TextPart(part.text())); - } - } - - return new SemconvChatMessage("system", parts); - } - - private static SemconvChatMessage transformDeveloperMessage( - ChatCompletionDeveloperMessageParam message) { - List parts = new ArrayList<>(); - ChatCompletionDeveloperMessageParam.Content content = message.content(); - - if (content.isText()) { - parts.add(new TextPart(content.asText())); - } else if (content.isArrayOfContentParts()) { - for (var part : content.asArrayOfContentParts()) { - parts.add(new TextPart(part.text())); - } - } - - return new SemconvChatMessage("system", parts); - } - - private static SemconvChatMessage transformAssistantMessage( - ChatCompletionAssistantMessageParam message) { - List parts = new ArrayList<>(); - - // Handle text content - message.content() - .ifPresent( - content -> { - if (content.isText()) { - parts.add(new TextPart(content.asText())); - } else if (content.isArrayOfContentParts()) { - for (var part : content.asArrayOfContentParts()) { - if (part.isText()) { - parts.add(new TextPart(part.asText().text())); - } else if (part.isRefusal()) { - parts.add(new TextPart(part.asRefusal().refusal())); - } - } - } - }); - - // Handle tool calls - message.toolCalls() - .ifPresent( - toolCalls -> { - for (var toolCall : toolCalls) { - FunctionAccess functionAccess = getFunctionAccess(toolCall); - if (functionAccess != null) { - parts.add( - new ToolCallRequestPart( - functionAccess.name(), - functionAccess.id(), - functionAccess.arguments())); - } - } - }); - - return new SemconvChatMessage("assistant", parts); - } - - private static SemconvChatMessage transformToolMessage(ChatCompletionToolMessageParam message) { - List parts = new ArrayList<>(); - String toolCallId = message.toolCallId(); - ChatCompletionToolMessageParam.Content content = message.content(); - - String responseContent = ""; - if (content.isText()) { - responseContent = content.asText(); - } else if (content.isArrayOfContentParts()) { - responseContent = joinContentParts(content.asArrayOfContentParts()); - } - - parts.add(new ToolCallResponsePart(toolCallId, responseContent)); - return new SemconvChatMessage("tool", parts); - } - - // Transform ChatCompletionMessage (output) to OTel GenAI semconv format - static SemconvOutputChatMessage transformOutputMessage( - ChatCompletionMessage message, String finishReason) { - List parts = new ArrayList<>(); - - // Handle text content - message.content() - .ifPresent( - content -> { - if (!content.isEmpty()) { - parts.add(new TextPart(content)); - } - }); - - // Handle tool calls - message.toolCalls() - .ifPresent( - toolCalls -> { - for (var toolCall : toolCalls) { - FunctionAccess functionAccess = getFunctionAccess(toolCall); - if (functionAccess != null) { - parts.add( - new ToolCallRequestPart( - functionAccess.name(), - functionAccess.id(), - functionAccess.arguments())); - } - } - }); - - // The role from ChatCompletionMessage is always "assistant" for output messages - return new SemconvOutputChatMessage("assistant", parts, finishReason); - } - - private static String joinContentParts(List contentParts) { - return contentParts.stream() - .map(ChatCompletionContentPartText::text) - .collect(Collectors.joining()); - } - - @SneakyThrows - static String serializeInputMessages(List messages) { - List semconvMessages = transformToSemconvMessages(messages); - return JSON_MAPPER.writeValueAsString(semconvMessages); - } - - @SneakyThrows - static String serializeOutputMessages(List choices) { - var semConvMessages = - choices.stream() - .map(c -> transformOutputMessage(c.message(), c.finishReason().toString())) - .toList(); - return JSON_MAPPER.writeValueAsString(semConvMessages); - } - - @Nullable - static FunctionAccess getFunctionAccess(ChatCompletionMessageToolCall call) { - if (V1FunctionAccess.isAvailable()) { - return V1FunctionAccess.create(call); - } - if (V3FunctionAccess.isAvailable()) { - return V3FunctionAccess.create(call); - } - - return null; - } - - interface FunctionAccess { - String id(); - - String name(); - - String arguments(); - } - - private static String invokeStringHandle(@Nullable MethodHandle methodHandle, Object object) { - if (methodHandle == null) { - return ""; - } - - try { - return (String) methodHandle.invoke(object); - } catch (Throwable ignore) { - return ""; - } - } - - private static class V1FunctionAccess implements FunctionAccess { - @Nullable private static final MethodHandle idHandle; - @Nullable private static final MethodHandle functionHandle; - @Nullable private static final MethodHandle nameHandle; - @Nullable private static final MethodHandle argumentsHandle; - - static { - MethodHandle id; - MethodHandle function; - MethodHandle name; - MethodHandle arguments; - - try { - MethodHandles.Lookup lookup = MethodHandles.lookup(); - id = - lookup.findVirtual( - ChatCompletionMessageToolCall.class, - "id", - MethodType.methodType(String.class)); - Class functionClass = - Class.forName( - "com.openai.models.chat.completions.ChatCompletionMessageToolCall$Function"); - function = - lookup.findVirtual( - ChatCompletionMessageToolCall.class, - "function", - MethodType.methodType(functionClass)); - name = - lookup.findVirtual( - functionClass, "name", MethodType.methodType(String.class)); - arguments = - lookup.findVirtual( - functionClass, "arguments", MethodType.methodType(String.class)); - } catch (Exception exception) { - id = null; - function = null; - name = null; - arguments = null; - } - idHandle = id; - functionHandle = function; - nameHandle = name; - argumentsHandle = arguments; - } - - private final ChatCompletionMessageToolCall toolCall; - private final Object function; - - V1FunctionAccess(ChatCompletionMessageToolCall toolCall, Object function) { - this.toolCall = toolCall; - this.function = function; - } - - @Nullable - static FunctionAccess create(ChatCompletionMessageToolCall toolCall) { - if (functionHandle == null) { - return null; - } - - try { - return new V1FunctionAccess(toolCall, functionHandle.invoke(toolCall)); - } catch (Throwable ignore) { - return null; - } - } - - static boolean isAvailable() { - return idHandle != null; - } - - @Override - public String id() { - return invokeStringHandle(idHandle, toolCall); - } - - @Override - public String name() { - return invokeStringHandle(nameHandle, function); - } - - @Override - public String arguments() { - return invokeStringHandle(argumentsHandle, function); - } - } - - static class V3FunctionAccess implements FunctionAccess { - @Nullable private static final MethodHandle functionToolCallHandle; - @Nullable private static final MethodHandle idHandle; - @Nullable private static final MethodHandle functionHandle; - @Nullable private static final MethodHandle nameHandle; - @Nullable private static final MethodHandle argumentsHandle; - - static { - MethodHandle functionToolCall; - MethodHandle id; - MethodHandle function; - MethodHandle name; - MethodHandle arguments; - - try { - MethodHandles.Lookup lookup = MethodHandles.lookup(); - functionToolCall = - lookup.findVirtual( - ChatCompletionMessageToolCall.class, - "function", - MethodType.methodType(Optional.class)); - Class functionToolCallClass = - Class.forName( - "com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall"); - id = - lookup.findVirtual( - functionToolCallClass, "id", MethodType.methodType(String.class)); - Class functionClass = - Class.forName( - "com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall$Function"); - function = - lookup.findVirtual( - functionToolCallClass, - "function", - MethodType.methodType(functionClass)); - name = - lookup.findVirtual( - functionClass, "name", MethodType.methodType(String.class)); - arguments = - lookup.findVirtual( - functionClass, "arguments", MethodType.methodType(String.class)); - } catch (Exception exception) { - functionToolCall = null; - id = null; - function = null; - name = null; - arguments = null; - } - functionToolCallHandle = functionToolCall; - idHandle = id; - functionHandle = function; - nameHandle = name; - argumentsHandle = arguments; - } - - private final Object functionToolCall; - private final Object function; - - V3FunctionAccess(Object functionToolCall, Object function) { - this.functionToolCall = functionToolCall; - this.function = function; - } - - @Nullable - @SuppressWarnings("unchecked") - static FunctionAccess create(ChatCompletionMessageToolCall toolCall) { - if (functionToolCallHandle == null || functionHandle == null) { - return null; - } - - try { - Optional optional = - (Optional) functionToolCallHandle.invoke(toolCall); - if (!optional.isPresent()) { - return null; - } - Object functionToolCall = optional.get(); - return new V3FunctionAccess( - functionToolCall, functionHandle.invoke(functionToolCall)); - } catch (Throwable ignore) { - return null; - } - } - - static boolean isAvailable() { - return idHandle != null; - } - - @Override - public String id() { - return invokeStringHandle(idHandle, functionToolCall); - } - - @Override - public String name() { - return invokeStringHandle(nameHandle, function); - } - - @Override - public String arguments() { - return invokeStringHandle(argumentsHandle, function); - } - } - - private GenAiSemconvSerializer() {} -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionService.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionService.java deleted file mode 100644 index 20ac1f57..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionService.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.core.RequestOptions; -import com.openai.core.http.StreamResponse; -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionChunk; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.services.blocking.chat.ChatCompletionService; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; - -final class InstrumentedChatCompletionService - extends DelegatingInvocationHandler< - ChatCompletionService, InstrumentedChatCompletionService> { - - private final Instrumenter instrumenter; - private final boolean captureMessageContent; - - InstrumentedChatCompletionService( - ChatCompletionService delegate, - Instrumenter instrumenter, - boolean captureMessageContent) { - super(delegate); - this.instrumenter = instrumenter; - this.captureMessageContent = captureMessageContent; - } - - @Override - protected Class getProxyType() { - return ChatCompletionService.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - - switch (methodName) { - case "create": - if (parameterTypes.length >= 1 - && parameterTypes[0] == ChatCompletionCreateParams.class) { - if (parameterTypes.length == 1) { - return create((ChatCompletionCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 - && parameterTypes[1] == RequestOptions.class) { - return create( - (ChatCompletionCreateParams) args[0], (RequestOptions) args[1]); - } - } - break; - case "createStreaming": - if (parameterTypes.length >= 1 - && parameterTypes[0] == ChatCompletionCreateParams.class) { - if (parameterTypes.length == 1) { - return createStreaming( - (ChatCompletionCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 - && parameterTypes[1] == RequestOptions.class) { - return createStreaming( - (ChatCompletionCreateParams) args[0], (RequestOptions) args[1]); - } - } - break; - default: - // fallthrough - } - - return super.invoke(proxy, method, args); - } - - private ChatCompletion create( - ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { - Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, chatCompletionCreateParams)) { - return createWithAttributes(parentContext, chatCompletionCreateParams, requestOptions); - } - - Context context = instrumenter.start(parentContext, chatCompletionCreateParams); - ChatCompletion completion; - try (Scope ignored = context.makeCurrent()) { - completion = createWithAttributes(context, chatCompletionCreateParams, requestOptions); - } catch (Throwable t) { - instrumenter.end(context, chatCompletionCreateParams, null, t); - throw t; - } - - instrumenter.end(context, chatCompletionCreateParams, completion, null); - return completion; - } - - private ChatCompletion createWithAttributes( - Context context, - ChatCompletionCreateParams chatCompletionCreateParams, - RequestOptions requestOptions) { - BraintrustOAISpanAttributes.setRequestAttributes( - Span.current(), chatCompletionCreateParams); - - long startTimeNanos = System.nanoTime(); - ChatCompletion result = delegate.create(chatCompletionCreateParams, requestOptions); - long elapsedNanos = System.nanoTime() - startTimeNanos; - double timeToFirstTokenSeconds = elapsedNanos / 1_000_000_000.0; - - BraintrustOAISpanAttributes.setTimeToFirstToken(Span.current(), timeToFirstTokenSeconds); - BraintrustOAISpanAttributes.setOutputMessagesFromCompletion(Span.current(), result); - return result; - } - - private StreamResponse createStreaming( - ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { - Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, chatCompletionCreateParams)) { - return createStreamingWithAttributes( - parentContext, chatCompletionCreateParams, requestOptions, false); - } - - Context context = instrumenter.start(parentContext, chatCompletionCreateParams); - try (Scope ignored = context.makeCurrent()) { - return createStreamingWithAttributes( - context, chatCompletionCreateParams, requestOptions, true); - } catch (Throwable t) { - instrumenter.end(context, chatCompletionCreateParams, null, t); - throw t; - } - } - - private StreamResponse createStreamingWithAttributes( - Context context, - ChatCompletionCreateParams chatCompletionCreateParams, - RequestOptions requestOptions, - boolean newSpan) { - BraintrustOAISpanAttributes.setRequestAttributes( - Span.current(), chatCompletionCreateParams); - long startTimeNanos = System.nanoTime(); - StreamResponse result = - delegate.createStreaming(chatCompletionCreateParams, requestOptions); - return new TracingStreamedResponse( - result, - new StreamListener( - context, - chatCompletionCreateParams, - instrumenter, - captureMessageContent, - newSpan, - startTimeNanos)); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionServiceAsync.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionServiceAsync.java deleted file mode 100644 index b66715f3..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionServiceAsync.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.core.RequestOptions; -import com.openai.core.http.AsyncStreamResponse; -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionChunk; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.services.async.chat.ChatCompletionServiceAsync; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; -import java.util.concurrent.CompletableFuture; - -final class InstrumentedChatCompletionServiceAsync - extends DelegatingInvocationHandler< - ChatCompletionServiceAsync, InstrumentedChatCompletionServiceAsync> { - - private final Instrumenter instrumenter; - private final boolean captureMessageContent; - - InstrumentedChatCompletionServiceAsync( - ChatCompletionServiceAsync delegate, - Instrumenter instrumenter, - boolean captureMessageContent) { - super(delegate); - this.instrumenter = instrumenter; - this.captureMessageContent = captureMessageContent; - } - - @Override - protected Class getProxyType() { - return ChatCompletionServiceAsync.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - - switch (methodName) { - case "create": - if (parameterTypes.length >= 1 - && parameterTypes[0] == ChatCompletionCreateParams.class) { - if (parameterTypes.length == 1) { - return create((ChatCompletionCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 - && parameterTypes[1] == RequestOptions.class) { - return create( - (ChatCompletionCreateParams) args[0], (RequestOptions) args[1]); - } - } - break; - case "createStreaming": - if (parameterTypes.length >= 1 - && parameterTypes[0] == ChatCompletionCreateParams.class) { - if (parameterTypes.length == 1) { - return createStreaming( - (ChatCompletionCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 - && parameterTypes[1] == RequestOptions.class) { - return createStreaming( - (ChatCompletionCreateParams) args[0], (RequestOptions) args[1]); - } - } - break; - default: - // fallthrough - } - - return super.invoke(proxy, method, args); - } - - private CompletableFuture create( - ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { - Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, chatCompletionCreateParams)) { - return createWithAttributes(parentContext, chatCompletionCreateParams, requestOptions); - } - - Context context = instrumenter.start(parentContext, chatCompletionCreateParams); - CompletableFuture future; - try (Scope ignored = context.makeCurrent()) { - future = createWithAttributes(context, chatCompletionCreateParams, requestOptions); - } catch (Throwable t) { - instrumenter.end(context, chatCompletionCreateParams, null, t); - throw t; - } - - future = - future.whenComplete( - (res, t) -> instrumenter.end(context, chatCompletionCreateParams, res, t)); - return CompletableFutureWrapper.wrap(future, parentContext); - } - - private CompletableFuture createWithAttributes( - Context context, - ChatCompletionCreateParams chatCompletionCreateParams, - RequestOptions requestOptions) { - BraintrustOAISpanAttributes.setRequestAttributes( - Span.current(), chatCompletionCreateParams); - - long startTimeNanos = System.nanoTime(); - CompletableFuture future = - delegate.create(chatCompletionCreateParams, requestOptions); - future.thenAccept( - r -> { - long elapsedNanos = System.nanoTime() - startTimeNanos; - double timeToFirstTokenSeconds = elapsedNanos / 1_000_000_000.0; - BraintrustOAISpanAttributes.setTimeToFirstToken( - Span.current(), timeToFirstTokenSeconds); - BraintrustOAISpanAttributes.setOutputMessagesFromCompletion(Span.current(), r); - }); - return future; - } - - private AsyncStreamResponse createStreaming( - ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { - Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, chatCompletionCreateParams)) { - return createStreamingWithAttributes( - parentContext, chatCompletionCreateParams, requestOptions, false); - } - - Context context = instrumenter.start(parentContext, chatCompletionCreateParams); - try (Scope ignored = context.makeCurrent()) { - return createStreamingWithAttributes( - context, chatCompletionCreateParams, requestOptions, true); - } catch (Throwable t) { - instrumenter.end(context, chatCompletionCreateParams, null, t); - throw t; - } - } - - private AsyncStreamResponse createStreamingWithAttributes( - Context context, - ChatCompletionCreateParams chatCompletionCreateParams, - RequestOptions requestOptions, - boolean newSpan) { - BraintrustOAISpanAttributes.setRequestAttributes( - Span.current(), chatCompletionCreateParams); - long startTimeNanos = System.nanoTime(); - AsyncStreamResponse result = - delegate.createStreaming(chatCompletionCreateParams, requestOptions); - return new TracingAsyncStreamedResponse( - result, - new StreamListener( - context, - chatCompletionCreateParams, - instrumenter, - captureMessageContent, - newSpan, - startTimeNanos)); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedEmbeddingService.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedEmbeddingService.java deleted file mode 100644 index 7d4405c6..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedEmbeddingService.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.core.RequestOptions; -import com.openai.models.embeddings.CreateEmbeddingResponse; -import com.openai.models.embeddings.EmbeddingCreateParams; -import com.openai.services.blocking.EmbeddingService; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; - -final class InstrumentedEmbeddingService - extends DelegatingInvocationHandler { - - private final Instrumenter instrumenter; - - public InstrumentedEmbeddingService( - EmbeddingService delegate, - Instrumenter instrumenter) { - super(delegate); - this.instrumenter = instrumenter; - } - - @Override - protected Class getProxyType() { - return EmbeddingService.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - - if (methodName.equals("create") - && parameterTypes.length >= 1 - && parameterTypes[0] == EmbeddingCreateParams.class) { - if (parameterTypes.length == 1) { - return create((EmbeddingCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 && parameterTypes[1] == RequestOptions.class) { - return create((EmbeddingCreateParams) args[0], (RequestOptions) args[1]); - } - } - - return super.invoke(proxy, method, args); - } - - private CreateEmbeddingResponse create( - EmbeddingCreateParams request, RequestOptions requestOptions) { - Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, request)) { - return delegate.create(request, requestOptions); - } - - Context context = instrumenter.start(parentContext, request); - CreateEmbeddingResponse response; - try (Scope ignored = context.makeCurrent()) { - response = delegate.create(request, requestOptions); - } catch (Throwable t) { - instrumenter.end(context, request, null, t); - throw t; - } - - instrumenter.end(context, request, response, null); - return response; - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedEmbeddingServiceAsync.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedEmbeddingServiceAsync.java deleted file mode 100644 index 432d60a4..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedEmbeddingServiceAsync.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.core.RequestOptions; -import com.openai.models.embeddings.CreateEmbeddingResponse; -import com.openai.models.embeddings.EmbeddingCreateParams; -import com.openai.services.async.EmbeddingServiceAsync; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; -import java.util.concurrent.CompletableFuture; - -final class InstrumentedEmbeddingServiceAsync - extends DelegatingInvocationHandler< - EmbeddingServiceAsync, InstrumentedEmbeddingServiceAsync> { - - private final Instrumenter instrumenter; - - public InstrumentedEmbeddingServiceAsync( - EmbeddingServiceAsync delegate, - Instrumenter instrumenter) { - super(delegate); - this.instrumenter = instrumenter; - } - - @Override - protected Class getProxyType() { - return EmbeddingServiceAsync.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - - if (methodName.equals("create") - && parameterTypes.length >= 1 - && parameterTypes[0] == EmbeddingCreateParams.class) { - if (parameterTypes.length == 1) { - return create((EmbeddingCreateParams) args[0], RequestOptions.none()); - } else if (parameterTypes.length == 2 && parameterTypes[1] == RequestOptions.class) { - return create((EmbeddingCreateParams) args[0], (RequestOptions) args[1]); - } - } - - return super.invoke(proxy, method, args); - } - - private CompletableFuture create( - EmbeddingCreateParams request, RequestOptions requestOptions) { - Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, request)) { - return delegate.create(request, requestOptions); - } - - Context context = instrumenter.start(parentContext, request); - CompletableFuture future; - try (Scope ignored = context.makeCurrent()) { - future = delegate.create(request, requestOptions); - } catch (Throwable t) { - instrumenter.end(context, request, null, t); - throw t; - } - - future = future.whenComplete((res, t) -> instrumenter.end(context, request, res, t)); - return CompletableFutureWrapper.wrap(future, parentContext); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClient.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClient.java deleted file mode 100644 index 0391c54e..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClient.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.client.OpenAIClient; -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.models.embeddings.CreateEmbeddingResponse; -import com.openai.models.embeddings.EmbeddingCreateParams; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; - -final class InstrumentedOpenAiClient - extends DelegatingInvocationHandler { - - private final Instrumenter chatInstrumenter; - private final Instrumenter - embeddingInstrumenter; - private final boolean captureMessageContent; - - InstrumentedOpenAiClient( - OpenAIClient delegate, - Instrumenter chatInstrumenter, - Instrumenter embeddingInstrumenter, - boolean captureMessageContent) { - super(delegate); - this.chatInstrumenter = chatInstrumenter; - this.embeddingInstrumenter = embeddingInstrumenter; - this.captureMessageContent = captureMessageContent; - } - - @Override - protected Class getProxyType() { - return OpenAIClient.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - if (methodName.equals("chat") && parameterTypes.length == 0) { - return createChatServiceProxy(); - } - if (methodName.equals("embeddings") && parameterTypes.length == 0) { - return new InstrumentedEmbeddingService(delegate.embeddings(), embeddingInstrumenter) - .createProxy(); - } - if (methodName.equals("async") && parameterTypes.length == 0) { - return new InstrumentedOpenAiClientAsync( - delegate.async(), - chatInstrumenter, - embeddingInstrumenter, - captureMessageContent) - .createProxy(); - } - return super.invoke(proxy, method, args); - } - - private Object createChatServiceProxy() { - return java.lang.reflect.Proxy.newProxyInstance( - com.openai.services.blocking.ChatService.class.getClassLoader(), - new Class[] {com.openai.services.blocking.ChatService.class}, - (p, m, a) -> { - if ("completions".equals(m.getName()) && m.getParameterCount() == 0) { - return new InstrumentedChatCompletionService( - delegate.chat().completions(), - chatInstrumenter, - captureMessageContent) - .createProxy(); - } - return m.invoke(delegate.chat(), a); - }); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClientAsync.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClientAsync.java deleted file mode 100644 index 48999b20..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClientAsync.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.client.OpenAIClientAsync; -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.models.embeddings.CreateEmbeddingResponse; -import com.openai.models.embeddings.EmbeddingCreateParams; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; - -final class InstrumentedOpenAiClientAsync - extends DelegatingInvocationHandler { - - private final Instrumenter chatInstrumenter; - private final Instrumenter - embeddingInstrumenter; - private final boolean captureMessageContent; - - InstrumentedOpenAiClientAsync( - OpenAIClientAsync delegate, - Instrumenter chatInstrumenter, - Instrumenter embeddingInstrumenter, - boolean captureMessageContent) { - super(delegate); - this.chatInstrumenter = chatInstrumenter; - this.embeddingInstrumenter = embeddingInstrumenter; - this.captureMessageContent = captureMessageContent; - } - - @Override - protected Class getProxyType() { - return OpenAIClientAsync.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - if (methodName.equals("chat") && parameterTypes.length == 0) { - return createChatServiceAsyncProxy(); - } - if (methodName.equals("embeddings") && parameterTypes.length == 0) { - return new InstrumentedEmbeddingServiceAsync( - delegate.embeddings(), embeddingInstrumenter) - .createProxy(); - } - if (methodName.equals("sync") && parameterTypes.length == 0) { - return new InstrumentedOpenAiClient( - delegate.sync(), - chatInstrumenter, - embeddingInstrumenter, - captureMessageContent) - .createProxy(); - } - return super.invoke(proxy, method, args); - } - - private Object createChatServiceAsyncProxy() { - return java.lang.reflect.Proxy.newProxyInstance( - com.openai.services.async.ChatServiceAsync.class.getClassLoader(), - new Class[] {com.openai.services.async.ChatServiceAsync.class}, - (p, m, a) -> { - if ("completions".equals(m.getName()) && m.getParameterCount() == 0) { - return new InstrumentedChatCompletionServiceAsync( - delegate.chat().completions(), - chatInstrumenter, - captureMessageContent) - .createProxy(); - } - return m.invoke(delegate.chat(), a); - }); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetry.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetry.java deleted file mode 100644 index 64f858ec..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetry.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.client.OpenAIClient; -import com.openai.client.OpenAIClientAsync; -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.models.embeddings.CreateEmbeddingResponse; -import com.openai.models.embeddings.EmbeddingCreateParams; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; - -/** Entrypoint for instrumenting OpenAI clients. */ -@SuppressWarnings("IdentifierName") // Want to match library's convention -public final class OpenAITelemetry { - /** Returns a new {@link OpenAITelemetry} configured with the given {@link OpenTelemetry}. */ - public static OpenAITelemetry create(OpenTelemetry openTelemetry) { - return builder(openTelemetry).build(); - } - - /** - * Returns a new {@link OpenAITelemetryBuilder} configured with the given {@link OpenTelemetry}. - */ - public static OpenAITelemetryBuilder builder(OpenTelemetry openTelemetry) { - return new OpenAITelemetryBuilder(openTelemetry); - } - - private final Instrumenter chatInstrumenter; - private final Instrumenter - embeddingsInstrumenter; - private final boolean captureMessageContent; - - OpenAITelemetry( - Instrumenter chatInstrumenter, - Instrumenter embeddingsInstrumenter, - boolean captureMessageContent) { - this.chatInstrumenter = chatInstrumenter; - this.embeddingsInstrumenter = embeddingsInstrumenter; - this.captureMessageContent = captureMessageContent; - } - - /** Wraps the provided OpenAIClient, enabling telemetry for it. */ - public OpenAIClient wrap(OpenAIClient client) { - return new InstrumentedOpenAiClient( - client, chatInstrumenter, embeddingsInstrumenter, captureMessageContent) - .createProxy(); - } - - /** Wraps the provided OpenAIClientAsync, enabling telemetry for it. */ - public OpenAIClientAsync wrap(OpenAIClientAsync client) { - return new InstrumentedOpenAiClientAsync( - client, chatInstrumenter, embeddingsInstrumenter, captureMessageContent) - .createProxy(); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetryBuilder.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetryBuilder.java deleted file mode 100644 index 3b4fd4c0..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetryBuilder.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.models.embeddings.CreateEmbeddingResponse; -import com.openai.models.embeddings.EmbeddingCreateParams; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor; -import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; - -/** A builder of {@link OpenAITelemetry}. */ -@SuppressWarnings("IdentifierName") // Want to match library's convention -public final class OpenAITelemetryBuilder { - static final String INSTRUMENTATION_NAME = "io.opentelemetry.openai-java-1.1"; - - private final OpenTelemetry openTelemetry; - - private boolean captureMessageContent; - - OpenAITelemetryBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; - } - - /** - * Sets whether emitted log events include full content of user and assistant messages. - * - *

Note that full content can have data privacy and size concerns and care should be taken - * when enabling this. - */ - @CanIgnoreReturnValue - public OpenAITelemetryBuilder setCaptureMessageContent(boolean captureMessageContent) { - this.captureMessageContent = captureMessageContent; - return this; - } - - /** - * Returns a new {@link OpenAITelemetry} with the settings of this {@link - * OpenAITelemetryBuilder}. - */ - public OpenAITelemetry build() { - // Use hardcoded span names to match Python/TypeScript SDKs - SpanNameExtractor chatSpanNameExtractor = - request -> "Chat Completion"; - SpanNameExtractor embeddingSpanNameExtractor = - request -> "Embedding"; - - Instrumenter chatInstrumenter = - Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, chatSpanNameExtractor) - .addAttributesExtractor( - GenAiAttributesExtractor.create(ChatAttributesGetter.INSTANCE)) - .addOperationMetrics(GenAiClientMetrics.get()) - .buildInstrumenter(); - - Instrumenter embeddingsInstrumenter = - Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, embeddingSpanNameExtractor) - .addAttributesExtractor( - GenAiAttributesExtractor.create(EmbeddingAttributesGetter.INSTANCE)) - .addOperationMetrics(GenAiClientMetrics.get()) - .buildInstrumenter(SpanKindExtractor.alwaysClient()); - - return new OpenAITelemetry(chatInstrumenter, embeddingsInstrumenter, captureMessageContent); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamListener.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamListener.java deleted file mode 100644 index b0d5f92a..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamListener.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionChunk; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.models.completions.CompletionUsage; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; -import javax.annotation.Nullable; -import lombok.SneakyThrows; - -final class StreamListener { - private final Context context; - private final ChatCompletionCreateParams request; - private final List choiceBuffers; - - private final Instrumenter instrumenter; - private final boolean captureMessageContent; - private final boolean newSpan; - private final AtomicBoolean hasEnded; - private final AtomicBoolean firstChunkReceived; - private final long startTimeNanos; - - @Nullable private CompletionUsage usage; - @Nullable private String model; - @Nullable private String responseId; - - StreamListener( - Context context, - ChatCompletionCreateParams request, - Instrumenter instrumenter, - boolean captureMessageContent, - boolean newSpan, - long startTimeNanos) { - this.context = context; - this.request = request; - this.instrumenter = instrumenter; - this.captureMessageContent = captureMessageContent; - this.newSpan = newSpan; - this.startTimeNanos = startTimeNanos; - choiceBuffers = new ArrayList<>(); - hasEnded = new AtomicBoolean(); - firstChunkReceived = new AtomicBoolean(); - } - - @SneakyThrows - void onChunk(ChatCompletionChunk chunk) { - // Calculate time to first token on the first chunk - if (firstChunkReceived.compareAndSet(false, true)) { - long elapsedNanos = System.nanoTime() - startTimeNanos; - double timeToFirstTokenSeconds = elapsedNanos / 1_000_000_000.0; - BraintrustOAISpanAttributes.setTimeToFirstToken( - Span.fromContext(context), timeToFirstTokenSeconds); - } - - model = chunk.model(); - responseId = chunk.id(); - chunk.usage().ifPresent(u -> usage = u); - - for (ChatCompletionChunk.Choice choice : chunk.choices()) { - while (choiceBuffers.size() <= choice.index()) { - choiceBuffers.add(null); - } - StreamedMessageBuffer buffer = choiceBuffers.get((int) choice.index()); - if (buffer == null) { - buffer = new StreamedMessageBuffer(choice.index(), captureMessageContent); - choiceBuffers.set((int) choice.index(), buffer); - } - buffer.append(choice.delta()); - if (choice.finishReason().isPresent()) { - buffer.finishReason = choice.finishReason().get().toString(); - } - } - } - - void endSpan(@Nullable Throwable error) { - // Use an atomic operation since close() type of methods are exposed to the user - // and can come from any thread. - if (!hasEnded.compareAndSet(false, true)) { - return; - } - - if (model == null || responseId == null) { - // Only happens if we got no chunks, so we have no response. - if (newSpan) { - instrumenter.end(context, request, null, error); - } - return; - } - - ChatCompletion.Builder result = - ChatCompletion.builder() - .created(0) - .model(model) - .id(responseId) - .choices( - choiceBuffers.stream() - .map(StreamedMessageBuffer::toChoice) - .collect(Collectors.toList())); - - if (usage != null) { - result.usage(usage); - } - - if (newSpan) { - ChatCompletion completion = result.build(); - BraintrustOAISpanAttributes.setOutputMessagesFromCompletion( - Span.fromContext(context), completion); - instrumenter.end(context, request, completion, error); - } - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamedMessageBuffer.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamedMessageBuffer.java deleted file mode 100644 index f805611e..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamedMessageBuffer.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.core.JsonField; -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionChunk; -import com.openai.models.chat.completions.ChatCompletionMessage; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import javax.annotation.Nullable; - -final class StreamedMessageBuffer { - private final long index; - private final boolean captureMessageContent; - - @Nullable String finishReason; - - @Nullable private StringBuilder message; - @Nullable private Map toolCalls; - - StreamedMessageBuffer(long index, boolean captureMessageContent) { - this.index = index; - this.captureMessageContent = captureMessageContent; - } - - ChatCompletion.Choice toChoice() { - ChatCompletion.Choice.Builder choice = - ChatCompletion.Choice.builder().index(index).logprobs(Optional.empty()); - if (finishReason != null) { - choice.finishReason(ChatCompletion.Choice.FinishReason.of(finishReason)); - } else { - // Can't happen in practice, mostly to satisfy null check - choice.finishReason(JsonField.ofNullable(null)); - } - if (message != null) { - choice.message( - ChatCompletionMessage.builder() - .content(message.toString()) - .refusal(Optional.empty()) - .build()); - } else { - choice.message(JsonField.ofNullable(null)); - } - return choice.build(); - } - - void append(ChatCompletionChunk.Choice.Delta delta) { - if (captureMessageContent) { - if (delta.content().isPresent()) { - if (message == null) { - message = new StringBuilder(); - } - message.append(delta.content().get()); - } - } - - if (delta.toolCalls().isPresent()) { - if (toolCalls == null) { - toolCalls = new HashMap<>(); - } - - for (ChatCompletionChunk.Choice.Delta.ToolCall toolCall : delta.toolCalls().get()) { - ToolCallBuffer buffer = - toolCalls.computeIfAbsent( - toolCall.index(), - unused -> new ToolCallBuffer(toolCall.id().orElse(""))); - toolCall.type().ifPresent(type -> buffer.type = type.toString()); - toolCall.function() - .ifPresent( - function -> { - function.name().ifPresent(name -> buffer.function.name = name); - if (captureMessageContent) { - function.arguments() - .ifPresent( - args -> { - if (buffer.function.arguments == null) { - buffer.function.arguments = - new StringBuilder(); - } - buffer.function.arguments.append(args); - }); - } - }); - } - } - } - - private static class FunctionBuffer { - @Nullable String name; - @Nullable StringBuilder arguments; - } - - private static class ToolCallBuffer { - final String id; - final FunctionBuffer function = new FunctionBuffer(); - @Nullable String type; - - ToolCallBuffer(String id) { - this.id = id; - } - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/TracingAsyncStreamedResponse.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/TracingAsyncStreamedResponse.java deleted file mode 100644 index 533dcbb4..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/TracingAsyncStreamedResponse.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.core.http.AsyncStreamResponse; -import com.openai.models.chat.completions.ChatCompletionChunk; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; - -final class TracingAsyncStreamedResponse implements AsyncStreamResponse { - - private final AsyncStreamResponse delegate; - private final StreamListener listener; - - TracingAsyncStreamedResponse( - AsyncStreamResponse delegate, StreamListener listener) { - this.delegate = delegate; - this.listener = listener; - } - - @Override - public void close() { - listener.endSpan(null); - delegate.close(); - } - - @Override - public AsyncStreamResponse subscribe( - Handler handler) { - delegate.subscribe(new TracingHandler(handler)); - return this; - } - - @Override - public AsyncStreamResponse subscribe( - Handler handler, Executor executor) { - delegate.subscribe(new TracingHandler(handler), executor); - return this; - } - - @Override - public CompletableFuture onCompleteFuture() { - return delegate.onCompleteFuture(); - } - - private class TracingHandler implements Handler { - - private final Handler delegate; - - private TracingHandler(Handler delegate) { - this.delegate = delegate; - } - - @Override - public void onNext(ChatCompletionChunk chunk) { - listener.onChunk(chunk); - delegate.onNext(chunk); - } - - @Override - public void onComplete(Optional error) { - listener.endSpan(error.orElse(null)); - delegate.onComplete(error); - } - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/TracingStreamedResponse.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/TracingStreamedResponse.java deleted file mode 100644 index 9a14b3b6..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/TracingStreamedResponse.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.core.http.StreamResponse; -import com.openai.models.chat.completions.ChatCompletionChunk; -import java.util.Comparator; -import java.util.Spliterator; -import java.util.function.Consumer; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import javax.annotation.Nullable; - -final class TracingStreamedResponse implements StreamResponse { - - private final StreamResponse delegate; - private final StreamListener listener; - - TracingStreamedResponse(StreamResponse delegate, StreamListener listener) { - this.delegate = delegate; - this.listener = listener; - } - - @Override - public Stream stream() { - return StreamSupport.stream(new TracingSpliterator(delegate.stream().spliterator()), false); - } - - @Override - public void close() { - listener.endSpan(null); - delegate.close(); - } - - private class TracingSpliterator implements Spliterator { - - private final Spliterator delegateSpliterator; - - private TracingSpliterator(Spliterator delegateSpliterator) { - this.delegateSpliterator = delegateSpliterator; - } - - @Override - public boolean tryAdvance(Consumer action) { - boolean chunkReceived = - delegateSpliterator.tryAdvance( - chunk -> { - listener.onChunk(chunk); - action.accept(chunk); - }); - if (!chunkReceived) { - listener.endSpan(null); - } - return chunkReceived; - } - - @Override - @Nullable - public Spliterator trySplit() { - // do not support parallelism to reliably catch the last chunk - return null; - } - - @Override - public long estimateSize() { - return delegateSpliterator.estimateSize(); - } - - @Override - public long getExactSizeIfKnown() { - return delegateSpliterator.getExactSizeIfKnown(); - } - - @Override - public int characteristics() { - return delegateSpliterator.characteristics(); - } - - @Override - public Comparator getComparator() { - return delegateSpliterator.getComparator(); - } - } -} diff --git a/src/test/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropicBetaTest.java b/src/test/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropicBetaTest.java deleted file mode 100644 index ef92d82d..00000000 --- a/src/test/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropicBetaTest.java +++ /dev/null @@ -1,159 +0,0 @@ -package dev.braintrust.instrumentation.anthropic; - -import static org.junit.jupiter.api.Assertions.*; - -import com.anthropic.client.AnthropicClient; -import com.anthropic.client.okhttp.AnthropicOkHttpClient; -import com.anthropic.models.beta.messages.MessageCreateParams; -import com.anthropic.models.messages.Model; -import dev.braintrust.TestHarness; -import dev.braintrust.json.BraintrustJsonMapper; -import io.opentelemetry.api.common.AttributeKey; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class BraintrustAnthropicBetaTest { - private TestHarness testHarness; - - @BeforeEach - void beforeEach() { - testHarness = TestHarness.setup(); - } - - @Test - @SneakyThrows - void testWrapAnthropicBeta() { - AnthropicClient anthropicClient = - AnthropicOkHttpClient.builder() - .baseUrl(testHarness.anthropicBaseUrl()) - .apiKey(testHarness.anthropicApiKey()) - .build(); - - anthropicClient = BraintrustAnthropic.wrap(testHarness.openTelemetry(), anthropicClient); - - var request = - MessageCreateParams.builder() - .model(Model.CLAUDE_3_HAIKU_20240307) - .system("You are a helpful assistant") - .addUserMessage("What is the capital of France?") - .maxTokens(50) - .temperature(0.0) - .build(); - - var response = anthropicClient.beta().messages().create(request); - - assertNotNull(response); - assertNotNull(response.id()); - var contentBlock = response.content().get(0); - assertTrue(contentBlock.isText()); - assertNotNull(contentBlock.asText().text()); - - var spans = testHarness.awaitExportedSpans(); - assertEquals(1, spans.size()); - var span = spans.get(0); - - assertEquals( - "anthropic", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.provider.name"))); - assertEquals( - "claude-3-haiku-20240307", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.request.model"))); - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("gen_ai.response.model"))); - assertNotNull( - span.getAttributes() - .get(AttributeKey.stringArrayKey("gen_ai.response.finish_reasons"))); - assertEquals( - "anthropic.messages.create", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name"))); - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("gen_ai.response.id"))); - assertNotNull(span.getAttributes().get(AttributeKey.longKey("gen_ai.usage.input_tokens"))); - assertNotNull(span.getAttributes().get(AttributeKey.longKey("gen_ai.usage.output_tokens"))); - - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("braintrust.input_json"))); - - String outputJson = - span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); - assertNotNull(outputJson); - var outputMessage = BraintrustJsonMapper.get().readTree(outputJson); - assertNotNull(outputMessage.get("id")); - assertEquals("message", outputMessage.get("type").asText()); - assertEquals("assistant", outputMessage.get("role").asText()); - - Double timeToFirstToken = - span.getAttributes() - .get(AttributeKey.doubleKey("braintrust.metrics.time_to_first_token")); - assertNotNull(timeToFirstToken, "time_to_first_token should be present"); - assertTrue(timeToFirstToken >= 0.0, "time_to_first_token should be non-negative"); - } - - @Test - @SneakyThrows - void testWrapAnthropicBetaStreaming() { - AnthropicClient anthropicClient = - AnthropicOkHttpClient.builder() - .baseUrl(testHarness.anthropicBaseUrl()) - .apiKey(testHarness.anthropicApiKey()) - .build(); - - anthropicClient = BraintrustAnthropic.wrap(testHarness.openTelemetry(), anthropicClient); - - var request = - MessageCreateParams.builder() - .model(Model.CLAUDE_3_HAIKU_20240307) - .system("You are a helpful assistant") - .addUserMessage("What is the capital of France?") - .maxTokens(50) - .temperature(0.0) - .build(); - - StringBuilder fullResponse = new StringBuilder(); - try (var stream = anthropicClient.beta().messages().createStreaming(request)) { - stream.stream() - .forEach( - event -> { - if (event.contentBlockDelta().isPresent()) { - var delta = event.contentBlockDelta().get(); - if (delta.delta().text().isPresent()) { - fullResponse.append(delta.delta().text().get().text()); - } - } - }); - } - - assertFalse(fullResponse.toString().isEmpty()); - - var spans = testHarness.awaitExportedSpans(); - assertEquals(1, spans.size()); - var span = spans.get(0); - - assertEquals( - "anthropic", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.provider.name"))); - assertEquals( - "claude-3-haiku-20240307", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.request.model"))); - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("gen_ai.response.model"))); - assertEquals( - "anthropic.messages.create", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name"))); - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("gen_ai.response.id"))); - assertNotNull(span.getAttributes().get(AttributeKey.longKey("gen_ai.usage.input_tokens"))); - assertNotNull(span.getAttributes().get(AttributeKey.longKey("gen_ai.usage.output_tokens"))); - - String outputJson = - span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); - assertNotNull(outputJson); - var outputMessages = BraintrustJsonMapper.get().readTree(outputJson); - assertTrue(outputMessages.size() > 0); - var messageZero = outputMessages.get(0); - assertEquals("assistant", messageZero.get("role").asText()); - assertFalse(messageZero.get("content").asText().isEmpty()); - - Double timeToFirstToken = - span.getAttributes() - .get(AttributeKey.doubleKey("braintrust.metrics.time_to_first_token")); - assertNotNull(timeToFirstToken, "time_to_first_token should be present for streaming"); - assertTrue(timeToFirstToken >= 0.0, "time_to_first_token should be non-negative"); - } -} diff --git a/src/test/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropicTest.java b/src/test/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropicTest.java index e4484645..78a9f507 100644 --- a/src/test/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropicTest.java +++ b/src/test/java/dev/braintrust/instrumentation/anthropic/BraintrustAnthropicTest.java @@ -6,9 +6,11 @@ import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import dev.braintrust.TestHarness; import io.opentelemetry.api.common.AttributeKey; +import java.util.concurrent.TimeUnit; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,19 +28,17 @@ void beforeEach() { @Test @SneakyThrows void testWrapAnthropic() { - // Create Anthropic client using VCR AnthropicClient anthropicClient = AnthropicOkHttpClient.builder() .baseUrl(testHarness.anthropicBaseUrl()) .apiKey(testHarness.anthropicApiKey()) .build(); - // Wrap with Braintrust instrumentation anthropicClient = BraintrustAnthropic.wrap(testHarness.openTelemetry(), anthropicClient); var request = MessageCreateParams.builder() - .model(Model.CLAUDE_3_5_HAIKU_20241022) + .model(Model.CLAUDE_3_HAIKU_20240307) .system("You are a helpful assistant") .addUserMessage("What is the capital of France?") .maxTokens(50) @@ -59,41 +59,38 @@ void testWrapAnthropic() { assertEquals(1, spans.size()); var span = spans.get(0); - // Verify standard GenAI attributes - assertEquals( - "anthropic", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.provider.name"))); - assertEquals( - "claude-3-5-haiku-20241022", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.request.model"))); - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("gen_ai.response.model"))); - assertNotNull( - span.getAttributes() - .get(AttributeKey.stringArrayKey("gen_ai.response.finish_reasons"))); - assertEquals( - "anthropic.messages.create", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name"))); - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("gen_ai.response.id"))); - assertEquals( - "project_name:" + TestHarness.defaultProjectName(), - span.getAttributes().get(AttributeKey.stringKey("braintrust.parent"))); - assertNotNull(span.getAttributes().get(AttributeKey.longKey("gen_ai.usage.input_tokens"))); - assertNotNull(span.getAttributes().get(AttributeKey.longKey("gen_ai.usage.output_tokens"))); - assertEquals( - 0.0, - span.getAttributes().get(AttributeKey.doubleKey("gen_ai.request.temperature"))); - assertEquals( - 50L, span.getAttributes().get(AttributeKey.longKey("gen_ai.request.max_tokens"))); - - // Verify Braintrust-specific attributes - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("braintrust.input_json"))); + assertEquals("anthropic.messages.create", span.getName()); + + // Verify span_attributes + String spanAttributesJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.span_attributes")); + assertNotNull(spanAttributesJson); + JsonNode spanAttributes = JSON_MAPPER.readTree(spanAttributesJson); + assertEquals("llm", spanAttributes.get("type").asText()); + + // Verify metadata + String metadataJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata")); + assertNotNull(metadataJson); + JsonNode metadata = JSON_MAPPER.readTree(metadataJson); + assertEquals("anthropic", metadata.get("provider").asText()); + assertTrue( + metadata.get("model").asText().startsWith("claude-3-haiku"), + "model should start with claude-3-haiku"); - // Verify output JSON + // Verify input + String inputJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.input_json")); + assertNotNull(inputJson); + JsonNode input = JSON_MAPPER.readTree(inputJson); + assertTrue(input.isArray()); + assertTrue(input.size() > 0); + + // Verify output — full Message object String outputJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); assertNotNull(outputJson); - - var outputMessage = JSON_MAPPER.readTree(outputJson); + JsonNode outputMessage = JSON_MAPPER.readTree(outputJson); assertNotNull(outputMessage.get("id")); assertEquals("message", outputMessage.get("type").asText()); assertEquals("assistant", outputMessage.get("role").asText()); @@ -101,37 +98,38 @@ void testWrapAnthropic() { assertTrue(outputMessage.get("usage").get("output_tokens").asInt() > 0); assertTrue(outputMessage.get("usage").get("input_tokens").asInt() > 0); - // Verify time to first token - Double timeToFirstToken = - span.getAttributes() - .get(AttributeKey.doubleKey("braintrust.metrics.time_to_first_token")); - assertNotNull(timeToFirstToken, "time_to_first_token should be present"); - assertTrue(timeToFirstToken >= 0.0, "time_to_first_token should be non-negative"); + // Verify metrics — tokens; non-streaming should NOT have time_to_first_token + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("prompt_tokens"), "prompt_tokens should be present"); + assertTrue(metrics.has("completion_tokens"), "completion_tokens should be present"); + assertTrue(metrics.has("tokens"), "tokens should be present"); + assertFalse( + metrics.has("time_to_first_token"), + "time_to_first_token should not be present for non-streaming"); } @Test @SneakyThrows void testWrapAnthropicStreaming() { - // Create Anthropic client using VCR AnthropicClient anthropicClient = AnthropicOkHttpClient.builder() .baseUrl(testHarness.anthropicBaseUrl()) .apiKey(testHarness.anthropicApiKey()) .build(); - // Wrap with Braintrust instrumentation anthropicClient = BraintrustAnthropic.wrap(testHarness.openTelemetry(), anthropicClient); var request = MessageCreateParams.builder() - .model(Model.CLAUDE_3_5_HAIKU_20241022) + .model(Model.CLAUDE_3_HAIKU_20240307) .system("You are a helpful assistant") .addUserMessage("What is the capital of France?") .maxTokens(50) .temperature(0.0) .build(); - // Consume the stream StringBuilder fullResponse = new StringBuilder(); try (var stream = anthropicClient.messages().createStreaming(request)) { stream.stream() @@ -146,46 +144,326 @@ void testWrapAnthropicStreaming() { }); } - // Verify the response assertFalse(fullResponse.toString().isEmpty()); - // Verify spans were exported var spans = testHarness.awaitExportedSpans(); assertEquals(1, spans.size()); var span = spans.get(0); - // Verify standard GenAI attributes - assertEquals( - "anthropic", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.provider.name"))); - assertEquals( - "claude-3-5-haiku-20241022", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.request.model"))); - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("gen_ai.response.model"))); - assertEquals( - "anthropic.messages.create", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name"))); - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("gen_ai.response.id"))); - - // Verify usage metrics were captured from streaming - assertNotNull(span.getAttributes().get(AttributeKey.longKey("gen_ai.usage.input_tokens"))); - assertNotNull(span.getAttributes().get(AttributeKey.longKey("gen_ai.usage.output_tokens"))); - - // Verify output JSON was captured + assertEquals("anthropic.messages.create", span.getName()); + + // Verify metadata + String metadataJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata")); + assertNotNull(metadataJson); + JsonNode metadata = JSON_MAPPER.readTree(metadataJson); + assertEquals("anthropic", metadata.get("provider").asText()); + + // Verify input + assertNotNull(span.getAttributes().get(AttributeKey.stringKey("braintrust.input_json"))); + + // Verify output — full Message object assembled from SSE stream String outputJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); assertNotNull(outputJson); - var outputMessages = JSON_MAPPER.readTree(outputJson); - assertTrue(outputMessages.size() > 0); - var messageZero = outputMessages.get(0); - assertEquals("assistant", messageZero.get("role").asText()); - assertFalse(messageZero.get("content").asText().isEmpty()); - - // Verify time to first token - Double timeToFirstToken = - span.getAttributes() - .get(AttributeKey.doubleKey("braintrust.metrics.time_to_first_token")); - assertNotNull(timeToFirstToken, "time_to_first_token should be present for streaming"); - assertTrue(timeToFirstToken >= 0.0, "time_to_first_token should be non-negative"); + JsonNode outputMessage = JSON_MAPPER.readTree(outputJson); + assertEquals("assistant", outputMessage.get("role").asText()); + assertFalse( + outputMessage.get("content").get(0).get("text").asText().isEmpty(), + "content should not be empty"); + + // Verify metrics — tokens and time_to_first_token + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("prompt_tokens"), "prompt_tokens should be present"); + assertTrue(metrics.has("completion_tokens"), "completion_tokens should be present"); + assertTrue(metrics.has("time_to_first_token"), "time_to_first_token should be present"); + assertTrue( + metrics.get("time_to_first_token").asDouble() >= 0.0, + "time_to_first_token should be non-negative"); + } + + @Test + @SneakyThrows + void testWrapAnthropicAsync() { + AnthropicClient anthropicClient = + AnthropicOkHttpClient.builder() + .baseUrl(testHarness.anthropicBaseUrl()) + .apiKey(testHarness.anthropicApiKey()) + .build(); + + anthropicClient = BraintrustAnthropic.wrap(testHarness.openTelemetry(), anthropicClient); + + var request = + MessageCreateParams.builder() + .model(Model.CLAUDE_3_HAIKU_20240307) + .system("You are a helpful assistant") + .addUserMessage("What is the capital of France?") + .maxTokens(50) + .temperature(0.0) + .build(); + + var response = anthropicClient.async().messages().create(request).get(); + + assertNotNull(response); + assertNotNull(response.id()); + var contentBlock = response.content().get(0); + assertTrue(contentBlock.isText()); + assertNotNull(contentBlock.asText().text()); + + var spans = testHarness.awaitExportedSpans(); + assertEquals(1, spans.size()); + var span = spans.get(0); + + assertEquals("anthropic.messages.create", span.getName()); + + String spanAttributesJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.span_attributes")); + assertNotNull(spanAttributesJson); + JsonNode spanAttributes = JSON_MAPPER.readTree(spanAttributesJson); + assertEquals("llm", spanAttributes.get("type").asText()); + + String metadataJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata")); + assertNotNull(metadataJson); + JsonNode metadata = JSON_MAPPER.readTree(metadataJson); + assertEquals("anthropic", metadata.get("provider").asText()); + + String outputJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); + assertNotNull(outputJson); + JsonNode outputMessage = JSON_MAPPER.readTree(outputJson); + assertEquals("message", outputMessage.get("type").asText()); + assertEquals("assistant", outputMessage.get("role").asText()); + assertNotNull(outputMessage.get("content").get(0).get("text")); + + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("prompt_tokens")); + assertTrue(metrics.has("completion_tokens")); + assertTrue(metrics.has("tokens")); + assertFalse( + metrics.has("time_to_first_token"), + "time_to_first_token should not be present for non-streaming"); + } + + @Test + @SneakyThrows + void testWrapAnthropicAsyncStreaming() { + AnthropicClient anthropicClient = + AnthropicOkHttpClient.builder() + .baseUrl(testHarness.anthropicBaseUrl()) + .apiKey(testHarness.anthropicApiKey()) + .build(); + + anthropicClient = BraintrustAnthropic.wrap(testHarness.openTelemetry(), anthropicClient); + + var request = + MessageCreateParams.builder() + .model(Model.CLAUDE_3_HAIKU_20240307) + .system("You are a helpful assistant") + .addUserMessage("What is the capital of France?") + .maxTokens(50) + .temperature(0.0) + .build(); + + var fullResponse = new StringBuilder(); + var stream = anthropicClient.async().messages().createStreaming(request); + stream.subscribe( + event -> { + if (event.contentBlockDelta().isPresent()) { + var delta = event.contentBlockDelta().get().delta(); + if (delta.text().isPresent()) { + fullResponse.append(delta.text().get().text()); + } + } + }); + stream.onCompleteFuture().get(30, TimeUnit.SECONDS); + + assertFalse(fullResponse.toString().isEmpty()); + + var spans = testHarness.awaitExportedSpans(); + assertEquals(1, spans.size()); + var span = spans.get(0); + + assertEquals("anthropic.messages.create", span.getName()); + + assertNotNull(span.getAttributes().get(AttributeKey.stringKey("braintrust.input_json"))); + + String outputJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); + assertNotNull(outputJson); + JsonNode outputMessage = JSON_MAPPER.readTree(outputJson); + assertEquals("assistant", outputMessage.get("role").asText()); + assertFalse(outputMessage.get("content").get(0).get("text").asText().isEmpty()); + + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("prompt_tokens")); + assertTrue(metrics.has("completion_tokens")); + assertTrue( + metrics.has("time_to_first_token"), + "time_to_first_token should be present for streaming"); + assertTrue(metrics.get("time_to_first_token").asDouble() >= 0.0); + } + + @Test + @SneakyThrows + void testWrapAnthropicBeta() { + AnthropicClient anthropicClient = + AnthropicOkHttpClient.builder() + .baseUrl(testHarness.anthropicBaseUrl()) + .apiKey(testHarness.anthropicApiKey()) + .build(); + + anthropicClient = BraintrustAnthropic.wrap(testHarness.openTelemetry(), anthropicClient); + + var request = + com.anthropic.models.beta.messages.MessageCreateParams.builder() + .model(Model.CLAUDE_3_HAIKU_20240307) + .system("You are a helpful assistant") + .addUserMessage("What is the capital of France?") + .maxTokens(50) + .temperature(0.0) + .build(); + + var response = anthropicClient.beta().messages().create(request); + + assertNotNull(response); + assertNotNull(response.id()); + var contentBlock = response.content().get(0); + assertTrue(contentBlock.isText()); + assertNotNull(contentBlock.asText().text()); + + var spans = testHarness.awaitExportedSpans(); + assertEquals(1, spans.size()); + var span = spans.get(0); + + assertEquals("anthropic.messages.create", span.getName()); + + // Verify span_attributes + String spanAttributesJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.span_attributes")); + assertNotNull(spanAttributesJson); + JsonNode spanAttributes = JSON_MAPPER.readTree(spanAttributesJson); + assertEquals("llm", spanAttributes.get("type").asText()); + + // Verify metadata + String metadataJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata")); + assertNotNull(metadataJson); + JsonNode metadata = JSON_MAPPER.readTree(metadataJson); + assertEquals("anthropic", metadata.get("provider").asText()); + assertTrue( + metadata.get("model").asText().startsWith("claude-3-haiku"), + "model should start with claude-3-haiku"); + + // Verify input + String inputJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.input_json")); + assertNotNull(inputJson); + JsonNode input = JSON_MAPPER.readTree(inputJson); + assertTrue(input.isArray()); + assertTrue(input.size() > 0); + + // Verify output — full BetaMessage object + String outputJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); + assertNotNull(outputJson); + JsonNode outputMessage = JSON_MAPPER.readTree(outputJson); + assertNotNull(outputMessage.get("id")); + assertEquals("message", outputMessage.get("type").asText()); + assertEquals("assistant", outputMessage.get("role").asText()); + assertNotNull(outputMessage.get("content").get(0).get("text")); + assertTrue(outputMessage.get("usage").get("output_tokens").asInt() > 0); + assertTrue(outputMessage.get("usage").get("input_tokens").asInt() > 0); + + // Verify metrics — tokens; non-streaming should NOT have time_to_first_token + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("prompt_tokens"), "prompt_tokens should be present"); + assertTrue(metrics.has("completion_tokens"), "completion_tokens should be present"); + assertTrue(metrics.has("tokens"), "tokens should be present"); + assertFalse( + metrics.has("time_to_first_token"), + "time_to_first_token should not be present for non-streaming"); + } + + @Test + @SneakyThrows + void testWrapAnthropicBetaStreaming() { + AnthropicClient anthropicClient = + AnthropicOkHttpClient.builder() + .baseUrl(testHarness.anthropicBaseUrl()) + .apiKey(testHarness.anthropicApiKey()) + .build(); + + anthropicClient = BraintrustAnthropic.wrap(testHarness.openTelemetry(), anthropicClient); + + var request = + com.anthropic.models.beta.messages.MessageCreateParams.builder() + .model(Model.CLAUDE_3_HAIKU_20240307) + .system("You are a helpful assistant") + .addUserMessage("What is the capital of France?") + .maxTokens(50) + .temperature(0.0) + .build(); + + StringBuilder fullResponse = new StringBuilder(); + try (var stream = anthropicClient.beta().messages().createStreaming(request)) { + stream.stream() + .forEach( + event -> { + if (event.contentBlockDelta().isPresent()) { + var delta = event.contentBlockDelta().get(); + if (delta.delta().text().isPresent()) { + fullResponse.append(delta.delta().text().get().text()); + } + } + }); + } + + assertFalse(fullResponse.toString().isEmpty()); + + var spans = testHarness.awaitExportedSpans(); + assertEquals(1, spans.size()); + var span = spans.get(0); + + assertEquals("anthropic.messages.create", span.getName()); + + // Verify metadata + String metadataJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata")); + assertNotNull(metadataJson); + JsonNode metadata = JSON_MAPPER.readTree(metadataJson); + assertEquals("anthropic", metadata.get("provider").asText()); + + // Verify input + assertNotNull(span.getAttributes().get(AttributeKey.stringKey("braintrust.input_json"))); + + // Verify output — full BetaMessage object assembled from SSE stream + String outputJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); + assertNotNull(outputJson); + JsonNode outputMessage = JSON_MAPPER.readTree(outputJson); + assertEquals("assistant", outputMessage.get("role").asText()); + assertFalse( + outputMessage.get("content").get(0).get("text").asText().isEmpty(), + "content should not be empty"); + + // Verify metrics — tokens and time_to_first_token + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("prompt_tokens"), "prompt_tokens should be present"); + assertTrue(metrics.has("completion_tokens"), "completion_tokens should be present"); + assertTrue(metrics.has("time_to_first_token"), "time_to_first_token should be present"); + assertTrue( + metrics.get("time_to_first_token").asDouble() >= 0.0, + "time_to_first_token should be non-negative"); } } diff --git a/src/test/java/dev/braintrust/instrumentation/langchain/BraintrustLangchainTest.java b/src/test/java/dev/braintrust/instrumentation/langchain/BraintrustLangchainTest.java index 153bcdf6..99e82fef 100644 --- a/src/test/java/dev/braintrust/instrumentation/langchain/BraintrustLangchainTest.java +++ b/src/test/java/dev/braintrust/instrumentation/langchain/BraintrustLangchainTest.java @@ -97,11 +97,9 @@ void testSyncChatCompletion() { assertTrue(metrics.get("prompt_tokens").asLong() > 0, "Prompt tokens should be > 0"); assertTrue( metrics.get("completion_tokens").asLong() > 0, "Completion tokens should be > 0"); - assertTrue( - metrics.has("time_to_first_token"), "Metrics should contain time_to_first_token"); - assertTrue( - metrics.get("time_to_first_token").isNumber(), - "time_to_first_token should be a number"); + assertFalse( + metrics.has("time_to_first_token"), + "time_to_first_token should not be present for non-streaming"); // Verify input String inputJson = attributes.get(AttributeKey.stringKey("braintrust.input_json")); diff --git a/src/test/java/dev/braintrust/instrumentation/openai/BraintrustOpenAITest.java b/src/test/java/dev/braintrust/instrumentation/openai/BraintrustOpenAITest.java index 32abcea9..9401241c 100644 --- a/src/test/java/dev/braintrust/instrumentation/openai/BraintrustOpenAITest.java +++ b/src/test/java/dev/braintrust/instrumentation/openai/BraintrustOpenAITest.java @@ -1,15 +1,22 @@ package dev.braintrust.instrumentation.openai; -import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.junit.jupiter.api.Assertions.*; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.openai.client.OpenAIClient; import com.openai.client.okhttp.OpenAIOkHttpClient; import com.openai.models.ChatModel; +import com.openai.models.Reasoning; +import com.openai.models.ReasoningEffort; import com.openai.models.chat.completions.*; +import com.openai.models.responses.EasyInputMessage; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseInputItem; import dev.braintrust.TestHarness; import io.opentelemetry.api.common.AttributeKey; +import java.util.List; +import java.util.concurrent.TimeUnit; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -63,45 +70,49 @@ void testWrapOpenAi() { // Verify span name matches other SDKs assertEquals("Chat Completion", span.getName()); - // Verify essential span attributes - assertEquals( - "openai", span.getAttributes().get(AttributeKey.stringKey("gen_ai.provider.name"))); - assertEquals( - "gpt-4o-mini", - span.getAttributes().get(AttributeKey.stringKey("gen_ai.request.model"))); - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("gen_ai.response.model"))); - assertEquals( - "chat", span.getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name"))); - assertNotNull(span.getAttributes().get(AttributeKey.stringKey("gen_ai.response.id"))); - - // Verify usage metrics exist - assertNotNull(span.getAttributes().get(AttributeKey.longKey("gen_ai.usage.input_tokens"))); - assertNotNull(span.getAttributes().get(AttributeKey.longKey("gen_ai.usage.output_tokens"))); - - // Verify time to first token metric was captured - Double timeToFirstToken = - span.getAttributes() - .get(AttributeKey.doubleKey("braintrust.metrics.time_to_first_token")); - assertNotNull(timeToFirstToken, "time_to_first_token should be set"); - assertTrue(timeToFirstToken >= 0.0, "time_to_first_token should be non-negative"); - - // Verify Braintrust metadata - assertEquals( - "openai", - span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata.provider"))); - assertEquals( - "gpt-4o-mini", - span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata.model"))); - - // Verify output messages structure + // Verify span_attributes JSON + String spanAttributesJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.span_attributes")); + assertNotNull(spanAttributesJson); + JsonNode spanAttributes = JSON_MAPPER.readTree(spanAttributesJson); + assertEquals("llm", spanAttributes.get("type").asText()); + + // Verify braintrust.metadata JSON + String metadataJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata")); + assertNotNull(metadataJson); + JsonNode metadata = JSON_MAPPER.readTree(metadataJson); + assertEquals("openai", metadata.get("provider").asText()); + assertTrue( + metadata.get("model").asText().startsWith("gpt-4o-mini"), + "model should start with gpt-4o-mini"); + + // Verify braintrust.metrics JSON (tokens) + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("prompt_tokens"), "prompt_tokens should be present"); + assertTrue( + metrics.get("prompt_tokens").asInt() >= 0, "prompt_tokens should be non-negative"); + assertTrue(metrics.has("completion_tokens"), "completion_tokens should be present"); + assertTrue( + metrics.get("completion_tokens").asInt() >= 0, + "completion_tokens should be non-negative"); + assertTrue(metrics.has("tokens"), "tokens should be present"); + assertTrue(metrics.get("tokens").asInt() >= 0, "tokens should be non-negative"); + assertFalse( + metrics.has("time_to_first_token"), + "time_to_first_token should not be present for non-streaming"); + + // Verify output (choices array) String outputJson = - span.getAttributes().get(AttributeKey.stringKey("gen_ai.output.messages")); + span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); assertNotNull(outputJson); - var outputMessages = JSON_MAPPER.readTree(outputJson); - assertEquals(1, outputMessages.size()); - var outputMessage = outputMessages.get(0); - assertEquals("assistant", outputMessage.get("role").asText()); - assertNotNull(outputMessage.get("finish_reason")); + var outputChoices = JSON_MAPPER.readTree(outputJson); + assertEquals(1, outputChoices.size()); + var choice = outputChoices.get(0); + assertEquals("assistant", choice.get("message").get("role").asText()); + assertNotNull(choice.get("finish_reason")); } @Test @@ -153,10 +164,238 @@ void testWrapOpenAiStreaming() { var span = spans.get(0); assertEquals("Chat Completion", span.getName()); - assertEquals( - "openai", span.getAttributes().get(AttributeKey.stringKey("gen_ai.provider.name"))); - assertNotNull( - span.getAttributes() - .get(AttributeKey.doubleKey("braintrust.metrics.time_to_first_token"))); + + // Verify braintrust.metadata has provider=openai + String metadataJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata")); + assertNotNull(metadataJson); + JsonNode metadata = JSON_MAPPER.readTree(metadataJson); + assertEquals("openai", metadata.get("provider").asText()); + + // Verify time_to_first_token is in braintrust.metrics JSON + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("time_to_first_token"), "time_to_first_token should be present"); + assertTrue( + metrics.get("time_to_first_token").asDouble() >= 0.0, + "time_to_first_token should be non-negative"); + } + + @Test + @SneakyThrows + void testWrapOpenAiResponses() { + OpenAIClient openAIClient = + OpenAIOkHttpClient.builder() + .baseUrl(testHarness.openAiBaseUrl()) + .apiKey(testHarness.openAiApiKey()) + .build(); + + openAIClient = BraintrustOpenAI.wrapOpenAI(testHarness.openTelemetry(), openAIClient); + + var inputMsg = + EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("What is the capital of France? Reply in one word.") + .build(); + + var request = + ResponseCreateParams.builder() + .model("o4-mini") + .reasoning( + Reasoning.builder() + .effort(ReasoningEffort.LOW) + .summary(Reasoning.Summary.AUTO) + .build()) + .inputOfResponse(List.of(ResponseInputItem.ofEasyInputMessage(inputMsg))) + .build(); + + var response = openAIClient.responses().create(request); + + assertNotNull(response); + assertNotNull(response.id()); + assertFalse(response.output().isEmpty(), "Response should have output items"); + + var spans = testHarness.awaitExportedSpans(); + assertEquals(1, spans.size()); + var span = spans.get(0); + + // Span name for /v1/responses endpoint + assertEquals("responses", span.getName()); + + // span_attributes type=llm + String spanAttributesJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.span_attributes")); + assertNotNull(spanAttributesJson); + JsonNode spanAttributes = JSON_MAPPER.readTree(spanAttributesJson); + assertEquals("llm", spanAttributes.get("type").asText()); + + // metadata: provider and model + String metadataJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata")); + assertNotNull(metadataJson); + JsonNode metadata = JSON_MAPPER.readTree(metadataJson); + assertEquals("openai", metadata.get("provider").asText()); + assertTrue(metadata.get("model").asText().startsWith("o4-mini")); + + // input_json: captured from "input" array + String inputJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.input_json")); + assertNotNull(inputJson, "braintrust.input_json should be set"); + JsonNode inputItems = JSON_MAPPER.readTree(inputJson); + assertTrue(inputItems.isArray() && inputItems.size() > 0); + assertEquals("user", inputItems.get(0).get("role").asText()); + + // output_json: captured from "output" array + String outputJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); + assertNotNull(outputJson, "braintrust.output_json should be set"); + JsonNode outputItems = JSON_MAPPER.readTree(outputJson); + assertTrue(outputItems.isArray() && outputItems.size() > 0); + + // metrics: tokens from Responses API usage fields + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("prompt_tokens"), "prompt_tokens should be present"); + assertTrue(metrics.get("prompt_tokens").asInt() >= 0); + assertTrue(metrics.has("completion_tokens"), "completion_tokens should be present"); + assertTrue(metrics.get("completion_tokens").asInt() >= 0); + assertTrue(metrics.has("tokens"), "tokens should be present"); + assertTrue(metrics.get("tokens").asInt() >= 0); + assertTrue( + metrics.has("completion_reasoning_tokens"), + "completion_reasoning_tokens should be present"); + assertTrue(metrics.get("completion_reasoning_tokens").asInt() >= 0); + } + + @Test + @SneakyThrows + void testWrapOpenAiAsync() { + OpenAIClient openAIClient = + OpenAIOkHttpClient.builder() + .baseUrl(testHarness.openAiBaseUrl()) + .apiKey(testHarness.openAiApiKey()) + .build(); + + openAIClient = BraintrustOpenAI.wrapOpenAI(testHarness.openTelemetry(), openAIClient); + + var request = + ChatCompletionCreateParams.builder() + .model(ChatModel.GPT_4O_MINI) + .addSystemMessage("You are a helpful assistant") + .addUserMessage("What is the capital of France?") + .temperature(0.0) + .build(); + + var response = openAIClient.async().chat().completions().create(request).get(); + + assertNotNull(response); + assertNotNull(response.id()); + assertTrue(response.choices().get(0).message().content().isPresent()); + String content = response.choices().get(0).message().content().get(); + assertTrue(content.toLowerCase().contains("paris"), "Response should mention Paris"); + + var spans = testHarness.awaitExportedSpans(); + assertEquals(1, spans.size()); + var span = spans.get(0); + + assertEquals("Chat Completion", span.getName()); + + String spanAttributesJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.span_attributes")); + assertNotNull(spanAttributesJson); + JsonNode spanAttributes = JSON_MAPPER.readTree(spanAttributesJson); + assertEquals("llm", spanAttributes.get("type").asText()); + + String metadataJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata")); + assertNotNull(metadataJson); + JsonNode metadata = JSON_MAPPER.readTree(metadataJson); + assertEquals("openai", metadata.get("provider").asText()); + + String outputJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); + assertNotNull(outputJson); + var outputChoices = JSON_MAPPER.readTree(outputJson); + assertEquals(1, outputChoices.size()); + assertEquals("assistant", outputChoices.get(0).get("message").get("role").asText()); + + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("prompt_tokens")); + assertTrue(metrics.has("completion_tokens")); + assertTrue(metrics.has("tokens")); + assertFalse( + metrics.has("time_to_first_token"), + "time_to_first_token should not be present for non-streaming"); + } + + @Test + @SneakyThrows + void testWrapOpenAiAsyncStreaming() { + OpenAIClient openAIClient = + OpenAIOkHttpClient.builder() + .baseUrl(testHarness.openAiBaseUrl()) + .apiKey(testHarness.openAiApiKey()) + .build(); + + openAIClient = BraintrustOpenAI.wrapOpenAI(testHarness.openTelemetry(), openAIClient); + + var request = + ChatCompletionCreateParams.builder() + .model(ChatModel.GPT_4O_MINI) + .addSystemMessage("You are a helpful assistant") + .addUserMessage("What is the capital of France?") + .temperature(0.0) + .streamOptions( + ChatCompletionStreamOptions.builder().includeUsage(true).build()) + .build(); + + var fullResponse = new StringBuilder(); + var stream = openAIClient.async().chat().completions().createStreaming(request); + stream.subscribe( + chunk -> { + if (!chunk.choices().isEmpty()) { + chunk.choices().get(0).delta().content().ifPresent(fullResponse::append); + } + }); + stream.onCompleteFuture().get(30, TimeUnit.SECONDS); + + assertFalse(fullResponse.toString().isEmpty()); + assertTrue(fullResponse.toString().toLowerCase().contains("paris")); + + var spans = testHarness.awaitExportedSpans(); + assertEquals(1, spans.size()); + var span = spans.get(0); + + assertEquals("Chat Completion", span.getName()); + + String metadataJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.metadata")); + assertNotNull(metadataJson); + JsonNode metadata = JSON_MAPPER.readTree(metadataJson); + assertEquals("openai", metadata.get("provider").asText()); + + assertNotNull(span.getAttributes().get(AttributeKey.stringKey("braintrust.input_json"))); + + String outputJson = + span.getAttributes().get(AttributeKey.stringKey("braintrust.output_json")); + assertNotNull(outputJson); + var outputChoices = JSON_MAPPER.readTree(outputJson); + assertEquals(1, outputChoices.size()); + assertEquals("assistant", outputChoices.get(0).get("message").get("role").asText()); + + String metricsJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.metrics")); + assertNotNull(metricsJson); + JsonNode metrics = JSON_MAPPER.readTree(metricsJson); + assertTrue(metrics.has("prompt_tokens")); + assertTrue(metrics.has("completion_tokens")); + assertTrue(metrics.has("tokens")); + assertTrue( + metrics.has("time_to_first_token"), + "time_to_first_token should be present for streaming"); + assertTrue(metrics.get("time_to_first_token").asDouble() >= 0.0); } } diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-163b371b-f5f0-4e1d-8564-aeb1a452329e.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-163b371b-f5f0-4e1d-8564-aeb1a452329e.json new file mode 100644 index 00000000..3ea61c14 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-163b371b-f5f0-4e1d-8564-aeb1a452329e.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_011YexbvYrhWJjD4kiTMSPn4","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-17680b84-4d29-4d65-8473-9e046e3720d7.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-17680b84-4d29-4d65-8473-9e046e3720d7.json new file mode 100644 index 00000000..05cce7bd --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-17680b84-4d29-4d65-8473-9e046e3720d7.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_01TLR7nVnZSSuk7EV5zwUMN2","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-22f11bf1-a6fb-4733-86b9-42bd18ee37ee.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-22f11bf1-a6fb-4733-86b9-42bd18ee37ee.json new file mode 100644 index 00000000..172580d9 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-22f11bf1-a6fb-4733-86b9-42bd18ee37ee.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_014eXq1R2mamAEsBb1EsNMMo","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-3a44dc7b-7c58-42c6-a05b-9701cde28008.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-3a44dc7b-7c58-42c6-a05b-9701cde28008.json new file mode 100644 index 00000000..035955d4 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-3a44dc7b-7c58-42c6-a05b-9701cde28008.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_0159ZtRK2GNuYXy6No1Qf8vL","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-3cf94b89-b7f2-4925-97c7-b592da1c7128.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-3cf94b89-b7f2-4925-97c7-b592da1c7128.json new file mode 100644 index 00000000..6b9e412d --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-3cf94b89-b7f2-4925-97c7-b592da1c7128.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_017XJT434iB9eNe9P1MKbjWG","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-3e84b268-5860-4614-9b16-27bc47be967c.txt b/src/test/resources/cassettes/anthropic/__files/v1_messages-3e84b268-5860-4614-9b16-27bc47be967c.txt new file mode 100644 index 00000000..1e2ba4cb --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-3e84b268-5860-4614-9b16-27bc47be967c.txt @@ -0,0 +1,27 @@ +event: message_start +data: {"type":"message_start","message":{"model":"claude-3-haiku-20240307","id":"msg_01UhmrPmEjEDcUKnakW45ptP","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":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":1,"service_tier":"standard","inference_geo":"not_available"}} } + +event: content_block_start +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"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" capital of France is"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} } + +event: message_stop +data: {"type":"message_stop" } + diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-409d6eae-0222-40f5-8c25-f85fed3083e7.txt b/src/test/resources/cassettes/anthropic/__files/v1_messages-409d6eae-0222-40f5-8c25-f85fed3083e7.txt new file mode 100644 index 00000000..1f9c7741 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-409d6eae-0222-40f5-8c25-f85fed3083e7.txt @@ -0,0 +1,27 @@ +event: message_start +data: {"type":"message_start","message":{"model":"claude-3-haiku-20240307","id":"msg_01UcVw5riM3WHwWgRmqZkX8s","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":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":1,"service_tier":"standard","inference_geo":"not_available"}}} + +event: content_block_start +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"}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" capital of France is"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} } + +event: message_stop +data: {"type":"message_stop" } + diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-44f66b34-c858-4ac0-8a76-a15873892386.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-44f66b34-c858-4ac0-8a76-a15873892386.json new file mode 100644 index 00000000..eed2ea11 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-44f66b34-c858-4ac0-8a76-a15873892386.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_01LtYGTdQzFDkqm5TfaEnm8T","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-4f0b5ed5-27e9-4320-be33-b0bd8da32323.txt b/src/test/resources/cassettes/anthropic/__files/v1_messages-4f0b5ed5-27e9-4320-be33-b0bd8da32323.txt new file mode 100644 index 00000000..cbd6e78e --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-4f0b5ed5-27e9-4320-be33-b0bd8da32323.txt @@ -0,0 +1,27 @@ +event: message_start +data: {"type":"message_start","message":{"model":"claude-3-haiku-20240307","id":"msg_01N43RKevLf89tMvKnUi24Uh","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":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":1,"service_tier":"standard","inference_geo":"not_available"}} } + +event: content_block_start +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"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" capital of France is"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} } + +event: message_stop +data: {"type":"message_stop" } + diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-6ba9356c-fcde-4279-95e0-9dc0c0d27dc7.txt b/src/test/resources/cassettes/anthropic/__files/v1_messages-6ba9356c-fcde-4279-95e0-9dc0c0d27dc7.txt new file mode 100644 index 00000000..73131543 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-6ba9356c-fcde-4279-95e0-9dc0c0d27dc7.txt @@ -0,0 +1,27 @@ +event: message_start +data: {"type":"message_start","message":{"model":"claude-3-haiku-20240307","id":"msg_01SXGWWDFXnzKFNY4PVKoBZJ","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":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":1,"service_tier":"standard","inference_geo":"not_available"}} } + +event: content_block_start +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"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" capital of France is"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}} + +event: message_stop +data: {"type":"message_stop" } + diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-7078924e-9396-4b07-885c-23d7d0c80090.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-7078924e-9396-4b07-885c-23d7d0c80090.json new file mode 100644 index 00000000..d9ea05f8 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-7078924e-9396-4b07-885c-23d7d0c80090.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_01AGLvWk4TyUBNQp7j6mxQAr","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-7b8cac9c-793a-4dc7-8d7d-71d773210345.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-7b8cac9c-793a-4dc7-8d7d-71d773210345.json new file mode 100644 index 00000000..d7f2181b --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-7b8cac9c-793a-4dc7-8d7d-71d773210345.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_01XgwSG9nF2hoJowGS2rDsuB","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-87b2b5f3-b093-475f-bbde-1ab031e72500.txt b/src/test/resources/cassettes/anthropic/__files/v1_messages-87b2b5f3-b093-475f-bbde-1ab031e72500.txt new file mode 100644 index 00000000..5a13aa86 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-87b2b5f3-b093-475f-bbde-1ab031e72500.txt @@ -0,0 +1,27 @@ +event: message_start +data: {"type":"message_start","message":{"model":"claude-3-haiku-20240307","id":"msg_018KNQMecVBYHNVfL8pAU8Yv","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":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":1,"service_tier":"standard","inference_geo":"not_available"}} } + +event: content_block_start +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"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" capital of France is"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} } + +event: message_stop +data: {"type":"message_stop" } + diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-893a66cc-08d6-423f-bda1-720822c1f022.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-893a66cc-08d6-423f-bda1-720822c1f022.json new file mode 100644 index 00000000..fbb6bd30 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-893a66cc-08d6-423f-bda1-720822c1f022.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_01YCJPuDzFCvEicDLEzMCt3k","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-91a6d654-41ea-41c3-8205-e1e5123246a8.txt b/src/test/resources/cassettes/anthropic/__files/v1_messages-91a6d654-41ea-41c3-8205-e1e5123246a8.txt new file mode 100644 index 00000000..76f59ceb --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-91a6d654-41ea-41c3-8205-e1e5123246a8.txt @@ -0,0 +1,27 @@ +event: message_start +data: {"type":"message_start","message":{"model":"claude-3-haiku-20240307","id":"msg_016BPKsdADqAch2QN3Lr3iL8","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":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":1,"service_tier":"standard","inference_geo":"not_available"}} } + +event: content_block_start +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"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" capital of France is"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} } + +event: message_stop +data: {"type":"message_stop" } + diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-9232af2f-2a7b-4d67-af05-fcc609fd5466.txt b/src/test/resources/cassettes/anthropic/__files/v1_messages-9232af2f-2a7b-4d67-af05-fcc609fd5466.txt new file mode 100644 index 00000000..ef3e6e60 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-9232af2f-2a7b-4d67-af05-fcc609fd5466.txt @@ -0,0 +1,27 @@ +event: message_start +data: {"type":"message_start","message":{"model":"claude-3-haiku-20240307","id":"msg_011cU9tVFheHT7zs7E5XB2ad","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":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":1,"service_tier":"standard","inference_geo":"not_available"}} } + +event: content_block_start +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"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" capital of France is"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} } + +event: message_stop +data: {"type":"message_stop" } + diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-b437c5ce-d528-4f0d-80f2-01cd5d7ba72e.txt b/src/test/resources/cassettes/anthropic/__files/v1_messages-b437c5ce-d528-4f0d-80f2-01cd5d7ba72e.txt new file mode 100644 index 00000000..6c9507b0 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-b437c5ce-d528-4f0d-80f2-01cd5d7ba72e.txt @@ -0,0 +1,27 @@ +event: message_start +data: {"type":"message_start","message":{"model":"claude-3-haiku-20240307","id":"msg_01HmbabrdFy4BV1LjsaQHvLz","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":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":1,"service_tier":"standard","inference_geo":"not_available"}} } + +event: content_block_start +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"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" capital of France is"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} } + +event: message_stop +data: {"type":"message_stop" } + diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-cf2b874d-3585-481a-b023-49e23a1d3b46.txt b/src/test/resources/cassettes/anthropic/__files/v1_messages-cf2b874d-3585-481a-b023-49e23a1d3b46.txt new file mode 100644 index 00000000..3e3bb712 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-cf2b874d-3585-481a-b023-49e23a1d3b46.txt @@ -0,0 +1,27 @@ +event: message_start +data: {"type":"message_start","message":{"model":"claude-3-haiku-20240307","id":"msg_013CqiP2vx2hE1GTsrpX8a64","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":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":1,"service_tier":"standard","inference_geo":"not_available"}} } + +event: content_block_start +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"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" capital of France is"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Paris."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} } + +event: message_stop +data: {"type":"message_stop" } + diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-da340447-86ca-40db-90b6-89c169f2eb65.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-da340447-86ca-40db-90b6-89c169f2eb65.json new file mode 100644 index 00000000..9b171bac --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-da340447-86ca-40db-90b6-89c169f2eb65.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_01MyXBqj7z6pwUXqQ2hZJmgd","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/__files/v1_messages-dc027840-62bc-4d7f-b133-7cb6488d1087.json b/src/test/resources/cassettes/anthropic/__files/v1_messages-dc027840-62bc-4d7f-b133-7cb6488d1087.json new file mode 100644 index 00000000..0e1d7643 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/__files/v1_messages-dc027840-62bc-4d7f-b133-7cb6488d1087.json @@ -0,0 +1 @@ +{"model":"claude-3-haiku-20240307","id":"msg_017WwtpAKUrtjVECTi3hjmJq","type":"message","role":"assistant","content":[{"type":"text","text":"The capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":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":10,"service_tier":"standard","inference_geo":"not_available"}} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-163b371b-f5f0-4e1d-8564-aeb1a452329e.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-163b371b-f5f0-4e1d-8564-aeb1a452329e.json new file mode 100644 index 00000000..77a7a9cc --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-163b371b-f5f0-4e1d-8564-aeb1a452329e.json @@ -0,0 +1,53 @@ +{ + "id" : "163b371b-f5f0-4e1d-8564-aeb1a452329e", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-163b371b-f5f0-4e1d-8564-aeb1a452329e.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:14:46 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:14:45Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:14:45Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:14:46Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:14:45Z", + "request-id" : "req_011CYkPVpRcyNDMcr6QXzPMp", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "304", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b1a282a80e945-SEA" + } + }, + "uuid" : "163b371b-f5f0-4e1d-8564-aeb1a452329e", + "persistent" : true, + "scenarioName" : "scenario-1-v1-messages", + "requiredScenarioState" : "scenario-1-v1-messages-2", + "insertionIndex" : 12 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-17680b84-4d29-4d65-8473-9e046e3720d7.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-17680b84-4d29-4d65-8473-9e046e3720d7.json new file mode 100644 index 00000000..600a2ad7 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-17680b84-4d29-4d65-8473-9e046e3720d7.json @@ -0,0 +1,53 @@ +{ + "id" : "17680b84-4d29-4d65-8473-9e046e3720d7", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-17680b84-4d29-4d65-8473-9e046e3720d7.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:41:18 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:41:18Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:41:18Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:41:18Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:41:18Z", + "request-id" : "req_011CYkRXDUxhSaB2gwBvUpQN", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "321", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b4109db3d7628-SEA" + } + }, + "uuid" : "17680b84-4d29-4d65-8473-9e046e3720d7", + "persistent" : true, + "scenarioName" : "scenario-2-v1-messages", + "requiredScenarioState" : "scenario-2-v1-messages-2", + "insertionIndex" : 23 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-22f11bf1-a6fb-4733-86b9-42bd18ee37ee.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-22f11bf1-a6fb-4733-86b9-42bd18ee37ee.json new file mode 100644 index 00000000..e098001a --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-22f11bf1-a6fb-4733-86b9-42bd18ee37ee.json @@ -0,0 +1,51 @@ +{ + "id" : "22f11bf1-a6fb-4733-86b9-42bd18ee37ee", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages?beta=true", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-22f11bf1-a6fb-4733-86b9-42bd18ee37ee.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 00:34:36 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T00:34:36Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T00:34:36Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T00:34:36Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T00:34:36Z", + "request-id" : "req_011CYizessocNRqkQjjCFttf", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "308", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d75092feb4c8b7a-SEA" + } + }, + "uuid" : "22f11bf1-a6fb-4733-86b9-42bd18ee37ee", + "persistent" : true, + "insertionIndex" : 6 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-3a44dc7b-7c58-42c6-a05b-9701cde28008.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-3a44dc7b-7c58-42c6-a05b-9701cde28008.json new file mode 100644 index 00000000..8eb43178 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-3a44dc7b-7c58-42c6-a05b-9701cde28008.json @@ -0,0 +1,51 @@ +{ + "id" : "3a44dc7b-7c58-42c6-a05b-9701cde28008", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages?beta=true", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-3a44dc7b-7c58-42c6-a05b-9701cde28008.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:14:43 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:14:43Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:14:43Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:14:43Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:14:43Z", + "request-id" : "req_011CYkPVdJPoS7pppWeQi75g", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "300", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b1a17dc9d75da-SEA" + } + }, + "uuid" : "3a44dc7b-7c58-42c6-a05b-9701cde28008", + "persistent" : true, + "insertionIndex" : 10 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-3cf94b89-b7f2-4925-97c7-b592da1c7128.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-3cf94b89-b7f2-4925-97c7-b592da1c7128.json new file mode 100644 index 00000000..c5977e06 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-3cf94b89-b7f2-4925-97c7-b592da1c7128.json @@ -0,0 +1,54 @@ +{ + "id" : "3cf94b89-b7f2-4925-97c7-b592da1c7128", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-3cf94b89-b7f2-4925-97c7-b592da1c7128.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:41:17 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:41:17Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:41:17Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:41:17Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:41:17Z", + "request-id" : "req_011CYkRX8N8nAktri448iud9", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "319", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b41026a2fc96d-SEA" + } + }, + "uuid" : "3cf94b89-b7f2-4925-97c7-b592da1c7128", + "persistent" : true, + "scenarioName" : "scenario-2-v1-messages", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-2-v1-messages-2", + "insertionIndex" : 22 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-3e84b268-5860-4614-9b16-27bc47be967c.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-3e84b268-5860-4614-9b16-27bc47be967c.json new file mode 100644 index 00000000..2439bc48 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-3e84b268-5860-4614-9b16-27bc47be967c.json @@ -0,0 +1,52 @@ +{ + "id" : "3e84b268-5860-4614-9b16-27bc47be967c", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-3e84b268-5860-4614-9b16-27bc47be967c.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:14:47 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "Cache-Control" : "no-cache", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:14:47Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:14:47Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:14:47Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:14:47Z", + "request-id" : "req_011CYkPVv8v87RFn8Zh5epja", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "240", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b1a306dcc281c-SEA" + } + }, + "uuid" : "3e84b268-5860-4614-9b16-27bc47be967c", + "persistent" : true, + "insertionIndex" : 13 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-409d6eae-0222-40f5-8c25-f85fed3083e7.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-409d6eae-0222-40f5-8c25-f85fed3083e7.json new file mode 100644 index 00000000..cb91b17f --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-409d6eae-0222-40f5-8c25-f85fed3083e7.json @@ -0,0 +1,52 @@ +{ + "id" : "409d6eae-0222-40f5-8c25-f85fed3083e7", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages?beta=true", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-409d6eae-0222-40f5-8c25-f85fed3083e7.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:37:44 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "Cache-Control" : "no-cache", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:37:44Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:37:44Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:37:44Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:37:44Z", + "request-id" : "req_011CYkRFTW8547GKCu3Cp4nj", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "204", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b3bd1cc087563-SEA" + } + }, + "uuid" : "409d6eae-0222-40f5-8c25-f85fed3083e7", + "persistent" : true, + "insertionIndex" : 14 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-44f66b34-c858-4ac0-8a76-a15873892386.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-44f66b34-c858-4ac0-8a76-a15873892386.json new file mode 100644 index 00000000..0c018675 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-44f66b34-c858-4ac0-8a76-a15873892386.json @@ -0,0 +1,54 @@ +{ + "id" : "44f66b34-c858-4ac0-8a76-a15873892386", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-44f66b34-c858-4ac0-8a76-a15873892386.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:37:47 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:37:47Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:37:47Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:37:47Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:37:47Z", + "request-id" : "req_011CYkRFeZdqVfexpAKAuEUZ", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "343", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b3be1e8b6c378-SEA" + } + }, + "uuid" : "44f66b34-c858-4ac0-8a76-a15873892386", + "persistent" : true, + "scenarioName" : "scenario-1-v1-messages", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-1-v1-messages-2", + "insertionIndex" : 16 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-4f0b5ed5-27e9-4320-be33-b0bd8da32323.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-4f0b5ed5-27e9-4320-be33-b0bd8da32323.json new file mode 100644 index 00000000..aa952fa7 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-4f0b5ed5-27e9-4320-be33-b0bd8da32323.json @@ -0,0 +1,52 @@ +{ + "id" : "4f0b5ed5-27e9-4320-be33-b0bd8da32323", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages?beta=true", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-4f0b5ed5-27e9-4320-be33-b0bd8da32323.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:41:13 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "Cache-Control" : "no-cache", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:41:13Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:41:13Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:41:13Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:41:13Z", + "request-id" : "req_011CYkRWrSvi4BKWsvW56iR4", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "188", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b40eb0fe649b2-SEA" + } + }, + "uuid" : "4f0b5ed5-27e9-4320-be33-b0bd8da32323", + "persistent" : true, + "insertionIndex" : 19 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-6ba9356c-fcde-4279-95e0-9dc0c0d27dc7.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-6ba9356c-fcde-4279-95e0-9dc0c0d27dc7.json new file mode 100644 index 00000000..eeba00c5 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-6ba9356c-fcde-4279-95e0-9dc0c0d27dc7.json @@ -0,0 +1,55 @@ +{ + "id" : "6ba9356c-fcde-4279-95e0-9dc0c0d27dc7", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-6ba9356c-fcde-4279-95e0-9dc0c0d27dc7.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:41:16 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "Cache-Control" : "no-cache", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:41:16Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:41:16Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:41:16Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:41:16Z", + "request-id" : "req_011CYkRX3CamVWQsxTF4L5Vs", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "223", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b40fadaa9dcfb-SEA" + } + }, + "uuid" : "6ba9356c-fcde-4279-95e0-9dc0c0d27dc7", + "persistent" : true, + "scenarioName" : "scenario-1-v1-messages", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-1-v1-messages-2", + "insertionIndex" : 21 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-7078924e-9396-4b07-885c-23d7d0c80090.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-7078924e-9396-4b07-885c-23d7d0c80090.json new file mode 100644 index 00000000..364502cc --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-7078924e-9396-4b07-885c-23d7d0c80090.json @@ -0,0 +1,54 @@ +{ + "id" : "7078924e-9396-4b07-885c-23d7d0c80090", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-7078924e-9396-4b07-885c-23d7d0c80090.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:14:44 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:14:44Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:14:44Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:14:44Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:14:44Z", + "request-id" : "req_011CYkPVjFKvz22EgGzbr6Tx", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "282", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b1a209d72be02-SEA" + } + }, + "uuid" : "7078924e-9396-4b07-885c-23d7d0c80090", + "persistent" : true, + "scenarioName" : "scenario-1-v1-messages", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-1-v1-messages-2", + "insertionIndex" : 11 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-7b8cac9c-793a-4dc7-8d7d-71d773210345.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-7b8cac9c-793a-4dc7-8d7d-71d773210345.json new file mode 100644 index 00000000..ce0d62d3 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-7b8cac9c-793a-4dc7-8d7d-71d773210345.json @@ -0,0 +1,53 @@ +{ + "id" : "7b8cac9c-793a-4dc7-8d7d-71d773210345", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-7b8cac9c-793a-4dc7-8d7d-71d773210345.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:37:48 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:37:48Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:37:48Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:37:48Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:37:48Z", + "request-id" : "req_011CYkRFk9FFhofJ15AFaFhn", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "302", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b3bea1dc7af88-SEA" + } + }, + "uuid" : "7b8cac9c-793a-4dc7-8d7d-71d773210345", + "persistent" : true, + "scenarioName" : "scenario-1-v1-messages", + "requiredScenarioState" : "scenario-1-v1-messages-2", + "insertionIndex" : 17 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-87b2b5f3-b093-475f-bbde-1ab031e72500.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-87b2b5f3-b093-475f-bbde-1ab031e72500.json new file mode 100644 index 00000000..dc5a7ee5 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-87b2b5f3-b093-475f-bbde-1ab031e72500.json @@ -0,0 +1,52 @@ +{ + "id" : "87b2b5f3-b093-475f-bbde-1ab031e72500", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-87b2b5f3-b093-475f-bbde-1ab031e72500.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 00:34:38 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "Cache-Control" : "no-cache", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T00:34:38Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T00:34:38Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T00:34:38Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T00:34:38Z", + "request-id" : "req_011CYizf3uoQ9dyNV1imPJH8", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "268", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d75093e8adf2403-SEA" + } + }, + "uuid" : "87b2b5f3-b093-475f-bbde-1ab031e72500", + "persistent" : true, + "insertionIndex" : 8 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-893a66cc-08d6-423f-bda1-720822c1f022.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-893a66cc-08d6-423f-bda1-720822c1f022.json new file mode 100644 index 00000000..2d67b69e --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-893a66cc-08d6-423f-bda1-720822c1f022.json @@ -0,0 +1,51 @@ +{ + "id" : "893a66cc-08d6-423f-bda1-720822c1f022", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages?beta=true", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-893a66cc-08d6-423f-bda1-720822c1f022.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:41:14 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:41:14Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:41:14Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:41:14Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:41:14Z", + "request-id" : "req_011CYkRWwKs5vVSGwGHwvjVY", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "304", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b40f24bb65b4d-SEA" + } + }, + "uuid" : "893a66cc-08d6-423f-bda1-720822c1f022", + "persistent" : true, + "insertionIndex" : 20 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-91a6d654-41ea-41c3-8205-e1e5123246a8.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-91a6d654-41ea-41c3-8205-e1e5123246a8.json new file mode 100644 index 00000000..a994f514 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-91a6d654-41ea-41c3-8205-e1e5123246a8.json @@ -0,0 +1,52 @@ +{ + "id" : "91a6d654-41ea-41c3-8205-e1e5123246a8", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages?beta=true", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-91a6d654-41ea-41c3-8205-e1e5123246a8.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:14:42 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "Cache-Control" : "no-cache", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:14:42Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:14:42Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:14:42Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:14:42Z", + "request-id" : "req_011CYkPVXvguL4JQEShs8vWq", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "337", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b1a0ffabe76c8-SEA" + } + }, + "uuid" : "91a6d654-41ea-41c3-8205-e1e5123246a8", + "persistent" : true, + "insertionIndex" : 9 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-9232af2f-2a7b-4d67-af05-fcc609fd5466.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-9232af2f-2a7b-4d67-af05-fcc609fd5466.json new file mode 100644 index 00000000..166250f9 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-9232af2f-2a7b-4d67-af05-fcc609fd5466.json @@ -0,0 +1,54 @@ +{ + "id" : "9232af2f-2a7b-4d67-af05-fcc609fd5466", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-9232af2f-2a7b-4d67-af05-fcc609fd5466.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:41:20 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "Cache-Control" : "no-cache", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:41:19Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:41:19Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:41:19Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:41:19Z", + "request-id" : "req_011CYkRXKPuhrFQXZLzCf8GY", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "382", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b41126db3d5ec-SEA" + } + }, + "uuid" : "9232af2f-2a7b-4d67-af05-fcc609fd5466", + "persistent" : true, + "scenarioName" : "scenario-1-v1-messages", + "requiredScenarioState" : "scenario-1-v1-messages-2", + "insertionIndex" : 24 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-b437c5ce-d528-4f0d-80f2-01cd5d7ba72e.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-b437c5ce-d528-4f0d-80f2-01cd5d7ba72e.json new file mode 100644 index 00000000..675e6f80 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-b437c5ce-d528-4f0d-80f2-01cd5d7ba72e.json @@ -0,0 +1,52 @@ +{ + "id" : "b437c5ce-d528-4f0d-80f2-01cd5d7ba72e", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages?beta=true", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-b437c5ce-d528-4f0d-80f2-01cd5d7ba72e.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 00:34:35 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "Cache-Control" : "no-cache", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T00:34:35Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T00:34:34Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T00:34:34Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T00:34:34Z", + "request-id" : "req_011CYizenhG5WtUx8dBK5ZDx", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "241", + "vary" : "Accept-Encoding", + "cf-cache-status" : "DYNAMIC", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "CF-RAY" : "9d7509285854eb9f-SEA" + } + }, + "uuid" : "b437c5ce-d528-4f0d-80f2-01cd5d7ba72e", + "persistent" : true, + "insertionIndex" : 5 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-cf2b874d-3585-481a-b023-49e23a1d3b46.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-cf2b874d-3585-481a-b023-49e23a1d3b46.json new file mode 100644 index 00000000..d1d2baaa --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-cf2b874d-3585-481a-b023-49e23a1d3b46.json @@ -0,0 +1,52 @@ +{ + "id" : "cf2b874d-3585-481a-b023-49e23a1d3b46", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-cf2b874d-3585-481a-b023-49e23a1d3b46.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:37:50 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "Cache-Control" : "no-cache", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:37:50Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:37:50Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:37:50Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:37:50Z", + "request-id" : "req_011CYkRFrHLGQy1GC2guXZzg", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "255", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b3bf31f96c66f-SEA" + } + }, + "uuid" : "cf2b874d-3585-481a-b023-49e23a1d3b46", + "persistent" : true, + "insertionIndex" : 18 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-da340447-86ca-40db-90b6-89c169f2eb65.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-da340447-86ca-40db-90b6-89c169f2eb65.json new file mode 100644 index 00000000..f5110f99 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-da340447-86ca-40db-90b6-89c169f2eb65.json @@ -0,0 +1,51 @@ +{ + "id" : "da340447-86ca-40db-90b6-89c169f2eb65", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages?beta=true", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-da340447-86ca-40db-90b6-89c169f2eb65.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:37:46 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T18:37:45Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T18:37:45Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T18:37:46Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T18:37:45Z", + "request-id" : "req_011CYkRFYmdNgym913iJmRkF", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "351", + "vary" : "Accept-Encoding", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "cf-cache-status" : "DYNAMIC", + "CF-RAY" : "9d7b3bd97e78b9cd-SEA" + } + }, + "uuid" : "da340447-86ca-40db-90b6-89c169f2eb65", + "persistent" : true, + "insertionIndex" : 15 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/anthropic/mappings/v1_messages-dc027840-62bc-4d7f-b133-7cb6488d1087.json b/src/test/resources/cassettes/anthropic/mappings/v1_messages-dc027840-62bc-4d7f-b133-7cb6488d1087.json new file mode 100644 index 00000000..f21452e6 --- /dev/null +++ b/src/test/resources/cassettes/anthropic/mappings/v1_messages-dc027840-62bc-4d7f-b133-7cb6488d1087.json @@ -0,0 +1,51 @@ +{ + "id" : "dc027840-62bc-4d7f-b133-7cb6488d1087", + "name" : "v1_messages", + "request" : { + "url" : "/v1/messages", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"max_tokens\":50,\"messages\":[{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"claude-3-haiku-20240307\",\"system\":\"You are a helpful assistant\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "v1_messages-dc027840-62bc-4d7f-b133-7cb6488d1087.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 00:34:37 GMT", + "Content-Type" : "application/json", + "anthropic-ratelimit-requests-limit" : "10000", + "anthropic-ratelimit-requests-remaining" : "9999", + "anthropic-ratelimit-requests-reset" : "2026-03-05T00:34:37Z", + "anthropic-ratelimit-input-tokens-limit" : "8000000", + "anthropic-ratelimit-input-tokens-remaining" : "8000000", + "anthropic-ratelimit-input-tokens-reset" : "2026-03-05T00:34:37Z", + "anthropic-ratelimit-output-tokens-limit" : "1500000", + "anthropic-ratelimit-output-tokens-remaining" : "1500000", + "anthropic-ratelimit-output-tokens-reset" : "2026-03-05T00:34:37Z", + "anthropic-ratelimit-tokens-limit" : "9500000", + "anthropic-ratelimit-tokens-remaining" : "9500000", + "anthropic-ratelimit-tokens-reset" : "2026-03-05T00:34:37Z", + "request-id" : "req_011CYizexMgaqLxrHxNrN31b", + "strict-transport-security" : "max-age=31536000; includeSubDomains; preload", + "anthropic-organization-id" : "27796668-7351-40ac-acc4-024aee8995a5", + "Server" : "cloudflare", + "x-envoy-upstream-service-time" : "363", + "vary" : "Accept-Encoding", + "cf-cache-status" : "DYNAMIC", + "X-Robots-Tag" : "none", + "Content-Security-Policy" : "default-src 'none'; frame-ancestors 'none'", + "CF-RAY" : "9d7509367ef2768a-SEA" + } + }, + "uuid" : "dc027840-62bc-4d7f-b133-7cb6488d1087", + "persistent" : true, + "insertionIndex" : 7 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-04ba6e68-856e-4b10-b296-a4d9829eb95a.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-04ba6e68-856e-4b10-b296-a4d9829eb95a.json new file mode 100644 index 00000000..8dedd5af --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-04ba6e68-856e-4b10-b296-a4d9829eb95a.json @@ -0,0 +1,39 @@ +{ + "id" : "04ba6e68-856e-4b10-b296-a4d9829eb95a", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CtAICrIBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAoiCg9zZXJ2aWNlLnZlcnNpb24SDwoNMC4yLjktYmEzOWYyYQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKYBwogCg9icmFpbnRydXN0LWphdmESDTAuMi45LWJhMzlmMmES8wYKEEUmbH3/cc6zvG2bO5mhkqwSCDPT3EX8r47ZKglyZXNwb25zZXMwATnIF1Na6ryZGEETm2gv67yZGEpqChVicmFpbnRydXN0LmlucHV0X2pzb24SUQpPW3siY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyBSZXBseSBpbiBvbmUgd29yZC4iLCJyb2xlIjoidXNlciJ9XUpvChJicmFpbnRydXN0Lm1ldHJpY3MSWQpXeyJjb21wbGV0aW9uX3Rva2VucyI6MjgsInByb21wdF90b2tlbnMiOjE4LCJ0b2tlbnMiOjQ2LCJjb21wbGV0aW9uX3JlYXNvbmluZ190b2tlbnMiOjB9SjIKEWJyYWludHJ1c3QucGFyZW50Eh0KG3Byb2plY3RfbmFtZTpqYXZhLXVuaXQtdGVzdEouChpicmFpbnRydXN0LnNwYW5fYXR0cmlidXRlcxIQCg57InR5cGUiOiJsbG0ifUrIAgoWYnJhaW50cnVzdC5vdXRwdXRfanNvbhKtAgqqAlt7ImlkIjoicnNfMDVjNjZmY2UzZWIwNzllNzAwNjlhODk0MWQ4MTUwODE5MGI4MzM2MzNmY2Y4ZmUxNjciLCJ0eXBlIjoicmVhc29uaW5nIiwic3VtbWFyeSI6W119LHsiaWQiOiJtc2dfMDVjNjZmY2UzZWIwNzllNzAwNjlhODk0MWUxZmU0ODE5MDgxMmM2ZTY4Mjk1NWZlODciLCJ0eXBlIjoibWVzc2FnZSIsInN0YXR1cyI6ImNvbXBsZXRlZCIsImNvbnRlbnQiOlt7InR5cGUiOiJvdXRwdXRfdGV4dCIsImFubm90YXRpb25zIjpbXSwibG9ncHJvYnMiOltdLCJ0ZXh0IjoiUGFyaXMifV0sInJvbGUiOiJhc3Npc3RhbnQifV1KoQEKE2JyYWludHJ1c3QubWV0YWRhdGESiQEKhgF7InByb3ZpZGVyIjoib3BlbmFpIiwicmVxdWVzdF9wYXRoIjoicmVzcG9uc2VzIiwibW9kZWwiOiJvNC1taW5pIiwicmVxdWVzdF9iYXNlX3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6NjUyMzkiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifXoAhQEBAQAA" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SFO53-P8", "SFO53-P1" ], + "Date" : "Wed, 04 Mar 2026 20:20:47 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "2fae1a22-8b3d-4007-b407-01961a751d1e", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a8941e000000003f6c62cd41a02bf2", + "x-amz-apigw-id" : "ZtwU3FEOoAMECwQ=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a8941e-63b9eed369e1ace54faef70b;Parent=6eb02788e9e68eb2;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 8a7956e959f93e6e09498016094dc012.cloudfront.net (CloudFront), 1.1 cb712125c0a106eae5fd0f1f475a2270.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "nEEgZNgyfbmwAFPyMRxiH_drpjzNumJAmIgMVbDvVck4MY6F3cjj2Q==" + } + }, + "uuid" : "04ba6e68-856e-4b10-b296-a4d9829eb95a", + "persistent" : true, + "insertionIndex" : 106 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-08b617f5-c0eb-4add-ab12-041c02da76fc.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-08b617f5-c0eb-4add-ab12-041c02da76fc.json new file mode 100644 index 00000000..9353c365 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-08b617f5-c0eb-4add-ab12-041c02da76fc.json @@ -0,0 +1,39 @@ +{ + "id" : "08b617f5-c0eb-4add-ab12-041c02da76fc", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpkLCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLbCQomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsAkKEFSZ6f7wveWA1YOx6Mla7vkSCBDVUkV3dMZ+KhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5uBo07hAGmhhB8LB0IxEGmhhKzAQKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SsQQKrgR7ImlkIjoibXNnXzAxTjQzUktldkxmODl0TXZLblVpMjRVaCIsImNvbnRlbnQiOlt7ImNpdGF0aW9ucyI6bnVsbCwidGV4dCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJ0eXBlIjoidGV4dCIsInZhbGlkIjp0cnVlfV0sIm1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInR5cGUiOiJtZXNzYWdlIiwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwidmFsaWQiOnRydWV9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnMiOjE5LCJvdXRwdXRfdG9rZW5zIjoxMCwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsInZhbGlkIjp0cnVlLCJpbmZlcmVuY2VfZ2VvIjoibm90X2F2YWlsYWJsZSJ9LCJ2YWxpZCI6dHJ1ZX1KoAEKE2JyYWludHJ1c3QubWV0YWRhdGESiAEKhQF7InByb3ZpZGVyIjoiYW50aHJvcGljIiwicmVxdWVzdF9wYXRoIjoidjEvbWVzc2FnZXMiLCJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3IiwicmVxdWVzdF9iYXNlX3VyaSI6IiIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyIsInJvbGUiOiJ1c2VyIn0seyJyb2xlIjoic3lzdGVtIiwiY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCJ9XUpxChJicmFpbnRydXN0Lm1ldHJpY3MSWwpZeyJjb21wbGV0aW9uX3Rva2VucyI6MTAsInByb21wdF90b2tlbnMiOjE5LCJ0b2tlbnMiOjI5LCJ0aW1lX3RvX2ZpcnN0X3Rva2VuIjowLjAwNjAwNjYyNX1KLgoaYnJhaW50cnVzdC5zcGFuX2F0dHJpYnV0ZXMSEAoOeyJ0eXBlIjoibGxtIn1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:41:14 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "00145192-c4b5-4316-bd65-d84906b5afcd", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ce4a00000000042e2aea0ee7b29b", + "x-amz-apigw-id" : "Zw0rpGtkoAMELhg=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ce4a-458b4c292d18f02e7e7c633c;Parent=7e2b690dbee36aec;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 e1832834d17ab65dd955f4e68cc524e6.cloudfront.net (CloudFront), 1.1 87247d9a9b2f9e51b0c72b364948aefa.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "Jvu3S207wAfX4VeHTaonx6abk_WfqF5UxxkMj3tAsL4-HqPRIzuh4w==" + } + }, + "uuid" : "08b617f5-c0eb-4add-ab12-041c02da76fc", + "persistent" : true, + "insertionIndex" : 132 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-10f21206-044c-41b0-be5e-a8985ebed935.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-10f21206-044c-41b0-be5e-a8985ebed935.json new file mode 100644 index 00000000..94b90578 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-10f21206-044c-41b0-be5e-a8985ebed935.json @@ -0,0 +1,39 @@ +{ + "id" : "10f21206-044c-41b0-be5e-a8985ebed935", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "Cs8JCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktZDE1NTA3OS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKRCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LWQxNTUwNzktRElSVFkS5gcKEOW1Zm3eSd1RPuNBhIIeX40SCNQyr1Eiz0h6KghNZXNzYWdlczABOSgelkrFypkYQbe2PnTFypkYSlcKFWJyYWludHJ1c3QuaW5wdXRfanNvbhI+CjxbeyJjb250ZW50IjoiV2hhdCBpcyB0aGUgY2FwaXRhbCBvZiBGcmFuY2U/Iiwicm9sZSI6InVzZXIifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9SvADChZicmFpbnRydXN0Lm91dHB1dF9qc29uEtUDCtIDeyJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3IiwiaWQiOiJtc2dfMDE3V3d0cEFLVXJ0alZFQ1RpM2hqbUpxIiwidHlwZSI6Im1lc3NhZ2UiLCJyb2xlIjoiYXNzaXN0YW50IiwiY29udGVudCI6W3sidHlwZSI6InRleHQiLCJ0ZXh0IjoiVGhlIGNhcGl0YWwgb2YgRnJhbmNlIGlzIFBhcmlzLiJ9XSwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MTksImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjoxMCwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoibm90X2F2YWlsYWJsZSJ9fUqgAQoTYnJhaW50cnVzdC5tZXRhZGF0YRKIAQqFAXsicHJvdmlkZXIiOiJhbnRocm9waWMiLCJyZXF1ZXN0X3BhdGgiOiJ2MS9tZXNzYWdlcyIsIm1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJyZXF1ZXN0X2Jhc2VfdXJpIjoiIiwicmVxdWVzdF9tZXRob2QiOiJQT1NUIn16AIUBAQEAAA==" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 00:34:38 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "d9d7cf73-fde1-4d52-ae79-166c8e1fbe92", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a8cf9e00000000001e0da851933c9a", + "x-amz-apigw-id" : "ZuVgwGSqoAMEpmA=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a8cf9e-09de158819a32f8602f21fbd;Parent=13216e4f54e8bf59;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 8e6c2cf5874f5e4093136cc3de4d856a.cloudfront.net (CloudFront), 1.1 87247d9a9b2f9e51b0c72b364948aefa.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "HlVhnvr0RcYNXc9GCUGoU10RBF42O2aUT-QyrBrIi3a1ICUvgo6eVg==" + } + }, + "uuid" : "10f21206-044c-41b0-be5e-a8985ebed935", + "persistent" : true, + "insertionIndex" : 111 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-1f56b82a-09ec-4399-84b1-daf0a596da46.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-1f56b82a-09ec-4399-84b1-daf0a596da46.json new file mode 100644 index 00000000..b1d801e5 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-1f56b82a-09ec-4399-84b1-daf0a596da46.json @@ -0,0 +1,39 @@ +{ + "id" : "1f56b82a-09ec-4399-84b1-daf0a596da46", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CqEICrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktYjZlMzkwZC1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLjBgomCg9icmFpbnRydXN0LWphdmESEzAuMi45LWI2ZTM5MGQtRElSVFkSuAYKEOIrAiUrXAIyYRSWq6uRzbcSCCNzB3frIUSfKg9DaGF0IENvbXBsZXRpb24wATlQCHQwFgOaGEGuH4FiFgOaGEqsAQoTYnJhaW50cnVzdC5tZXRhZGF0YRKUAQqRAXsicHJvdmlkZXIiOiJvcGVuYWkiLCJyZXF1ZXN0X3BhdGgiOiJjaGF0L2NvbXBsZXRpb25zIiwibW9kZWwiOiJncHQtNG8tbWluaSIsInJlcXVlc3RfYmFzZV91cmkiOiJodHRwOi8vbG9jYWxob3N0OjQ5ODQ3IiwicmVxdWVzdF9tZXRob2QiOiJQT1NUIn1K1gEKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SuwEKuAFbeyJmaW5pc2hfcmVhc29uIjoic3RvcCIsImluZGV4IjowLCJsb2dwcm9icyI6bnVsbCwibWVzc2FnZSI6eyJjb250ZW50IjoiVGhlIGNhcGl0YWwgb2YgRnJhbmNlIGlzIFBhcmlzLiIsInJlZnVzYWwiOm51bGwsInJvbGUiOiJhc3Npc3RhbnQiLCJ0b29sX2NhbGxzIjpbXSwidmFsaWQiOnRydWV9LCJ2YWxpZCI6dHJ1ZX1dSjIKEWJyYWludHJ1c3QucGFyZW50Eh0KG3Byb2plY3RfbmFtZTpqYXZhLXVuaXQtdGVzdEouChpicmFpbnRydXN0LnNwYW5fYXR0cmlidXRlcxIQCg57InR5cGUiOiJsbG0ifUqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQiLCJyb2xlIjoic3lzdGVtIn0seyJjb250ZW50IjoiV2hhdCBpcyB0aGUgY2FwaXRhbCBvZiBGcmFuY2U/Iiwicm9sZSI6InVzZXIifV1KbQoSYnJhaW50cnVzdC5tZXRyaWNzElcKVXsiY29tcGxldGlvbl90b2tlbnMiOjcsInByb21wdF90b2tlbnMiOjIzLCJ0b2tlbnMiOjMwLCJ0aW1lX3RvX2ZpcnN0X3Rva2VuIjowLjAwMjY0MX16AIUBAQEAAA==" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 17:46:38 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "a86cc541-7c4d-4f34-b203-e85d1d4ced85", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9c17e000000003ecf088ec8fb940c", + "x-amz-apigw-id" : "ZwsrxGmDIAMEF0w=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9c17e-7dadde70727ed7f34a6258cc;Parent=12c37396c6fadf03;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 db84db36e16ca0c80b0992006d731900.cloudfront.net (CloudFront), 1.1 170efbc424be9181bda5d0fcd6e41f30.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "Sk4RIAfZtGKbfEiL2HXofvscGCodjnZGxHIn3lqk3pjsf9F-nF2PLA==" + } + }, + "uuid" : "1f56b82a-09ec-4399-84b1-daf0a596da46", + "persistent" : true, + "insertionIndex" : 115 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-1f5ca854-23f3-4c0b-a654-952c3f3c857c.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-1f5ca854-23f3-4c0b-a654-952c3f3c857c.json new file mode 100644 index 00000000..6de4a86c --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-1f5ca854-23f3-4c0b-a654-952c3f3c857c.json @@ -0,0 +1,39 @@ +{ + "id" : "1f5ca854-23f3-4c0b-a654-952c3f3c857c", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CtwICrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKeBwomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkS8wYKEO2uB2D4e5Zpl7bXDoq//PISCBYqNf8f91FdKglyZXNwb25zZXMwATkgejjWwQWaGEF4/nlMwgWaGErIAgoWYnJhaW50cnVzdC5vdXRwdXRfanNvbhKtAgqqAlt7ImlkIjoicnNfMGU0OTNmN2VmY2U2MWJjNDAwNjlhOWNjZjY4MzVjODE5MGJmZmNlOGE2ZTBlZGRiNjQiLCJ0eXBlIjoicmVhc29uaW5nIiwic3VtbWFyeSI6W119LHsiaWQiOiJtc2dfMGU0OTNmN2VmY2U2MWJjNDAwNjlhOWNjZjZmZDU0ODE5MGJlMjg0NTA4YmRmMGFjMGMiLCJ0eXBlIjoibWVzc2FnZSIsInN0YXR1cyI6ImNvbXBsZXRlZCIsImNvbnRlbnQiOlt7InR5cGUiOiJvdXRwdXRfdGV4dCIsImFubm90YXRpb25zIjpbXSwibG9ncHJvYnMiOltdLCJ0ZXh0IjoiUGFyaXMifV0sInJvbGUiOiJhc3Npc3RhbnQifV1KoQEKE2JyYWludHJ1c3QubWV0YWRhdGESiQEKhgF7InByb3ZpZGVyIjoib3BlbmFpIiwicmVxdWVzdF9wYXRoIjoicmVzcG9uc2VzIiwibW9kZWwiOiJvNC1taW5pIiwicmVxdWVzdF9iYXNlX3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTEwOTciLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifUpqChVicmFpbnRydXN0LmlucHV0X2pzb24SUQpPW3siY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyBSZXBseSBpbiBvbmUgd29yZC4iLCJyb2xlIjoidXNlciJ9XUpvChJicmFpbnRydXN0Lm1ldHJpY3MSWQpXeyJjb21wbGV0aW9uX3Rva2VucyI6MzAsInByb21wdF90b2tlbnMiOjE4LCJ0b2tlbnMiOjQ4LCJjb21wbGV0aW9uX3JlYXNvbmluZ190b2tlbnMiOjB9SjIKEWJyYWludHJ1c3QucGFyZW50Eh0KG3Byb2plY3RfbmFtZTpqYXZhLXVuaXQtdGVzdEouChpicmFpbnRydXN0LnNwYW5fYXR0cmlidXRlcxIQCg57InR5cGUiOiJsbG0ifXoAhQEBAQAA" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:35:35 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "c8ab4c65-9b5f-4db3-b6f6-670d4083bd18", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ccf700000000462240adeec42956", + "x-amz-apigw-id" : "Zwz2vGj2oAMEe8w=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ccf7-3ecbc6a729003d2108afabb4;Parent=3dea263dd4d774d9;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 4cb8a7f3f7a5d9d545889e0d3926b9c2.cloudfront.net (CloudFront), 1.1 65f2e9f7f1475de54aa452d3ceb9bcf6.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "XknXgA0HG2PUfOTMYKbmwjyofITVQCcsGIa9roT0tyYz4CREwwKJaw==" + } + }, + "uuid" : "1f5ca854-23f3-4c0b-a654-952c3f3c857c", + "persistent" : true, + "insertionIndex" : 122 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-257bf382-a2d2-4d57-ae05-223870928d40.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-257bf382-a2d2-4d57-ae05-223870928d40.json new file mode 100644 index 00000000..459057bc --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-257bf382-a2d2-4d57-ae05-223870928d40.json @@ -0,0 +1,39 @@ +{ + "id" : "257bf382-a2d2-4d57-ae05-223870928d40", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CqMICrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLlBgomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSugYKEMaJIM4KchsQb25fXFlw6AQSCJ5rIzHbYE3aKg9DaGF0IENvbXBsZXRpb24wATl4UJcLwwWaGEFEguItwwWaGErWAQoWYnJhaW50cnVzdC5vdXRwdXRfanNvbhK7AQq4AVt7ImZpbmlzaF9yZWFzb24iOiJzdG9wIiwiaW5kZXgiOjAsImxvZ3Byb2JzIjpudWxsLCJtZXNzYWdlIjp7ImNvbnRlbnQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIiwicmVmdXNhbCI6bnVsbCwicm9sZSI6ImFzc2lzdGFudCIsInRvb2xfY2FsbHMiOltdLCJ2YWxpZCI6dHJ1ZX0sInZhbGlkIjp0cnVlfV1KrAEKE2JyYWludHJ1c3QubWV0YWRhdGESlAEKkQF7InByb3ZpZGVyIjoib3BlbmFpIiwicmVxdWVzdF9wYXRoIjoiY2hhdC9jb21wbGV0aW9ucyIsIm1vZGVsIjoiZ3B0LTRvLW1pbmkiLCJyZXF1ZXN0X2Jhc2VfdXJpIjoiaHR0cDovL2xvY2FsaG9zdDo1MTA5NyIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCIsInJvbGUiOiJzeXN0ZW0ifSx7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9XUpvChJicmFpbnRydXN0Lm1ldHJpY3MSWQpXeyJjb21wbGV0aW9uX3Rva2VucyI6NywicHJvbXB0X3Rva2VucyI6MjMsInRva2VucyI6MzAsInRpbWVfdG9fZmlyc3RfdG9rZW4iOjAuMDAzODMyMjV9SjIKEWJyYWludHJ1c3QucGFyZW50Eh0KG3Byb2plY3RfbmFtZTpqYXZhLXVuaXQtdGVzdEouChpicmFpbnRydXN0LnNwYW5fYXR0cmlidXRlcxIQCg57InR5cGUiOiJsbG0ifXoAhQEBAQAA" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:35:39 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "ae797ece-2a3c-4145-9752-a016723b87f4", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ccfb000000003534ad4f04d7791f", + "x-amz-apigw-id" : "Zwz3VEWAoAMEWVA=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ccfb-44e96d0d7d463184773a8293;Parent=1d12c43354426a8a;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 b7d7903ada432685f0e90f0ca261d864.cloudfront.net (CloudFront), 1.1 170efbc424be9181bda5d0fcd6e41f30.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "XK3YrU_XcPNdAT_GE0PWfMTX7NhzcXbhXl3tUO95CTX3poY5m9fcCw==" + } + }, + "uuid" : "257bf382-a2d2-4d57-ae05-223870928d40", + "persistent" : true, + "insertionIndex" : 125 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-2bb3d8b2-3d45-497b-9a9c-c0ce54081fe0.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-2bb3d8b2-3d45-497b-9a9c-c0ce54081fe0.json new file mode 100644 index 00000000..8a506b2e --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-2bb3d8b2-3d45-497b-9a9c-c0ce54081fe0.json @@ -0,0 +1,39 @@ +{ + "id" : "2bb3d8b2-3d45-497b-9a9c-c0ce54081fe0", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpkLCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLbCQomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsAkKEJlkXLzK+M19A0RqVgFGv5gSCCTYXf2sHaouKhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5iBR8kBEGmhhBlERgthEGmhhKzAQKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SsQQKrgR7ImlkIjoibXNnXzAxU1hHV1dERlhuektGTlk0UFZLb0JaSiIsImNvbnRlbnQiOlt7ImNpdGF0aW9ucyI6bnVsbCwidGV4dCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJ0eXBlIjoidGV4dCIsInZhbGlkIjp0cnVlfV0sIm1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInR5cGUiOiJtZXNzYWdlIiwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwidmFsaWQiOnRydWV9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnMiOjE5LCJvdXRwdXRfdG9rZW5zIjoxMCwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsInZhbGlkIjp0cnVlLCJpbmZlcmVuY2VfZ2VvIjoibm90X2F2YWlsYWJsZSJ9LCJ2YWxpZCI6dHJ1ZX1KoAEKE2JyYWludHJ1c3QubWV0YWRhdGESiAEKhQF7InByb3ZpZGVyIjoiYW50aHJvcGljIiwicmVxdWVzdF9wYXRoIjoidjEvbWVzc2FnZXMiLCJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3IiwicmVxdWVzdF9iYXNlX3VyaSI6IiIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyIsInJvbGUiOiJ1c2VyIn0seyJyb2xlIjoic3lzdGVtIiwiY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCJ9XUpxChJicmFpbnRydXN0Lm1ldHJpY3MSWwpZeyJjb21wbGV0aW9uX3Rva2VucyI6MTAsInByb21wdF90b2tlbnMiOjE5LCJ0b2tlbnMiOjI5LCJ0aW1lX3RvX2ZpcnN0X3Rva2VuIjowLjAwMzAzNjU4M31KLgoaYnJhaW50cnVzdC5zcGFuX2F0dHJpYnV0ZXMSEAoOeyJ0eXBlIjoibGxtIn1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:41:16 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "e791c24d-368c-4e8c-809f-62413457567d", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ce4c000000005bd45a378f604a4f", + "x-amz-apigw-id" : "Zw0sCEtNoAMEBHA=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ce4c-402777976cc1d4d63152bfb7;Parent=6f74be08351d1f9f;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 9257f9c4051fe8bd6cc4a09855b66350.cloudfront.net (CloudFront), 1.1 83d24992402f7b214901ab76fbdc11e2.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "JIVnmvuzQs77geuvgMBBsCEFt1RKvonB33MtsV8noOIAlCW2BVLfCw==" + } + }, + "uuid" : "2bb3d8b2-3d45-497b-9a9c-c0ce54081fe0", + "persistent" : true, + "insertionIndex" : 134 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-41549fb6-299f-4f8b-bb40-4782a27f21c5.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-41549fb6-299f-4f8b-bb40-4782a27f21c5.json new file mode 100644 index 00000000..69bd96a4 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-41549fb6-299f-4f8b-bb40-4782a27f21c5.json @@ -0,0 +1,39 @@ +{ + "id" : "41549fb6-299f-4f8b-bb40-4782a27f21c5", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpgICrIBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAoiCg9zZXJ2aWNlLnZlcnNpb24SDwoNMC4yLjktYmEzOWYyYQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLgBgogCg9icmFpbnRydXN0LWphdmESDTAuMi45LWJhMzlmMmESuwYKEBXDR6rEONG7s9tgUy+JmWkSCKP/0DahjAnJKg9DaGF0IENvbXBsZXRpb24wATm4rodN67yZGEEUcBF967yZGEqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQiLCJyb2xlIjoic3lzdGVtIn0seyJjb250ZW50IjoiV2hhdCBpcyB0aGUgY2FwaXRhbCBvZiBGcmFuY2U/Iiwicm9sZSI6InVzZXIifV1KcAoSYnJhaW50cnVzdC5tZXRyaWNzEloKWHsiY29tcGxldGlvbl90b2tlbnMiOjcsInByb21wdF90b2tlbnMiOjIzLCJ0b2tlbnMiOjMwLCJ0aW1lX3RvX2ZpcnN0X3Rva2VuIjowLjAwNjQ4NzYyNX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9StYBChZicmFpbnRydXN0Lm91dHB1dF9qc29uErsBCrgBW3siZmluaXNoX3JlYXNvbiI6InN0b3AiLCJpbmRleCI6MCwibG9ncHJvYnMiOm51bGwsIm1lc3NhZ2UiOnsiY29udGVudCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJyZWZ1c2FsIjpudWxsLCJyb2xlIjoiYXNzaXN0YW50IiwidG9vbF9jYWxscyI6W10sInZhbGlkIjp0cnVlfSwidmFsaWQiOnRydWV9XUqsAQoTYnJhaW50cnVzdC5tZXRhZGF0YRKUAQqRAXsicHJvdmlkZXIiOiJvcGVuYWkiLCJyZXF1ZXN0X3BhdGgiOiJjaGF0L2NvbXBsZXRpb25zIiwibW9kZWwiOiJncHQtNG8tbWluaSIsInJlcXVlc3RfYmFzZV91cmkiOiJodHRwOi8vbG9jYWxob3N0OjY1MjM5IiwicmVxdWVzdF9tZXRob2QiOiJQT1NUIn16AIUBAQEAAA==" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SFO53-P8", "SFO53-P1" ], + "Date" : "Wed, 04 Mar 2026 20:20:48 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "d6168b0d-1e1a-4bb1-8587-b3ddbb33e066", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a894200000000070d5d4f79be2a868", + "x-amz-apigw-id" : "ZtwVEFEaoAMEY9g=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a89420-445203300ae3a0f016b0c69e;Parent=5d7c1a8d749c5ac5;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 f38dffa5f68963b42a7e9d52afc9ea08.cloudfront.net (CloudFront), 1.1 e254430e2f05073f8b60d988c9070962.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "cqo_ALL8Qiop0-ZtoVjZVqhUIE74d-Bu28BOsDLW58OL5KSErlNNAQ==" + } + }, + "uuid" : "41549fb6-299f-4f8b-bb40-4782a27f21c5", + "persistent" : true, + "insertionIndex" : 107 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-45a6a104-3e21-4c79-9155-9d2c8439f6b6.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-45a6a104-3e21-4c79-9155-9d2c8439f6b6.json new file mode 100644 index 00000000..65722e5d --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-45a6a104-3e21-4c79-9155-9d2c8439f6b6.json @@ -0,0 +1,39 @@ +{ + "id" : "45a6a104-3e21-4c79-9155-9d2c8439f6b6", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpsKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLdCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsggKECs3/V8PqFSzaJVIX3fSp3gSCNvzqntH2Vg7KhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5mB6Z8+AFmhhBrVgjH+EFmhhK8AMKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24S1QMK0gN7Im1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJpZCI6Im1zZ18wMUx0WUdUZFF6RkRrcW01VGZhRW5tOFQiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEwLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifUqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9LHsicm9sZSI6InN5c3RlbSIsImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:37:48 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "3780c52c-8d36-40a1-beb1-716fc83df39a", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9cd7b0000000008924f66545d5ac6", + "x-amz-apigw-id" : "Zw0LbGMvIAMEdeg=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9cd7b-525b5aba209f3ed0521a97e3;Parent=74a11c1beb541417;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 9257f9c4051fe8bd6cc4a09855b66350.cloudfront.net (CloudFront), 1.1 566cc276dff9847158a5a9854be4df42.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "4vnKSRL-rv55ck1dqkVT82aFbuc_G5EKHKkpqhquGA7_ohHvuI3gXg==" + } + }, + "uuid" : "45a6a104-3e21-4c79-9155-9d2c8439f6b6", + "persistent" : true, + "insertionIndex" : 129 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-4fc40df7-b1c5-4a5e-8079-b4c3cb1cc2b7.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-4fc40df7-b1c5-4a5e-8079-b4c3cb1cc2b7.json new file mode 100644 index 00000000..e392ca3a --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-4fc40df7-b1c5-4a5e-8079-b4c3cb1cc2b7.json @@ -0,0 +1,39 @@ +{ + "id" : "4fc40df7-b1c5-4a5e-8079-b4c3cb1cc2b7", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpsKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLdCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsggKEC5YEel5WrAGS/CMBcwht2QSCBGDV3JcmIvxKhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE52Iy5PxEGmhhBSPDIaxEGmhhK8AMKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24S1QMK0gN7Im1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJpZCI6Im1zZ18wMVlDSlB1RHpGQ3ZFaWNETEV6TUN0M2siLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEwLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifUqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9LHsicm9sZSI6InN5c3RlbSIsImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KLgoaYnJhaW50cnVzdC5zcGFuX2F0dHJpYnV0ZXMSEAoOeyJ0eXBlIjoibGxtIn1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:41:15 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "135c57fc-337e-42ea-a764-f3a1cfd2ae33", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ce4b000000001016b14b178ea49c", + "x-amz-apigw-id" : "Zw0r1GSzoAMEchQ=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ce4b-766d4df70677235914347e34;Parent=08b3a53bcc3899d8;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 487082619948f670d3b30fb3db8fbabc.cloudfront.net (CloudFront), 1.1 170efbc424be9181bda5d0fcd6e41f30.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "HEg-B2cROwxT33q-PRDZ1e1hQQJ_dz9rBY_dfGh6iEL0J7pksomAKA==" + } + }, + "uuid" : "4fc40df7-b1c5-4a5e-8079-b4c3cb1cc2b7", + "persistent" : true, + "insertionIndex" : 133 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-54cae228-050e-4747-81c7-6cbfa190fc76.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-54cae228-050e-4747-81c7-6cbfa190fc76.json new file mode 100644 index 00000000..0daf8a9e --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-54cae228-050e-4747-81c7-6cbfa190fc76.json @@ -0,0 +1,39 @@ +{ + "id" : "54cae228-050e-4747-81c7-6cbfa190fc76", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpkLCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLbCQomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsAkKEJ9RvpXFWchAGzony8rq/p8SCOKn/chU4shRKhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5mFB3l+EFmhhBr3OKveEFmhhKzAQKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SsQQKrgR7ImlkIjoibXNnXzAxM0NxaVAydngyaEUxR1RzcnBYOGE2NCIsImNvbnRlbnQiOlt7ImNpdGF0aW9ucyI6bnVsbCwidGV4dCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJ0eXBlIjoidGV4dCIsInZhbGlkIjp0cnVlfV0sIm1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInR5cGUiOiJtZXNzYWdlIiwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwidmFsaWQiOnRydWV9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnMiOjE5LCJvdXRwdXRfdG9rZW5zIjoxMCwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsInZhbGlkIjp0cnVlLCJpbmZlcmVuY2VfZ2VvIjoibm90X2F2YWlsYWJsZSJ9LCJ2YWxpZCI6dHJ1ZX1KoAEKE2JyYWludHJ1c3QubWV0YWRhdGESiAEKhQF7InByb3ZpZGVyIjoiYW50aHJvcGljIiwicmVxdWVzdF9wYXRoIjoidjEvbWVzc2FnZXMiLCJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3IiwicmVxdWVzdF9iYXNlX3VyaSI6IiIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyIsInJvbGUiOiJ1c2VyIn0seyJyb2xlIjoic3lzdGVtIiwiY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCJ9XUpxChJicmFpbnRydXN0Lm1ldHJpY3MSWwpZeyJjb21wbGV0aW9uX3Rva2VucyI6MTAsInByb21wdF90b2tlbnMiOjE5LCJ0b2tlbnMiOjI5LCJ0aW1lX3RvX2ZpcnN0X3Rva2VuIjowLjAwMjc1MDA4NH1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:37:50 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "6c1da751-cd35-4758-846b-0393db3d56f6", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9cd7e00000000544fea78c68a666f", + "x-amz-apigw-id" : "Zw0L1G_QIAMEmhQ=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9cd7e-43d00e5115456dc925e783f9;Parent=1dae4b85b4e23f84;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 20b3731a0ef4aba3db1fcd63c3ef2b2a.cloudfront.net (CloudFront), 1.1 82fa7f20ab5a12301da8e01f9493e222.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "La_-o8sJM8lh2D6cgAyWF64L1xj5OnPW0UZLk3joxhvIMLDfTZjmwg==" + } + }, + "uuid" : "54cae228-050e-4747-81c7-6cbfa190fc76", + "persistent" : true, + "insertionIndex" : 131 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-552715c6-64c7-438f-b76a-e09ea52dc1dd.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-552715c6-64c7-438f-b76a-e09ea52dc1dd.json new file mode 100644 index 00000000..d465cb34 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-552715c6-64c7-438f-b76a-e09ea52dc1dd.json @@ -0,0 +1,39 @@ +{ + "id" : "552715c6-64c7-438f-b76a-e09ea52dc1dd", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpsKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLdCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsggKEIK9Lw3/IFqGVo1hKir1UWgSCLTbxCJUD+qdKhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5uHXt2BEGmhhB7wzJAxIGmhhK8AMKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24S1QMK0gN7Im1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJpZCI6Im1zZ18wMTdYSlQ0MzRpQjllTmU5UDFNS2JqV0ciLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEwLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifUqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9LHsicm9sZSI6InN5c3RlbSIsImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KLgoaYnJhaW50cnVzdC5zcGFuX2F0dHJpYnV0ZXMSEAoOeyJ0eXBlIjoibGxtIn1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:41:18 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "05efed58-58a3-4f0e-a35c-0ceaf51bd95a", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ce4d0000000062773f4cb44ffc15", + "x-amz-apigw-id" : "Zw0sPE7AIAMEOTw=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ce4d-64cff1a93454b368010effa2;Parent=3448bf9a15f555dd;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 2c24d855455b80190edd9e2dcdca3ee8.cloudfront.net (CloudFront), 1.1 73b0c4a85645a8031ba157e0b3e28ffc.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "ehhEsQB7w-Qg18UHgaIBiKhGPkfXUihWx9Lye_IEiqlsIetHS81OiA==" + } + }, + "uuid" : "552715c6-64c7-438f-b76a-e09ea52dc1dd", + "persistent" : true, + "insertionIndex" : 135 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-5b52d206-fff7-48e2-92ef-15476c7a3d9e.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-5b52d206-fff7-48e2-92ef-15476c7a3d9e.json new file mode 100644 index 00000000..6c6c9b16 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-5b52d206-fff7-48e2-92ef-15476c7a3d9e.json @@ -0,0 +1,39 @@ +{ + "id" : "5b52d206-fff7-48e2-92ef-15476c7a3d9e", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpkLCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLbCQomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsAkKEADDSaHszwBnyiWaZ5pm57kSCL+b6PqmUpQ6KhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5yLGQTeAFmhhBpNjwheAFmhhKzAQKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SsQQKrgR7ImlkIjoibXNnXzAxVWNWdzVyaU0zV0h3V2dSbXFaa1g4cyIsImNvbnRlbnQiOlt7ImNpdGF0aW9ucyI6bnVsbCwidGV4dCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJ0eXBlIjoidGV4dCIsInZhbGlkIjp0cnVlfV0sIm1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInR5cGUiOiJtZXNzYWdlIiwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwidmFsaWQiOnRydWV9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnMiOjE5LCJvdXRwdXRfdG9rZW5zIjoxMCwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsInZhbGlkIjp0cnVlLCJpbmZlcmVuY2VfZ2VvIjoibm90X2F2YWlsYWJsZSJ9LCJ2YWxpZCI6dHJ1ZX1KoAEKE2JyYWludHJ1c3QubWV0YWRhdGESiAEKhQF7InByb3ZpZGVyIjoiYW50aHJvcGljIiwicmVxdWVzdF9wYXRoIjoidjEvbWVzc2FnZXMiLCJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3IiwicmVxdWVzdF9iYXNlX3VyaSI6IiIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyIsInJvbGUiOiJ1c2VyIn0seyJyb2xlIjoic3lzdGVtIiwiY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCJ9XUpxChJicmFpbnRydXN0Lm1ldHJpY3MSWwpZeyJjb21wbGV0aW9uX3Rva2VucyI6MTAsInByb21wdF90b2tlbnMiOjE5LCJ0b2tlbnMiOjI5LCJ0aW1lX3RvX2ZpcnN0X3Rva2VuIjowLjAwNTQxNDM3NX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:37:45 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "9c15cbbf-88ca-47ee-8958-ddc07c514b0f", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9cd79000000006c1ecf7a567a4cca", + "x-amz-apigw-id" : "Zw0LBHSfoAMENcg=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9cd79-0e313c47444940ac65fc0be2;Parent=03055c3b90516865;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 05717f654525d5f71688fb57ace6362a.cloudfront.net (CloudFront), 1.1 d525041695bdb6325f78ebba5c11b8a2.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "jNj0OTcQTemN6kAm4SZ5usxmmLcgh5PqYgDtW2drMcm_xz6mkZUsLw==" + } + }, + "uuid" : "5b52d206-fff7-48e2-92ef-15476c7a3d9e", + "persistent" : true, + "insertionIndex" : 127 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-62561358-8498-4f7e-9e6c-0745a358a9bc.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-62561358-8498-4f7e-9e6c-0745a358a9bc.json new file mode 100644 index 00000000..1d0e524a --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-62561358-8498-4f7e-9e6c-0745a358a9bc.json @@ -0,0 +1,39 @@ +{ + "id" : "62561358-8498-4f7e-9e6c-0745a358a9bc", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CtwICrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktYjZlMzkwZC1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKeBwomCg9icmFpbnRydXN0LWphdmESEzAuMi45LWI2ZTM5MGQtRElSVFkS8wYKEPYlPTKYGmKDw9+/m8vzVnkSCP52+LGslXlIKglyZXNwb25zZXMwATmQA1tQFQOaGEHgH1O3FQOaGEqhAQoTYnJhaW50cnVzdC5tZXRhZGF0YRKJAQqGAXsicHJvdmlkZXIiOiJvcGVuYWkiLCJyZXF1ZXN0X3BhdGgiOiJyZXNwb25zZXMiLCJtb2RlbCI6Im80LW1pbmkiLCJyZXF1ZXN0X2Jhc2VfdXJpIjoiaHR0cDovL2xvY2FsaG9zdDo0OTg0NyIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9SsgCChZicmFpbnRydXN0Lm91dHB1dF9qc29uEq0CCqoCW3siaWQiOiJyc18wOWViOGU4MDI5OGM1NTI4MDA2OWE5YzE3YTNkYjg4MTk1YmJhYTQ2ZWZkZjc0MDhmOCIsInR5cGUiOiJyZWFzb25pbmciLCJzdW1tYXJ5IjpbXX0seyJpZCI6Im1zZ18wOWViOGU4MDI5OGM1NTI4MDA2OWE5YzE3YWNjZjA4MTk1OWIzYTAwNDRjYWM1YzM1OCIsInR5cGUiOiJtZXNzYWdlIiwic3RhdHVzIjoiY29tcGxldGVkIiwiY29udGVudCI6W3sidHlwZSI6Im91dHB1dF90ZXh0IiwiYW5ub3RhdGlvbnMiOltdLCJsb2dwcm9icyI6W10sInRleHQiOiJQYXJpcyJ9XSwicm9sZSI6ImFzc2lzdGFudCJ9XUoyChFicmFpbnRydXN0LnBhcmVudBIdChtwcm9qZWN0X25hbWU6amF2YS11bml0LXRlc3RKLgoaYnJhaW50cnVzdC5zcGFuX2F0dHJpYnV0ZXMSEAoOeyJ0eXBlIjoibGxtIn1KagoVYnJhaW50cnVzdC5pbnB1dF9qc29uElEKT1t7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8gUmVwbHkgaW4gb25lIHdvcmQuIiwicm9sZSI6InVzZXIifV1KbwoSYnJhaW50cnVzdC5tZXRyaWNzElkKV3siY29tcGxldGlvbl90b2tlbnMiOjM4LCJwcm9tcHRfdG9rZW5zIjoxOCwidG9rZW5zIjo1NiwiY29tcGxldGlvbl9yZWFzb25pbmdfdG9rZW5zIjowfXoAhQEBAQAA" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 17:46:35 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "e33303b3-118e-4530-8526-4dbbbe526eb6", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9c17b00000000292d42701617457c", + "x-amz-apigw-id" : "ZwsrUHEPoAMEBIg=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9c17b-0806c957195d94ed1fbba53d;Parent=12729e4cce80f404;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 a642518ef4d5fb78c3190de112922a38.cloudfront.net (CloudFront), 1.1 a624be98cd5b264f373d8ac17f78ee50.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "SEOEH5ozUic2Lveo1NBPB-QDhbBTJ-OryaxZCu9J8rWIOGtqwuyBRw==" + } + }, + "uuid" : "62561358-8498-4f7e-9e6c-0745a358a9bc", + "persistent" : true, + "insertionIndex" : 113 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-69a9c81e-30ec-49c4-ad1a-e1962184280f.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-69a9c81e-30ec-49c4-ad1a-e1962184280f.json new file mode 100644 index 00000000..063d7706 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-69a9c81e-30ec-49c4-ad1a-e1962184280f.json @@ -0,0 +1,39 @@ +{ + "id" : "69a9c81e-30ec-49c4-ad1a-e1962184280f", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpsKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNjllMjNiYi1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLdCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTY5ZTIzYmItRElSVFkSsggKEKLtgpYzqSyJDT97Ut8l40oSCJGPn9oF0sRyKhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5+FTTUZ8EmhhBX920e58EmhhK8AMKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24S1QMK0gN7Im1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJpZCI6Im1zZ18wMTFZZXhidllyaFdKakQ0a2lUTVNQbjQiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEwLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifUqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9LHsicm9sZSI6InN5c3RlbSIsImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:14:46 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "cab128b9-a6f6-4f43-a0d2-22aa29bbc3cf", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9c816000000004bb14fd2a8b22a1d", + "x-amz-apigw-id" : "ZwwzlF08oAMEW3w=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9c816-78a006045cb257d819448b70;Parent=1fa0751a2127b5d3;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 e1832834d17ab65dd955f4e68cc524e6.cloudfront.net (CloudFront), 1.1 65f2e9f7f1475de54aa452d3ceb9bcf6.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "Q_7pLAW2ANy42oF7MnD73UedftubZLko44UhYGGJ9OKoPaygTzSxdQ==" + } + }, + "uuid" : "69a9c81e-30ec-49c4-ad1a-e1962184280f", + "persistent" : true, + "insertionIndex" : 120 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-6f11ffbe-72bc-4b49-8442-3d1d253d4dc1.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-6f11ffbe-72bc-4b49-8442-3d1d253d4dc1.json new file mode 100644 index 00000000..ebfe39e4 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-6f11ffbe-72bc-4b49-8442-3d1d253d4dc1.json @@ -0,0 +1,39 @@ +{ + "id" : "6f11ffbe-72bc-4b49-8442-3d1d253d4dc1", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpsKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLdCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsggKEKjN48XFqj/PT/Aigc9P9VUSCP4miETE4UEAKhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5IOMpo+AFmhhBBjsvx+AFmhhK8AMKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24S1QMK0gN7Im1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJpZCI6Im1zZ18wMU15WEJxajd6NnB3VVhxUTJoWkptZ2QiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEwLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifUqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9LHsicm9sZSI6InN5c3RlbSIsImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:37:46 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "7053440f-a15f-4e24-8ca9-14077d77dbfe", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9cd7a000000006ec4b9e12deffa05", + "x-amz-apigw-id" : "Zw0LNH60oAMEUNg=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9cd7a-60c2d2e53d4c513b0c4609e7;Parent=6a6fc7b1975fb917;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 e1832834d17ab65dd955f4e68cc524e6.cloudfront.net (CloudFront), 1.1 170efbc424be9181bda5d0fcd6e41f30.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "rU33fHsnv479aeAZ-tq3H_x2va_4qTxr4fvW5Vj76d5e5sau2bcDwg==" + } + }, + "uuid" : "6f11ffbe-72bc-4b49-8442-3d1d253d4dc1", + "persistent" : true, + "insertionIndex" : 128 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-75bd8a50-e6c6-464a-b337-dab45cf28025.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-75bd8a50-e6c6-464a-b337-dab45cf28025.json new file mode 100644 index 00000000..e56062d3 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-75bd8a50-e6c6-464a-b337-dab45cf28025.json @@ -0,0 +1,39 @@ +{ + "id" : "75bd8a50-e6c6-464a-b337-dab45cf28025", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CqQICrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLmBgomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSuwYKELNGfKJg64dSSbi20MRUsD4SCOApmu0evQEvKg9DaGF0IENvbXBsZXRpb24wATnILNy9wgWaGEGO4+PjwgWaGErWAQoWYnJhaW50cnVzdC5vdXRwdXRfanNvbhK7AQq4AVt7ImZpbmlzaF9yZWFzb24iOiJzdG9wIiwiaW5kZXgiOjAsImxvZ3Byb2JzIjpudWxsLCJtZXNzYWdlIjp7ImNvbnRlbnQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIiwicmVmdXNhbCI6bnVsbCwicm9sZSI6ImFzc2lzdGFudCIsInRvb2xfY2FsbHMiOltdLCJ2YWxpZCI6dHJ1ZX0sInZhbGlkIjp0cnVlfV1KrAEKE2JyYWludHJ1c3QubWV0YWRhdGESlAEKkQF7InByb3ZpZGVyIjoib3BlbmFpIiwicmVxdWVzdF9wYXRoIjoiY2hhdC9jb21wbGV0aW9ucyIsIm1vZGVsIjoiZ3B0LTRvLW1pbmkiLCJyZXF1ZXN0X2Jhc2VfdXJpIjoiaHR0cDovL2xvY2FsaG9zdDo1MTA5NyIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCIsInJvbGUiOiJzeXN0ZW0ifSx7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9XUpwChJicmFpbnRydXN0Lm1ldHJpY3MSWgpYeyJjb21wbGV0aW9uX3Rva2VucyI6NywicHJvbXB0X3Rva2VucyI6MjMsInRva2VucyI6MzAsInRpbWVfdG9fZmlyc3RfdG9rZW4iOjAuMDA1MDEyNTgzfUoyChFicmFpbnRydXN0LnBhcmVudBIdChtwcm9qZWN0X25hbWU6amF2YS11bml0LXRlc3RKLgoaYnJhaW50cnVzdC5zcGFuX2F0dHJpYnV0ZXMSEAoOeyJ0eXBlIjoibGxtIn16AIUBAQEAAA==" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:35:38 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "975a4764-8866-4a68-ae97-b1431eede6bc", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ccfa000000006d54f5debcba7db3", + "x-amz-apigw-id" : "Zwz3IG1KIAMEvvw=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ccfa-0456bc7a4435a76d64f07ebb;Parent=4ef246cf4ed766b0;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 4cb8a7f3f7a5d9d545889e0d3926b9c2.cloudfront.net (CloudFront), 1.1 fbb003dfc0617e3e058e3dac791dfd5a.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "5QHjHhG4uYXOhqoPh7omkuCWoVGNNZ8vqJRPklDFHY9kYtuWI-K9eg==" + } + }, + "uuid" : "75bd8a50-e6c6-464a-b337-dab45cf28025", + "persistent" : true, + "insertionIndex" : 124 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-a5d45134-a913-4a61-ada7-721417b2027b.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-a5d45134-a913-4a61-ada7-721417b2027b.json new file mode 100644 index 00000000..422b8d1d --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-a5d45134-a913-4a61-ada7-721417b2027b.json @@ -0,0 +1,39 @@ +{ + "id" : "a5d45134-a913-4a61-ada7-721417b2027b", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "Cs8JCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktZDE1NTA3OS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKRCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LWQxNTUwNzktRElSVFkS5gcKEOvklsl4SJqyvNOsGGV9OIkSCDjG+t60/YNCKghNZXNzYWdlczABObDRJQzFypkYQRLjeTDFypkYSlcKFWJyYWludHJ1c3QuaW5wdXRfanNvbhI+CjxbeyJjb250ZW50IjoiV2hhdCBpcyB0aGUgY2FwaXRhbCBvZiBGcmFuY2U/Iiwicm9sZSI6InVzZXIifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9SvADChZicmFpbnRydXN0Lm91dHB1dF9qc29uEtUDCtIDeyJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3IiwiaWQiOiJtc2dfMDE0ZVhxMVIybWFtQUVzQmIxRXNOTU1vIiwidHlwZSI6Im1lc3NhZ2UiLCJyb2xlIjoiYXNzaXN0YW50IiwiY29udGVudCI6W3sidHlwZSI6InRleHQiLCJ0ZXh0IjoiVGhlIGNhcGl0YWwgb2YgRnJhbmNlIGlzIFBhcmlzLiJ9XSwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MTksImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjoxMCwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoibm90X2F2YWlsYWJsZSJ9fUqgAQoTYnJhaW50cnVzdC5tZXRhZGF0YRKIAQqFAXsicHJvdmlkZXIiOiJhbnRocm9waWMiLCJyZXF1ZXN0X3BhdGgiOiJ2MS9tZXNzYWdlcyIsIm1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJyZXF1ZXN0X2Jhc2VfdXJpIjoiIiwicmVxdWVzdF9tZXRob2QiOiJQT1NUIn16AIUBAQEAAA==" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 00:34:37 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "aaae5927-be23-4141-b1c3-1ae3cf01bf69", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a8cf9c0000000078e6f7c12944a905", + "x-amz-apigw-id" : "ZuVgjH9hoAMEGuA=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a8cf9c-05d5074771717f5879aba24f;Parent=5f525ba9736b7a15;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 b47176981720d8607d309e56e9510316.cloudfront.net (CloudFront), 1.1 73b0c4a85645a8031ba157e0b3e28ffc.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "VvbveqdKCcyfEEk_3Hffk_n5gTCeCXTJftmsOoMerO2TpcevO5nJ7w==" + } + }, + "uuid" : "a5d45134-a913-4a61-ada7-721417b2027b", + "persistent" : true, + "insertionIndex" : 110 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-acca271e-4bac-40c4-adfb-f3612a297cc6.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-acca271e-4bac-40c4-adfb-f3612a297cc6.json new file mode 100644 index 00000000..e9e57e13 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-acca271e-4bac-40c4-adfb-f3612a297cc6.json @@ -0,0 +1,39 @@ +{ + "id" : "acca271e-4bac-40c4-adfb-f3612a297cc6", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpkLCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNjllMjNiYi1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLbCQomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTY5ZTIzYmItRElSVFkSsAkKEL+WS6zEhj1wJvqE52ole3sSCMZADD0HzmAfKhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5eLm9oJ8EmhhB52+3x58EmhhKzAQKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SsQQKrgR7ImlkIjoibXNnXzAxVWhtclBtRWpFRGNVS25ha1c0NXB0UCIsImNvbnRlbnQiOlt7ImNpdGF0aW9ucyI6bnVsbCwidGV4dCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJ0eXBlIjoidGV4dCIsInZhbGlkIjp0cnVlfV0sIm1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInR5cGUiOiJtZXNzYWdlIiwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwidmFsaWQiOnRydWV9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnMiOjE5LCJvdXRwdXRfdG9rZW5zIjoxMCwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsInZhbGlkIjp0cnVlLCJpbmZlcmVuY2VfZ2VvIjoibm90X2F2YWlsYWJsZSJ9LCJ2YWxpZCI6dHJ1ZX1KoAEKE2JyYWludHJ1c3QubWV0YWRhdGESiAEKhQF7InByb3ZpZGVyIjoiYW50aHJvcGljIiwicmVxdWVzdF9wYXRoIjoidjEvbWVzc2FnZXMiLCJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3IiwicmVxdWVzdF9iYXNlX3VyaSI6IiIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyIsInJvbGUiOiJ1c2VyIn0seyJyb2xlIjoic3lzdGVtIiwiY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCJ9XUpxChJicmFpbnRydXN0Lm1ldHJpY3MSWwpZeyJjb21wbGV0aW9uX3Rva2VucyI6MTAsInByb21wdF90b2tlbnMiOjE5LCJ0b2tlbnMiOjI5LCJ0aW1lX3RvX2ZpcnN0X3Rva2VuIjowLjAwMjU3NjQ1OH1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:14:48 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "53efbdfd-4e69-459a-9e78-781c5d131a4e", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9c817000000001b5b33e8005b2940", + "x-amz-apigw-id" : "ZwwzxHyqIAMEK3Q=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9c817-35b01274712a825a2b453c99;Parent=1f7b8481efc6fdb3;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 487082619948f670d3b30fb3db8fbabc.cloudfront.net (CloudFront), 1.1 0eb43913f9caf453beb959a8a836a688.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "sszUoVaD0jfAIjH8Q4hY5SADefQbG-aS2Wlma1smFxWYGuxW6emyLQ==" + } + }, + "uuid" : "acca271e-4bac-40c4-adfb-f3612a297cc6", + "persistent" : true, + "insertionIndex" : 121 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-b4045be6-a661-4c2e-8fb6-793a3e42fc14.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-b4045be6-a661-4c2e-8fb6-793a3e42fc14.json new file mode 100644 index 00000000..8cb838a2 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-b4045be6-a661-4c2e-8fb6-793a3e42fc14.json @@ -0,0 +1,39 @@ +{ + "id" : "b4045be6-a661-4c2e-8fb6-793a3e42fc14", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpsKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNjllMjNiYi1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLdCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTY5ZTIzYmItRElSVFkSsggKEBjopfrPEYc5rdJzgfZeW5kSCNPretjPK449KhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5IAYlCZ8EmhhBk0d2KZ8EmhhK8AMKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24S1QMK0gN7Im1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJpZCI6Im1zZ18wMUFHTHZXazRUeVVCTlFwN2o2bXhRQXIiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEwLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifUqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9LHsicm9sZSI6InN5c3RlbSIsImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:14:45 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "54436bc9-b8c2-4af2-a9be-82e61120ebb4", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9c8150000000048de85946c1b2775", + "x-amz-apigw-id" : "ZwwzXHKroAMEXdg=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9c815-76f2d4396e6310a45d96160e;Parent=5ce52d91e2e8f699;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 2c24d855455b80190edd9e2dcdca3ee8.cloudfront.net (CloudFront), 1.1 65f2e9f7f1475de54aa452d3ceb9bcf6.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "P8nzNFvx1ga89EKGsnAISuWu0-0MdYFdouptaktVqACci6UjnfDFTg==" + } + }, + "uuid" : "b4045be6-a661-4c2e-8fb6-793a3e42fc14", + "persistent" : true, + "insertionIndex" : 119 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-bd973d23-2b00-4141-a7a4-4588bb926e33.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-bd973d23-2b00-4141-a7a4-4588bb926e33.json new file mode 100644 index 00000000..9b0c9700 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-bd973d23-2b00-4141-a7a4-4588bb926e33.json @@ -0,0 +1,39 @@ +{ + "id" : "bd973d23-2b00-4141-a7a4-4588bb926e33", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CswKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktZDE1NTA3OS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKOCQomCg9icmFpbnRydXN0LWphdmESEzAuMi45LWQxNTUwNzktRElSVFkS4wgKEPtE7y061Gg9LHFA4nT72tQSCMnlnSace5+aKghNZXNzYWdlczABOciUALbEypkYQd7K4+rEypkYSlcKFWJyYWludHJ1c3QuaW5wdXRfanNvbhI+CjxbeyJjb250ZW50IjoiV2hhdCBpcyB0aGUgY2FwaXRhbCBvZiBGcmFuY2U/Iiwicm9sZSI6InVzZXIifV1KcAoSYnJhaW50cnVzdC5tZXRyaWNzEloKWHsiY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOSwidGltZV90b19maXJzdF90b2tlbiI6MC4wMDQ1NTQ3NX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9SswEChZicmFpbnRydXN0Lm91dHB1dF9qc29uErEECq4EeyJpZCI6Im1zZ18wMUhtYmFicmRGeTRCVjFManNhUUh2THoiLCJjb250ZW50IjpbeyJjaXRhdGlvbnMiOm51bGwsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIiwidHlwZSI6InRleHQiLCJ2YWxpZCI6dHJ1ZX1dLCJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3Iiwicm9sZSI6ImFzc2lzdGFudCIsInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ0eXBlIjoibWVzc2FnZSIsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsInZhbGlkIjp0cnVlfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5wdXRfdG9rZW5zIjoxOSwib3V0cHV0X3Rva2VucyI6MTAsInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJ2YWxpZCI6dHJ1ZSwiaW5mZXJlbmNlX2dlbyI6Im5vdF9hdmFpbGFibGUifSwidmFsaWQiOnRydWV9SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifXoAhQEBAQAA" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 00:34:35 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "6f3a5bde-7543-4403-b150-d286a7d87585", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a8cf9b0000000033b9572ca98c23f9", + "x-amz-apigw-id" : "ZuVgYFwAIAMETAA=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a8cf9b-63f40fdc0721cf3014773538;Parent=5ba2df3285a845d7;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 4c9457912580c6114eec78b8fa604a20.cloudfront.net (CloudFront), 1.1 77f3c89ffd619275648d49ad13868570.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "9CJexppONjPeRUxkB-EUANv8Ysxv42V7L93kTLh-HqfGldcFGHkVtg==" + } + }, + "uuid" : "bd973d23-2b00-4141-a7a4-4588bb926e33", + "persistent" : true, + "insertionIndex" : 109 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-bde0a1f8-cf6f-4ce5-b0bc-e3554b7da133.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-bde0a1f8-cf6f-4ce5-b0bc-e3554b7da133.json new file mode 100644 index 00000000..2542c558 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-bde0a1f8-cf6f-4ce5-b0bc-e3554b7da133.json @@ -0,0 +1,39 @@ +{ + "id" : "bde0a1f8-cf6f-4ce5-b0bc-e3554b7da133", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "Ct0HCrIBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAoiCg9zZXJ2aWNlLnZlcnNpb24SDwoNMC4yLjktYmEzOWYyYQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKlBgogCg9icmFpbnRydXN0LWphdmESDTAuMi45LWJhMzlmMmESgAYKEILpmfgzC5KZWRdDQq7xjtISCNi5akNM6ZYqKg9DaGF0IENvbXBsZXRpb24wATk4sy6V67yZGEELoOjB67yZGEqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQiLCJyb2xlIjoic3lzdGVtIn0seyJjb250ZW50IjoiV2hhdCBpcyB0aGUgY2FwaXRhbCBvZiBGcmFuY2U/Iiwicm9sZSI6InVzZXIifV1KTgoSYnJhaW50cnVzdC5tZXRyaWNzEjgKNnsiY29tcGxldGlvbl90b2tlbnMiOjcsInByb21wdF90b2tlbnMiOjIzLCJ0b2tlbnMiOjMwfUoyChFicmFpbnRydXN0LnBhcmVudBIdChtwcm9qZWN0X25hbWU6amF2YS11bml0LXRlc3RKLgoaYnJhaW50cnVzdC5zcGFuX2F0dHJpYnV0ZXMSEAoOeyJ0eXBlIjoibGxtIn1KvQEKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SogEKnwFbeyJpbmRleCI6MCwibWVzc2FnZSI6eyJyb2xlIjoiYXNzaXN0YW50IiwiY29udGVudCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJyZWZ1c2FsIjpudWxsLCJhbm5vdGF0aW9ucyI6W119LCJsb2dwcm9icyI6bnVsbCwiZmluaXNoX3JlYXNvbiI6InN0b3AifV1KrAEKE2JyYWludHJ1c3QubWV0YWRhdGESlAEKkQF7InByb3ZpZGVyIjoib3BlbmFpIiwicmVxdWVzdF9wYXRoIjoiY2hhdC9jb21wbGV0aW9ucyIsIm1vZGVsIjoiZ3B0LTRvLW1pbmkiLCJyZXF1ZXN0X2Jhc2VfdXJpIjoiaHR0cDovL2xvY2FsaG9zdDo2NTIzOSIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SFO53-P8", "SFO53-P1" ], + "Date" : "Wed, 04 Mar 2026 20:20:49 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "eb9e8db2-5f86-40a0-be25-1ec6b4354154", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a89421000000007e5232010d6258cc", + "x-amz-apigw-id" : "ZtwVPGITIAMEffw=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a89421-2315c725487c03a4030c3c1c;Parent=12e6bbdf29b316fd;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 dcd6c7d5f9e83c64e0ef0f23f0704dfa.cloudfront.net (CloudFront), 1.1 6354bde44a975facce9c0ed03828827e.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "M5zv-spKrR2td1X2fnvJ4tkiaGCaq2_2YhL5O1ynFeBCczws8bvN-g==" + } + }, + "uuid" : "bde0a1f8-cf6f-4ce5-b0bc-e3554b7da133", + "persistent" : true, + "insertionIndex" : 108 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-c0c2b527-4b6d-44b5-89ee-5fdd93190269.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-c0c2b527-4b6d-44b5-89ee-5fdd93190269.json new file mode 100644 index 00000000..43b1ca34 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-c0c2b527-4b6d-44b5-89ee-5fdd93190269.json @@ -0,0 +1,39 @@ +{ + "id" : "c0c2b527-4b6d-44b5-89ee-5fdd93190269", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CukHCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktYjZlMzkwZC1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKrBgomCg9icmFpbnRydXN0LWphdmESEzAuMi45LWI2ZTM5MGQtRElSVFkSgAYKEM45MukNtVt8TKxg+aTGAqwSCCbpx5p09bexKg9DaGF0IENvbXBsZXRpb24wATnAtaqLFgOaGEGScEO8FgOaGEqsAQoTYnJhaW50cnVzdC5tZXRhZGF0YRKUAQqRAXsicHJvdmlkZXIiOiJvcGVuYWkiLCJyZXF1ZXN0X3BhdGgiOiJjaGF0L2NvbXBsZXRpb25zIiwibW9kZWwiOiJncHQtNG8tbWluaSIsInJlcXVlc3RfYmFzZV91cmkiOiJodHRwOi8vbG9jYWxob3N0OjQ5ODQ3IiwicmVxdWVzdF9tZXRob2QiOiJQT1NUIn1KvQEKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SogEKnwFbeyJpbmRleCI6MCwibWVzc2FnZSI6eyJyb2xlIjoiYXNzaXN0YW50IiwiY29udGVudCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJyZWZ1c2FsIjpudWxsLCJhbm5vdGF0aW9ucyI6W119LCJsb2dwcm9icyI6bnVsbCwiZmluaXNoX3JlYXNvbiI6InN0b3AifV1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCIsInJvbGUiOiJzeXN0ZW0ifSx7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9XUpOChJicmFpbnRydXN0Lm1ldHJpY3MSOAo2eyJjb21wbGV0aW9uX3Rva2VucyI6NywicHJvbXB0X3Rva2VucyI6MjMsInRva2VucyI6MzB9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 17:46:40 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "bbdfd22a-badf-412b-bdba-17faa2aa3694", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9c17f000000007120616032c5c75c", + "x-amz-apigw-id" : "ZwssAEjhIAMEJvA=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9c17f-4d1f4b676805e5493f17d6aa;Parent=2b6939c2275b6696;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 c9f68a0c96944962731456040c591f26.cloudfront.net (CloudFront), 1.1 77f3c89ffd619275648d49ad13868570.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "ZGn-67jv0NAlFbM1CAm628WVe0hkgX6XxinyB4rqEt4yuGtTJ4BRJw==" + } + }, + "uuid" : "c0c2b527-4b6d-44b5-89ee-5fdd93190269", + "persistent" : true, + "insertionIndex" : 116 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-c38f590a-f707-48a0-84bc-d8a027e63d93.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-c38f590a-f707-48a0-84bc-d8a027e63d93.json new file mode 100644 index 00000000..9fe05780 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-c38f590a-f707-48a0-84bc-d8a027e63d93.json @@ -0,0 +1,39 @@ +{ + "id" : "c38f590a-f707-48a0-84bc-d8a027e63d93", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpsKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNjllMjNiYi1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLdCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTY5ZTIzYmItRElSVFkSsggKEPS/vL4Ugf6/5Hbs8hjB6SoSCJcZCZ/6iL1/KhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5EPawtp4EmhhBKsaT3p4EmhhK8AMKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24S1QMK0gN7Im1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJpZCI6Im1zZ18wMTU5WnRSSzJHTnVZWHk2Tm8xUWY4dkwiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEwLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifUqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9LHsicm9sZSI6InN5c3RlbSIsImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:14:44 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "438b5505-42c2-4c65-a567-15959a935af9", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9c81300000000364ebe0a59e9e13f", + "x-amz-apigw-id" : "ZwwzKHqqIAMEEDA=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9c813-0f6cafde63c87784033e942f;Parent=5e94fc9ff59b2747;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 2d0eb1433209b25c3712ac0793d56bc0.cloudfront.net (CloudFront), 1.1 e6b2537b87653726af8a79e6da505188.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "yxEwthih7CYbOirSNFKdwrxT26MLf9v-ljU9iWpMZsgIMOqq8paFJw==" + } + }, + "uuid" : "c38f590a-f707-48a0-84bc-d8a027e63d93", + "persistent" : true, + "insertionIndex" : 118 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d01e12fe-bf47-4ac6-b850-2731129652ce.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d01e12fe-bf47-4ac6-b850-2731129652ce.json new file mode 100644 index 00000000..d6e8fbad --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d01e12fe-bf47-4ac6-b850-2731129652ce.json @@ -0,0 +1,39 @@ +{ + "id" : "d01e12fe-bf47-4ac6-b850-2731129652ce", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CswKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktZDE1NTA3OS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKOCQomCg9icmFpbnRydXN0LWphdmESEzAuMi45LWQxNTUwNzktRElSVFkS4wgKEAbeTmAYIAxmV66PMo1Tiw4SCFTHDQzT6jK/KghNZXNzYWdlczABOaCPDpjFypkYQQWpYbrFypkYSlcKFWJyYWludHJ1c3QuaW5wdXRfanNvbhI+CjxbeyJjb250ZW50IjoiV2hhdCBpcyB0aGUgY2FwaXRhbCBvZiBGcmFuY2U/Iiwicm9sZSI6InVzZXIifV1KcAoSYnJhaW50cnVzdC5tZXRyaWNzEloKWHsiY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOSwidGltZV90b19maXJzdF90b2tlbiI6MC4wMDE1MDI3NX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9SswEChZicmFpbnRydXN0Lm91dHB1dF9qc29uErEECq4EeyJpZCI6Im1zZ18wMThLTlFNZWNWQllITlZmTDhwQVU4WXYiLCJjb250ZW50IjpbeyJjaXRhdGlvbnMiOm51bGwsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIiwidHlwZSI6InRleHQiLCJ2YWxpZCI6dHJ1ZX1dLCJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3Iiwicm9sZSI6ImFzc2lzdGFudCIsInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ0eXBlIjoibWVzc2FnZSIsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsInZhbGlkIjp0cnVlfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5wdXRfdG9rZW5zIjoxOSwib3V0cHV0X3Rva2VucyI6MTAsInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJ2YWxpZCI6dHJ1ZSwiaW5mZXJlbmNlX2dlbyI6Im5vdF9hdmFpbGFibGUifSwidmFsaWQiOnRydWV9SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifXoAhQEBAQAA" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 00:34:39 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "0df5b55b-adee-49fe-89cf-4e5b35ad4bb4", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a8cf9f000000001841bc8eb2cb0bc2", + "x-amz-apigw-id" : "ZuVg6HueoAMEf1A=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a8cf9f-0d1e615805cdc2153accf953;Parent=084bc5fd89584004;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 8e6c2cf5874f5e4093136cc3de4d856a.cloudfront.net (CloudFront), 1.1 d525041695bdb6325f78ebba5c11b8a2.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "maqeaKn1TWaa79JXufy6DEMc0PiTwgf7ACJ3iN_2SWud7JRpdY5r5Q==" + } + }, + "uuid" : "d01e12fe-bf47-4ac6-b850-2731129652ce", + "persistent" : true, + "insertionIndex" : 112 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d2d1f78a-060f-4e26-9f02-5b2aa209cbab.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d2d1f78a-060f-4e26-9f02-5b2aa209cbab.json new file mode 100644 index 00000000..bd0e8c8f --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d2d1f78a-060f-4e26-9f02-5b2aa209cbab.json @@ -0,0 +1,39 @@ +{ + "id" : "d2d1f78a-060f-4e26-9f02-5b2aa209cbab", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpsKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLdCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsggKEEcoG0K5Z2kfSZMnjus6mQASCBAJoyhzk+vwKhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5qNoyQuEFmhhB9k/ubOEFmhhK8AMKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24S1QMK0gN7Im1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJpZCI6Im1zZ18wMVhnd1NHOW5GMmhvSm93R1MyckRzdUIiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEwLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifUqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9LHsicm9sZSI6InN5c3RlbSIsImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:37:49 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "c3237363-cbde-4f3f-a99d-6f744cac2832", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9cd7d00000000436bf3a6806734e4", + "x-amz-apigw-id" : "Zw0LpHrJoAMElgQ=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9cd7d-2a0b20fd4d77876919c49304;Parent=39b8a2ef7b2b4844;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 db84db36e16ca0c80b0992006d731900.cloudfront.net (CloudFront), 1.1 d525041695bdb6325f78ebba5c11b8a2.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "DSlAZwpjdVY7_VwpQue0GfSuxiWM_99ZDnbOd89-rvK4Ay0i9g7sRw==" + } + }, + "uuid" : "d2d1f78a-060f-4e26-9f02-5b2aa209cbab", + "persistent" : true, + "insertionIndex" : 130 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d446c200-01f2-4468-a9b9-bbf2ac66713b.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d446c200-01f2-4468-a9b9-bbf2ac66713b.json new file mode 100644 index 00000000..6ac709a0 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d446c200-01f2-4468-a9b9-bbf2ac66713b.json @@ -0,0 +1,39 @@ +{ + "id" : "d446c200-01f2-4468-a9b9-bbf2ac66713b", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpkLCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNjllMjNiYi1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLbCQomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTY5ZTIzYmItRElSVFkSsAkKECMeTb5h9bhECA/uRGWNoYASCDYpIUjQ0hY+KhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5aAtLX54EmhhBbhDgnZ4EmhhKzAQKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SsQQKrgR7ImlkIjoibXNnXzAxNkJQS3NkQURxQWNoMlFOM0xyM2lMOCIsImNvbnRlbnQiOlt7ImNpdGF0aW9ucyI6bnVsbCwidGV4dCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJ0eXBlIjoidGV4dCIsInZhbGlkIjp0cnVlfV0sIm1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInR5cGUiOiJtZXNzYWdlIiwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwidmFsaWQiOnRydWV9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnMiOjE5LCJvdXRwdXRfdG9rZW5zIjoxMCwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsInZhbGlkIjp0cnVlLCJpbmZlcmVuY2VfZ2VvIjoibm90X2F2YWlsYWJsZSJ9LCJ2YWxpZCI6dHJ1ZX1KoAEKE2JyYWludHJ1c3QubWV0YWRhdGESiAEKhQF7InByb3ZpZGVyIjoiYW50aHJvcGljIiwicmVxdWVzdF9wYXRoIjoidjEvbWVzc2FnZXMiLCJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3IiwicmVxdWVzdF9iYXNlX3VyaSI6IiIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyIsInJvbGUiOiJ1c2VyIn0seyJyb2xlIjoic3lzdGVtIiwiY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCJ9XUpxChJicmFpbnRydXN0Lm1ldHJpY3MSWwpZeyJjb21wbGV0aW9uX3Rva2VucyI6MTAsInByb21wdF90b2tlbnMiOjE5LCJ0b2tlbnMiOjI5LCJ0aW1lX3RvX2ZpcnN0X3Rva2VuIjowLjAwNDU4NzY2Nn1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:14:42 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "ad609e53-9fca-4316-80fa-8c3e385488d4", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9c812000000001f90aa2dad664777", + "x-amz-apigw-id" : "Zwwy_FAJoAMEjkQ=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9c812-100e21bd74ea922b157f8436;Parent=6a4927ba76d36e8f;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 2c24d855455b80190edd9e2dcdca3ee8.cloudfront.net (CloudFront), 1.1 fbb003dfc0617e3e058e3dac791dfd5a.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "L-Uhvas0ol3jZJeaXiq9ZFFwtwiu2XRxMAXfp4bNAJml9OFH8f_BnA==" + } + }, + "uuid" : "d446c200-01f2-4468-a9b9-bbf2ac66713b", + "persistent" : true, + "insertionIndex" : 117 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d6ffcad2-4e05-4263-8071-7a8afca93ed7.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d6ffcad2-4e05-4263-8071-7a8afca93ed7.json new file mode 100644 index 00000000..ea677883 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-d6ffcad2-4e05-4263-8071-7a8afca93ed7.json @@ -0,0 +1,39 @@ +{ + "id" : "d6ffcad2-4e05-4263-8071-7a8afca93ed7", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CukHCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKrBgomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSgAYKEImdfgSCBNd+2c+P1s+NLW8SCHU9TWkQcxnYKg9DaGF0IENvbXBsZXRpb24wATmAMI5pwgWaGEFB6QSVwgWaGEq9AQoWYnJhaW50cnVzdC5vdXRwdXRfanNvbhKiAQqfAVt7ImluZGV4IjowLCJtZXNzYWdlIjp7InJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjoiVGhlIGNhcGl0YWwgb2YgRnJhbmNlIGlzIFBhcmlzLiIsInJlZnVzYWwiOm51bGwsImFubm90YXRpb25zIjpbXX0sImxvZ3Byb2JzIjpudWxsLCJmaW5pc2hfcmVhc29uIjoic3RvcCJ9XUqsAQoTYnJhaW50cnVzdC5tZXRhZGF0YRKUAQqRAXsicHJvdmlkZXIiOiJvcGVuYWkiLCJyZXF1ZXN0X3BhdGgiOiJjaGF0L2NvbXBsZXRpb25zIiwibW9kZWwiOiJncHQtNG8tbWluaSIsInJlcXVlc3RfYmFzZV91cmkiOiJodHRwOi8vbG9jYWxob3N0OjUxMDk3IiwicmVxdWVzdF9tZXRob2QiOiJQT1NUIn1KkQEKFWJyYWludHJ1c3QuaW5wdXRfanNvbhJ4CnZbeyJjb250ZW50IjoiWW91IGFyZSBhIGhlbHBmdWwgYXNzaXN0YW50Iiwicm9sZSI6InN5c3RlbSJ9LHsiY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyIsInJvbGUiOiJ1c2VyIn1dSk4KEmJyYWludHJ1c3QubWV0cmljcxI4CjZ7ImNvbXBsZXRpb25fdG9rZW5zIjo3LCJwcm9tcHRfdG9rZW5zIjoyMywidG9rZW5zIjozMH1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:35:37 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "b608a1f3-6632-4bc7-8507-662ab7c591c0", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ccf8000000006e288884ac2a4f50", + "x-amz-apigw-id" : "Zwz27G-LoAMEc8g=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ccf8-40ee75683ef125f215934dfe;Parent=1fea54959cc61012;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 2d0eb1433209b25c3712ac0793d56bc0.cloudfront.net (CloudFront), 1.1 65f2e9f7f1475de54aa452d3ceb9bcf6.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "7cRpCBe5X9odkrUKhL8W23OQvGtm13THyhDawbNfptz4kt2I5nfQdg==" + } + }, + "uuid" : "d6ffcad2-4e05-4263-8071-7a8afca93ed7", + "persistent" : true, + "insertionIndex" : 123 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-de8bbca7-225e-419a-8ee5-4763f8cc58e6.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-de8bbca7-225e-419a-8ee5-4763f8cc58e6.json new file mode 100644 index 00000000..550fbc67 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-de8bbca7-225e-419a-8ee5-4763f8cc58e6.json @@ -0,0 +1,39 @@ +{ + "id" : "de8bbca7-225e-419a-8ee5-4763f8cc58e6", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CukHCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKrBgomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSgAYKEL2hJJs6pwoVVj49ONxM32ISCOvRZs5MpX8WKg9DaGF0IENvbXBsZXRpb24wATlo5SdQwwWaGEGkR5N2wwWaGEq9AQoWYnJhaW50cnVzdC5vdXRwdXRfanNvbhKiAQqfAVt7ImluZGV4IjowLCJtZXNzYWdlIjp7InJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjoiVGhlIGNhcGl0YWwgb2YgRnJhbmNlIGlzIFBhcmlzLiIsInJlZnVzYWwiOm51bGwsImFubm90YXRpb25zIjpbXX0sImxvZ3Byb2JzIjpudWxsLCJmaW5pc2hfcmVhc29uIjoic3RvcCJ9XUqsAQoTYnJhaW50cnVzdC5tZXRhZGF0YRKUAQqRAXsicHJvdmlkZXIiOiJvcGVuYWkiLCJyZXF1ZXN0X3BhdGgiOiJjaGF0L2NvbXBsZXRpb25zIiwibW9kZWwiOiJncHQtNG8tbWluaSIsInJlcXVlc3RfYmFzZV91cmkiOiJodHRwOi8vbG9jYWxob3N0OjUxMDk3IiwicmVxdWVzdF9tZXRob2QiOiJQT1NUIn1KkQEKFWJyYWludHJ1c3QuaW5wdXRfanNvbhJ4CnZbeyJjb250ZW50IjoiWW91IGFyZSBhIGhlbHBmdWwgYXNzaXN0YW50Iiwicm9sZSI6InN5c3RlbSJ9LHsiY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyIsInJvbGUiOiJ1c2VyIn1dSk4KEmJyYWludHJ1c3QubWV0cmljcxI4CjZ7ImNvbXBsZXRpb25fdG9rZW5zIjo3LCJwcm9tcHRfdG9rZW5zIjoyMywidG9rZW5zIjozMH1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:35:40 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "535e50a8-d23a-41b2-8d2b-de4d38fa43a6", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ccfc0000000045b4ca09ad4a16b0", + "x-amz-apigw-id" : "Zwz3hGxIIAMEG9Q=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ccfc-5f2cbd231eb9db4a1042df46;Parent=6ab4719f19ce09c3;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 d5e9313fa5148ebdba4664d3e2a90f58.cloudfront.net (CloudFront), 1.1 77f3c89ffd619275648d49ad13868570.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "uzFgCKxynLfSL_PbvNJVTgkYPFLS7u1EvbnCW93vAWMMbgQJ4lGcrg==" + } + }, + "uuid" : "de8bbca7-225e-419a-8ee5-4763f8cc58e6", + "persistent" : true, + "insertionIndex" : 126 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-e16cbe95-b83f-449c-882b-b4631a32812a.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-e16cbe95-b83f-449c-882b-b4631a32812a.json new file mode 100644 index 00000000..ebaff861 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-e16cbe95-b83f-449c-882b-b4631a32812a.json @@ -0,0 +1,39 @@ +{ + "id" : "e16cbe95-b83f-449c-882b-b4631a32812a", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpkLCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLbCQomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsAkKEGUNE/nnRNJJOXiEGdSB7C0SCBnVdBw8NC1GKhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5CNUscRIGmhhBJul3sBIGmhhKzAQKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SsQQKrgR7ImlkIjoibXNnXzAxMWNVOXRWRmhlSFQ3enM3RTVYQjJhZCIsImNvbnRlbnQiOlt7ImNpdGF0aW9ucyI6bnVsbCwidGV4dCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJ0eXBlIjoidGV4dCIsInZhbGlkIjp0cnVlfV0sIm1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInR5cGUiOiJtZXNzYWdlIiwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwidmFsaWQiOnRydWV9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnMiOjE5LCJvdXRwdXRfdG9rZW5zIjoxMCwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsInZhbGlkIjp0cnVlLCJpbmZlcmVuY2VfZ2VvIjoibm90X2F2YWlsYWJsZSJ9LCJ2YWxpZCI6dHJ1ZX1KoAEKE2JyYWludHJ1c3QubWV0YWRhdGESiAEKhQF7InByb3ZpZGVyIjoiYW50aHJvcGljIiwicmVxdWVzdF9wYXRoIjoidjEvbWVzc2FnZXMiLCJtb2RlbCI6ImNsYXVkZS0zLWhhaWt1LTIwMjQwMzA3IiwicmVxdWVzdF9iYXNlX3VyaSI6IiIsInJlcXVlc3RfbWV0aG9kIjoiUE9TVCJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IldoYXQgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlPyIsInJvbGUiOiJ1c2VyIn0seyJyb2xlIjoic3lzdGVtIiwiY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCJ9XUpxChJicmFpbnRydXN0Lm1ldHJpY3MSWwpZeyJjb21wbGV0aW9uX3Rva2VucyI6MTAsInByb21wdF90b2tlbnMiOjE5LCJ0b2tlbnMiOjI5LCJ0aW1lX3RvX2ZpcnN0X3Rva2VuIjowLjAwMjc4NTU0Mn1KLgoaYnJhaW50cnVzdC5zcGFuX2F0dHJpYnV0ZXMSEAoOeyJ0eXBlIjoibGxtIn1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:41:21 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "b7675956-7c13-4199-82e9-e32b41288173", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ce50000000001ccec3e8ebd602a3", + "x-amz-apigw-id" : "Zw0ssEPSoAMEWWA=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ce50-00b27198553906ec3b973685;Parent=216992702dea2c35;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 b7d7903ada432685f0e90f0ca261d864.cloudfront.net (CloudFront), 1.1 87247d9a9b2f9e51b0c72b364948aefa.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "BVzRrOWI6LAfKeK2xP1WfShgUSZ54Z8yS94zow3X_GfkGnsqRGDqMA==" + } + }, + "uuid" : "e16cbe95-b83f-449c-882b-b4631a32812a", + "persistent" : true, + "insertionIndex" : 137 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-ee4bc331-f9de-49c7-900b-39803a832a46.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-ee4bc331-f9de-49c7-900b-39803a832a46.json new file mode 100644 index 00000000..66628e48 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-ee4bc331-f9de-49c7-900b-39803a832a46.json @@ -0,0 +1,39 @@ +{ + "id" : "ee4bc331-f9de-49c7-900b-39803a832a46", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CpsKCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktNmJjYTExMS1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBLdCAomCg9icmFpbnRydXN0LWphdmESEzAuMi45LTZiY2ExMTEtRElSVFkSsggKEAND32wbF61nAQuJkDT/uocSCK7hEKJXTk5EKhlhbnRocm9waWMubWVzc2FnZXMuY3JlYXRlMAE5uPt0IBIGmhhBk1cETRIGmhhK8AMKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24S1QMK0gN7Im1vZGVsIjoiY2xhdWRlLTMtaGFpa3UtMjAyNDAzMDciLCJpZCI6Im1zZ18wMVRMUjduVm5aU1N1azdFVjV6d1VNTjIiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgY2FwaXRhbCBvZiBGcmFuY2UgaXMgUGFyaXMuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEwLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19SqABChNicmFpbnRydXN0Lm1ldGFkYXRhEogBCoUBeyJwcm92aWRlciI6ImFudGhyb3BpYyIsInJlcXVlc3RfcGF0aCI6InYxL21lc3NhZ2VzIiwibW9kZWwiOiJjbGF1ZGUtMy1oYWlrdS0yMDI0MDMwNyIsInJlcXVlc3RfYmFzZV91cmkiOiIiLCJyZXF1ZXN0X21ldGhvZCI6IlBPU1QifUqRAQoVYnJhaW50cnVzdC5pbnB1dF9qc29uEngKdlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9LHsicm9sZSI6InN5c3RlbSIsImNvbnRlbnQiOiJZb3UgYXJlIGEgaGVscGZ1bCBhc3Npc3RhbnQifV1KTwoSYnJhaW50cnVzdC5tZXRyaWNzEjkKN3siY29tcGxldGlvbl90b2tlbnMiOjEwLCJwcm9tcHRfdG9rZW5zIjoxOSwidG9rZW5zIjoyOX1KLgoaYnJhaW50cnVzdC5zcGFuX2F0dHJpYnV0ZXMSEAoOeyJ0eXBlIjoibGxtIn1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 18:41:19 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "1009071d-319d-459d-8616-36306e782353", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9ce4f0000000075f8648aa69c3a0c", + "x-amz-apigw-id" : "Zw0sbH-CoAMEqJw=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9ce4f-0e74a9135238af86702f3fc5;Parent=5bc27fa2316b8ad5;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 9257f9c4051fe8bd6cc4a09855b66350.cloudfront.net (CloudFront), 1.1 4ac8d091dce10e726cfc5404bfed72b8.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "2NG1UC7eStl4qCAmpqdZUxgxwtmYbNCgxx7vBAqSQ32BmXjpCZ93Qg==" + } + }, + "uuid" : "ee4bc331-f9de-49c7-900b-39803a832a46", + "persistent" : true, + "insertionIndex" : 136 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-ee8f7fc6-a6b5-42f0-b8c3-be57dc4ab97e.json b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-ee8f7fc6-a6b5-42f0-b8c3-be57dc4ab97e.json new file mode 100644 index 00000000..a58668d2 --- /dev/null +++ b/src/test/resources/cassettes/braintrust/mappings/otel_v1_traces-ee8f7fc6-a6b5-42f0-b8c3-be57dc4ab97e.json @@ -0,0 +1,39 @@ +{ + "id" : "ee8f7fc6-a6b5-42f0-b8c3-be57dc4ab97e", + "name" : "otel_v1_traces", + "request" : { + "url" : "/otel/v1/traces", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/x-protobuf" + } + }, + "bodyPatterns" : [ { + "binaryEqualTo" : "CukHCrgBCiAKDHNlcnZpY2UubmFtZRIQCg5icmFpbnRydXN0LWFwcAooCg9zZXJ2aWNlLnZlcnNpb24SFQoTMC4yLjktYjZlMzkwZC1ESVJUWQogChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEgYKBGphdmEKJQoSdGVsZW1ldHJ5LnNkay5uYW1lEg8KDW9wZW50ZWxlbWV0cnkKIQoVdGVsZW1ldHJ5LnNkay52ZXJzaW9uEggKBjEuNTkuMBKrBgomCg9icmFpbnRydXN0LWphdmESEzAuMi45LWI2ZTM5MGQtRElSVFkSgAYKEM4Ita0Nf5ucJbYfqlnn0jUSCFMR3tRntJLiKg9DaGF0IENvbXBsZXRpb24wATngg57cFQOaGEHfhpUEFgOaGEqsAQoTYnJhaW50cnVzdC5tZXRhZGF0YRKUAQqRAXsicHJvdmlkZXIiOiJvcGVuYWkiLCJyZXF1ZXN0X3BhdGgiOiJjaGF0L2NvbXBsZXRpb25zIiwibW9kZWwiOiJncHQtNG8tbWluaSIsInJlcXVlc3RfYmFzZV91cmkiOiJodHRwOi8vbG9jYWxob3N0OjQ5ODQ3IiwicmVxdWVzdF9tZXRob2QiOiJQT1NUIn1KvQEKFmJyYWludHJ1c3Qub3V0cHV0X2pzb24SogEKnwFbeyJpbmRleCI6MCwibWVzc2FnZSI6eyJyb2xlIjoiYXNzaXN0YW50IiwiY29udGVudCI6IlRoZSBjYXBpdGFsIG9mIEZyYW5jZSBpcyBQYXJpcy4iLCJyZWZ1c2FsIjpudWxsLCJhbm5vdGF0aW9ucyI6W119LCJsb2dwcm9icyI6bnVsbCwiZmluaXNoX3JlYXNvbiI6InN0b3AifV1KMgoRYnJhaW50cnVzdC5wYXJlbnQSHQobcHJvamVjdF9uYW1lOmphdmEtdW5pdC10ZXN0Si4KGmJyYWludHJ1c3Quc3Bhbl9hdHRyaWJ1dGVzEhAKDnsidHlwZSI6ImxsbSJ9SpEBChVicmFpbnRydXN0LmlucHV0X2pzb24SeAp2W3siY29udGVudCI6IllvdSBhcmUgYSBoZWxwZnVsIGFzc2lzdGFudCIsInJvbGUiOiJzeXN0ZW0ifSx7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSBjYXBpdGFsIG9mIEZyYW5jZT8iLCJyb2xlIjoidXNlciJ9XUpOChJicmFpbnRydXN0Lm1ldHJpY3MSOAo2eyJjb21wbGV0aW9uX3Rva2VucyI6NywicHJvbXB0X3Rva2VucyI6MjMsInRva2VucyI6MzB9egCFAQEBAAA=" + } ] + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/x-protobuf", + "X-Amz-Cf-Pop" : [ "SEA900-P1", "SEA900-P10" ], + "Date" : "Thu, 05 Mar 2026 17:46:36 GMT", + "access-control-allow-credentials" : "true", + "x-amzn-RequestId" : "6a3841ea-6dab-43e1-b232-0c0c0d506f73", + "x-amzn-Remapped-content-length" : "0", + "x-bt-internal-trace-id" : "69a9c17c000000003d14782f0ac392ea", + "x-amz-apigw-id" : "ZwsriFI7IAMEjkQ=", + "vary" : "Origin", + "etag" : "W/\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"", + "access-control-expose-headers" : "x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms", + "X-Amzn-Trace-Id" : "Root=1-69a9c17c-0d640e5379c0f677088308a4;Parent=09f94b5305451a5e;Sampled=0;Lineage=1:24be3d11:0", + "Via" : "1.1 a642518ef4d5fb78c3190de112922a38.cloudfront.net (CloudFront), 1.1 77f3c89ffd619275648d49ad13868570.cloudfront.net (CloudFront)", + "X-Cache" : "Miss from cloudfront", + "X-Amz-Cf-Id" : "b9wF-nHDQ06Gde2bsu8hz9pVNjX4Ox0fc4y7hkMwgSXNgnvIHuu7QQ==" + } + }, + "uuid" : "ee8f7fc6-a6b5-42f0-b8c3-be57dc4ab97e", + "persistent" : true, + "insertionIndex" : 114 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/__files/chat_completions-1b32d91a-cccf-4083-89d8-c6704238514d.json b/src/test/resources/cassettes/openai/__files/chat_completions-1b32d91a-cccf-4083-89d8-c6704238514d.json new file mode 100644 index 00000000..b53cf402 --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/chat_completions-1b32d91a-cccf-4083-89d8-c6704238514d.json @@ -0,0 +1,36 @@ +{ + "id": "chatcmpl-DFn04K70Wc2N4emgclLjhKrHId53W", + "object": "chat.completion", + "created": 1772655648, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The capital of France is Paris.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 23, + "completion_tokens": 7, + "total_tokens": 30, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_373a14eb6f" +} diff --git a/src/test/resources/cassettes/openai/__files/chat_completions-692d5db8-32fd-4a4a-adde-68831cf174bf.json b/src/test/resources/cassettes/openai/__files/chat_completions-692d5db8-32fd-4a4a-adde-68831cf174bf.json new file mode 100644 index 00000000..9ba616c7 --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/chat_completions-692d5db8-32fd-4a4a-adde-68831cf174bf.json @@ -0,0 +1,36 @@ +{ + "id": "chatcmpl-DG7poskXbjWh4AfJr41noETfYaQWV", + "object": "chat.completion", + "created": 1772735736, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The capital of France is Paris.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 23, + "completion_tokens": 7, + "total_tokens": 30, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_373a14eb6f" +} diff --git a/src/test/resources/cassettes/openai/__files/chat_completions-751a7200-62cc-4c4b-8324-00024a9462bf.txt b/src/test/resources/cassettes/openai/__files/chat_completions-751a7200-62cc-4c4b-8324-00024a9462bf.txt new file mode 100644 index 00000000..1e5c7634 --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/chat_completions-751a7200-62cc-4c4b-8324-00024a9462bf.txt @@ -0,0 +1,22 @@ +data: {"id":"chatcmpl-DG7pqqsKv0NEV256aO26UdvKmKajT","object":"chat.completion.chunk","created":1772735738,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WsYpYuOPq"} + +data: {"id":"chatcmpl-DG7pqqsKv0NEV256aO26UdvKmKajT","object":"chat.completion.chunk","created":1772735738,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"K6LcK9m7"} + +data: {"id":"chatcmpl-DG7pqqsKv0NEV256aO26UdvKmKajT","object":"chat.completion.chunk","created":1772735738,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" capital"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xk4"} + +data: {"id":"chatcmpl-DG7pqqsKv0NEV256aO26UdvKmKajT","object":"chat.completion.chunk","created":1772735738,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"8GO7Zp8X"} + +data: {"id":"chatcmpl-DG7pqqsKv0NEV256aO26UdvKmKajT","object":"chat.completion.chunk","created":1772735738,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" France"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"lO3Q"} + +data: {"id":"chatcmpl-DG7pqqsKv0NEV256aO26UdvKmKajT","object":"chat.completion.chunk","created":1772735738,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RLgLuE76"} + +data: {"id":"chatcmpl-DG7pqqsKv0NEV256aO26UdvKmKajT","object":"chat.completion.chunk","created":1772735738,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" Paris"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"FtxdW"} + +data: {"id":"chatcmpl-DG7pqqsKv0NEV256aO26UdvKmKajT","object":"chat.completion.chunk","created":1772735738,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"BLPWIIBZ6O"} + +data: {"id":"chatcmpl-DG7pqqsKv0NEV256aO26UdvKmKajT","object":"chat.completion.chunk","created":1772735738,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"DTNO5"} + +data: {"id":"chatcmpl-DG7pqqsKv0NEV256aO26UdvKmKajT","object":"chat.completion.chunk","created":1772735738,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[],"usage":{"prompt_tokens":23,"completion_tokens":7,"total_tokens":30,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"7e6UOJ8rC4E"} + +data: [DONE] + diff --git a/src/test/resources/cassettes/openai/__files/chat_completions-8306fcb1-24bb-4510-8a32-dddb526d55e3.json b/src/test/resources/cassettes/openai/__files/chat_completions-8306fcb1-24bb-4510-8a32-dddb526d55e3.json new file mode 100644 index 00000000..4bf268d1 --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/chat_completions-8306fcb1-24bb-4510-8a32-dddb526d55e3.json @@ -0,0 +1,36 @@ +{ + "id": "chatcmpl-DG7prWVGCPVOYgEfpt0rBiuEWB8yl", + "object": "chat.completion", + "created": 1772735739, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The capital of France is Paris.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 23, + "completion_tokens": 7, + "total_tokens": 30, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_373a14eb6f" +} diff --git a/src/test/resources/cassettes/openai/__files/chat_completions-9e4757a5-01e8-45d7-a2ea-fce13f052e3f.json b/src/test/resources/cassettes/openai/__files/chat_completions-9e4757a5-01e8-45d7-a2ea-fce13f052e3f.json new file mode 100644 index 00000000..19e34d87 --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/chat_completions-9e4757a5-01e8-45d7-a2ea-fce13f052e3f.json @@ -0,0 +1,36 @@ +{ + "id": "chatcmpl-DG74Obaox9fWVr8dhxGjECoHuOIBk", + "object": "chat.completion", + "created": 1772732796, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The capital of France is Paris.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 23, + "completion_tokens": 7, + "total_tokens": 30, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_373a14eb6f" +} diff --git a/src/test/resources/cassettes/openai/__files/chat_completions-b01eaa78-92e7-4c32-9c5a-5d5c682c1eab.txt b/src/test/resources/cassettes/openai/__files/chat_completions-b01eaa78-92e7-4c32-9c5a-5d5c682c1eab.txt new file mode 100644 index 00000000..9c63354c --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/chat_completions-b01eaa78-92e7-4c32-9c5a-5d5c682c1eab.txt @@ -0,0 +1,22 @@ +data: {"id":"chatcmpl-DG7ppH9OCTGChYdWCP6xYrQRKjPMX","object":"chat.completion.chunk","created":1772735737,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"EKXH5Htd2"} + +data: {"id":"chatcmpl-DG7ppH9OCTGChYdWCP6xYrQRKjPMX","object":"chat.completion.chunk","created":1772735737,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"9tinMgFn"} + +data: {"id":"chatcmpl-DG7ppH9OCTGChYdWCP6xYrQRKjPMX","object":"chat.completion.chunk","created":1772735737,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" capital"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"pcs"} + +data: {"id":"chatcmpl-DG7ppH9OCTGChYdWCP6xYrQRKjPMX","object":"chat.completion.chunk","created":1772735737,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"MCXLWOa0"} + +data: {"id":"chatcmpl-DG7ppH9OCTGChYdWCP6xYrQRKjPMX","object":"chat.completion.chunk","created":1772735737,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" France"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5QjV"} + +data: {"id":"chatcmpl-DG7ppH9OCTGChYdWCP6xYrQRKjPMX","object":"chat.completion.chunk","created":1772735737,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GSpqntqG"} + +data: {"id":"chatcmpl-DG7ppH9OCTGChYdWCP6xYrQRKjPMX","object":"chat.completion.chunk","created":1772735737,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" Paris"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"oQDhe"} + +data: {"id":"chatcmpl-DG7ppH9OCTGChYdWCP6xYrQRKjPMX","object":"chat.completion.chunk","created":1772735737,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"FolAg0gArn"} + +data: {"id":"chatcmpl-DG7ppH9OCTGChYdWCP6xYrQRKjPMX","object":"chat.completion.chunk","created":1772735737,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"fKZG2"} + +data: {"id":"chatcmpl-DG7ppH9OCTGChYdWCP6xYrQRKjPMX","object":"chat.completion.chunk","created":1772735737,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[],"usage":{"prompt_tokens":23,"completion_tokens":7,"total_tokens":30,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"LwH5xWDncfN"} + +data: [DONE] + diff --git a/src/test/resources/cassettes/openai/__files/chat_completions-b70b2684-b6b0-42e3-8d18-ad0ff374e753.json b/src/test/resources/cassettes/openai/__files/chat_completions-b70b2684-b6b0-42e3-8d18-ad0ff374e753.json new file mode 100644 index 00000000..98c0321a --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/chat_completions-b70b2684-b6b0-42e3-8d18-ad0ff374e753.json @@ -0,0 +1,36 @@ +{ + "id": "chatcmpl-DG74QX3bDc8f7mN31mXJ5ZxLiVGhS", + "object": "chat.completion", + "created": 1772732798, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The capital of France is Paris.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 23, + "completion_tokens": 7, + "total_tokens": 30, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_373a14eb6f" +} diff --git a/src/test/resources/cassettes/openai/__files/chat_completions-e2bf36d0-37c4-4e63-9e8c-c155759089f5.txt b/src/test/resources/cassettes/openai/__files/chat_completions-e2bf36d0-37c4-4e63-9e8c-c155759089f5.txt new file mode 100644 index 00000000..e12dca79 --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/chat_completions-e2bf36d0-37c4-4e63-9e8c-c155759089f5.txt @@ -0,0 +1,22 @@ +data: {"id":"chatcmpl-DFn03RJ5bGDTzdXMxmnMoud5UcqpD","object":"chat.completion.chunk","created":1772655647,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"0XnZl0gLc"} + +data: {"id":"chatcmpl-DFn03RJ5bGDTzdXMxmnMoud5UcqpD","object":"chat.completion.chunk","created":1772655647,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"qWOaPRL2"} + +data: {"id":"chatcmpl-DFn03RJ5bGDTzdXMxmnMoud5UcqpD","object":"chat.completion.chunk","created":1772655647,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" capital"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"9kQ"} + +data: {"id":"chatcmpl-DFn03RJ5bGDTzdXMxmnMoud5UcqpD","object":"chat.completion.chunk","created":1772655647,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"aIPqdySE"} + +data: {"id":"chatcmpl-DFn03RJ5bGDTzdXMxmnMoud5UcqpD","object":"chat.completion.chunk","created":1772655647,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" France"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"bDQr"} + +data: {"id":"chatcmpl-DFn03RJ5bGDTzdXMxmnMoud5UcqpD","object":"chat.completion.chunk","created":1772655647,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5La1DLj6"} + +data: {"id":"chatcmpl-DFn03RJ5bGDTzdXMxmnMoud5UcqpD","object":"chat.completion.chunk","created":1772655647,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" Paris"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"zuim2"} + +data: {"id":"chatcmpl-DFn03RJ5bGDTzdXMxmnMoud5UcqpD","object":"chat.completion.chunk","created":1772655647,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"7hlAuDoMyK"} + +data: {"id":"chatcmpl-DFn03RJ5bGDTzdXMxmnMoud5UcqpD","object":"chat.completion.chunk","created":1772655647,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"bEhjE"} + +data: {"id":"chatcmpl-DFn03RJ5bGDTzdXMxmnMoud5UcqpD","object":"chat.completion.chunk","created":1772655647,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[],"usage":{"prompt_tokens":23,"completion_tokens":7,"total_tokens":30,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"BFaSUh7JsVh"} + +data: [DONE] + diff --git a/src/test/resources/cassettes/openai/__files/chat_completions-f19b40d4-4b5b-4d7b-accc-cc484a48e5b3.txt b/src/test/resources/cassettes/openai/__files/chat_completions-f19b40d4-4b5b-4d7b-accc-cc484a48e5b3.txt new file mode 100644 index 00000000..9dae5ac6 --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/chat_completions-f19b40d4-4b5b-4d7b-accc-cc484a48e5b3.txt @@ -0,0 +1,22 @@ +data: {"id":"chatcmpl-DG74PksDb68x2U3ZMMZERPaMXAt5L","object":"chat.completion.chunk","created":1772732797,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"4tWCZ4pfO"} + +data: {"id":"chatcmpl-DG74PksDb68x2U3ZMMZERPaMXAt5L","object":"chat.completion.chunk","created":1772732797,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"sN2pfa2H"} + +data: {"id":"chatcmpl-DG74PksDb68x2U3ZMMZERPaMXAt5L","object":"chat.completion.chunk","created":1772732797,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" capital"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TRX"} + +data: {"id":"chatcmpl-DG74PksDb68x2U3ZMMZERPaMXAt5L","object":"chat.completion.chunk","created":1772732797,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PvVuy2sA"} + +data: {"id":"chatcmpl-DG74PksDb68x2U3ZMMZERPaMXAt5L","object":"chat.completion.chunk","created":1772732797,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" France"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"zoVs"} + +data: {"id":"chatcmpl-DG74PksDb68x2U3ZMMZERPaMXAt5L","object":"chat.completion.chunk","created":1772732797,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"euxNnwP1"} + +data: {"id":"chatcmpl-DG74PksDb68x2U3ZMMZERPaMXAt5L","object":"chat.completion.chunk","created":1772732797,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":" Paris"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"T5xnJ"} + +data: {"id":"chatcmpl-DG74PksDb68x2U3ZMMZERPaMXAt5L","object":"chat.completion.chunk","created":1772732797,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"MHK7WXZ6sx"} + +data: {"id":"chatcmpl-DG74PksDb68x2U3ZMMZERPaMXAt5L","object":"chat.completion.chunk","created":1772732797,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"fMFnI"} + +data: {"id":"chatcmpl-DG74PksDb68x2U3ZMMZERPaMXAt5L","object":"chat.completion.chunk","created":1772732797,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_373a14eb6f","choices":[],"usage":{"prompt_tokens":23,"completion_tokens":7,"total_tokens":30,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"JK3RALpiFyh"} + +data: [DONE] + diff --git a/src/test/resources/cassettes/openai/__files/responses-67a1eb69-9269-491a-8e9d-f7361e6e4b26.json b/src/test/resources/cassettes/openai/__files/responses-67a1eb69-9269-491a-8e9d-f7361e6e4b26.json new file mode 100644 index 00000000..c3442735 --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/responses-67a1eb69-9269-491a-8e9d-f7361e6e4b26.json @@ -0,0 +1,76 @@ +{ + "id": "resp_05c66fce3eb079e70069a8941be3548190bf09e72c2c39e36a", + "object": "response", + "created_at": 1772655643, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1772655646, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "o4-mini-2025-04-16", + "output": [ + { + "id": "rs_05c66fce3eb079e70069a8941d81508190b833633fcf8fe167", + "type": "reasoning", + "summary": [] + }, + { + "id": "msg_05c66fce3eb079e70069a8941e1fe48190812c6e682955fe87", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Paris" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": "low", + "summary": "detailed" + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 18, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 28, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 46 + }, + "user": null, + "metadata": {} +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/__files/responses-d601536c-c724-406b-b517-78acf9766f8f.json b/src/test/resources/cassettes/openai/__files/responses-d601536c-c724-406b-b517-78acf9766f8f.json new file mode 100644 index 00000000..5b76803c --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/responses-d601536c-c724-406b-b517-78acf9766f8f.json @@ -0,0 +1,76 @@ +{ + "id": "resp_0e493f7efce61bc40069a9ccf5dec88190af368675ae1b2ad3", + "object": "response", + "created_at": 1772735733, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1772735735, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "o4-mini-2025-04-16", + "output": [ + { + "id": "rs_0e493f7efce61bc40069a9ccf6835c8190bffce8a6e0eddb64", + "type": "reasoning", + "summary": [] + }, + { + "id": "msg_0e493f7efce61bc40069a9ccf6fd548190be284508bdf0ac0c", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Paris" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": "low", + "summary": "detailed" + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 18, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 30, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 48 + }, + "user": null, + "metadata": {} +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/__files/responses-fb26b741-b06e-4a90-8bbe-b43a48b00c78.json b/src/test/resources/cassettes/openai/__files/responses-fb26b741-b06e-4a90-8bbe-b43a48b00c78.json new file mode 100644 index 00000000..d30c82f0 --- /dev/null +++ b/src/test/resources/cassettes/openai/__files/responses-fb26b741-b06e-4a90-8bbe-b43a48b00c78.json @@ -0,0 +1,76 @@ +{ + "id": "resp_09eb8e80298c55280069a9c179ca848195a885ce649cc1adc4", + "object": "response", + "created_at": 1772732793, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1772732794, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "o4-mini-2025-04-16", + "output": [ + { + "id": "rs_09eb8e80298c55280069a9c17a3db88195bbaa46efdf7408f8", + "type": "reasoning", + "summary": [] + }, + { + "id": "msg_09eb8e80298c55280069a9c17accf081959b3a0044cac5c358", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Paris" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": "low", + "summary": "detailed" + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 18, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 38, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 56 + }, + "user": null, + "metadata": {} +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/chat_completions-1b32d91a-cccf-4083-89d8-c6704238514d.json b/src/test/resources/cassettes/openai/mappings/chat_completions-1b32d91a-cccf-4083-89d8-c6704238514d.json new file mode 100644 index 00000000..3940d526 --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/chat_completions-1b32d91a-cccf-4083-89d8-c6704238514d.json @@ -0,0 +1,49 @@ +{ + "id" : "1b32d91a-cccf-4083-89d8-c6704238514d", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"You are a helpful assistant\",\"role\":\"system\"},{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"gpt-4o-mini\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "chat_completions-1b32d91a-cccf-4083-89d8-c6704238514d.json", + "headers" : { + "Date" : "Wed, 04 Mar 2026 20:20:49 GMT", + "Content-Type" : "application/json", + "access-control-expose-headers" : "X-Request-ID", + "openai-organization" : "braintrust-data", + "openai-processing-ms" : "293", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version" : "2020-10-01", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999982", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "x-request-id" : "req_9f8867880f204fc08c77f453f0798d15", + "x-openai-proxy-wasm" : "v0.1", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=hN_uAYbU62MWX5bc5pBLFitfSkDqPmRLwo3iuWCVfFQ-1772655648.4182355-1.0.1.1-458X17Eq5x3o4W3OGYvFhk8VBfWitLa6zaJIgQwQXl11qhfjcgrqQ6.MrycfAmoH1QKCxVxqm4v.S9jQUsVBPMWjazGZJmUe8FLUz0mxJ4tFZzZAXFSng74Abm_exDEo; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 04 Mar 2026 20:50:49 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d73956a9a695585-SJC", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "1b32d91a-cccf-4083-89d8-c6704238514d", + "persistent" : true, + "insertionIndex" : 9 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/chat_completions-692d5db8-32fd-4a4a-adde-68831cf174bf.json b/src/test/resources/cassettes/openai/mappings/chat_completions-692d5db8-32fd-4a4a-adde-68831cf174bf.json new file mode 100644 index 00000000..91ca9ac2 --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/chat_completions-692d5db8-32fd-4a4a-adde-68831cf174bf.json @@ -0,0 +1,52 @@ +{ + "id" : "692d5db8-32fd-4a4a-adde-68831cf174bf", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"You are a helpful assistant\",\"role\":\"system\"},{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"gpt-4o-mini\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "chat_completions-692d5db8-32fd-4a4a-adde-68831cf174bf.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:35:36 GMT", + "Content-Type" : "application/json", + "access-control-expose-headers" : "X-Request-ID", + "openai-organization" : "braintrust-data", + "openai-processing-ms" : "270", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version" : "2020-10-01", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999982", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "x-request-id" : "req_15b29eaa3796440c8287eea727a714e1", + "x-openai-proxy-wasm" : "v0.1", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=ldCKDdCd9AmpBfrWmFzOcr5O4H4ilXhTMp7MkSEKoV4-1772735736.037364-1.0.1.1-zPAJBJV5NWDfssCd4b.GoGzqWzEXLiUN6GxGlPEzdQF4kDNSbbR.LEldF6nzjazr_pPgQrDurthK6dzPedshSbPVO_YHWso8JjWDQ9.j403d50eUbhnmc67rmct9BFs_; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Thu, 05 Mar 2026 19:05:36 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d7b38ae38d0ba15-SEA", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "692d5db8-32fd-4a4a-adde-68831cf174bf", + "persistent" : true, + "scenarioName" : "scenario-1-chat-completions", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-1-chat-completions-2", + "insertionIndex" : 15 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/chat_completions-751a7200-62cc-4c4b-8324-00024a9462bf.json b/src/test/resources/cassettes/openai/mappings/chat_completions-751a7200-62cc-4c4b-8324-00024a9462bf.json new file mode 100644 index 00000000..0b972c4d --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/chat_completions-751a7200-62cc-4c4b-8324-00024a9462bf.json @@ -0,0 +1,51 @@ +{ + "id" : "751a7200-62cc-4c4b-8324-00024a9462bf", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"You are a helpful assistant\",\"role\":\"system\"},{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"gpt-4o-mini\",\"stream_options\":{\"include_usage\":true},\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "chat_completions-751a7200-62cc-4c4b-8324-00024a9462bf.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:35:38 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "access-control-expose-headers" : "X-Request-ID", + "openai-organization" : "braintrust-data", + "openai-processing-ms" : "145", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version" : "2020-10-01", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999982", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "x-request-id" : "req_4d07682197514e0ca4c3a5f56a886184", + "x-openai-proxy-wasm" : "v0.1", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=CskPMagPDixKA_rrqaZkp5v.5IXTz02fCyaItfRx11k-1772735738.7381837-1.0.1.1-gBjMcmMbj8DWrVWM0vFFc82tUtyopp9ARpQY..9MGbz3YvL_NelP6iUgzbBm_R96tedWgze7NBakLEpUvuadY5kZd1TSs1jiD1T5zU8VDipI5L5mQbCC5lxWjhN9wm8m; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Thu, 05 Mar 2026 19:05:38 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d7b38bf1f46980d-SEA", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "751a7200-62cc-4c4b-8324-00024a9462bf", + "persistent" : true, + "scenarioName" : "scenario-2-chat-completions", + "requiredScenarioState" : "scenario-2-chat-completions-2", + "insertionIndex" : 17 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/chat_completions-8306fcb1-24bb-4510-8a32-dddb526d55e3.json b/src/test/resources/cassettes/openai/mappings/chat_completions-8306fcb1-24bb-4510-8a32-dddb526d55e3.json new file mode 100644 index 00000000..cbc19d29 --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/chat_completions-8306fcb1-24bb-4510-8a32-dddb526d55e3.json @@ -0,0 +1,51 @@ +{ + "id" : "8306fcb1-24bb-4510-8a32-dddb526d55e3", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"You are a helpful assistant\",\"role\":\"system\"},{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"gpt-4o-mini\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "chat_completions-8306fcb1-24bb-4510-8a32-dddb526d55e3.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:35:40 GMT", + "Content-Type" : "application/json", + "access-control-expose-headers" : "X-Request-ID", + "openai-organization" : "braintrust-data", + "openai-processing-ms" : "391", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version" : "2020-10-01", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999982", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "x-request-id" : "req_8094198bf5714b98870113cbb1f07232", + "x-openai-proxy-wasm" : "v0.1", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=Yu3tv4MBJ.SismSvyEsMesGzNx4_c9MV3vqjy8hlkpY-1772735739.8853748-1.0.1.1-666PIFpnDgiOLOR4F0O5qpya8i_R9CAZfGnGFfAAjJFXoinlB_jKDXK0n3R.QEEg7c9Lb2k5q3Z_IeCJJHgeex6Uckkf9N1Eqd6YFQ7LNdv8Yk5liCClD_sZ3ZEsr..q; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Thu, 05 Mar 2026 19:05:40 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d7b38c64a43a49b-SEA", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "8306fcb1-24bb-4510-8a32-dddb526d55e3", + "persistent" : true, + "scenarioName" : "scenario-1-chat-completions", + "requiredScenarioState" : "scenario-1-chat-completions-2", + "insertionIndex" : 18 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/chat_completions-9e4757a5-01e8-45d7-a2ea-fce13f052e3f.json b/src/test/resources/cassettes/openai/mappings/chat_completions-9e4757a5-01e8-45d7-a2ea-fce13f052e3f.json new file mode 100644 index 00000000..063cc690 --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/chat_completions-9e4757a5-01e8-45d7-a2ea-fce13f052e3f.json @@ -0,0 +1,52 @@ +{ + "id" : "9e4757a5-01e8-45d7-a2ea-fce13f052e3f", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"You are a helpful assistant\",\"role\":\"system\"},{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"gpt-4o-mini\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "chat_completions-9e4757a5-01e8-45d7-a2ea-fce13f052e3f.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 17:46:36 GMT", + "Content-Type" : "application/json", + "access-control-expose-headers" : "X-Request-ID", + "openai-organization" : "braintrust-data", + "openai-processing-ms" : "289", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version" : "2020-10-01", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999982", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "x-request-id" : "req_8291585e9dac4928bec969415954f349", + "x-openai-proxy-wasm" : "v0.1", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=bNT94eTRNRefi1S2U7nZ28UgsfJ6oxRfs344Z58rRmc-1772732795.9051461-1.0.1.1-uvZkXu7y8cGRbb1R0PzqpNCejhWBNtB7uvUbfHMO0KBoPzvlA9gJSP3YFOgqe4cV52DdYVTFip0MiTSj4.VuO7ZQzMyDpcEqBS4WjFiPXjBTme8..ESK3xYYi1C00c9Q; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Thu, 05 Mar 2026 18:16:36 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d7af0e66927a386-SEA", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "9e4757a5-01e8-45d7-a2ea-fce13f052e3f", + "persistent" : true, + "scenarioName" : "scenario-1-chat-completions", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-1-chat-completions-2", + "insertionIndex" : 11 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/chat_completions-b01eaa78-92e7-4c32-9c5a-5d5c682c1eab.json b/src/test/resources/cassettes/openai/mappings/chat_completions-b01eaa78-92e7-4c32-9c5a-5d5c682c1eab.json new file mode 100644 index 00000000..12c2a4cf --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/chat_completions-b01eaa78-92e7-4c32-9c5a-5d5c682c1eab.json @@ -0,0 +1,52 @@ +{ + "id" : "b01eaa78-92e7-4c32-9c5a-5d5c682c1eab", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"You are a helpful assistant\",\"role\":\"system\"},{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"gpt-4o-mini\",\"stream_options\":{\"include_usage\":true},\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "chat_completions-b01eaa78-92e7-4c32-9c5a-5d5c682c1eab.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:35:37 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "access-control-expose-headers" : "X-Request-ID", + "openai-organization" : "braintrust-data", + "openai-processing-ms" : "173", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version" : "2020-10-01", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999982", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "x-request-id" : "req_3333829ec399462ea6ba4f1c36ca7b43", + "x-openai-proxy-wasm" : "v0.1", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=rjabjiKviAO8XkGYJsd6Cz2BSkX0rSOxvNVIdvvOJYs-1772735737.4398413-1.0.1.1-VQ5vn7Io5jaETVJsOwtkS4OLQ4DBnM7yEMKensoknRsXV0Ai2pQQATk55SqhZLumhuZQ_dnio2s_Pmjqeb6H5CPOtnnRBBUaar5DSO0CIBNqwXtY4VH3Gzgz7krKVvzn; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Thu, 05 Mar 2026 19:05:37 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d7b38b6fb87a3b3-SEA", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "b01eaa78-92e7-4c32-9c5a-5d5c682c1eab", + "persistent" : true, + "scenarioName" : "scenario-2-chat-completions", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-2-chat-completions-2", + "insertionIndex" : 16 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/chat_completions-b70b2684-b6b0-42e3-8d18-ad0ff374e753.json b/src/test/resources/cassettes/openai/mappings/chat_completions-b70b2684-b6b0-42e3-8d18-ad0ff374e753.json new file mode 100644 index 00000000..553e88c6 --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/chat_completions-b70b2684-b6b0-42e3-8d18-ad0ff374e753.json @@ -0,0 +1,51 @@ +{ + "id" : "b70b2684-b6b0-42e3-8d18-ad0ff374e753", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"You are a helpful assistant\",\"role\":\"system\"},{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"gpt-4o-mini\",\"temperature\":0.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "chat_completions-b70b2684-b6b0-42e3-8d18-ad0ff374e753.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 17:46:39 GMT", + "Content-Type" : "application/json", + "access-control-expose-headers" : "X-Request-ID", + "openai-organization" : "braintrust-data", + "openai-processing-ms" : "421", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version" : "2020-10-01", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999982", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "x-request-id" : "req_31af55efa71b4c659a6802a8603072b6", + "x-openai-proxy-wasm" : "v0.1", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=49vkeWE4hzVKEvZIZSiypSQrJDMfPlXa0IolAZO4AJM-1772732798.8239963-1.0.1.1-YL1TBerPBKleSqJzRZl2xCeAvWBFmK1F8JdJpkBKG4ZHL__WoBhUAP7i4_X2HKE20MIg5v1rgkuqQL37RAWBX5.iPdSepdeAxtM7qD8qbXweTardGM5ovfuhqFZLNkS9; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Thu, 05 Mar 2026 18:16:39 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d7af0f8aa8d7640-SEA", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "b70b2684-b6b0-42e3-8d18-ad0ff374e753", + "persistent" : true, + "scenarioName" : "scenario-1-chat-completions", + "requiredScenarioState" : "scenario-1-chat-completions-2", + "insertionIndex" : 13 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/chat_completions-e2bf36d0-37c4-4e63-9e8c-c155759089f5.json b/src/test/resources/cassettes/openai/mappings/chat_completions-e2bf36d0-37c4-4e63-9e8c-c155759089f5.json new file mode 100644 index 00000000..233f51cd --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/chat_completions-e2bf36d0-37c4-4e63-9e8c-c155759089f5.json @@ -0,0 +1,49 @@ +{ + "id" : "e2bf36d0-37c4-4e63-9e8c-c155759089f5", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"You are a helpful assistant\",\"role\":\"system\"},{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"gpt-4o-mini\",\"stream_options\":{\"include_usage\":true},\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "chat_completions-e2bf36d0-37c4-4e63-9e8c-c155759089f5.txt", + "headers" : { + "Date" : "Wed, 04 Mar 2026 20:20:47 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "access-control-expose-headers" : "X-Request-ID", + "openai-organization" : "braintrust-data", + "openai-processing-ms" : "131", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version" : "2020-10-01", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999980", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "x-request-id" : "req_0ff53b5e0646477787fd4fb48483d433", + "x-openai-proxy-wasm" : "v0.1", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=odsGAyOZp5fwlmyLtPjzNHXKiqqJLi9xmWuLFq17_P8-1772655647.235088-1.0.1.1-BC4h19oU93noNQDeQw3HkiN5h652yXTvH_bjNJg5PL4v1kCegm.W0YSv96OgEAkH2Hk6Bb6fik_kWa2If.zKSmeWDccRhF.8T7fkyIa0khNCg28ebc2GGZ1UuyWcAvkL; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 04 Mar 2026 20:50:47 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d7395633c287a33-SJC", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "e2bf36d0-37c4-4e63-9e8c-c155759089f5", + "persistent" : true, + "insertionIndex" : 8 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/chat_completions-f19b40d4-4b5b-4d7b-accc-cc484a48e5b3.json b/src/test/resources/cassettes/openai/mappings/chat_completions-f19b40d4-4b5b-4d7b-accc-cc484a48e5b3.json new file mode 100644 index 00000000..548d1f0c --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/chat_completions-f19b40d4-4b5b-4d7b-accc-cc484a48e5b3.json @@ -0,0 +1,49 @@ +{ + "id" : "f19b40d4-4b5b-4d7b-accc-cc484a48e5b3", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"You are a helpful assistant\",\"role\":\"system\"},{\"content\":\"What is the capital of France?\",\"role\":\"user\"}],\"model\":\"gpt-4o-mini\",\"stream_options\":{\"include_usage\":true},\"temperature\":0.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "chat_completions-f19b40d4-4b5b-4d7b-accc-cc484a48e5b3.txt", + "headers" : { + "Date" : "Thu, 05 Mar 2026 17:46:37 GMT", + "Content-Type" : "text/event-stream; charset=utf-8", + "access-control-expose-headers" : "X-Request-ID", + "openai-organization" : "braintrust-data", + "openai-processing-ms" : "156", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version" : "2020-10-01", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999982", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "x-request-id" : "req_6f8a437faa1646cbb40f798a2117a26e", + "x-openai-proxy-wasm" : "v0.1", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=cKo3djGdNvxPLPjqMlbgQ3.mWI4e4nOkwFCVZHoAVW0-1772732797.293048-1.0.1.1-yL5_NVBJXzN6_JWnAfdcrL2NxhO_qDasacpglo7li4gUFQV0owx5i3Mvgbh9u_Kue7iK04N2pokMzrWnFNlF88BosdrQhiAKjDmN8hCfYFAMyl72UFiOgZMXOoX.pEoq; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Thu, 05 Mar 2026 18:16:37 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d7af0ef1d147682-SEA", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "f19b40d4-4b5b-4d7b-accc-cc484a48e5b3", + "persistent" : true, + "insertionIndex" : 12 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/responses-67a1eb69-9269-491a-8e9d-f7361e6e4b26.json b/src/test/resources/cassettes/openai/mappings/responses-67a1eb69-9269-491a-8e9d-f7361e6e4b26.json new file mode 100644 index 00000000..fff414df --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/responses-67a1eb69-9269-491a-8e9d-f7361e6e4b26.json @@ -0,0 +1,47 @@ +{ + "id" : "67a1eb69-9269-491a-8e9d-f7361e6e4b26", + "name" : "responses", + "request" : { + "url" : "/responses", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"input\":[{\"content\":\"What is the capital of France? Reply in one word.\",\"role\":\"user\"}],\"model\":\"o4-mini\",\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"}}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "responses-67a1eb69-9269-491a-8e9d-f7361e6e4b26.json", + "headers" : { + "Date" : "Wed, 04 Mar 2026 20:20:46 GMT", + "Content-Type" : "application/json", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999775", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "openai-version" : "2020-10-01", + "openai-organization" : "braintrust-data", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "x-request-id" : "req_9d46dce148724b52aeb0609edaa44dc3", + "openai-processing-ms" : "2620", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=z2ivwvdc.jN_7UGfGb6Zb4L2s2iyKtk4mlbewvOSghw-1772655643.3606954-1.0.1.1-bWFvfxlMhmsZimvgxT.UBMZOcfrurmP80F5fCP_pSqlxCTbmf0KmUtj9BVwzwZxd4jFRmpCU9Am_mE1hsgNcbQ6s9vsXt2dr7yj22bvtyghOcDuaAQUA0DsQ3mEFpOg1; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 04 Mar 2026 20:50:46 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d73954af9c8cfdd-SJC", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "67a1eb69-9269-491a-8e9d-f7361e6e4b26", + "persistent" : true, + "insertionIndex" : 7 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/responses-d601536c-c724-406b-b517-78acf9766f8f.json b/src/test/resources/cassettes/openai/mappings/responses-d601536c-c724-406b-b517-78acf9766f8f.json new file mode 100644 index 00000000..d8852dac --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/responses-d601536c-c724-406b-b517-78acf9766f8f.json @@ -0,0 +1,47 @@ +{ + "id" : "d601536c-c724-406b-b517-78acf9766f8f", + "name" : "responses", + "request" : { + "url" : "/responses", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"input\":[{\"content\":\"What is the capital of France? Reply in one word.\",\"role\":\"user\"}],\"model\":\"o4-mini\",\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"}}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "responses-d601536c-c724-406b-b517-78acf9766f8f.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 18:35:35 GMT", + "Content-Type" : "application/json", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999775", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "openai-version" : "2020-10-01", + "openai-organization" : "braintrust-data", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "x-request-id" : "req_d392afbaf14446f693a8c51786640681", + "openai-processing-ms" : "1367", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=SqTAnZ6p0OH8tzQe7Kq4gx6mmuYYXseHI6D2zja4HOs-1772735733.799452-1.0.1.1-yXa6eZBhXmLLrXLqu9Ub710kn7WX8y8zBQ1kBrAytI5PI0UnIcRpf4EpdHsnpwzflV8ZF.oJgQQKZU6l3HhVyYpLBKK90zOFt15nqbLaOiiBiDLxf0k1rZAjyug8L3DA; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Thu, 05 Mar 2026 19:05:35 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d7b38a03fb89917-SEA", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "d601536c-c724-406b-b517-78acf9766f8f", + "persistent" : true, + "insertionIndex" : 14 +} \ No newline at end of file diff --git a/src/test/resources/cassettes/openai/mappings/responses-fb26b741-b06e-4a90-8bbe-b43a48b00c78.json b/src/test/resources/cassettes/openai/mappings/responses-fb26b741-b06e-4a90-8bbe-b43a48b00c78.json new file mode 100644 index 00000000..33763b3d --- /dev/null +++ b/src/test/resources/cassettes/openai/mappings/responses-fb26b741-b06e-4a90-8bbe-b43a48b00c78.json @@ -0,0 +1,47 @@ +{ + "id" : "fb26b741-b06e-4a90-8bbe-b43a48b00c78", + "name" : "responses", + "request" : { + "url" : "/responses", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + }, + "bodyPatterns" : [ { + "equalToJson" : "{\"input\":[{\"content\":\"What is the capital of France? Reply in one word.\",\"role\":\"user\"}],\"model\":\"o4-mini\",\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"}}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "responses-fb26b741-b06e-4a90-8bbe-b43a48b00c78.json", + "headers" : { + "Date" : "Thu, 05 Mar 2026 17:46:35 GMT", + "Content-Type" : "application/json", + "Server" : "cloudflare", + "x-ratelimit-limit-requests" : "30000", + "x-ratelimit-limit-tokens" : "150000000", + "x-ratelimit-remaining-requests" : "29999", + "x-ratelimit-remaining-tokens" : "149999775", + "x-ratelimit-reset-requests" : "2ms", + "x-ratelimit-reset-tokens" : "0s", + "openai-version" : "2020-10-01", + "openai-organization" : "braintrust-data", + "openai-project" : "proj_vsCSXafhhByzWOThMrJcZiw9", + "x-request-id" : "req_6f3e3285fe0d465fa435e2eca8bef62c", + "openai-processing-ms" : "1229", + "cf-cache-status" : "DYNAMIC", + "set-cookie" : "__cf_bm=FBordmnldmmgrxSLfM_bbHJSv8MDAvW.UsKHgivZ5lc-1772732793.743964-1.0.1.1-GcPE.1GkWfXYdUTp5lV14ELe6KiENP7E2woN8Sv2k44TSKwUY9J3mbxnPZPeJgW8mFVao8vcGjMgs7rql.592U0NSUppTFVUA4mKpO00MfB.3rGNKuUVOMwPHy.Xqnu8; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Thu, 05 Mar 2026 18:16:35 GMT", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options" : "nosniff", + "CF-RAY" : "9d7af0d8ecdc45f7-SEA", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "fb26b741-b06e-4a90-8bbe-b43a48b00c78", + "persistent" : true, + "insertionIndex" : 10 +} \ No newline at end of file