From 4228d2efd80a735020d2a7bbb5ec9a8a534a02d0 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Fri, 27 Feb 2026 14:21:26 -0500 Subject: [PATCH] chore(useragent): drop model detail option --- agent.go | 13 --- agent_useragent_test.go | 66 ----------- model.go | 2 - object.go | 2 - providers/anthropic/anthropic.go | 12 +- providers/anthropic/call_useragent.go | 2 +- providers/anthropic/useragent_test.go | 47 -------- providers/azure/azure.go | 9 -- providers/azure/useragent_test.go | 14 --- providers/bedrock/bedrock.go | 9 -- providers/bedrock/useragent_test.go | 19 ---- providers/google/call_useragent.go | 4 +- providers/google/google.go | 15 +-- providers/google/useragent_test.go | 41 ------- providers/internal/httpheaders/httpheaders.go | 47 +------- .../internal/httpheaders/httpheaders_test.go | 58 ++-------- providers/openai/call_useragent.go | 4 +- providers/openai/openai.go | 12 +- providers/openai/openai_test.go | 103 ------------------ providers/openaicompat/openaicompat.go | 9 -- providers/openrouter/openrouter.go | 9 -- providers/openrouter/useragent_test.go | 14 --- providers/vercel/vercel.go | 9 -- 23 files changed, 23 insertions(+), 497 deletions(-) diff --git a/agent.go b/agent.go index 32c8849ddfabf771f108e9f0e250f49ce9ea7433..6d2d62dfb3f363491fde42eab4959069a66bead9 100644 --- a/agent.go +++ b/agent.go @@ -139,7 +139,6 @@ type agentSettings struct { frequencyPenalty *float64 headers map[string]string userAgent string - modelSegment string providerOptions ProviderOptions // TODO: add support for provider tools @@ -451,7 +450,6 @@ func (a *agent) Generate(ctx context.Context, opts AgentCall) (*AgentResult, err Tools: preparedTools, ToolChoice: &stepToolChoice, UserAgent: a.settings.userAgent, - ModelSegment: a.settings.modelSegment, ProviderOptions: opts.ProviderOptions, }) }) @@ -834,7 +832,6 @@ func (a *agent) Stream(ctx context.Context, opts AgentStreamCall) (*AgentResult, Tools: preparedTools, ToolChoice: &stepToolChoice, UserAgent: a.settings.userAgent, - ModelSegment: a.settings.modelSegment, ProviderOptions: call.ProviderOptions, } @@ -1432,16 +1429,6 @@ func WithUserAgent(ua string) AgentOption { } } -// WithModelSegment sets the model segment appended to the default User-Agent -// header. The default UA becomes "Fantasy/ ()". An empty -// string clears any previously set segment. This is overridden by WithUserAgent -// at either the agent or provider level. -func WithModelSegment(segment string) AgentOption { - return func(s *agentSettings) { - s.modelSegment = segment - } -} - // WithProviderOptions sets the provider options for the agent. func WithProviderOptions(providerOptions ProviderOptions) AgentOption { return func(s *agentSettings) { diff --git a/agent_useragent_test.go b/agent_useragent_test.go index 76e25b10328e3488dcd3273c8272157a06f1abe3..95cc81d8ec3d667e6135a478b8bce72898f2662a 100644 --- a/agent_useragent_test.go +++ b/agent_useragent_test.go @@ -26,28 +26,6 @@ func TestAgent_WithUserAgent_PropagatesOnGenerate(t *testing.T) { _, err := agent.Generate(context.Background(), AgentCall{Prompt: "hi"}) require.NoError(t, err) assert.Equal(t, "MyApp/2.0", capturedCall.UserAgent) - assert.Empty(t, capturedCall.ModelSegment) -} - -func TestAgent_WithModelSegment_PropagatesOnGenerate(t *testing.T) { - t.Parallel() - - var capturedCall Call - model := &mockLanguageModel{ - generateFunc: func(_ context.Context, call Call) (*Response, error) { - capturedCall = call - return &Response{ - Content: []Content{TextContent{Text: "ok"}}, - FinishReason: FinishReasonStop, - }, nil - }, - } - - agent := NewAgent(model, WithModelSegment("Claude 4.6 Opus")) - _, err := agent.Generate(context.Background(), AgentCall{Prompt: "hi"}) - require.NoError(t, err) - assert.Empty(t, capturedCall.UserAgent) - assert.Equal(t, "Claude 4.6 Opus", capturedCall.ModelSegment) } func TestAgent_WithUserAgent_PropagatesOnStream(t *testing.T) { @@ -72,28 +50,6 @@ func TestAgent_WithUserAgent_PropagatesOnStream(t *testing.T) { assert.Equal(t, "StreamApp/1.0", capturedCall.UserAgent) } -func TestAgent_WithModelSegment_PropagatesOnStream(t *testing.T) { - t.Parallel() - - var capturedCall Call - model := &mockLanguageModel{ - streamFunc: func(_ context.Context, call Call) (StreamResponse, error) { - capturedCall = call - return func(yield func(StreamPart) bool) { - yield(StreamPart{ - Type: StreamPartTypeFinish, - FinishReason: FinishReasonStop, - }) - }, nil - }, - } - - agent := NewAgent(model, WithModelSegment("GPT-5")) - _, err := agent.Stream(context.Background(), AgentStreamCall{Prompt: "hi"}) - require.NoError(t, err) - assert.Equal(t, "GPT-5", capturedCall.ModelSegment) -} - func TestAgent_NoUA_OmitsCallLevelFields(t *testing.T) { t.Parallel() @@ -112,26 +68,4 @@ func TestAgent_NoUA_OmitsCallLevelFields(t *testing.T) { _, err := agent.Generate(context.Background(), AgentCall{Prompt: "hi"}) require.NoError(t, err) assert.Empty(t, capturedCall.UserAgent) - assert.Empty(t, capturedCall.ModelSegment) -} - -func TestAgent_WithUserAgentAndModelSegment_BothPropagate(t *testing.T) { - t.Parallel() - - var capturedCall Call - model := &mockLanguageModel{ - generateFunc: func(_ context.Context, call Call) (*Response, error) { - capturedCall = call - return &Response{ - Content: []Content{TextContent{Text: "ok"}}, - FinishReason: FinishReasonStop, - }, nil - }, - } - - agent := NewAgent(model, WithUserAgent("App/1.0"), WithModelSegment("Claude 4.6")) - _, err := agent.Generate(context.Background(), AgentCall{Prompt: "hi"}) - require.NoError(t, err) - assert.Equal(t, "App/1.0", capturedCall.UserAgent) - assert.Equal(t, "Claude 4.6", capturedCall.ModelSegment) } diff --git a/model.go b/model.go index 92dabf3377db4f9311184a0ed55f0abc58eaf026..16980da108c9887a2fcd2bd5d6ae9bfedc1d1e29 100644 --- a/model.go +++ b/model.go @@ -220,8 +220,6 @@ type Call struct { // UserAgent overrides the provider-level User-Agent header for this call. UserAgent string `json:"-"` - // ModelSegment overrides the provider-level model segment for this call. - ModelSegment string `json:"-"` // for provider specific options, the key is the provider id ProviderOptions ProviderOptions `json:"provider_options"` diff --git a/object.go b/object.go index 3e434e3818b9d774b41d2b020f2886270ba7bda7..3cbcbd3eb9c9624bc3de121b5b943e023b37ecd2 100644 --- a/object.go +++ b/object.go @@ -43,8 +43,6 @@ type ObjectCall struct { // UserAgent overrides the provider-level User-Agent header for this call. UserAgent string `json:"-"` - // ModelSegment overrides the provider-level model segment for this call. - ModelSegment string `json:"-"` ProviderOptions ProviderOptions diff --git a/providers/anthropic/anthropic.go b/providers/anthropic/anthropic.go index dfa3dd3792a09c80f543c12d59805865b74be525..10f845b9d88be697d82ab76c20ad6fcca172f76f 100644 --- a/providers/anthropic/anthropic.go +++ b/providers/anthropic/anthropic.go @@ -37,7 +37,6 @@ type options struct { name string headers map[string]string userAgent string - modelSegment string client option.HTTPClient vertexProject string @@ -136,15 +135,6 @@ func WithUserAgent(ua string) Option { } } -// WithModelSegment sets the model segment appended to the default User-Agent. -// The resulting header is "Fantasy/ ()". Pass an empty string -// to clear a previously set segment. -func WithModelSegment(model string) Option { - return func(o *options) { - o.modelSegment = model - } -} - // WithObjectMode sets the object generation mode. func WithObjectMode(om fantasy.ObjectMode) Option { return func(o *options) { @@ -166,7 +156,7 @@ func (a *provider) LanguageModel(ctx context.Context, modelID string) (fantasy.L if a.options.baseURL != "" { clientOptions = append(clientOptions, option.WithBaseURL(a.options.baseURL)) } - defaultUA := httpheaders.DefaultUserAgent(fantasy.Version, a.options.modelSegment) + defaultUA := httpheaders.DefaultUserAgent(fantasy.Version) resolved := httpheaders.ResolveHeaders(a.options.headers, a.options.userAgent, defaultUA) for key, value := range resolved { clientOptions = append(clientOptions, option.WithHeader(key, value)) diff --git a/providers/anthropic/call_useragent.go b/providers/anthropic/call_useragent.go index d1fc97780f848a27ad772fc1b3c9a24d2828df8a..4aaffcf87a51d5783de324eb4e2d42f5eefc1d85 100644 --- a/providers/anthropic/call_useragent.go +++ b/providers/anthropic/call_useragent.go @@ -7,7 +7,7 @@ import ( ) func callUARequestOptions(call fantasy.Call) []option.RequestOption { - if ua, ok := httpheaders.CallUserAgent(fantasy.Version, call.UserAgent, call.ModelSegment); ok { + if ua, ok := httpheaders.CallUserAgent(call.UserAgent); ok { return []option.RequestOption{option.WithHeader("User-Agent", ua)} } return nil diff --git a/providers/anthropic/useragent_test.go b/providers/anthropic/useragent_test.go index dcc1f517d6460d82066487a067eee7f7b958e943..6ae7a4ec1a683d6cce1e46408e67e35913616070 100644 --- a/providers/anthropic/useragent_test.go +++ b/providers/anthropic/useragent_test.go @@ -52,34 +52,6 @@ func TestUserAgent(t *testing.T) { assert.Equal(t, "Charm Fantasy/"+fantasy.Version, (*captured)[0]["User-Agent"]) }) - t.Run("model segment format", func(t *testing.T) { - t.Parallel() - server, captured := newUAServer() - defer server.Close() - - p, err := New(WithAPIKey("k"), WithBaseURL(server.URL), WithModelSegment("Claude 4.6 Opus")) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "claude-sonnet-4-20250514") - _, _ = model.Generate(t.Context(), fantasy.Call{Prompt: prompt}) - - require.Len(t, *captured, 1) - assert.Equal(t, "Charm Fantasy/"+fantasy.Version+" (Claude 4.6 Opus)", (*captured)[0]["User-Agent"]) - }) - - t.Run("WithHeaders User-Agent wins over default", func(t *testing.T) { - t.Parallel() - server, captured := newUAServer() - defer server.Close() - - p, err := New(WithAPIKey("k"), WithBaseURL(server.URL), WithHeaders(map[string]string{"User-Agent": "custom-from-headers"})) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "claude-sonnet-4-20250514") - _, _ = model.Generate(t.Context(), fantasy.Call{Prompt: prompt}) - - require.Len(t, *captured, 1) - assert.Equal(t, "custom-from-headers", (*captured)[0]["User-Agent"]) - }) - t.Run("WithUserAgent wins over both", func(t *testing.T) { t.Parallel() server, captured := newUAServer() @@ -98,23 +70,4 @@ func TestUserAgent(t *testing.T) { require.Len(t, *captured, 1) assert.Equal(t, "explicit-ua", (*captured)[0]["User-Agent"]) }) - - t.Run("WithModelSegment empty clears segment", func(t *testing.T) { - t.Parallel() - server, captured := newUAServer() - defer server.Close() - - p, err := New( - WithAPIKey("k"), - WithBaseURL(server.URL), - WithModelSegment("initial"), - WithModelSegment(""), - ) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "claude-sonnet-4-20250514") - _, _ = model.Generate(t.Context(), fantasy.Call{Prompt: prompt}) - - require.Len(t, *captured, 1) - assert.Equal(t, "Charm Fantasy/"+fantasy.Version, (*captured)[0]["User-Agent"]) - }) } diff --git a/providers/azure/azure.go b/providers/azure/azure.go index 7b42611d81a7ac5608f4034ff32ec6ee5c9920a7..a68df7251d1d8dbb62d802e5e44c6915e06b54bb 100644 --- a/providers/azure/azure.go +++ b/providers/azure/azure.go @@ -117,15 +117,6 @@ func WithUserAgent(ua string) Option { } } -// WithModelSegment sets the model segment appended to the default User-Agent. -// The resulting header is "Fantasy/ ()". Pass an empty string -// to clear a previously set segment. -func WithModelSegment(model string) Option { - return func(o *options) { - o.openaiOptions = append(o.openaiOptions, openai.WithModelSegment(model)) - } -} - // WithUseResponsesAPI configures the provider to use the responses API for models that support it. func WithUseResponsesAPI() Option { return func(o *options) { diff --git a/providers/azure/useragent_test.go b/providers/azure/useragent_test.go index 91472078118f13bd3e8fbbdd1ce35c254952932d..c190f5db7f7a14c10f265547c6e92db6d31855a0 100644 --- a/providers/azure/useragent_test.go +++ b/providers/azure/useragent_test.go @@ -52,20 +52,6 @@ func TestUserAgent(t *testing.T) { assert.Equal(t, "Charm Fantasy/"+fantasy.Version, (*captured)[0]["User-Agent"]) }) - t.Run("model segment format", func(t *testing.T) { - t.Parallel() - server, captured := newUAServer() - defer server.Close() - - p, err := New(WithAPIKey("k"), WithBaseURL(server.URL), WithModelSegment("Claude 4.6 Opus")) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "gpt-4") - _, _ = model.Generate(t.Context(), fantasy.Call{Prompt: prompt}) - - require.Len(t, *captured, 1) - assert.Equal(t, "Charm Fantasy/"+fantasy.Version+" (Claude 4.6 Opus)", (*captured)[0]["User-Agent"]) - }) - t.Run("WithUserAgent wins over default", func(t *testing.T) { t.Parallel() server, captured := newUAServer() diff --git a/providers/bedrock/bedrock.go b/providers/bedrock/bedrock.go index 8b216f190cc2e15dc893ce296d8db8618c993991..c8889398cfce102499dc030a8af0d7528ed83042 100644 --- a/providers/bedrock/bedrock.go +++ b/providers/bedrock/bedrock.go @@ -65,15 +65,6 @@ func WithUserAgent(ua string) Option { } } -// WithModelSegment sets the model segment appended to the default User-Agent. -// The resulting header is "Fantasy/ ()". Pass an empty string -// to clear a previously set segment. -func WithModelSegment(model string) Option { - return func(o *options) { - o.anthropicOptions = append(o.anthropicOptions, anthropic.WithModelSegment(model)) - } -} - // WithSkipAuth configures whether to skip authentication for the Bedrock provider. func WithSkipAuth(skipAuth bool) Option { return func(o *options) { diff --git a/providers/bedrock/useragent_test.go b/providers/bedrock/useragent_test.go index 02b068f5a5f7663fe63fedbdd9d125ce417f21c9..d6935d6dc547be489b190db3aafaec8f898f3bd9 100644 --- a/providers/bedrock/useragent_test.go +++ b/providers/bedrock/useragent_test.go @@ -56,25 +56,6 @@ func TestUserAgent(t *testing.T) { assert.Equal(t, "Charm Fantasy/"+fantasy.Version, (*captured)[0]["User-Agent"]) }) - t.Run("model segment format", func(t *testing.T) { - t.Parallel() - server, captured := newUAServer() - defer server.Close() - - p, err := New( - WithAPIKey("k"), - WithSkipAuth(true), - WithHTTPClient(&http.Client{Transport: redirectTransport(server.URL)}), - WithModelSegment("Claude 4.6 Opus"), - ) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "us.anthropic.claude-sonnet-4-20250514-v1:0") - _, _ = model.Generate(t.Context(), fantasy.Call{Prompt: prompt}) - - require.Len(t, *captured, 1) - assert.Equal(t, "Charm Fantasy/"+fantasy.Version+" (Claude 4.6 Opus)", (*captured)[0]["User-Agent"]) - }) - t.Run("WithUserAgent wins over default", func(t *testing.T) { t.Parallel() server, captured := newUAServer() diff --git a/providers/google/call_useragent.go b/providers/google/call_useragent.go index 57d5d3927f591412d50f04ac6f9f23bff4ade514..a3521dd75b7ba6272684a16a391c24dcf10a267e 100644 --- a/providers/google/call_useragent.go +++ b/providers/google/call_useragent.go @@ -11,14 +11,14 @@ import ( type callUAKey struct{} func withCallUA(ctx context.Context, call fantasy.Call) context.Context { - if ua, ok := httpheaders.CallUserAgent(fantasy.Version, call.UserAgent, call.ModelSegment); ok { + if ua, ok := httpheaders.CallUserAgent(call.UserAgent); ok { return context.WithValue(ctx, callUAKey{}, ua) } return ctx } func withObjectCallUA(ctx context.Context, call fantasy.ObjectCall) context.Context { - if ua, ok := httpheaders.CallUserAgent(fantasy.Version, call.UserAgent, call.ModelSegment); ok { + if ua, ok := httpheaders.CallUserAgent(call.UserAgent); ok { return context.WithValue(ctx, callUAKey{}, ua) } return ctx diff --git a/providers/google/google.go b/providers/google/google.go index 2b9dd6b08df49c28a0d9dd76b15ae471d08d4b4c..5d3f0515224860e8760f712373de327bca8bb1bc 100644 --- a/providers/google/google.go +++ b/providers/google/google.go @@ -38,7 +38,6 @@ type options struct { baseURL string headers map[string]string userAgent string - modelSegment string client *http.Client backend genai.Backend project string @@ -143,15 +142,6 @@ func WithUserAgent(ua string) Option { } } -// WithModelSegment sets the model segment appended to the default User-Agent. -// The resulting header is "Fantasy/ ()". Pass an empty string -// to clear a previously set segment. -func WithModelSegment(model string) Option { - return func(o *options) { - o.modelSegment = model - } -} - // WithObjectMode sets the object generation mode for the Google provider. func WithObjectMode(om fantasy.ObjectMode) Option { return func(o *options) { @@ -182,9 +172,6 @@ func (a *provider) LanguageModel(ctx context.Context, modelID string) (fantasy.L if a.options.userAgent != "" { anthropicOpts = append(anthropicOpts, anthropic.WithUserAgent(a.options.userAgent)) } - if a.options.modelSegment != "" { - anthropicOpts = append(anthropicOpts, anthropic.WithModelSegment(a.options.modelSegment)) - } p, err := anthropic.New(anthropicOpts...) if err != nil { return nil, err @@ -207,7 +194,7 @@ func (a *provider) LanguageModel(ctx context.Context, modelID string) (fantasy.L } } - defaultUA := httpheaders.DefaultUserAgent(fantasy.Version, a.options.modelSegment) + defaultUA := httpheaders.DefaultUserAgent(fantasy.Version) resolved := httpheaders.ResolveHeaders(a.options.headers, a.options.userAgent, defaultUA) headers := http.Header{} diff --git a/providers/google/useragent_test.go b/providers/google/useragent_test.go index 82e36b19c6b45c30785730c5b3319d259df00a53..1494ff9a84c02b6987341f2ee995e696fd85feea 100644 --- a/providers/google/useragent_test.go +++ b/providers/google/useragent_test.go @@ -83,26 +83,6 @@ func TestUserAgent(t *testing.T) { assert.True(t, findUA(captured, "Charm Fantasy/"+fantasy.Version)) }) - t.Run("model segment format", func(t *testing.T) { - t.Parallel() - server, captured := newUAServer() - defer server.Close() - - p, err := New( - WithVertex("test-project", "us-central1"), - WithBaseURL(server.URL), - WithSkipAuth(true), - WithModelSegment("Claude 4.6 Opus"), - ) - require.NoError(t, err) - model, err := p.LanguageModel(t.Context(), "gemini-2.0-flash") - require.NoError(t, err) - _, _ = model.Generate(t.Context(), fantasy.Call{Prompt: prompt}) - - require.NotEmpty(t, *captured) - assert.True(t, findUA(captured, "Charm Fantasy/"+fantasy.Version+" (Claude 4.6 Opus)")) - }) - t.Run("WithUserAgent wins over default", func(t *testing.T) { t.Parallel() server, captured := newUAServer() @@ -163,25 +143,4 @@ func TestUserAgent(t *testing.T) { require.NotEmpty(t, *captured) assert.True(t, findUA(captured, "explicit-ua")) }) - - t.Run("WithModelSegment empty clears segment", func(t *testing.T) { - t.Parallel() - server, captured := newUAServer() - defer server.Close() - - p, err := New( - WithVertex("test-project", "us-central1"), - WithBaseURL(server.URL), - WithSkipAuth(true), - WithModelSegment("initial"), - WithModelSegment(""), - ) - require.NoError(t, err) - model, err := p.LanguageModel(t.Context(), "gemini-2.0-flash") - require.NoError(t, err) - _, _ = model.Generate(t.Context(), fantasy.Call{Prompt: prompt}) - - require.NotEmpty(t, *captured) - assert.True(t, findUA(captured, "Charm Fantasy/"+fantasy.Version)) - }) } diff --git a/providers/internal/httpheaders/httpheaders.go b/providers/internal/httpheaders/httpheaders.go index 263ff204613f2bebeff09356a4070db9a129ac0b..c4b205b989e11a043e718abaec2a99a07cc957b7 100644 --- a/providers/internal/httpheaders/httpheaders.go +++ b/providers/internal/httpheaders/httpheaders.go @@ -1,23 +1,12 @@ // Package httpheaders provides shared User-Agent resolution for all HTTP-based providers. package httpheaders -import ( - "strings" - "unicode" -) - -const maxAgentLength = 64 +import "strings" // DefaultUserAgent returns the default User-Agent string for the SDK. -// If agent is non-empty, the result is "Charm Fantasy/ ()". -// Otherwise, the result is "Charm Fantasy/". -func DefaultUserAgent(version, agent string) string { - const sdk = "Charm Fantasy/" - agent = sanitizeAgent(agent) - if agent == "" { - return sdk + version - } - return sdk + version + " (" + agent + ")" +// The result is "Charm Fantasy/". +func DefaultUserAgent(version string) string { + return "Charm Fantasy/" + version } // ResolveHeaders returns a new header map with User-Agent resolved according to precedence: @@ -55,35 +44,9 @@ func ResolveHeaders(headers map[string]string, explicitUA, defaultUA string) map // CallUserAgent resolves the User-Agent for a single API call. It returns the // resolved UA string and true if a per-call override should be applied, or // empty string and false if the client-level UA should be used as-is. -// -// Precedence: -// 1. callUA (agent-level WithUserAgent) — highest -// 2. callSegment used to build default UA (agent-level WithModelSegment) -// 3. empty — use client-level UA (return false) -func CallUserAgent(version, callUA, callSegment string) (string, bool) { +func CallUserAgent(callUA string) (string, bool) { if callUA != "" { return callUA, true } - if callSegment != "" { - return DefaultUserAgent(version, callSegment), true - } return "", false } - -func sanitizeAgent(s string) string { - s = strings.TrimSpace(s) - var b strings.Builder - b.Grow(len(s)) - count := 0 - for _, r := range s { - if r < 0x20 || r == '(' || r == ')' { - continue - } - if count >= maxAgentLength { - break - } - b.WriteRune(r) - count++ - } - return strings.TrimRightFunc(b.String(), unicode.IsSpace) -} diff --git a/providers/internal/httpheaders/httpheaders_test.go b/providers/internal/httpheaders/httpheaders_test.go index b04cc4aae2c79190bd50b4192db25fa71cd22a9f..3a70293a77231e39945d060c14e47af61667cabc 100644 --- a/providers/internal/httpheaders/httpheaders_test.go +++ b/providers/internal/httpheaders/httpheaders_test.go @@ -1,7 +1,6 @@ package httpheaders import ( - "strings" "testing" "github.com/stretchr/testify/assert" @@ -14,22 +13,16 @@ func TestDefaultUserAgent(t *testing.T) { tests := []struct { name string version string - agent string want string }{ - {name: "no agent", version: "0.11.0", agent: "", want: "Charm Fantasy/0.11.0"}, - {name: "with agent", version: "0.11.0", agent: "Claude 4.6 Opus", want: "Charm Fantasy/0.11.0 (Claude 4.6 Opus)"}, - {name: "agent trimmed", version: "1.0.0", agent: " spaces ", want: "Charm Fantasy/1.0.0 (spaces)"}, - {name: "agent strips parens", version: "1.0.0", agent: "foo(bar)", want: "Charm Fantasy/1.0.0 (foobar)"}, - {name: "agent strips control chars", version: "1.0.0", agent: "foo\x01bar", want: "Charm Fantasy/1.0.0 (foobar)"}, - {name: "agent capped at 64 chars", version: "1.0.0", agent: strings.Repeat("a", 100), want: "Charm Fantasy/1.0.0 (" + strings.Repeat("a", 64) + ")"}, - {name: "whitespace-only agent treated as empty", version: "1.0.0", agent: " ", want: "Charm Fantasy/1.0.0"}, + {name: "basic version", version: "0.11.0", want: "Charm Fantasy/0.11.0"}, + {name: "another version", version: "1.0.0", want: "Charm Fantasy/1.0.0"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - got := DefaultUserAgent(tt.version, tt.agent) + got := DefaultUserAgent(tt.version) assert.Equal(t, tt.want, got) }) } @@ -99,34 +92,6 @@ func TestResolveHeaders_PreservesOtherHeaders(t *testing.T) { assert.Equal(t, "Charm Fantasy/1.0.0", got["User-Agent"]) } -func TestSanitizeAgent(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - input string - want string - }{ - {name: "normal text", input: "Claude 4.6 Opus", want: "Claude 4.6 Opus"}, - {name: "leading trailing spaces", input: " spaced ", want: "spaced"}, - {name: "parentheses removed", input: "agent(v2)", want: "agentv2"}, - {name: "control chars removed", input: "a\x00b\x1fc", want: "abc"}, - {name: "capped at 64", input: strings.Repeat("x", 100), want: strings.Repeat("x", 64)}, - {name: "multibyte runes capped at 64 chars", input: strings.Repeat("é", 100), want: strings.Repeat("é", 64)}, - {name: "empty stays empty", input: "", want: ""}, - {name: "only spaces", input: " ", want: ""}, - {name: "trailing space after cap", input: strings.Repeat("a", 63) + " b", want: strings.Repeat("a", 63)}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got := sanitizeAgent(tt.input) - assert.Equal(t, tt.want, got) - }) - } -} - func TestResolveHeaders_DuplicateCaseInsensitiveKeys(t *testing.T) { t.Parallel() @@ -144,21 +109,18 @@ func TestCallUserAgent(t *testing.T) { t.Parallel() tests := []struct { - name string - callUA string - callSegment string - wantUA string - wantOK bool + name string + callUA string + wantUA string + wantOK bool }{ - {name: "no override", callUA: "", callSegment: "", wantUA: "", wantOK: false}, - {name: "explicit UA", callUA: "MyAgent/1.0", callSegment: "", wantUA: "MyAgent/1.0", wantOK: true}, - {name: "model segment only", callUA: "", callSegment: "Claude 4.6", wantUA: "Charm Fantasy/0.11.0 (Claude 4.6)", wantOK: true}, - {name: "explicit UA wins over segment", callUA: "MyAgent/1.0", callSegment: "Claude 4.6", wantUA: "MyAgent/1.0", wantOK: true}, + {name: "no override", callUA: "", wantUA: "", wantOK: false}, + {name: "explicit UA", callUA: "MyAgent/1.0", wantUA: "MyAgent/1.0", wantOK: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - ua, ok := CallUserAgent("0.11.0", tt.callUA, tt.callSegment) + ua, ok := CallUserAgent(tt.callUA) assert.Equal(t, tt.wantOK, ok) assert.Equal(t, tt.wantUA, ua) }) diff --git a/providers/openai/call_useragent.go b/providers/openai/call_useragent.go index 007ded93b0822cf3d7efeae688b7db8aa3138234..4c7f2a4f7658e7e132f2accac4925450bb427e31 100644 --- a/providers/openai/call_useragent.go +++ b/providers/openai/call_useragent.go @@ -9,7 +9,7 @@ import ( // callUARequestOptions returns per-request options that override the // client-level User-Agent header when the Call carries agent-level UA settings. func callUARequestOptions(call fantasy.Call) []option.RequestOption { - if ua, ok := httpheaders.CallUserAgent(fantasy.Version, call.UserAgent, call.ModelSegment); ok { + if ua, ok := httpheaders.CallUserAgent(call.UserAgent); ok { return []option.RequestOption{option.WithHeader("User-Agent", ua)} } return nil @@ -18,7 +18,7 @@ func callUARequestOptions(call fantasy.Call) []option.RequestOption { // objectCallUARequestOptions returns per-request options that override the // client-level User-Agent header when the ObjectCall carries agent-level UA settings. func objectCallUARequestOptions(call fantasy.ObjectCall) []option.RequestOption { - if ua, ok := httpheaders.CallUserAgent(fantasy.Version, call.UserAgent, call.ModelSegment); ok { + if ua, ok := httpheaders.CallUserAgent(call.UserAgent); ok { return []option.RequestOption{option.WithHeader("User-Agent", ua)} } return nil diff --git a/providers/openai/openai.go b/providers/openai/openai.go index a66c1da9b63746c3d40142255cbbe8dae996d926..928f3dd287c974fb31f95e881f283808ff3b071d 100644 --- a/providers/openai/openai.go +++ b/providers/openai/openai.go @@ -32,7 +32,6 @@ type options struct { useResponsesAPI bool headers map[string]string userAgent string - modelSegment string client option.HTTPClient sdkOptions []option.RequestOption objectMode fantasy.ObjectMode @@ -143,15 +142,6 @@ func WithUserAgent(ua string) Option { } } -// WithModelSegment sets the model segment appended to the default User-Agent. -// The resulting header is "Fantasy/ ()". Pass an empty string -// to clear a previously set segment. -func WithModelSegment(model string) Option { - return func(o *options) { - o.modelSegment = model - } -} - // WithObjectMode sets the object generation mode. func WithObjectMode(om fantasy.ObjectMode) Option { return func(o *options) { @@ -175,7 +165,7 @@ func (o *provider) LanguageModel(_ context.Context, modelID string) (fantasy.Lan openaiClientOptions = append(openaiClientOptions, option.WithBaseURL(o.options.baseURL)) } - defaultUA := httpheaders.DefaultUserAgent(fantasy.Version, o.options.modelSegment) + defaultUA := httpheaders.DefaultUserAgent(fantasy.Version) resolved := httpheaders.ResolveHeaders(o.options.headers, o.options.userAgent, defaultUA) for key, value := range resolved { openaiClientOptions = append(openaiClientOptions, option.WithHeader(key, value)) diff --git a/providers/openai/openai_test.go b/providers/openai/openai_test.go index 67d863bea22f28ca359b3df52db0f1db8e2e8e63..3e56aeb090d1980a3396c7f2278cb148e2a1fc0d 100644 --- a/providers/openai/openai_test.go +++ b/providers/openai/openai_test.go @@ -3323,22 +3323,6 @@ func TestUserAgent(t *testing.T) { assert.Equal(t, "Charm Fantasy/"+fantasy.Version, server.calls[0].headers["User-Agent"]) }) - t.Run("model segment format", func(t *testing.T) { - t.Parallel() - - server := newMockServer() - defer server.close() - server.prepareJSONResponse(map[string]any{}) - - p, err := New(WithAPIKey("k"), WithBaseURL(server.server.URL), WithModelSegment("Claude 4.6 Opus")) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "gpt-4") - _, _ = model.Generate(t.Context(), fantasy.Call{Prompt: testPrompt}) - - require.Len(t, server.calls, 1) - assert.Equal(t, "Charm Fantasy/"+fantasy.Version+" (Claude 4.6 Opus)", server.calls[0].headers["User-Agent"]) - }) - t.Run("WithHeaders User-Agent wins over default", func(t *testing.T) { t.Parallel() @@ -3376,72 +3360,6 @@ func TestUserAgent(t *testing.T) { assert.Equal(t, "explicit-ua", server.calls[0].headers["User-Agent"]) }) - t.Run("WithModelSegment empty clears segment", func(t *testing.T) { - t.Parallel() - - server := newMockServer() - defer server.close() - server.prepareJSONResponse(map[string]any{}) - - p, err := New( - WithAPIKey("k"), - WithBaseURL(server.server.URL), - WithModelSegment("initial"), - WithModelSegment(""), - ) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "gpt-4") - _, _ = model.Generate(t.Context(), fantasy.Call{Prompt: testPrompt}) - - require.Len(t, server.calls, 1) - assert.Equal(t, "Charm Fantasy/"+fantasy.Version, server.calls[0].headers["User-Agent"]) - }) - - t.Run("Call.UserAgent overrides provider UA", func(t *testing.T) { - t.Parallel() - - server := newMockServer() - defer server.close() - server.prepareJSONResponse(map[string]any{}) - - p, err := New( - WithAPIKey("k"), - WithBaseURL(server.server.URL), - WithUserAgent("provider-ua"), - ) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "gpt-4") - _, _ = model.Generate(t.Context(), fantasy.Call{ - Prompt: testPrompt, - UserAgent: "agent-ua", - }) - - require.Len(t, server.calls, 1) - assert.Equal(t, "agent-ua", server.calls[0].headers["User-Agent"]) - }) - - t.Run("Call.ModelSegment overrides provider default", func(t *testing.T) { - t.Parallel() - - server := newMockServer() - defer server.close() - server.prepareJSONResponse(map[string]any{}) - - p, err := New( - WithAPIKey("k"), - WithBaseURL(server.server.URL), - ) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "gpt-4") - _, _ = model.Generate(t.Context(), fantasy.Call{ - Prompt: testPrompt, - ModelSegment: "GPT-5", - }) - - require.Len(t, server.calls, 1) - assert.Equal(t, "Charm Fantasy/"+fantasy.Version+" (GPT-5)", server.calls[0].headers["User-Agent"]) - }) - t.Run("Call.UserAgent overrides provider WithHeaders UA", func(t *testing.T) { t.Parallel() @@ -3507,27 +3425,6 @@ func TestUserAgent(t *testing.T) { assert.Equal(t, "agent-ua", server.calls[0].headers["User-Agent"]) }) - t.Run("agent WithModelSegment overrides provider default end-to-end", func(t *testing.T) { - t.Parallel() - - server := newMockServer() - defer server.close() - server.prepareJSONResponse(map[string]any{}) - - p, err := New( - WithAPIKey("k"), - WithBaseURL(server.server.URL), - ) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "gpt-4") - - agent := fantasy.NewAgent(model, fantasy.WithModelSegment("Claude 4.6")) - _, _ = agent.Generate(t.Context(), fantasy.AgentCall{Prompt: "hi"}) - - require.Len(t, server.calls, 1) - assert.Equal(t, "Charm Fantasy/"+fantasy.Version+" (Claude 4.6)", server.calls[0].headers["User-Agent"]) - }) - t.Run("agent without UA falls through to provider UA end-to-end", func(t *testing.T) { t.Parallel() diff --git a/providers/openaicompat/openaicompat.go b/providers/openaicompat/openaicompat.go index 39a5e684e671368ae1edd83faa1b95b6be27719c..1029676346e0b701fb7ac0660364a71a2119cd76 100644 --- a/providers/openaicompat/openaicompat.go +++ b/providers/openaicompat/openaicompat.go @@ -116,15 +116,6 @@ func WithUserAgent(ua string) Option { } } -// WithModelSegment sets the model segment appended to the default User-Agent. -// The resulting header is "Fantasy/ ()". Pass an empty string -// to clear a previously set segment. -func WithModelSegment(model string) Option { - return func(o *options) { - o.openaiOptions = append(o.openaiOptions, openai.WithModelSegment(model)) - } -} - // WithUseResponsesAPI configures the provider to use the responses API for models that support it. func WithUseResponsesAPI() Option { return func(o *options) { diff --git a/providers/openrouter/openrouter.go b/providers/openrouter/openrouter.go index dfbb0345cc32698f5769c25664eb7dc160b91842..0b3e51d24f360cca30583b02ee7aaada2fb3d19f 100644 --- a/providers/openrouter/openrouter.go +++ b/providers/openrouter/openrouter.go @@ -97,15 +97,6 @@ func WithUserAgent(ua string) Option { } } -// WithModelSegment sets the model segment appended to the default User-Agent. -// The resulting header is "Fantasy/ ()". Pass an empty string -// to clear a previously set segment. -func WithModelSegment(model string) Option { - return func(o *options) { - o.openaiOptions = append(o.openaiOptions, openai.WithModelSegment(model)) - } -} - // WithObjectMode sets the object generation mode for the OpenRouter provider. // Supported modes: ObjectModeTool, ObjectModeText. // ObjectModeAuto and ObjectModeJSON are automatically converted to ObjectModeTool diff --git a/providers/openrouter/useragent_test.go b/providers/openrouter/useragent_test.go index b00acd6cb194a7e8a36d87a5be7e0b8dc7927d74..15ff2d5e454c193918ca27b80f59c2f8eb5f21bd 100644 --- a/providers/openrouter/useragent_test.go +++ b/providers/openrouter/useragent_test.go @@ -59,20 +59,6 @@ func TestUserAgent(t *testing.T) { assert.Equal(t, "Charm Fantasy/"+fantasy.Version, (*captured)[0]["User-Agent"]) }) - t.Run("model segment format", func(t *testing.T) { - t.Parallel() - server, captured := newUAServer() - defer server.Close() - - p, err := New(WithAPIKey("k"), withBaseURL(server.URL), WithModelSegment("Claude 4.6 Opus")) - require.NoError(t, err) - model, _ := p.LanguageModel(t.Context(), "openai/gpt-4") - _, _ = model.Generate(t.Context(), fantasy.Call{Prompt: prompt}) - - require.Len(t, *captured, 1) - assert.Equal(t, "Charm Fantasy/"+fantasy.Version+" (Claude 4.6 Opus)", (*captured)[0]["User-Agent"]) - }) - t.Run("WithUserAgent wins over default", func(t *testing.T) { t.Parallel() server, captured := newUAServer() diff --git a/providers/vercel/vercel.go b/providers/vercel/vercel.go index 1dd3ee7c635b9b8ca78b623ccf12592617f5d4b6..43712c1ac2209e91e1f029a584a285e432ea3d6d 100644 --- a/providers/vercel/vercel.go +++ b/providers/vercel/vercel.go @@ -104,15 +104,6 @@ func WithUserAgent(ua string) Option { } } -// WithModelSegment sets the model segment appended to the default User-Agent. -// The resulting header is "Fantasy/ ()". Pass an empty string -// to clear a previously set segment. -func WithModelSegment(model string) Option { - return func(o *options) { - o.openaiOptions = append(o.openaiOptions, openai.WithModelSegment(model)) - } -} - // WithSDKOptions sets the SDK options for the Vercel provider. func WithSDKOptions(opts ...option.RequestOption) Option { return func(o *options) {