diff --git a/pkg/model/provider/openai/client.go b/pkg/model/provider/openai/client.go index 98ac96c9c..3d10f775f 100644 --- a/pkg/model/provider/openai/client.go +++ b/pkg/model/provider/openai/client.go @@ -375,8 +375,13 @@ func (c *Client) CreateResponseStream( // Skip reasoning configuration entirely if thinking is explicitly disabled (via /think command) thinkingEnabled := c.ModelOptions.Thinking() == nil || *c.ModelOptions.Thinking() if isOpenAIReasoningModel(c.ModelConfig.Model) && thinkingEnabled { - params.Reasoning = shared.ReasoningParam{ - Summary: shared.ReasoningSummaryDetailed, + // Only set reasoning.summary for models that support it. + // Some reasoning models (e.g. o1-pro) reject this parameter. + if supportsReasoningSummary(c.ModelConfig.Model) { + params.Reasoning = shared.ReasoningParam{ + Summary: shared.ReasoningSummaryDetailed, + } + slog.Debug("OpenAI responses request configured with reasoning summary", "model", c.ModelConfig.Model, "summary", "detailed") } // Apply thinking budget as reasoning effort if configured if c.ModelConfig.ThinkingBudget != nil { @@ -388,7 +393,6 @@ func (c *Client) CreateResponseStream( params.Reasoning.Effort = shared.ReasoningEffort(effort) slog.Debug("OpenAI responses request using thinking_budget", "reasoning_effort", effort) } - slog.Debug("OpenAI responses request configured with reasoning summary", "model", c.ModelConfig.Model, "summary", "detailed") } // Apply structured output configuration @@ -903,12 +907,33 @@ func isResponsesModel(model string) bool { func isOpenAIReasoningModel(model string) bool { m := strings.ToLower(model) + + // gpt-5-chat variants are non-reasoning chat models. + if strings.HasPrefix(m, "gpt-5-chat") { + return false + } + return strings.HasPrefix(m, "o1") || strings.HasPrefix(m, "o3") || strings.HasPrefix(m, "o4") || strings.HasPrefix(m, "gpt-5") } +// supportsReasoningSummary returns true for reasoning models that support the +// reasoning.summary parameter. Some reasoning models (e.g. o1-pro) do not +// support it and will reject the request if it is set. +func supportsReasoningSummary(model string) bool { + if !isOpenAIReasoningModel(model) { + return false + } + m := strings.ToLower(model) + // o1-pro does not support reasoning.summary. + if strings.HasPrefix(m, "o1-pro") { + return false + } + return true +} + // getOpenAIReasoningEffort resolves the reasoning effort value from the // model configuration's ThinkingBudget. Returns the effort (minimal|low|medium|high) or an error func getOpenAIReasoningEffort(cfg *latest.ModelConfig) (effort string, err error) { diff --git a/pkg/model/provider/openai/thinking_budget_test.go b/pkg/model/provider/openai/thinking_budget_test.go index ed7d62fbd..bcc026190 100644 --- a/pkg/model/provider/openai/thinking_budget_test.go +++ b/pkg/model/provider/openai/thinking_budget_test.go @@ -40,6 +40,11 @@ func TestIsOpenAIReasoningModel(t *testing.T) { {"gpt-5-turbo", "gpt-5-turbo", true}, {"GPT-5 uppercase", "GPT-5", true}, + // GPT-5-chat variants are non-reasoning chat models + {"gpt-5-chat-latest", "gpt-5-chat-latest", false}, + {"gpt-5-chat", "gpt-5-chat", false}, + {"GPT-5-CHAT uppercase", "GPT-5-CHAT-LATEST", false}, + // GPT-4 series models - should NOT support reasoning {"gpt-4", "gpt-4", false}, {"gpt-4o", "gpt-4o", false}, @@ -51,6 +56,11 @@ func TestIsOpenAIReasoningModel(t *testing.T) { {"gpt-3.5-turbo", "gpt-3.5-turbo", false}, {"gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k", false}, + // O1-pro series models - should support reasoning + {"o1-pro", "o1-pro", true}, + {"o1-pro-2025-03-19", "o1-pro-2025-03-19", true}, + {"O1-PRO uppercase", "O1-PRO", true}, + // Other models - should NOT support reasoning {"text-davinci-003", "text-davinci-003", false}, {"gpt-3", "gpt-3", false}, @@ -70,6 +80,44 @@ func TestIsOpenAIReasoningModel(t *testing.T) { } } +func TestSupportsReasoningSummary(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + model string + expected bool + }{ + // Standard reasoning models - should support summary + {"o1-preview", "o1-preview", true}, + {"o1-mini", "o1-mini", true}, + {"o3-mini", "o3-mini", true}, + {"o4-preview", "o4-preview", true}, + {"gpt-5", "gpt-5", true}, + {"gpt-5-mini", "gpt-5-mini", true}, + + // o1-pro models - do NOT support summary + {"o1-pro", "o1-pro", false}, + {"o1-pro-2025-03-19", "o1-pro-2025-03-19", false}, + {"O1-PRO uppercase", "O1-PRO", false}, + + // Non-reasoning models - do NOT support summary + {"gpt-4o", "gpt-4o", false}, + {"gpt-4-turbo", "gpt-4-turbo", false}, + {"gpt-5-chat-latest", "gpt-5-chat-latest", false}, + {"empty string", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := supportsReasoningSummary(tt.model) + assert.Equal(t, tt.expected, result, "Model %s should return %v", tt.model, tt.expected) + }) + } +} + func TestGetOpenAIReasoningEffort_Success(t *testing.T) { t.Parallel()