From 21786bfccf6320b5467c5bba202b37763f453449 Mon Sep 17 00:00:00 2001 From: kujtimiihoxha Date: Fri, 19 Sep 2025 11:40:52 +0200 Subject: [PATCH] chore: add actual provider options to the call --- openrouter/language_model_hooks.go | 59 ++++++++++++++++++++++++++++++ openrouter/openrouter.go | 33 ++++++++--------- openrouter/provider_options.go | 40 ++++++++++---------- 3 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 openrouter/language_model_hooks.go diff --git a/openrouter/language_model_hooks.go b/openrouter/language_model_hooks.go new file mode 100644 index 0000000000000000000000000000000000000000..ac8640591066b159505852664c6ec7ea2d04c42f --- /dev/null +++ b/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 +} diff --git a/openrouter/openrouter.go b/openrouter/openrouter.go index eae2c1dd8fcbb47fe76008642cbaee09ea71286f..4e7cee3e53af047db4e85ef2d2d1fc04c9ae324e 100644 --- a/openrouter/openrouter.go +++ b/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 +} diff --git a/openrouter/provider_options.go b/openrouter/provider_options.go index a9bbbbd6738d4776cca9627e2bc9171b4bcdd5e5..c3b5c4b54efc7165ac6440a661602c951edcf3c6 100644 --- a/openrouter/provider_options.go +++ b/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 }