33import static dev .braintrust .json .BraintrustJsonMapper .toJson ;
44
55import com .fasterxml .jackson .databind .JsonNode ;
6+ import com .fasterxml .jackson .databind .node .ArrayNode ;
67import dev .braintrust .json .BraintrustJsonMapper ;
78import io .opentelemetry .api .trace .Span ;
89import io .opentelemetry .api .trace .StatusCode ;
1516
1617public class InstrumentationSemConv {
1718 public static final String PROVIDER_NAME_OPENAI = "openai" ;
19+ public static final String PROVIDER_NAME_ANTHROPIC = "anthropic" ;
1820 public static final String PROVIDER_NAME_OTHER = "generic-ai-provider" ;
1921 public static final String UNSET_LLM_SPAN_NAME = "llm" ;
2022
@@ -34,6 +36,9 @@ public static void tagLLMSpanRequest(
3436 case PROVIDER_NAME_OPENAI ->
3537 tagOpenAIRequest (
3638 span , providerName , baseUrl , pathSegments , method , requestBody );
39+ case PROVIDER_NAME_ANTHROPIC ->
40+ tagAnthropicRequest (
41+ span , providerName , baseUrl , pathSegments , method , requestBody );
3742 default ->
3843 tagOpenAIRequest (
3944 span , providerName , baseUrl , pathSegments , method , requestBody );
@@ -54,6 +59,8 @@ public static void tagLLMSpanResponse(
5459 switch (providerName ) {
5560 case PROVIDER_NAME_OPENAI ->
5661 tagOpenAIResponse (span , responseBody , timeToFirstTokenNanoseconds );
62+ case PROVIDER_NAME_ANTHROPIC ->
63+ tagAnthropicResponse (span , responseBody , timeToFirstTokenNanoseconds );
5764 default -> tagOpenAIResponse (span , responseBody , timeToFirstTokenNanoseconds );
5865 }
5966 }
@@ -75,7 +82,7 @@ private static void tagOpenAIRequest(
7582 List <String > pathSegments ,
7683 String method ,
7784 @ Nullable String requestBody ) {
78- span .updateName (getSpanName (pathSegments ));
85+ span .updateName (getSpanName (providerName , pathSegments ));
7986 span .setAttribute ("braintrust.span_attributes" , toJson (Map .of ("type" , "llm" )));
8087
8188 Map <String , String > metadata = new HashMap <>();
@@ -148,19 +155,95 @@ private static void tagOpenAIResponse(
148155 }
149156 }
150157
158+ // -------------------------------------------------------------------------
159+ // Anthropic provider implementation
160+ // -------------------------------------------------------------------------
161+
162+ @ SneakyThrows
163+ private static void tagAnthropicRequest (
164+ Span span ,
165+ String providerName ,
166+ String baseUrl ,
167+ List <String > pathSegments ,
168+ String method ,
169+ @ Nullable String requestBody ) {
170+ span .updateName (getSpanName (providerName , pathSegments ));
171+ span .setAttribute ("braintrust.span_attributes" , toJson (Map .of ("type" , "llm" )));
172+
173+ Map <String , String > metadata = new HashMap <>();
174+ metadata .put ("provider" , providerName );
175+ metadata .put ("request_path" , String .join ("/" , pathSegments ));
176+ metadata .put ("request_base_uri" , baseUrl );
177+ metadata .put ("request_method" , method );
178+
179+ if (requestBody != null ) {
180+ JsonNode requestJson = BraintrustJsonMapper .get ().readTree (requestBody );
181+ if (requestJson .has ("model" )) {
182+ metadata .put ("model" , requestJson .get ("model" ).asText ());
183+ }
184+ // Build input array: messages + system (as a synthetic system-role entry)
185+ if (requestJson .has ("messages" )) {
186+ ArrayNode inputArray = BraintrustJsonMapper .get ().createArrayNode ();
187+ // Append messages first
188+ requestJson .get ("messages" ).forEach (inputArray ::add );
189+ // Append system prompt as a {role:"system", content:"..."} entry if present
190+ if (requestJson .has ("system" )) {
191+ var systemNode = BraintrustJsonMapper .get ().createObjectNode ();
192+ systemNode .put ("role" , "system" );
193+ systemNode .set ("content" , requestJson .get ("system" ));
194+ inputArray .add (systemNode );
195+ }
196+ span .setAttribute ("braintrust.input_json" , toJson (inputArray ));
197+ }
198+ }
199+
200+ span .setAttribute ("braintrust.metadata" , toJson (metadata ));
201+ }
202+
203+ @ SneakyThrows
204+ private static void tagAnthropicResponse (
205+ Span span , String responseBody , @ Nullable Long timeToFirstTokenNanoseconds ) {
206+ JsonNode responseJson = BraintrustJsonMapper .get ().readTree (responseBody );
207+
208+ // Anthropic response is the full Message object — output it whole
209+ span .setAttribute ("braintrust.output_json" , responseBody );
210+
211+ Map <String , Object > metrics = new HashMap <>();
212+ if (timeToFirstTokenNanoseconds != null ) {
213+ metrics .put ("time_to_first_token" , timeToFirstTokenNanoseconds / 1_000_000_000.0 );
214+ }
215+
216+ if (responseJson .has ("usage" )) {
217+ JsonNode usage = responseJson .get ("usage" );
218+ if (usage .has ("input_tokens" )) metrics .put ("prompt_tokens" , usage .get ("input_tokens" ));
219+ if (usage .has ("output_tokens" ))
220+ metrics .put ("completion_tokens" , usage .get ("output_tokens" ));
221+ if (usage .has ("input_tokens" ) && usage .has ("output_tokens" )) {
222+ metrics .put (
223+ "tokens" ,
224+ usage .get ("input_tokens" ).asLong () + usage .get ("output_tokens" ).asLong ());
225+ }
226+ }
227+
228+ if (!metrics .isEmpty ()) {
229+ span .setAttribute ("braintrust.metrics" , toJson (metrics ));
230+ }
231+ }
232+
151233 // -------------------------------------------------------------------------
152234 // Shared helpers
153235 // -------------------------------------------------------------------------
154236
155- private static String getSpanName (List <String > pathSegments ) {
237+ private static String getSpanName (String providerName , List <String > pathSegments ) {
156238 if (pathSegments .isEmpty ()) {
157239 return UNSET_LLM_SPAN_NAME ;
158240 }
159- return switch (pathSegments .get (pathSegments .size () - 1 )) {
160- case "completions" -> "Chat Completion" ;
161- case "embeddings" -> "Embeddings" ;
162- case "messages" -> "Messages" ;
163- default -> pathSegments .get (pathSegments .size () - 1 );
241+ String lastSegment = pathSegments .get (pathSegments .size () - 1 );
242+ return switch (providerName + ":" + lastSegment ) {
243+ case PROVIDER_NAME_OPENAI + ":completions" -> "Chat Completion" ;
244+ case PROVIDER_NAME_OPENAI + ":embeddings" -> "Embeddings" ;
245+ case PROVIDER_NAME_ANTHROPIC + ":messages" -> "anthropic.messages.create" ;
246+ default -> lastSegment ;
164247 };
165248 }
166249}
0 commit comments