chore: add actual provider options to the call

kujtimiihoxha created

Change summary

openrouter/language_model_hooks.go | 59 ++++++++++++++++++++++++++++++++
openrouter/openrouter.go           | 33 ++++++++---------
openrouter/provider_options.go     | 40 ++++++++++----------
3 files changed, 95 insertions(+), 37 deletions(-)

Detailed changes

openrouter/language_model_hooks.go 🔗

@@ -0,0 +1,59 @@
+package openrouter
+
+import (
+	"maps"
+
+	"github.com/charmbracelet/fantasy/ai"
+	openaisdk "github.com/openai/openai-go/v2"
+	"github.com/openai/openai-go/v2/packages/param"
+)
+
+func prepareLanguageModelCall(model ai.LanguageModel, params *openaisdk.ChatCompletionNewParams, call ai.Call) ([]ai.CallWarning, error) {
+	providerOptions := &ProviderOptions{}
+	if v, ok := call.ProviderOptions[Name]; ok {
+		providerOptions, ok = v.(*ProviderOptions)
+		if !ok {
+			return nil, ai.NewInvalidArgumentError("providerOptions", "openrouter provider options should be *openrouter.ProviderOptions", nil)
+		}
+	}
+
+	extraFields := make(map[string]any)
+
+	if providerOptions.Provider != nil {
+		data, err := structToMapJSON(providerOptions.Provider)
+		if err != nil {
+			return nil, err
+		}
+		extraFields["provider"] = data
+	}
+
+	if providerOptions.Reasoning != nil {
+		data, err := structToMapJSON(providerOptions.Reasoning)
+		if err != nil {
+			return nil, err
+		}
+		extraFields["reasoning"] = data
+	}
+
+	if providerOptions.IncludeUsage != nil {
+		extraFields["usage"] = map[string]any{
+			"include": *providerOptions.IncludeUsage,
+		}
+	}
+	if providerOptions.LogitBias != nil {
+		params.LogitBias = providerOptions.LogitBias
+	}
+	if providerOptions.LogProbs != nil {
+		params.Logprobs = param.NewOpt(*providerOptions.LogProbs)
+	}
+	if providerOptions.User != nil {
+		params.User = param.NewOpt(*providerOptions.User)
+	}
+	if providerOptions.ParallelToolCalls != nil {
+		params.ParallelToolCalls = param.NewOpt(*providerOptions.ParallelToolCalls)
+	}
+
+	maps.Copy(extraFields, providerOptions.ExtraBody)
+	params.SetExtraFields(extraFields)
+	return nil, nil
+}

openrouter/openrouter.go 🔗

@@ -1,9 +1,10 @@
 package openrouter
 
 import (
+	"encoding/json"
+
 	"github.com/charmbracelet/fantasy/ai"
 	"github.com/charmbracelet/fantasy/openai"
-	openaisdk "github.com/openai/openai-go/v2"
 	"github.com/openai/openai-go/v2/option"
 )
 
