Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/provider/anthropic/anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ func (c *client) Stream(ctx context.Context, req provider.Request) (<-chan provi
httpReq.Header.Set("Accept", "text/event-stream")
httpReq.Header.Set("x-api-key", c.apiKey)
httpReq.Header.Set("anthropic-version", anthropicVersion)
httpReq.Header.Set("User-Agent", "Reasonix")
return httpReq, nil
}
resp, err := provider.SendWithRetry(ctx, c.http, c.sendOpts(), newReq)
Expand Down
29 changes: 29 additions & 0 deletions internal/provider/anthropic/anthropic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

Expand Down Expand Up @@ -113,6 +114,34 @@ func TestMapStopReason(t *testing.T) {
}
}

// TestStreamSendsReasonixUserAgent verifies outbound Messages API requests carry
// an identifying User-Agent so an upstream gateway can attribute the traffic to
// Reasonix instead of Go's default UA (#5226).
func TestStreamSendsReasonixUserAgent(t *testing.T) {
var gotUA string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotUA = r.Header.Get("User-Agent")
w.Header().Set("Content-Type", "text/event-stream")
_, _ = io.WriteString(w, "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")
}))
defer srv.Close()

p, err := New(provider.Config{Name: "anthropic", BaseURL: srv.URL, Model: "claude-opus-4-8", APIKey: "k"})
if err != nil {
t.Fatalf("New: %v", err)
}
ch, err := p.Stream(context.Background(), provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "hi"}}})
if err != nil {
t.Fatalf("Stream: %v", err)
}
for range ch { // drain
}

if gotUA != "Reasonix" {
t.Errorf("User-Agent = %q, want %q", gotUA, "Reasonix")
}
}

const sseFixture = `event: message_start
data: {"type":"message_start","message":{"usage":{"input_tokens":100,"cache_creation_input_tokens":0,"cache_read_input_tokens":50,"output_tokens":1}}}

Expand Down
1 change: 1 addition & 0 deletions internal/provider/openai/openai.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ func (c *client) Stream(ctx context.Context, req provider.Request) (<-chan provi
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
}
httpReq.Header.Set("Accept", "text/event-stream")
httpReq.Header.Set("User-Agent", "Reasonix")
return httpReq, nil
}
resp, err := provider.SendWithRetry(ctx, c.http, c.sendOpts(), newReq)
Expand Down
28 changes: 28 additions & 0 deletions internal/provider/openai/openai_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,34 @@ func TestStreamAuthError(t *testing.T) {
}
}

// TestStreamSendsReasonixUserAgent verifies outbound requests carry an
// identifying User-Agent so an upstream gateway can attribute the traffic to
// Reasonix instead of seeing Go's default UA (#5226).
func TestStreamSendsReasonixUserAgent(t *testing.T) {
var gotUA string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotUA = r.Header.Get("User-Agent")
w.Header().Set("Content-Type", "text/event-stream")
_, _ = io.WriteString(w, "data: [DONE]\n\n")
}))
defer srv.Close()

p, err := New(provider.Config{Name: "deepseek", BaseURL: srv.URL, Model: "deepseek-v4", APIKey: "k"})
if err != nil {
t.Fatalf("New: %v", err)
}
ch, err := p.Stream(context.Background(), provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "hi"}}})
if err != nil {
t.Fatalf("Stream: %v", err)
}
for range ch { // drain
}

if gotUA != "Reasonix" {
t.Errorf("User-Agent = %q, want %q", gotUA, "Reasonix")
}
}

// TestBuildRequestAlwaysSerializesContent guards the DeepSeek 400 regression:
// DeepSeek rejects a message missing the `content` field, so every message must
// serialize one. A pure tool_calls assistant turn carries null (OpenAI-spec,
Expand Down