diff --git a/braintrust-sdk/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java b/braintrust-sdk/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java index 43452486..83128a21 100644 --- a/braintrust-sdk/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java +++ b/braintrust-sdk/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java @@ -199,11 +199,14 @@ private static void tagOpenAIRequest( @SneakyThrows private static void tagOpenAIResponse( Span span, JsonNode responseJson, @Nullable Long timeToFirstTokenNanoseconds) { - // Output — chat completions API uses "choices"; Responses API uses "output" + // Output — chat completions API uses "choices"; Responses API uses "output"; audio + // transcriptions and translations return a text-keyed object. 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"))); + } else if (responseJson.has("text")) { + span.setAttribute("braintrust.output_json", toJson(responseJson)); } Map metrics = new HashMap<>(); diff --git a/braintrust-sdk/src/test/java/dev/braintrust/instrumentation/InstrumentationSemConvTest.java b/braintrust-sdk/src/test/java/dev/braintrust/instrumentation/InstrumentationSemConvTest.java index a6951500..3649d7c9 100644 --- a/braintrust-sdk/src/test/java/dev/braintrust/instrumentation/InstrumentationSemConvTest.java +++ b/braintrust-sdk/src/test/java/dev/braintrust/instrumentation/InstrumentationSemConvTest.java @@ -20,11 +20,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -/** - * Covers {@link InstrumentationSemConv#addServerSideChildSpans}: only vendor-executed (server-side) - * tool calls in an OpenAI Responses body become child spans nested under the LLM span. Client-side - * calls ({@code function_call}, {@code computer_call}) are excluded. - */ +/** Covers provider response tagging and {@link InstrumentationSemConv#addServerSideChildSpans}. */ class InstrumentationSemConvTest { private static final AttributeKey SPAN_ATTRIBUTES = @@ -81,6 +77,44 @@ private static JsonNode json(String s) { return BraintrustJsonMapper.fromJson(s, JsonNode.class); } + private SpanData tagOpenAIResponse(String responseBody) { + Span span = tracer.spanBuilder("llm").startSpan(); + try { + InstrumentationSemConv.tagLLMSpanResponse( + tracer, span, InstrumentationSemConv.PROVIDER_NAME_OPENAI, responseBody); + } finally { + span.end(); + } + return byName(exporter.getFinishedSpanItems(), "llm"); + } + + @Test + void tagsOpenAIAudioTranscriptionOutput() { + SpanData span = tagOpenAIResponse("{\"text\":\"Hello from the recording.\"}"); + + assertEquals( + json("{\"text\":\"Hello from the recording.\"}"), + json(span.getAttributes().get(OUTPUT_JSON))); + } + + @Test + void tagsOpenAIVerboseAudioTranscriptionOutput() { + String body = + """ + { + "text": "Hello from the recording.", + "language": "english", + "duration": 1.25, + "segments": [{"id": 0, "text": "Hello from the recording."}], + "words": [{"word": "Hello", "start": 0.0, "end": 0.4}] + } + """; + + SpanData span = tagOpenAIResponse(body); + + assertEquals(json(body), json(span.getAttributes().get(OUTPUT_JSON))); + } + @Test void emitsWebSearchCallToolSpanParentedToLlm() { String body =