Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions pkg/model/provider/openai/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
48 changes: 48 additions & 0 deletions pkg/model/provider/openai/thinking_budget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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},
Expand All @@ -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()

Expand Down
Loading