Detailed changes
@@ -7,8 +7,16 @@ 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 {
+// client-level User-Agent header when the Call carries agent-level UA
+// settings.
+//
+// When noDefaultUA is true the SDK's own User-Agent is preserved and no
+// override is applied (needed for providers like OpenRouter, which reject
+// User-Agents headers they don't expect).
+func callUARequestOptions(call fantasy.Call, noDefaultUA bool) []option.RequestOption {
+ if noDefaultUA {
+ return nil
+ }
if ua, ok := httpheaders.CallUserAgent(call.UserAgent); ok {
return []option.RequestOption{option.WithHeader("User-Agent", ua)}
}
@@ -16,8 +24,12 @@ 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 {
+// client-level User-Agent header when the ObjectCall carries agent-level UA
+// settings.
+func objectCallUARequestOptions(call fantasy.ObjectCall, noDefaultUA bool) []option.RequestOption {
+ if noDefaultUA {
+ return nil
+ }
if ua, ok := httpheaders.CallUserAgent(call.UserAgent); ok {
return []option.RequestOption{option.WithHeader("User-Agent", ua)}
}
@@ -32,6 +32,7 @@ type languageModel struct {
streamExtraFunc LanguageModelStreamExtraFunc
streamProviderMetadataFunc LanguageModelStreamProviderMetadataFunc
toPromptFunc LanguageModelToPromptFunc
+ noDefaultUserAgent bool
}
// LanguageModelOption is a function that configures a languageModel.
@@ -86,6 +87,16 @@ func WithLanguageModelToPromptFunc(fn LanguageModelToPromptFunc) LanguageModelOp
}
}
+// WithLanguageModelSkipUserAgent prevents per-call User-Agent overrides. This
+// exists solely for OpenRouter, which rejects User-Agent overrides.
+//
+// This function is provisional and may be removed in a future release.
+func WithLanguageModelSkipUserAgent() LanguageModelOption {
+ return func(l *languageModel) {
+ l.noDefaultUserAgent = true
+ }
+}
+
// WithLanguageModelObjectMode sets the object generation mode.
func WithLanguageModelObjectMode(om fantasy.ObjectMode) LanguageModelOption {
return func(l *languageModel) {
@@ -246,7 +257,7 @@ func (o languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantas
if err != nil {
return nil, err
}
- response, err := o.client.Chat.Completions.New(ctx, *params, callUARequestOptions(call)...)
+ response, err := o.client.Chat.Completions.New(ctx, *params, callUARequestOptions(call, o.noDefaultUserAgent)...)
if err != nil {
return nil, toProviderErr(err)
}
@@ -314,7 +325,7 @@ func (o languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.S
IncludeUsage: openai.Bool(true),
}
- stream := o.client.Chat.Completions.NewStreaming(ctx, *params, callUARequestOptions(call)...)
+ stream := o.client.Chat.Completions.NewStreaming(ctx, *params, callUARequestOptions(call, o.noDefaultUserAgent)...)
isActiveText := false
toolCalls := make(map[int64]streamToolCall)
@@ -733,7 +744,7 @@ func (o languageModel) generateObjectWithJSONMode(ctx context.Context, call fant
},
}
- response, err := o.client.Chat.Completions.New(ctx, *params, objectCallUARequestOptions(call)...)
+ response, err := o.client.Chat.Completions.New(ctx, *params, objectCallUARequestOptions(call, o.noDefaultUserAgent)...)
if err != nil {
return nil, toProviderErr(err)
}
@@ -817,7 +828,7 @@ func (o languageModel) streamObjectWithJSONMode(ctx context.Context, call fantas
IncludeUsage: openai.Bool(true),
}
- stream := o.client.Chat.Completions.NewStreaming(ctx, *params, objectCallUARequestOptions(call)...)
+ stream := o.client.Chat.Completions.NewStreaming(ctx, *params, objectCallUARequestOptions(call, o.noDefaultUserAgent)...)
return func(yield func(fantasy.ObjectStreamPart) bool) {
if len(warnings) > 0 {
@@ -32,6 +32,7 @@ type options struct {
useResponsesAPI bool
headers map[string]string
userAgent string
+ noDefaultUserAgent bool
client option.HTTPClient
sdkOptions []option.RequestOption
objectMode fantasy.ObjectMode
@@ -142,6 +143,18 @@ func WithUserAgent(ua string) Option {
}
}
+// WithSkipUserAgent prevents the provider from setting a default
+// User-Agent header, preserving the underlying SDK's own User-Agent.
+// This is needed for providers like OpenRouter whose API behaviour depends
+// on the User-Agent matching the SDK that is making the request.
+//
+// This function is provisional and may be removed in a future release.
+func WithSkipUserAgent() Option {
+ return func(o *options) {
+ o.noDefaultUserAgent = true
+ }
+}
+
// WithObjectMode sets the object generation mode.
func WithObjectMode(om fantasy.ObjectMode) Option {
return func(o *options) {
@@ -165,10 +178,19 @@ func (o *provider) LanguageModel(_ context.Context, modelID string) (fantasy.Lan
openaiClientOptions = append(openaiClientOptions, option.WithBaseURL(o.options.baseURL))
}
- 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))
+ if o.options.noDefaultUserAgent {
+ for key, value := range o.options.headers {
+ openaiClientOptions = append(openaiClientOptions, option.WithHeader(key, value))
+ }
+ if o.options.userAgent != "" {
+ openaiClientOptions = append(openaiClientOptions, option.WithHeader("User-Agent", o.options.userAgent))
+ }
+ } else {
+ 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))
+ }
}
if o.options.client != nil {
@@ -189,6 +211,9 @@ func (o *provider) LanguageModel(_ context.Context, modelID string) (fantasy.Lan
}
o.options.languageModelOptions = append(o.options.languageModelOptions, WithLanguageModelObjectMode(o.options.objectMode))
+ if o.options.noDefaultUserAgent {
+ o.options.languageModelOptions = append(o.options.languageModelOptions, WithLanguageModelSkipUserAgent())
+ }
return newLanguageModel(
modelID,
@@ -668,7 +668,7 @@ func toResponsesTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, opti
func (o responsesLanguageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
params, warnings := o.prepareParams(call)
- response, err := o.client.Responses.New(ctx, *params, callUARequestOptions(call)...)
+ response, err := o.client.Responses.New(ctx, *params, callUARequestOptions(call, false)...)
if err != nil {
return nil, toProviderErr(err)
}
@@ -806,7 +806,7 @@ func mapResponsesFinishReason(reason string, hasFunctionCall bool) fantasy.Finis
func (o responsesLanguageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
params, warnings := o.prepareParams(call)
- stream := o.client.Responses.NewStreaming(ctx, *params, callUARequestOptions(call)...)
+ stream := o.client.Responses.NewStreaming(ctx, *params, callUARequestOptions(call, false)...)
finishReason := fantasy.FinishReasonUnknown
var usage fantasy.Usage
@@ -1106,7 +1106,7 @@ func (o responsesLanguageModel) generateObjectWithJSONMode(ctx context.Context,
}
// Make request
- response, err := o.client.Responses.New(ctx, *params, objectCallUARequestOptions(call)...)
+ response, err := o.client.Responses.New(ctx, *params, objectCallUARequestOptions(call, false)...)
if err != nil {
return nil, toProviderErr(err)
}
@@ -1216,7 +1216,7 @@ func (o responsesLanguageModel) streamObjectWithJSONMode(ctx context.Context, ca
Format: responses.ResponseFormatTextConfigParamOfJSONSchema(schemaName, jsonSchemaMap),
}
- stream := o.client.Responses.NewStreaming(ctx, *params, objectCallUARequestOptions(call)...)
+ stream := o.client.Responses.NewStreaming(ctx, *params, objectCallUARequestOptions(call, false)...)
return func(yield func(fantasy.ObjectStreamPart) bool) {
if len(warnings) > 0 {
@@ -31,6 +31,7 @@ func New(opts ...Option) (fantasy.Provider, error) {
openaiOptions: []openai.Option{
openai.WithName(Name),
openai.WithBaseURL(DefaultURL),
+ openai.WithSkipUserAgent(),
},
languageModelOptions: []openai.LanguageModelOption{
openai.WithLanguageModelPrepareCallFunc(languagePrepareModelCall),
@@ -56,7 +56,7 @@ func TestUserAgent(t *testing.T) {
_, _ = model.Generate(t.Context(), fantasy.Call{Prompt: prompt})
require.Len(t, *captured, 1)
- assert.Equal(t, "Charm Fantasy/"+fantasy.Version, (*captured)[0]["User-Agent"])
+ assert.Equal(t, "OpenAI/Go 2.7.1", (*captured)[0]["User-Agent"])
})
t.Run("WithUserAgent wins over default", func(t *testing.T) {