@@ -27,6 +27,7 @@ final class StreamListener {
2727 private final boolean captureMessageContent ;
2828 private final boolean newSpan ;
2929 private final AtomicBoolean hasEnded ;
30+ private final long startTimeNanos ;
3031
3132 private final StringBuilder contentBuilder = new StringBuilder ();
3233
@@ -35,23 +36,32 @@ final class StreamListener {
3536 @ Nullable private Model model ;
3637 @ Nullable private String responseId ;
3738 @ Nullable private String stopReason ;
39+ @ Nullable private Double timeToFirstToken ;
3840
3941 StreamListener (
4042 Context context ,
4143 MessageCreateParams request ,
4244 Instrumenter <MessageCreateParams , Message > instrumenter ,
4345 boolean captureMessageContent ,
44- boolean newSpan ) {
46+ boolean newSpan ,
47+ long startTimeNanos ) {
4548 this .context = context ;
4649 this .request = request ;
4750 this .instrumenter = instrumenter ;
4851 this .captureMessageContent = captureMessageContent ;
4952 this .newSpan = newSpan ;
53+ this .startTimeNanos = startTimeNanos ;
5054 hasEnded = new AtomicBoolean ();
5155 }
5256
5357 @ SneakyThrows
5458 void onEvent (RawMessageStreamEvent event ) {
59+ // Capture time to first token on the first event
60+ if (timeToFirstToken == null ) {
61+ long firstEventTimeNanos = System .nanoTime ();
62+ timeToFirstToken = (firstEventTimeNanos - startTimeNanos ) / 1_000_000_000.0 ;
63+ }
64+
5565 // Handle message_start event
5666 if (event .messageStart ().isPresent ()) {
5767 var messageStart = event .messageStart ().get ();
@@ -144,6 +154,11 @@ void endSpan(@Nullable Throwable error) {
144154 span .setAttribute ("gen_ai.usage.output_tokens" , usage .outputTokens ());
145155 }
146156
157+ // Set time to first token if captured
158+ if (timeToFirstToken != null ) {
159+ span .setAttribute ("braintrust.metrics.time_to_first_token" , timeToFirstToken );
160+ }
161+
147162 instrumenter .end (context , request , null , error );
148163 }
149164}
0 commit comments