diff --git a/api/types.go b/api/types.go index a0acd6641..7d600ebce 100644 --- a/api/types.go +++ b/api/types.go @@ -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 diff --git a/api/types_test.go b/api/types_test.go index 69d9c5a3d..f7cbfe3ef 100644 --- a/api/types_test.go +++ b/api/types_test.go @@ -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" }`, diff --git a/cmd/cmd.go b/cmd/cmd.go index e7bfb28ea..a5e8c087a 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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 diff --git a/openai/openai.go b/openai/openai.go index d77bcf7e1..3fb4b8b13 100644 --- a/openai/openai.go +++ b/openai/openai.go @@ -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" { diff --git a/openai/openai_test.go b/openai/openai_test.go index bd3eeac77..6c262e1e4 100644 --- a/openai/openai_test.go +++ b/openai/openai_test.go @@ -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) diff --git a/server/routes.go b/server/routes.go index 515929516..ef3aff174 100644 --- a/server/routes.go +++ b/server/routes.go @@ -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