From 07ed752353dff39191fc333877629faf36dae1d0 Mon Sep 17 00:00:00 2001 From: Eva H <63033505+hoyyeva@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:45:16 -0700 Subject: [PATCH] launch: add thinking capability detection to opencode (#15434) --- cmd/launch/opencode.go | 37 ++++++++++++++++++++++++++++ cmd/launch/opencode_test.go | 49 +++++++++++++++++++++++++++++++++++++ llm/llama_server_test.go | 42 +++++++++++++++++-------------- 3 files changed, 109 insertions(+), 19 deletions(-) diff --git a/cmd/launch/opencode.go b/cmd/launch/opencode.go index d0a6d7d3b..80d333ada 100644 --- a/cmd/launch/opencode.go +++ b/cmd/launch/opencode.go @@ -8,6 +8,7 @@ import ( "path/filepath" "runtime" "slices" + "strings" "github.com/ollama/ollama/cmd/internal/fileutil" "github.com/ollama/ollama/envconfig" @@ -278,6 +279,25 @@ func buildModelEntries(modelList []LaunchModel) map[string]any { "output": []string{"text"}, } } + if model.HasCapability("thinking") { + entry["reasoning"] = true + if openCodeModelSupportsThinkingLevels(model) { + entry["options"] = map[string]any{"reasoningEffort": "medium"} + entry["variants"] = map[string]any{ + "low": map[string]any{"reasoningEffort": "low"}, + "medium": map[string]any{"reasoningEffort": "medium"}, + "high": map[string]any{"reasoningEffort": "high"}, + "max": map[string]any{"reasoningEffort": "max"}, + } + } else { + entry["variants"] = map[string]any{ + "none": map[string]any{"reasoningEffort": "none"}, + "low": map[string]any{"disabled": true}, + "medium": map[string]any{"disabled": true}, + "high": map[string]any{"disabled": true}, + } + } + } if model.MaxOutputTokens > 0 { limit := make(map[string]any) if model.ContextLength > 0 { @@ -290,3 +310,20 @@ func buildModelEntries(modelList []LaunchModel) map[string]any { } return models } + +func openCodeModelSupportsThinkingLevels(model LaunchModel) bool { + for _, family := range append([]string{model.Details.Family}, model.Details.Families...) { + if normalizeOpenCodeModelFamily(family) == "gptoss" { + return true + } + } + + return strings.Contains(normalizeOpenCodeModelFamily(model.Name), "gptoss") +} + +func normalizeOpenCodeModelFamily(s string) string { + s = strings.ToLower(s) + s = strings.ReplaceAll(s, "-", "") + s = strings.ReplaceAll(s, "_", "") + return s +} diff --git a/cmd/launch/opencode_test.go b/cmd/launch/opencode_test.go index 45efff5e0..fd23b52b6 100644 --- a/cmd/launch/opencode_test.go +++ b/cmd/launch/opencode_test.go @@ -8,6 +8,7 @@ import ( "runtime" "testing" + "github.com/ollama/ollama/api" "github.com/ollama/ollama/types/model" ) @@ -174,6 +175,54 @@ func TestOpenCodeEdit(t *testing.T) { t.Fatalf("modalities.output = %v, want [text]", output) } }) + + t.Run("thinking model gets on off reasoning variants", func(t *testing.T) { + models := buildModelEntries([]LaunchModel{{Name: "thinking-model", Capabilities: []model.Capability{model.CapabilityThinking}}}) + entry, _ := models["thinking-model"].(map[string]any) + + if entry["reasoning"] != true { + t.Fatalf("reasoning = %v, want true", entry["reasoning"]) + } + variants, _ := entry["variants"].(map[string]any) + none, _ := variants["none"].(map[string]any) + if none["reasoningEffort"] != "none" { + t.Fatalf("variants.none.reasoningEffort = %v, want none", none["reasoningEffort"]) + } + for _, level := range []string{"low", "medium", "high"} { + variant, _ := variants[level].(map[string]any) + if variant["disabled"] != true { + t.Fatalf("variants.%s.disabled = %v, want true", level, variant["disabled"]) + } + } + }) + + t.Run("gpt oss gets reasoning level variants", func(t *testing.T) { + models := buildModelEntries([]LaunchModel{{Name: "gpt-oss:120b-cloud", Capabilities: []model.Capability{model.CapabilityThinking}}}) + entry, _ := models["gpt-oss:120b-cloud"].(map[string]any) + options, _ := entry["options"].(map[string]any) + + if options["reasoningEffort"] != "medium" { + t.Fatalf("options.reasoningEffort = %v, want medium", options["reasoningEffort"]) + } + variants, _ := entry["variants"].(map[string]any) + for _, level := range []string{"low", "medium", "high", "max"} { + variant, _ := variants[level].(map[string]any) + if variant["reasoningEffort"] != level { + t.Fatalf("variants.%s.reasoningEffort = %v, want %s", level, variant["reasoningEffort"], level) + } + } + }) + + t.Run("gpt oss family gets reasoning level variants", func(t *testing.T) { + models := buildModelEntries([]LaunchModel{{Name: "reasoning-model", Capabilities: []model.Capability{model.CapabilityThinking}, Details: api.ModelDetails{Families: []string{"gptoss"}}}}) + entry, _ := models["reasoning-model"].(map[string]any) + variants, _ := entry["variants"].(map[string]any) + max, _ := variants["max"].(map[string]any) + + if max["reasoningEffort"] != "max" { + t.Fatalf("variants.max.reasoningEffort = %v, want max", max["reasoningEffort"]) + } + }) } func TestBuildModelEntries(t *testing.T) { diff --git a/llm/llama_server_test.go b/llm/llama_server_test.go index 93094e4d6..906f39ad5 100644 --- a/llm/llama_server_test.go +++ b/llm/llama_server_test.go @@ -986,15 +986,35 @@ func TestLlamaServerWaitUntilRunningTimesOutWhenLoadStalls(t *testing.T) { } func TestLlamaServerWaitUntilRunningExtendsTimeoutOnOutputActivity(t *testing.T) { - t.Setenv("OLLAMA_LOAD_TIMEOUT", "20ms") + t.Setenv("OLLAMA_LOAD_TIMEOUT", "100ms") var activityCount atomic.Int32 + var activityStarted atomic.Bool + var runner *llamaServerRunner + done := make(chan struct{}) + defer close(done) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/health" { t.Errorf("unexpected path: %s", r.URL.Path) return } - if activityCount.Load() < 5 { + if !activityStarted.Swap(true) { + go func() { + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-done: + return + case <-ticker.C: + activityCount.Add(1) + _, _ = runner.output.Write([]byte(".")) + } + } + }() + } + if activityCount.Load() < 3 { w.WriteHeader(http.StatusServiceUnavailable) fmt.Fprint(w, `{"error":{"message":"Loading model","type":"unavailable_error","code":503}}`) return @@ -1007,28 +1027,12 @@ func TestLlamaServerWaitUntilRunningExtendsTimeoutOnOutputActivity(t *testing.T) var portInt int fmt.Sscanf(parts[len(parts)-1], "%d", &portInt) - runner := &llamaServerRunner{ + runner = &llamaServerRunner{ port: portInt, cmd: fakeRunningCmd(), } runner.output = &memoryParsingWriter{inner: io.Discard, runner: runner} - done := make(chan struct{}) - defer close(done) - go func() { - ticker := time.NewTicker(5 * time.Millisecond) - defer ticker.Stop() - for { - select { - case <-done: - return - case <-ticker.C: - activityCount.Add(1) - _, _ = runner.output.Write([]byte(".")) - } - } - }() - if err := runner.WaitUntilRunning(t.Context()); err != nil { t.Fatalf("WaitUntilRunning error: %v", err) }