From bea0735fc63e1e43298291155a3010f3be03716a Mon Sep 17 00:00:00 2001 From: ekeith <55766816+evanmkeith@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:56:21 -0700 Subject: [PATCH] fix: trace Anthropic messages behind proxy path prefixes ## Summary Fix Anthropic Go SDK tracing when the Anthropic client is configured with a proxy/base URL that adds a path prefix before `/v1/messages`. `traceanthropic` previously matched the request path exactly against `/v1/messages`, so paths like `/clapi/transparent/aws_bedrock/v1/messages` returned `nil` from the router and bypassed tracing. This updates the router to suffix-match the Anthropic Messages endpoint, matching the existing OpenAI tracing behavior for custom/proxy base URLs. ## Test Plan - Added a regression test for a proxy-prefixed Anthropic Messages path: `/clapi/transparent/aws_bedrock/v1/messages` - Ran the focused repro test locally in a disposable copy and confirmed it fails before the fix. --- trace/contrib/anthropic/traceanthropic.go | 3 +- .../contrib/anthropic/traceanthropic_test.go | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/trace/contrib/anthropic/traceanthropic.go b/trace/contrib/anthropic/traceanthropic.go index 1b052411..4ef91bf2 100644 --- a/trace/contrib/anthropic/traceanthropic.go +++ b/trace/contrib/anthropic/traceanthropic.go @@ -36,6 +36,7 @@ package anthropic import ( "net/http" + "strings" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -103,7 +104,7 @@ func NewMiddleware(opts ...MiddlewareOption) func(*http.Request, NextMiddleware) // anthropicRouter maps Anthropic API paths to their corresponding tracers. func anthropicRouter(cfg *middlewareConfig, path string) internal.MiddlewareTracer { - if path == "/v1/messages" { + if strings.HasSuffix(path, "/v1/messages") { return newMessagesTracer(cfg) } return nil diff --git a/trace/contrib/anthropic/traceanthropic_test.go b/trace/contrib/anthropic/traceanthropic_test.go index 00644cdd..b556b048 100644 --- a/trace/contrib/anthropic/traceanthropic_test.go +++ b/trace/contrib/anthropic/traceanthropic_test.go @@ -90,6 +90,64 @@ func TestMiddleware(t *testing.T) { require.NoError(t, err) } +func TestMiddlewareMatchesProxyPrefixedMessagesPath(t *testing.T) { + tp, exporter := oteltest.Setup(t) + + requestBody := `{ + "model": "claude-3-haiku-20240307", + "max_tokens": 1024, + "messages": [ + { + "role": "user", + "content": "Hello, Claude!" + } + ] + }` + + req := httptest.NewRequest("POST", "/clapi/transparent/aws_bedrock/v1/messages", strings.NewReader(requestBody)) + req.Header.Set("Content-Type", "application/json") + + responseBody := `{ + "id": "msg_01Aq9w938a90dw8q", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Hello! How can I help you today?" + } + ], + "model": "claude-3-haiku-20240307", + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 12, + "output_tokens": 9 + } + }` + + next := func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(responseBody)), + }, nil + } + + middleware := NewMiddleware(WithTracerProvider(tp)) //nolint:bodyclose // false positive - NewMiddleware returns middleware func + resp, err := middleware(req, next) + require.NoError(t, err) + require.NotNil(t, resp) + + _, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + span := exporter.FlushOne() + span.AssertNameIs("anthropic.messages.create") + assert.Equal(t, "/v1/messages", span.Metadata()["endpoint"]) +} + func TestMessagesTracer(t *testing.T) { tp, _ := oteltest.Setup(t) cfg := &middlewareConfig{tracerProvider: tp}