launch: add thinking capability detection to opencode (#15434)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/ollama/ollama/cmd/internal/fileutil"
|
"github.com/ollama/ollama/cmd/internal/fileutil"
|
||||||
"github.com/ollama/ollama/envconfig"
|
"github.com/ollama/ollama/envconfig"
|
||||||
@@ -278,6 +279,25 @@ func buildModelEntries(modelList []LaunchModel) map[string]any {
|
|||||||
"output": []string{"text"},
|
"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 {
|
if model.MaxOutputTokens > 0 {
|
||||||
limit := make(map[string]any)
|
limit := make(map[string]any)
|
||||||
if model.ContextLength > 0 {
|
if model.ContextLength > 0 {
|
||||||
@@ -290,3 +310,20 @@ func buildModelEntries(modelList []LaunchModel) map[string]any {
|
|||||||
}
|
}
|
||||||
return models
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
"github.com/ollama/ollama/types/model"
|
"github.com/ollama/ollama/types/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -174,6 +175,54 @@ func TestOpenCodeEdit(t *testing.T) {
|
|||||||
t.Fatalf("modalities.output = %v, want [text]", output)
|
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) {
|
func TestBuildModelEntries(t *testing.T) {
|
||||||
|
|||||||
@@ -986,15 +986,35 @@ func TestLlamaServerWaitUntilRunningTimesOutWhenLoadStalls(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLlamaServerWaitUntilRunningExtendsTimeoutOnOutputActivity(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 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) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/health" {
|
if r.URL.Path != "/health" {
|
||||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||||
return
|
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)
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
fmt.Fprint(w, `{"error":{"message":"Loading model","type":"unavailable_error","code":503}}`)
|
fmt.Fprint(w, `{"error":{"message":"Loading model","type":"unavailable_error","code":503}}`)
|
||||||
return
|
return
|
||||||
@@ -1007,28 +1027,12 @@ func TestLlamaServerWaitUntilRunningExtendsTimeoutOnOutputActivity(t *testing.T)
|
|||||||
var portInt int
|
var portInt int
|
||||||
fmt.Sscanf(parts[len(parts)-1], "%d", &portInt)
|
fmt.Sscanf(parts[len(parts)-1], "%d", &portInt)
|
||||||
|
|
||||||
runner := &llamaServerRunner{
|
runner = &llamaServerRunner{
|
||||||
port: portInt,
|
port: portInt,
|
||||||
cmd: fakeRunningCmd(),
|
cmd: fakeRunningCmd(),
|
||||||
}
|
}
|
||||||
runner.output = &memoryParsingWriter{inner: io.Discard, runner: runner}
|
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 {
|
if err := runner.WaitUntilRunning(t.Context()); err != nil {
|
||||||
t.Fatalf("WaitUntilRunning error: %v", err)
|
t.Fatalf("WaitUntilRunning error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user