Compare commits

...

2 Commits

Author SHA1 Message Date
Parth Sareen
ea01af6f76 openai: map responses reasoning effort to think (#15789) 2026-04-24 02:49:36 -07:00
Parth Sareen
c2ebb4d57c api: accept "max" as a think value (#15787) 2026-04-24 01:49:39 -07:00
8 changed files with 180 additions and 15 deletions

View File

@@ -1080,7 +1080,7 @@ func DefaultOptions() Options {
}
}
// ThinkValue represents a value that can be a boolean or a string ("high", "medium", "low")
// ThinkValue represents a value that can be a boolean or a string ("high", "medium", "low", "max")
type ThinkValue struct {
// Value can be a bool or string
Value interface{}
@@ -1096,7 +1096,7 @@ func (t *ThinkValue) IsValid() bool {
case bool:
return true
case string:
return v == "high" || v == "medium" || v == "low"
return v == "high" || v == "medium" || v == "low" || v == "max"
default:
return false
}
@@ -1130,8 +1130,8 @@ func (t *ThinkValue) Bool() bool {
case bool:
return v
case string:
// Any string value ("high", "medium", "low") means thinking is enabled
return v == "high" || v == "medium" || v == "low"
// Any string value ("high", "medium", "low", "max") means thinking is enabled
return v == "high" || v == "medium" || v == "low" || v == "max"
default:
return false
}
@@ -1169,14 +1169,14 @@ func (t *ThinkValue) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err == nil {
// Validate string values
if s != "high" && s != "medium" && s != "low" {
return fmt.Errorf("invalid think value: %q (must be \"high\", \"medium\", \"low\", true, or false)", s)
if s != "high" && s != "medium" && s != "low" && s != "max" {
return fmt.Errorf("invalid think value: %q (must be \"high\", \"medium\", \"low\", \"max\", true, or false)", s)
}
t.Value = s
return nil
}
return fmt.Errorf("think must be a boolean or string (\"high\", \"medium\", \"low\", true, or false)")
return fmt.Errorf("think must be a boolean or string (\"high\", \"medium\", \"low\", \"max\", true, or false)")
}
// MarshalJSON implements json.Marshaler

View File

@@ -495,6 +495,11 @@ func TestThinking_UnmarshalJSON(t *testing.T) {
input: `{ "think": "low" }`,
expectedThinking: &ThinkValue{Value: "low"},
},
{
name: "string_max",
input: `{ "think": "max" }`,
expectedThinking: &ThinkValue{Value: "max"},
},
{
name: "invalid_string",
input: `{ "think": "invalid" }`,

View File

@@ -582,10 +582,10 @@ func RunHandler(cmd *cobra.Command, args []string) error {
opts.Think = &api.ThinkValue{Value: true}
case "false":
opts.Think = &api.ThinkValue{Value: false}
case "high", "medium", "low":
case "high", "medium", "low", "max":
opts.Think = &api.ThinkValue{Value: thinkStr}
default:
return fmt.Errorf("invalid value for --think: %q (must be true, false, high, medium, or low)", thinkStr)
return fmt.Errorf("invalid value for --think: %q (must be true, false, high, medium, low, or max)", thinkStr)
}
} else {
opts.Think = nil

View File

@@ -632,8 +632,8 @@ func FromChatRequest(r ChatCompletionRequest) (*api.ChatRequest, error) {
}
if effort != "" {
if !slices.Contains([]string{"high", "medium", "low", "none"}, effort) {
return nil, fmt.Errorf("invalid reasoning value: '%s' (must be \"high\", \"medium\", \"low\", or \"none\")", effort)
if !slices.Contains([]string{"high", "medium", "low", "max", "none"}, effort) {
return nil, fmt.Errorf("invalid reasoning value: '%s' (must be \"high\", \"medium\", \"low\", \"max\", or \"none\")", effort)
}
if effort == "none" {

View File

@@ -55,6 +55,57 @@ func TestFromChatRequest_Basic(t *testing.T) {
}
}
func TestFromChatRequest_ReasoningEffort(t *testing.T) {
effort := func(s string) *string { return &s }
cases := []struct {
name string
effort *string
want any // expected ThinkValue.Value; nil means req.Think should be nil
wantErr bool
}{
{name: "unset", effort: nil, want: nil},
{name: "high", effort: effort("high"), want: "high"},
{name: "medium", effort: effort("medium"), want: "medium"},
{name: "low", effort: effort("low"), want: "low"},
{name: "max", effort: effort("max"), want: "max"},
{name: "none disables", effort: effort("none"), want: false},
{name: "invalid", effort: effort("extreme"), wantErr: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := ChatCompletionRequest{
Model: "test-model",
Messages: []Message{{Role: "user", Content: "hi"}},
ReasoningEffort: tc.effort,
}
result, err := FromChatRequest(req)
if tc.wantErr {
if err == nil {
t.Fatalf("expected error for effort=%v, got none", *tc.effort)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tc.want == nil {
if result.Think != nil {
t.Fatalf("expected nil Think, got %+v", result.Think)
}
return
}
if result.Think == nil {
t.Fatalf("expected Think=%v, got nil", tc.want)
}
if result.Think.Value != tc.want {
t.Fatalf("got Think.Value=%v, want %v", result.Think.Value, tc.want)
}
})
}
}
func TestFromChatRequest_WithImage(t *testing.T) {
imgData, _ := base64.StdEncoding.DecodeString(image)

View File

@@ -525,6 +525,18 @@ func FromResponsesRequest(r ResponsesRequest) (*api.ChatRequest, error) {
options["num_predict"] = *r.MaxOutputTokens
}
var think *api.ThinkValue
if effort := r.Reasoning.Effort; effort != "" {
switch effort {
case "none":
think = &api.ThinkValue{Value: false}
case "low", "medium", "high", "max":
think = &api.ThinkValue{Value: effort}
default:
return nil, fmt.Errorf("invalid reasoning value: %q (must be \"high\", \"medium\", \"low\", \"max\", or \"none\")", effort)
}
}
// Convert tools from Responses API format to api.Tool format
var tools []api.Tool
for _, t := range r.Tools {
@@ -552,6 +564,7 @@ func FromResponsesRequest(r ResponsesRequest) (*api.ChatRequest, error) {
Options: options,
Tools: tools,
Format: format,
Think: think,
}, nil
}

View File

@@ -415,6 +415,86 @@ func TestFromResponsesRequest_Tools(t *testing.T) {
}
}
func TestFromResponsesRequest_ReasoningEffort(t *testing.T) {
tests := []struct {
name string
effort string
wantThink any
wantErr bool
}{
{
name: "unset",
},
{
name: "low",
effort: "low",
wantThink: "low",
},
{
name: "medium",
effort: "medium",
wantThink: "medium",
},
{
name: "high",
effort: "high",
wantThink: "high",
},
{
name: "max",
effort: "max",
wantThink: "max",
},
{
name: "none",
effort: "none",
wantThink: false,
},
{
name: "invalid",
effort: "extreme",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := ResponsesRequest{
Model: "deepseek-v4-flash",
Input: ResponsesInput{Text: "hi"},
}
if tt.effort != "" {
req.Reasoning.Effort = tt.effort
}
chatReq, err := FromResponsesRequest(req)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tt.wantThink == nil {
if chatReq.Think != nil {
t.Fatalf("Think = %#v, want nil", chatReq.Think)
}
return
}
if chatReq.Think == nil {
t.Fatalf("Think = nil, want %v", tt.wantThink)
}
if chatReq.Think.Value != tt.wantThink {
t.Errorf("Think.Value = %v, want %v", chatReq.Think.Value, tt.wantThink)
}
})
}
}
func TestFromResponsesRequest_FunctionCallOutput(t *testing.T) {
// Test a complete tool call round-trip:
// 1. User message asking about weather

View File

@@ -375,8 +375,16 @@ func (s *Server) GenerateHandler(c *gin.Context) {
}
var builtinParser parsers.Parser
if shouldUseHarmony(m) && m.Config.Parser == "" {
m.Config.Parser = "harmony"
if shouldUseHarmony(m) {
// harmony's Reasoning field only understands low/medium/high; map "max" to "high"
if req.Think != nil {
if s, ok := req.Think.Value.(string); ok && s == "max" {
req.Think.Value = "high"
}
}
if m.Config.Parser == "" {
m.Config.Parser = "harmony"
}
}
if !req.Raw && m.Config.Parser != "" {
@@ -2320,8 +2328,16 @@ func (s *Server) ChatHandler(c *gin.Context) {
}
msgs = filterThinkTags(msgs, m)
if shouldUseHarmony(m) && m.Config.Parser == "" {
m.Config.Parser = "harmony"
if shouldUseHarmony(m) {
// harmony's Reasoning field only understands low/medium/high; map "max" to "high"
if req.Think != nil {
if s, ok := req.Think.Value.(string); ok && s == "max" {
req.Think.Value = "high"
}
}
if m.Config.Parser == "" {
m.Config.Parser = "harmony"
}
}
var builtinParser parsers.Parser