From 525a37908886bb46c80ccef26499b6193e40c4d3 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 24 Feb 2026 15:43:39 -0500 Subject: [PATCH] refactor(chatprovider): move fallback option merging out of chatd --- coderd/chatd/chatd.go | 349 +----------------- coderd/chatd/chatprovider/chatprovider.go | 331 +++++++++++++++++ .../chatd/chatprovider/chatprovider_test.go | 110 ++++++ 3 files changed, 442 insertions(+), 348 deletions(-) diff --git a/coderd/chatd/chatd.go b/coderd/chatd/chatd.go index 1711e774f4..5a10643f8b 100644 --- a/coderd/chatd/chatd.go +++ b/coderd/chatd/chatd.go @@ -1398,360 +1398,13 @@ func applyFallbackChatModelConfig( return config } - mergeMissingChatModelCallConfig( + chatprovider.MergeMissingCallConfig( &config.ChatModelCallConfig, defaults.ChatModelCallConfig, ) return config } -func mergeMissingChatModelCallConfig( - dst *codersdk.ChatModelCallConfig, - defaults codersdk.ChatModelCallConfig, -) { - if dst.MaxOutputTokens == nil { - dst.MaxOutputTokens = defaults.MaxOutputTokens - } - if dst.Temperature == nil { - dst.Temperature = defaults.Temperature - } - if dst.TopP == nil { - dst.TopP = defaults.TopP - } - if dst.TopK == nil { - dst.TopK = defaults.TopK - } - if dst.PresencePenalty == nil { - dst.PresencePenalty = defaults.PresencePenalty - } - if dst.FrequencyPenalty == nil { - dst.FrequencyPenalty = defaults.FrequencyPenalty - } - mergeMissingChatModelProviderOptions(&dst.ProviderOptions, defaults.ProviderOptions) -} - -func mergeMissingChatModelProviderOptions( - dst **codersdk.ChatModelProviderOptions, - defaults *codersdk.ChatModelProviderOptions, -) { - if defaults == nil { - return - } - if *dst == nil { - copied := *defaults - *dst = &copied - return - } - - mergeMissingOpenAIProviderOptions(&(*dst).OpenAI, defaults.OpenAI) - mergeMissingAnthropicProviderOptions(&(*dst).Anthropic, defaults.Anthropic) - mergeMissingGoogleProviderOptions(&(*dst).Google, defaults.Google) - mergeMissingOpenAICompatProviderOptions(&(*dst).OpenAICompat, defaults.OpenAICompat) - mergeMissingOpenRouterProviderOptions(&(*dst).OpenRouter, defaults.OpenRouter) - mergeMissingVercelProviderOptions(&(*dst).Vercel, defaults.Vercel) -} - -func mergeMissingOpenAIProviderOptions( - dst **codersdk.ChatModelOpenAIProviderOptions, - defaults *codersdk.ChatModelOpenAIProviderOptions, -) { - if defaults == nil { - return - } - if *dst == nil { - copied := *defaults - *dst = &copied - return - } - - current := *dst - if current.Include == nil { - current.Include = defaults.Include - } - if current.Instructions == nil { - current.Instructions = defaults.Instructions - } - if current.LogitBias == nil { - current.LogitBias = defaults.LogitBias - } - if current.LogProbs == nil { - current.LogProbs = defaults.LogProbs - } - if current.TopLogProbs == nil { - current.TopLogProbs = defaults.TopLogProbs - } - if current.MaxToolCalls == nil { - current.MaxToolCalls = defaults.MaxToolCalls - } - if current.ParallelToolCalls == nil { - current.ParallelToolCalls = defaults.ParallelToolCalls - } - if current.User == nil { - current.User = defaults.User - } - if current.ReasoningEffort == nil { - current.ReasoningEffort = defaults.ReasoningEffort - } - if current.ReasoningSummary == nil { - current.ReasoningSummary = defaults.ReasoningSummary - } - if current.MaxCompletionTokens == nil { - current.MaxCompletionTokens = defaults.MaxCompletionTokens - } - if current.TextVerbosity == nil { - current.TextVerbosity = defaults.TextVerbosity - } - if current.Prediction == nil { - current.Prediction = defaults.Prediction - } - if current.Store == nil { - current.Store = defaults.Store - } - if current.Metadata == nil { - current.Metadata = defaults.Metadata - } - if current.PromptCacheKey == nil { - current.PromptCacheKey = defaults.PromptCacheKey - } - if current.SafetyIdentifier == nil { - current.SafetyIdentifier = defaults.SafetyIdentifier - } - if current.ServiceTier == nil { - current.ServiceTier = defaults.ServiceTier - } - if current.StructuredOutputs == nil { - current.StructuredOutputs = defaults.StructuredOutputs - } - if current.StrictJSONSchema == nil { - current.StrictJSONSchema = defaults.StrictJSONSchema - } -} - -func mergeMissingAnthropicProviderOptions( - dst **codersdk.ChatModelAnthropicProviderOptions, - defaults *codersdk.ChatModelAnthropicProviderOptions, -) { - if defaults == nil { - return - } - if *dst == nil { - copied := *defaults - *dst = &copied - return - } - - current := *dst - if current.SendReasoning == nil { - current.SendReasoning = defaults.SendReasoning - } - if current.Thinking == nil { - current.Thinking = defaults.Thinking - } else if defaults.Thinking != nil && current.Thinking.BudgetTokens == nil { - current.Thinking.BudgetTokens = defaults.Thinking.BudgetTokens - } - if current.Effort == nil { - current.Effort = defaults.Effort - } - if current.DisableParallelToolUse == nil { - current.DisableParallelToolUse = defaults.DisableParallelToolUse - } -} - -func mergeMissingGoogleProviderOptions( - dst **codersdk.ChatModelGoogleProviderOptions, - defaults *codersdk.ChatModelGoogleProviderOptions, -) { - if defaults == nil { - return - } - if *dst == nil { - copied := *defaults - *dst = &copied - return - } - - current := *dst - if current.ThinkingConfig == nil { - current.ThinkingConfig = defaults.ThinkingConfig - } else if defaults.ThinkingConfig != nil { - if current.ThinkingConfig.ThinkingBudget == nil { - current.ThinkingConfig.ThinkingBudget = defaults.ThinkingConfig.ThinkingBudget - } - if current.ThinkingConfig.IncludeThoughts == nil { - current.ThinkingConfig.IncludeThoughts = defaults.ThinkingConfig.IncludeThoughts - } - } - if strings.TrimSpace(current.CachedContent) == "" { - current.CachedContent = defaults.CachedContent - } - if current.SafetySettings == nil { - current.SafetySettings = defaults.SafetySettings - } - if strings.TrimSpace(current.Threshold) == "" { - current.Threshold = defaults.Threshold - } -} - -func mergeMissingOpenAICompatProviderOptions( - dst **codersdk.ChatModelOpenAICompatProviderOptions, - defaults *codersdk.ChatModelOpenAICompatProviderOptions, -) { - if defaults == nil { - return - } - if *dst == nil { - copied := *defaults - *dst = &copied - return - } - - current := *dst - if current.User == nil { - current.User = defaults.User - } - if current.ReasoningEffort == nil { - current.ReasoningEffort = defaults.ReasoningEffort - } -} - -func mergeMissingOpenRouterProviderOptions( - dst **codersdk.ChatModelOpenRouterProviderOptions, - defaults *codersdk.ChatModelOpenRouterProviderOptions, -) { - if defaults == nil { - return - } - if *dst == nil { - copied := *defaults - *dst = &copied - return - } - - current := *dst - if current.Reasoning == nil { - current.Reasoning = defaults.Reasoning - } else if defaults.Reasoning != nil { - if current.Reasoning.Enabled == nil { - current.Reasoning.Enabled = defaults.Reasoning.Enabled - } - if current.Reasoning.Exclude == nil { - current.Reasoning.Exclude = defaults.Reasoning.Exclude - } - if current.Reasoning.MaxTokens == nil { - current.Reasoning.MaxTokens = defaults.Reasoning.MaxTokens - } - if current.Reasoning.Effort == nil { - current.Reasoning.Effort = defaults.Reasoning.Effort - } - } - if current.ExtraBody == nil { - current.ExtraBody = defaults.ExtraBody - } - if current.IncludeUsage == nil { - current.IncludeUsage = defaults.IncludeUsage - } - if current.LogitBias == nil { - current.LogitBias = defaults.LogitBias - } - if current.LogProbs == nil { - current.LogProbs = defaults.LogProbs - } - if current.ParallelToolCalls == nil { - current.ParallelToolCalls = defaults.ParallelToolCalls - } - if current.User == nil { - current.User = defaults.User - } - if current.Provider == nil { - current.Provider = defaults.Provider - } else if defaults.Provider != nil { - if current.Provider.Order == nil { - current.Provider.Order = defaults.Provider.Order - } - if current.Provider.AllowFallbacks == nil { - current.Provider.AllowFallbacks = defaults.Provider.AllowFallbacks - } - if current.Provider.RequireParameters == nil { - current.Provider.RequireParameters = defaults.Provider.RequireParameters - } - if current.Provider.DataCollection == nil { - current.Provider.DataCollection = defaults.Provider.DataCollection - } - if current.Provider.Only == nil { - current.Provider.Only = defaults.Provider.Only - } - if current.Provider.Ignore == nil { - current.Provider.Ignore = defaults.Provider.Ignore - } - if current.Provider.Quantizations == nil { - current.Provider.Quantizations = defaults.Provider.Quantizations - } - if current.Provider.Sort == nil { - current.Provider.Sort = defaults.Provider.Sort - } - } -} - -func mergeMissingVercelProviderOptions( - dst **codersdk.ChatModelVercelProviderOptions, - defaults *codersdk.ChatModelVercelProviderOptions, -) { - if defaults == nil { - return - } - if *dst == nil { - copied := *defaults - *dst = &copied - return - } - - current := *dst - if current.Reasoning == nil { - current.Reasoning = defaults.Reasoning - } else if defaults.Reasoning != nil { - if current.Reasoning.Enabled == nil { - current.Reasoning.Enabled = defaults.Reasoning.Enabled - } - if current.Reasoning.MaxTokens == nil { - current.Reasoning.MaxTokens = defaults.Reasoning.MaxTokens - } - if current.Reasoning.Effort == nil { - current.Reasoning.Effort = defaults.Reasoning.Effort - } - if current.Reasoning.Exclude == nil { - current.Reasoning.Exclude = defaults.Reasoning.Exclude - } - } - if current.ProviderOptions == nil { - current.ProviderOptions = defaults.ProviderOptions - } else if defaults.ProviderOptions != nil { - if current.ProviderOptions.Order == nil { - current.ProviderOptions.Order = defaults.ProviderOptions.Order - } - if current.ProviderOptions.Models == nil { - current.ProviderOptions.Models = defaults.ProviderOptions.Models - } - } - if current.User == nil { - current.User = defaults.User - } - if current.LogitBias == nil { - current.LogitBias = defaults.LogitBias - } - if current.LogProbs == nil { - current.LogProbs = defaults.LogProbs - } - if current.TopLogProbs == nil { - current.TopLogProbs = defaults.TopLogProbs - } - if current.ParallelToolCalls == nil { - current.ParallelToolCalls = defaults.ParallelToolCalls - } - if current.ExtraBody == nil { - current.ExtraBody = defaults.ExtraBody - } -} - func (p *Processor) resolveFallbackChatModelConfig( ctx context.Context, providerKeys ProviderAPIKeys, diff --git a/coderd/chatd/chatprovider/chatprovider.go b/coderd/chatd/chatprovider/chatprovider.go index 9b71069b71..9c778f6649 100644 --- a/coderd/chatd/chatprovider/chatprovider.go +++ b/coderd/chatd/chatprovider/chatprovider.go @@ -557,6 +557,337 @@ func normalizedEnumValue(value string, allowed ...string) *string { return nil } +// MergeMissingCallConfig fills unset call config values from defaults. +func MergeMissingCallConfig( + dst *codersdk.ChatModelCallConfig, + defaults codersdk.ChatModelCallConfig, +) { + if dst.MaxOutputTokens == nil { + dst.MaxOutputTokens = defaults.MaxOutputTokens + } + if dst.Temperature == nil { + dst.Temperature = defaults.Temperature + } + if dst.TopP == nil { + dst.TopP = defaults.TopP + } + if dst.TopK == nil { + dst.TopK = defaults.TopK + } + if dst.PresencePenalty == nil { + dst.PresencePenalty = defaults.PresencePenalty + } + if dst.FrequencyPenalty == nil { + dst.FrequencyPenalty = defaults.FrequencyPenalty + } + MergeMissingProviderOptions(&dst.ProviderOptions, defaults.ProviderOptions) +} + +// MergeMissingProviderOptions fills unset provider option fields from defaults. +func MergeMissingProviderOptions( + dst **codersdk.ChatModelProviderOptions, + defaults *codersdk.ChatModelProviderOptions, +) { + if defaults == nil { + return + } + if *dst == nil { + copied := *defaults + *dst = &copied + return + } + + current := *dst + for _, provider := range []string{ + fantasyopenai.Name, + fantasyanthropic.Name, + fantasygoogle.Name, + fantasyopenaicompat.Name, + fantasyopenrouter.Name, + fantasyvercel.Name, + } { + switch provider { + case fantasyopenai.Name: + if defaults.OpenAI == nil { + continue + } + if current.OpenAI == nil { + copied := *defaults.OpenAI + current.OpenAI = &copied + continue + } + dstOpenAI := current.OpenAI + defaultOpenAI := defaults.OpenAI + if dstOpenAI.Include == nil { + dstOpenAI.Include = defaultOpenAI.Include + } + if dstOpenAI.Instructions == nil { + dstOpenAI.Instructions = defaultOpenAI.Instructions + } + if dstOpenAI.LogitBias == nil { + dstOpenAI.LogitBias = defaultOpenAI.LogitBias + } + if dstOpenAI.LogProbs == nil { + dstOpenAI.LogProbs = defaultOpenAI.LogProbs + } + if dstOpenAI.TopLogProbs == nil { + dstOpenAI.TopLogProbs = defaultOpenAI.TopLogProbs + } + if dstOpenAI.MaxToolCalls == nil { + dstOpenAI.MaxToolCalls = defaultOpenAI.MaxToolCalls + } + if dstOpenAI.ParallelToolCalls == nil { + dstOpenAI.ParallelToolCalls = defaultOpenAI.ParallelToolCalls + } + if dstOpenAI.User == nil { + dstOpenAI.User = defaultOpenAI.User + } + if dstOpenAI.ReasoningEffort == nil { + dstOpenAI.ReasoningEffort = defaultOpenAI.ReasoningEffort + } + if dstOpenAI.ReasoningSummary == nil { + dstOpenAI.ReasoningSummary = defaultOpenAI.ReasoningSummary + } + if dstOpenAI.MaxCompletionTokens == nil { + dstOpenAI.MaxCompletionTokens = defaultOpenAI.MaxCompletionTokens + } + if dstOpenAI.TextVerbosity == nil { + dstOpenAI.TextVerbosity = defaultOpenAI.TextVerbosity + } + if dstOpenAI.Prediction == nil { + dstOpenAI.Prediction = defaultOpenAI.Prediction + } + if dstOpenAI.Store == nil { + dstOpenAI.Store = defaultOpenAI.Store + } + if dstOpenAI.Metadata == nil { + dstOpenAI.Metadata = defaultOpenAI.Metadata + } + if dstOpenAI.PromptCacheKey == nil { + dstOpenAI.PromptCacheKey = defaultOpenAI.PromptCacheKey + } + if dstOpenAI.SafetyIdentifier == nil { + dstOpenAI.SafetyIdentifier = defaultOpenAI.SafetyIdentifier + } + if dstOpenAI.ServiceTier == nil { + dstOpenAI.ServiceTier = defaultOpenAI.ServiceTier + } + if dstOpenAI.StructuredOutputs == nil { + dstOpenAI.StructuredOutputs = defaultOpenAI.StructuredOutputs + } + if dstOpenAI.StrictJSONSchema == nil { + dstOpenAI.StrictJSONSchema = defaultOpenAI.StrictJSONSchema + } + + case fantasyanthropic.Name: + if defaults.Anthropic == nil { + continue + } + if current.Anthropic == nil { + copied := *defaults.Anthropic + current.Anthropic = &copied + continue + } + dstAnthropic := current.Anthropic + defaultAnthropic := defaults.Anthropic + if dstAnthropic.SendReasoning == nil { + dstAnthropic.SendReasoning = defaultAnthropic.SendReasoning + } + if dstAnthropic.Thinking == nil { + dstAnthropic.Thinking = defaultAnthropic.Thinking + } else if defaultAnthropic.Thinking != nil && + dstAnthropic.Thinking.BudgetTokens == nil { + dstAnthropic.Thinking.BudgetTokens = defaultAnthropic.Thinking.BudgetTokens + } + if dstAnthropic.Effort == nil { + dstAnthropic.Effort = defaultAnthropic.Effort + } + if dstAnthropic.DisableParallelToolUse == nil { + dstAnthropic.DisableParallelToolUse = defaultAnthropic.DisableParallelToolUse + } + + case fantasygoogle.Name: + if defaults.Google == nil { + continue + } + if current.Google == nil { + copied := *defaults.Google + current.Google = &copied + continue + } + dstGoogle := current.Google + defaultGoogle := defaults.Google + if dstGoogle.ThinkingConfig == nil { + dstGoogle.ThinkingConfig = defaultGoogle.ThinkingConfig + } else if defaultGoogle.ThinkingConfig != nil { + if dstGoogle.ThinkingConfig.ThinkingBudget == nil { + dstGoogle.ThinkingConfig.ThinkingBudget = defaultGoogle.ThinkingConfig.ThinkingBudget + } + if dstGoogle.ThinkingConfig.IncludeThoughts == nil { + dstGoogle.ThinkingConfig.IncludeThoughts = defaultGoogle.ThinkingConfig.IncludeThoughts + } + } + if strings.TrimSpace(dstGoogle.CachedContent) == "" { + dstGoogle.CachedContent = defaultGoogle.CachedContent + } + if dstGoogle.SafetySettings == nil { + dstGoogle.SafetySettings = defaultGoogle.SafetySettings + } + if strings.TrimSpace(dstGoogle.Threshold) == "" { + dstGoogle.Threshold = defaultGoogle.Threshold + } + + case fantasyopenaicompat.Name: + if defaults.OpenAICompat == nil { + continue + } + if current.OpenAICompat == nil { + copied := *defaults.OpenAICompat + current.OpenAICompat = &copied + continue + } + dstCompat := current.OpenAICompat + defaultCompat := defaults.OpenAICompat + if dstCompat.User == nil { + dstCompat.User = defaultCompat.User + } + if dstCompat.ReasoningEffort == nil { + dstCompat.ReasoningEffort = defaultCompat.ReasoningEffort + } + + case fantasyopenrouter.Name: + if defaults.OpenRouter == nil { + continue + } + if current.OpenRouter == nil { + copied := *defaults.OpenRouter + current.OpenRouter = &copied + continue + } + dstRouter := current.OpenRouter + defaultRouter := defaults.OpenRouter + if dstRouter.Reasoning == nil { + dstRouter.Reasoning = defaultRouter.Reasoning + } else if defaultRouter.Reasoning != nil { + if dstRouter.Reasoning.Enabled == nil { + dstRouter.Reasoning.Enabled = defaultRouter.Reasoning.Enabled + } + if dstRouter.Reasoning.Exclude == nil { + dstRouter.Reasoning.Exclude = defaultRouter.Reasoning.Exclude + } + if dstRouter.Reasoning.MaxTokens == nil { + dstRouter.Reasoning.MaxTokens = defaultRouter.Reasoning.MaxTokens + } + if dstRouter.Reasoning.Effort == nil { + dstRouter.Reasoning.Effort = defaultRouter.Reasoning.Effort + } + } + if dstRouter.ExtraBody == nil { + dstRouter.ExtraBody = defaultRouter.ExtraBody + } + if dstRouter.IncludeUsage == nil { + dstRouter.IncludeUsage = defaultRouter.IncludeUsage + } + if dstRouter.LogitBias == nil { + dstRouter.LogitBias = defaultRouter.LogitBias + } + if dstRouter.LogProbs == nil { + dstRouter.LogProbs = defaultRouter.LogProbs + } + if dstRouter.ParallelToolCalls == nil { + dstRouter.ParallelToolCalls = defaultRouter.ParallelToolCalls + } + if dstRouter.User == nil { + dstRouter.User = defaultRouter.User + } + if dstRouter.Provider == nil { + dstRouter.Provider = defaultRouter.Provider + } else if defaultRouter.Provider != nil { + if dstRouter.Provider.Order == nil { + dstRouter.Provider.Order = defaultRouter.Provider.Order + } + if dstRouter.Provider.AllowFallbacks == nil { + dstRouter.Provider.AllowFallbacks = defaultRouter.Provider.AllowFallbacks + } + if dstRouter.Provider.RequireParameters == nil { + dstRouter.Provider.RequireParameters = defaultRouter.Provider.RequireParameters + } + if dstRouter.Provider.DataCollection == nil { + dstRouter.Provider.DataCollection = defaultRouter.Provider.DataCollection + } + if dstRouter.Provider.Only == nil { + dstRouter.Provider.Only = defaultRouter.Provider.Only + } + if dstRouter.Provider.Ignore == nil { + dstRouter.Provider.Ignore = defaultRouter.Provider.Ignore + } + if dstRouter.Provider.Quantizations == nil { + dstRouter.Provider.Quantizations = defaultRouter.Provider.Quantizations + } + if dstRouter.Provider.Sort == nil { + dstRouter.Provider.Sort = defaultRouter.Provider.Sort + } + } + + case fantasyvercel.Name: + if defaults.Vercel == nil { + continue + } + if current.Vercel == nil { + copied := *defaults.Vercel + current.Vercel = &copied + continue + } + dstVercel := current.Vercel + defaultVercel := defaults.Vercel + if dstVercel.Reasoning == nil { + dstVercel.Reasoning = defaultVercel.Reasoning + } else if defaultVercel.Reasoning != nil { + if dstVercel.Reasoning.Enabled == nil { + dstVercel.Reasoning.Enabled = defaultVercel.Reasoning.Enabled + } + if dstVercel.Reasoning.MaxTokens == nil { + dstVercel.Reasoning.MaxTokens = defaultVercel.Reasoning.MaxTokens + } + if dstVercel.Reasoning.Effort == nil { + dstVercel.Reasoning.Effort = defaultVercel.Reasoning.Effort + } + if dstVercel.Reasoning.Exclude == nil { + dstVercel.Reasoning.Exclude = defaultVercel.Reasoning.Exclude + } + } + if dstVercel.ProviderOptions == nil { + dstVercel.ProviderOptions = defaultVercel.ProviderOptions + } else if defaultVercel.ProviderOptions != nil { + if dstVercel.ProviderOptions.Order == nil { + dstVercel.ProviderOptions.Order = defaultVercel.ProviderOptions.Order + } + if dstVercel.ProviderOptions.Models == nil { + dstVercel.ProviderOptions.Models = defaultVercel.ProviderOptions.Models + } + } + if dstVercel.User == nil { + dstVercel.User = defaultVercel.User + } + if dstVercel.LogitBias == nil { + dstVercel.LogitBias = defaultVercel.LogitBias + } + if dstVercel.LogProbs == nil { + dstVercel.LogProbs = defaultVercel.LogProbs + } + if dstVercel.TopLogProbs == nil { + dstVercel.TopLogProbs = defaultVercel.TopLogProbs + } + if dstVercel.ParallelToolCalls == nil { + dstVercel.ParallelToolCalls = defaultVercel.ParallelToolCalls + } + if dstVercel.ExtraBody == nil { + dstVercel.ExtraBody = defaultVercel.ExtraBody + } + } + } +} + // ModelFromConfig resolves a provider/model pair and constructs a fantasy // language model client using the provided provider credentials. func ModelFromConfig( diff --git a/coderd/chatd/chatprovider/chatprovider_test.go b/coderd/chatd/chatprovider/chatprovider_test.go index b1f3ae887d..277aead26c 100644 --- a/coderd/chatd/chatprovider/chatprovider_test.go +++ b/coderd/chatd/chatprovider/chatprovider_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/chatd/chatprovider" + "github.com/coder/coder/v2/codersdk" ) func TestReasoningEffortFromChat(t *testing.T) { @@ -76,6 +77,115 @@ func TestReasoningEffortFromChat(t *testing.T) { } } +func TestMergeMissingProviderOptions_OpenRouterNested(t *testing.T) { + t.Parallel() + + options := &codersdk.ChatModelProviderOptions{ + OpenRouter: &codersdk.ChatModelOpenRouterProviderOptions{ + Reasoning: &codersdk.ChatModelOpenRouterReasoningOptions{ + Enabled: boolPtr(true), + }, + Provider: &codersdk.ChatModelOpenRouterProvider{ + Order: []string{"openai"}, + }, + }, + } + defaults := &codersdk.ChatModelProviderOptions{ + OpenRouter: &codersdk.ChatModelOpenRouterProviderOptions{ + Reasoning: &codersdk.ChatModelOpenRouterReasoningOptions{ + Enabled: boolPtr(false), + Exclude: boolPtr(true), + MaxTokens: int64Ptr(123), + Effort: stringPtr("high"), + }, + IncludeUsage: boolPtr(true), + Provider: &codersdk.ChatModelOpenRouterProvider{ + Order: []string{"anthropic"}, + AllowFallbacks: boolPtr(true), + RequireParameters: boolPtr(false), + DataCollection: stringPtr("allow"), + Only: []string{"openai"}, + Ignore: []string{"foo"}, + Quantizations: []string{"int8"}, + Sort: stringPtr("latency"), + }, + }, + } + + chatprovider.MergeMissingProviderOptions(&options, defaults) + + require.NotNil(t, options) + require.NotNil(t, options.OpenRouter) + require.NotNil(t, options.OpenRouter.Reasoning) + require.True(t, *options.OpenRouter.Reasoning.Enabled) + require.Equal(t, true, *options.OpenRouter.Reasoning.Exclude) + require.EqualValues(t, 123, *options.OpenRouter.Reasoning.MaxTokens) + require.Equal(t, "high", *options.OpenRouter.Reasoning.Effort) + require.NotNil(t, options.OpenRouter.IncludeUsage) + require.True(t, *options.OpenRouter.IncludeUsage) + + require.NotNil(t, options.OpenRouter.Provider) + require.Equal(t, []string{"openai"}, options.OpenRouter.Provider.Order) + require.NotNil(t, options.OpenRouter.Provider.AllowFallbacks) + require.True(t, *options.OpenRouter.Provider.AllowFallbacks) + require.NotNil(t, options.OpenRouter.Provider.RequireParameters) + require.False(t, *options.OpenRouter.Provider.RequireParameters) + require.Equal(t, "allow", *options.OpenRouter.Provider.DataCollection) + require.Equal(t, []string{"openai"}, options.OpenRouter.Provider.Only) + require.Equal(t, []string{"foo"}, options.OpenRouter.Provider.Ignore) + require.Equal(t, []string{"int8"}, options.OpenRouter.Provider.Quantizations) + require.Equal(t, "latency", *options.OpenRouter.Provider.Sort) +} + +func TestMergeMissingCallConfig_FillsUnsetFields(t *testing.T) { + t.Parallel() + + dst := codersdk.ChatModelCallConfig{ + Temperature: float64Ptr(0.2), + ProviderOptions: &codersdk.ChatModelProviderOptions{ + OpenAI: &codersdk.ChatModelOpenAIProviderOptions{ + User: stringPtr("alice"), + }, + }, + } + defaults := codersdk.ChatModelCallConfig{ + MaxOutputTokens: int64Ptr(512), + Temperature: float64Ptr(0.9), + TopP: float64Ptr(0.8), + ProviderOptions: &codersdk.ChatModelProviderOptions{ + OpenAI: &codersdk.ChatModelOpenAIProviderOptions{ + User: stringPtr("bob"), + ReasoningEffort: stringPtr("medium"), + }, + }, + } + + chatprovider.MergeMissingCallConfig(&dst, defaults) + + require.NotNil(t, dst.MaxOutputTokens) + require.EqualValues(t, 512, *dst.MaxOutputTokens) + require.NotNil(t, dst.Temperature) + require.Equal(t, 0.2, *dst.Temperature) + require.NotNil(t, dst.TopP) + require.Equal(t, 0.8, *dst.TopP) + require.NotNil(t, dst.ProviderOptions) + require.NotNil(t, dst.ProviderOptions.OpenAI) + require.Equal(t, "alice", *dst.ProviderOptions.OpenAI.User) + require.Equal(t, "medium", *dst.ProviderOptions.OpenAI.ReasoningEffort) +} + func stringPtr(value string) *string { return &value } + +func boolPtr(value bool) *bool { + return &value +} + +func int64Ptr(value int64) *int64 { + return &value +} + +func float64Ptr(value float64) *float64 { + return &value +}