@@ -17,27 +18,12 @@ const (
 
 type Option = func(*options)
 
-func prepareCallWithOptions(model ai.LanguageModel, params *openaisdk.ChatCompletionNewParams, call ai.Call) ([]ai.CallWarning, error) {
-	providerOptions := &ProviderOptions{}
-	if v, ok := call.ProviderOptions[Name]; ok {
-		providerOptions, ok = v.(*ProviderOptions)
-		if !ok {
-			return nil, ai.NewInvalidArgumentError("providerOptions", "openrouter provider options should be *openrouter.ProviderOptions", nil)
-		}
-	}
-	_ = providerOptions
-
-	// HANDLE OPENROUTER call modification here
-
-	return nil, nil
-}
-
 func New(opts ...Option) ai.Provider {
 	providerOptions := options{
 		openaiOptions: []openai.Option{
 			openai.WithBaseURL(DefaultURL),
 			openai.WithLanguageModelOptions(
-				openai.WithPrepareLanguageModelCallFunc(prepareCallWithOptions),
+				openai.WithPrepareLanguageModelCallFunc(prepareLanguageModelCall),
 			),
 		},
 	}
@@ -70,3 +56,16 @@ func WithHTTPClient(client option.HTTPClient) Option {
 		o.openaiOptions = append(o.openaiOptions, openai.WithHTTPClient(client))
 	}
 }
+
+func structToMapJSON(s any) (map[string]any, error) {
+	var result map[string]any
+	jsonBytes, err := json.Marshal(s)
+	if err != nil {
+		return nil, err
+	}
+	err = json.Unmarshal(jsonBytes, &result)
+	if err != nil {
+		return nil, err
+	}
+	return result, nil
+}

openrouter/provider_options.go 🔗

@@ -20,51 +20,51 @@ func (*ProviderMetadata) Options() {}
 
 type ReasoningOptions struct {
 	// Whether reasoning is enabled
-	Enabled *bool `json:"enabled"`
+	Enabled *bool `json:"enabled,omitempty"`
 	// Whether to exclude reasoning from the response
-	Exclude *bool `json:"exclude"`
+	Exclude *bool `json:"exclude,omitempty"`
 	// Maximum number of tokens to use for reasoning
-	MaxTokens *int64 `json:"max_tokens"`
+	MaxTokens *int64 `json:"max_tokens,omitempty"`
 	// Reasoning effort level: "low" | "medium" | "high"
-	Effort *ReasoningEffort `json:"effort"`
+	Effort *ReasoningEffort `json:"effort,omitempty"`
 }
 
 type Provider struct {
 	// List of provider slugs to try in order (e.g. ["anthropic", "openai"])
-	Order []string `json:"order"`
+	Order []string `json:"order,omitempty"`
 	// Whether to allow backup providers when primary is unavailable (default: true)
-	AllowFallbacks *bool `json:"allow_fallbacks"`
+	AllowFallbacks *bool `json:"allow_fallbacks,omitempty"`
 	// Only use providers that support all parameters in your request (default: false)
-	RequireParameters *bool `json:"require_parameters"`
+	RequireParameters *bool `json:"require_parameters,omitempty"`
 	// Control whether to use providers that may store data: "allow" | "deny"
-	DataCollection *string `json:"data_collection"`
+	DataCollection *string `json:"data_collection,omitempty"`
 	// List of provider slugs to allow for this request
-	Only []string `json:"only"`
+	Only []string `json:"only,omitempty"`
 	// List of provider slugs to skip for this request
-	Ignore []string `json:"ignore"`
+	Ignore []string `json:"ignore,omitempty"`
 	// List of quantization levels to filter by (e.g. ["int4", "int8"])
-	Quantizations []string `json:"quantizations"`
+	Quantizations []string `json:"quantizations,omitempty"`
 	// Sort providers by "price" | "throughput" | "latency"
-	Sort *string `json:"sort"`
+	Sort *string `json:"sort,omitempty"`
 }
 
 type ProviderOptions struct {
-	Reasoning    *ReasoningOptions `json:"reasoning"`
-	ExtraBody    map[string]any    `json:"extra_body"`
-	IncludeUsage *bool             `json:"include_usage"`
+	Reasoning    *ReasoningOptions `json:"reasoning,omitempty"`
+	ExtraBody    map[string]any    `json:"extra_body,omitempty"`
+	IncludeUsage *bool             `json:"include_usage,omitempty"`
 	// Modify the likelihood of specified tokens appearing in the completion.
 	// Accepts a map that maps tokens (specified by their token ID) to an associated bias value from -100 to 100.
 	// The bias is added to the logits generated by the model prior to sampling.
-	LogitBias map[string]int64 `json:"logit_bias"`
+	LogitBias map[string]int64 `json:"logit_bias,omitempty"`
 	// Return the log probabilities of the tokens. Including logprobs will increase the response size.
 	// Setting to true will return the log probabilities of the tokens that were generated.
-	LogProbs *bool `json:"log_probs"`
+	LogProbs *bool `json:"log_probs,omitempty"`
 	// Whether to enable parallel function calling during tool use. Default to true.
-	ParallelToolCalls *bool `json:"parallel_tool_calls"`
+	ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
 	// A unique identifier representing your end-user, which can help OpenRouter to monitor and detect abuse.
-	User *string `json:"user"`
+	User *string `json:"user,omitempty"`
 	// Provider routing preferences to control request routing behavior
-	Provider *Provider `json:"provider"`
+	Provider *Provider `json:"provider,omitempty"`
 	// TODO: add the web search plugin config
 }