mirror of
https://github.com/ollama/ollama.git
synced 2026-04-30 16:08:07 -05:00
cmd: set context limits for cloud models in opencode (#14107)
This commit is contained in:
@@ -482,6 +482,8 @@ Examples:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if saved, err := loadIntegration(name); err == nil && len(saved.Models) > 0 && !configFlag {
|
||||||
|
return runIntegration(name, saved.Models[0], passArgs)
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
models, err = selectModels(cmd.Context(), name, "")
|
models, err = selectModels(cmd.Context(), name, "")
|
||||||
|
|||||||
@@ -502,6 +502,28 @@ func TestBuildModelList_ReturnsExistingAndCloudMaps(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEditorIntegration_SavedConfigSkipsSelection(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
|
||||||
|
// Save a config for opencode so it looks like a previous launch
|
||||||
|
if err := saveIntegration("opencode", []string{"llama3.2"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify loadIntegration returns the saved models
|
||||||
|
saved, err := loadIntegration("opencode")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(saved.Models) == 0 {
|
||||||
|
t.Fatal("expected saved models")
|
||||||
|
}
|
||||||
|
if saved.Models[0] != "llama3.2" {
|
||||||
|
t.Errorf("expected llama3.2, got %s", saved.Models[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAliasConfigurerInterface(t *testing.T) {
|
func TestAliasConfigurerInterface(t *testing.T) {
|
||||||
t.Run("claude implements AliasConfigurer", func(t *testing.T) {
|
t.Run("claude implements AliasConfigurer", func(t *testing.T) {
|
||||||
claude := &Claude{}
|
claude := &Claude{}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
@@ -10,12 +11,52 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
"github.com/ollama/ollama/envconfig"
|
"github.com/ollama/ollama/envconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpenCode implements Runner and Editor for OpenCode integration
|
// OpenCode implements Runner and Editor for OpenCode integration
|
||||||
type OpenCode struct{}
|
type OpenCode struct{}
|
||||||
|
|
||||||
|
// cloudModelLimit holds context and output token limits for a cloud model.
|
||||||
|
type cloudModelLimit struct {
|
||||||
|
Context int
|
||||||
|
Output int
|
||||||
|
}
|
||||||
|
|
||||||
|
// cloudModelLimits maps cloud model base names to their token limits.
|
||||||
|
// TODO(parthsareen): grab context/output limits from model info instead of hardcoding
|
||||||
|
var cloudModelLimits = map[string]cloudModelLimit{
|
||||||
|
"cogito-2.1:671b": {Context: 163_840, Output: 65_536},
|
||||||
|
"deepseek-v3.1:671b": {Context: 163_840, Output: 163_840},
|
||||||
|
"deepseek-v3.2": {Context: 163_840, Output: 65_536},
|
||||||
|
"glm-4.6": {Context: 202_752, Output: 131_072},
|
||||||
|
"glm-4.7": {Context: 202_752, Output: 131_072},
|
||||||
|
"gpt-oss:120b": {Context: 131_072, Output: 131_072},
|
||||||
|
"gpt-oss:20b": {Context: 131_072, Output: 131_072},
|
||||||
|
"kimi-k2:1t": {Context: 262_144, Output: 262_144},
|
||||||
|
"kimi-k2.5": {Context: 262_144, Output: 262_144},
|
||||||
|
"kimi-k2-thinking": {Context: 262_144, Output: 262_144},
|
||||||
|
"nemotron-3-nano:30b": {Context: 1_048_576, Output: 131_072},
|
||||||
|
"qwen3-coder:480b": {Context: 262_144, Output: 65_536},
|
||||||
|
"qwen3-next:80b": {Context: 262_144, Output: 32_768},
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupCloudModelLimit returns the token limits for a cloud model.
|
||||||
|
// It tries the exact name first, then strips the ":cloud" suffix.
|
||||||
|
func lookupCloudModelLimit(name string) (cloudModelLimit, bool) {
|
||||||
|
if l, ok := cloudModelLimits[name]; ok {
|
||||||
|
return l, true
|
||||||
|
}
|
||||||
|
base := strings.TrimSuffix(name, ":cloud")
|
||||||
|
if base != name {
|
||||||
|
if l, ok := cloudModelLimits[base]; ok {
|
||||||
|
return l, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cloudModelLimit{}, false
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenCode) String() string { return "OpenCode" }
|
func (o *OpenCode) String() string { return "OpenCode" }
|
||||||
|
|
||||||
func (o *OpenCode) Run(model string, args []string) error {
|
func (o *OpenCode) Run(model string, args []string) error {
|
||||||
@@ -113,6 +154,8 @@ func (o *OpenCode) Edit(modelList []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client, _ := api.ClientFromEnvironment()
|
||||||
|
|
||||||
for _, model := range modelList {
|
for _, model := range modelList {
|
||||||
if existing, ok := models[model].(map[string]any); ok {
|
if existing, ok := models[model].(map[string]any); ok {
|
||||||
// migrate existing models without _launch marker
|
// migrate existing models without _launch marker
|
||||||
@@ -122,12 +165,29 @@ func (o *OpenCode) Edit(modelList []string) error {
|
|||||||
existing["name"] = strings.TrimSuffix(name, " [Ollama]")
|
existing["name"] = strings.TrimSuffix(name, " [Ollama]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if isCloudModel(context.Background(), client, model) {
|
||||||
|
if l, ok := lookupCloudModelLimit(model); ok {
|
||||||
|
existing["limit"] = map[string]any{
|
||||||
|
"context": l.Context,
|
||||||
|
"output": l.Output,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
models[model] = map[string]any{
|
entry := map[string]any{
|
||||||
"name": model,
|
"name": model,
|
||||||
"_launch": true,
|
"_launch": true,
|
||||||
}
|
}
|
||||||
|
if isCloudModel(context.Background(), client, model) {
|
||||||
|
if l, ok := lookupCloudModelLimit(model); ok {
|
||||||
|
entry["limit"] = map[string]any{
|
||||||
|
"context": l.Context,
|
||||||
|
"output": l.Output,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
models[model] = entry
|
||||||
}
|
}
|
||||||
|
|
||||||
ollama["models"] = models
|
ollama["models"] = models
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -495,6 +496,165 @@ func TestOpenCodeEdit_SpecialCharsInModelName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readOpenCodeModel(t *testing.T, configPath, model string) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var cfg map[string]any
|
||||||
|
json.Unmarshal(data, &cfg)
|
||||||
|
provider := cfg["provider"].(map[string]any)
|
||||||
|
ollama := provider["ollama"].(map[string]any)
|
||||||
|
models := ollama["models"].(map[string]any)
|
||||||
|
entry, ok := models[model].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("model %s not found in config", model)
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenCodeEdit_LocalModelNoLimit(t *testing.T) {
|
||||||
|
o := &OpenCode{}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
|
||||||
|
configPath := filepath.Join(tmpDir, ".config", "opencode", "opencode.json")
|
||||||
|
|
||||||
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := readOpenCodeModel(t, configPath, "llama3.2")
|
||||||
|
if entry["limit"] != nil {
|
||||||
|
t.Errorf("local model should not have limit set, got %v", entry["limit"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenCodeEdit_PreservesUserLimit(t *testing.T) {
|
||||||
|
o := &OpenCode{}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
|
||||||
|
configDir := filepath.Join(tmpDir, ".config", "opencode")
|
||||||
|
configPath := filepath.Join(configDir, "opencode.json")
|
||||||
|
|
||||||
|
// Set up a model with a user-configured limit
|
||||||
|
os.MkdirAll(configDir, 0o755)
|
||||||
|
os.WriteFile(configPath, []byte(`{
|
||||||
|
"provider": {
|
||||||
|
"ollama": {
|
||||||
|
"models": {
|
||||||
|
"llama3.2": {
|
||||||
|
"name": "llama3.2",
|
||||||
|
"_launch": true,
|
||||||
|
"limit": {"context": 8192, "output": 4096}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`), 0o644)
|
||||||
|
|
||||||
|
// Re-edit should preserve the user's limit (not delete it)
|
||||||
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := readOpenCodeModel(t, configPath, "llama3.2")
|
||||||
|
limit, ok := entry["limit"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("user-configured limit was removed")
|
||||||
|
}
|
||||||
|
if limit["context"] != float64(8192) {
|
||||||
|
t.Errorf("context limit changed: got %v, want 8192", limit["context"])
|
||||||
|
}
|
||||||
|
if limit["output"] != float64(4096) {
|
||||||
|
t.Errorf("output limit changed: got %v, want 4096", limit["output"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenCodeEdit_CloudModelLimitStructure(t *testing.T) {
|
||||||
|
// Verify that when a cloud model entry has limits set (as Edit would do),
|
||||||
|
// the structure matches what opencode expects and re-edit preserves them.
|
||||||
|
o := &OpenCode{}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
|
||||||
|
configDir := filepath.Join(tmpDir, ".config", "opencode")
|
||||||
|
configPath := filepath.Join(configDir, "opencode.json")
|
||||||
|
|
||||||
|
expected := cloudModelLimits["glm-4.7"]
|
||||||
|
|
||||||
|
// Simulate a cloud model that already has the limit set by a previous Edit
|
||||||
|
os.MkdirAll(configDir, 0o755)
|
||||||
|
os.WriteFile(configPath, []byte(fmt.Sprintf(`{
|
||||||
|
"provider": {
|
||||||
|
"ollama": {
|
||||||
|
"models": {
|
||||||
|
"glm-4.7:cloud": {
|
||||||
|
"name": "glm-4.7:cloud",
|
||||||
|
"_launch": true,
|
||||||
|
"limit": {"context": %d, "output": %d}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`, expected.Context, expected.Output)), 0o644)
|
||||||
|
|
||||||
|
// Re-edit should preserve the cloud model limit
|
||||||
|
if err := o.Edit([]string{"glm-4.7:cloud"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := readOpenCodeModel(t, configPath, "glm-4.7:cloud")
|
||||||
|
limit, ok := entry["limit"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("cloud model limit was removed on re-edit")
|
||||||
|
}
|
||||||
|
if limit["context"] != float64(expected.Context) {
|
||||||
|
t.Errorf("context = %v, want %d", limit["context"], expected.Context)
|
||||||
|
}
|
||||||
|
if limit["output"] != float64(expected.Output) {
|
||||||
|
t.Errorf("output = %v, want %d", limit["output"], expected.Output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupCloudModelLimit(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
wantOK bool
|
||||||
|
wantContext int
|
||||||
|
wantOutput int
|
||||||
|
}{
|
||||||
|
{"glm-4.7", true, 202_752, 131_072},
|
||||||
|
{"glm-4.7:cloud", true, 202_752, 131_072},
|
||||||
|
{"kimi-k2.5", true, 262_144, 262_144},
|
||||||
|
{"kimi-k2.5:cloud", true, 262_144, 262_144},
|
||||||
|
{"deepseek-v3.2", true, 163_840, 65_536},
|
||||||
|
{"deepseek-v3.2:cloud", true, 163_840, 65_536},
|
||||||
|
{"qwen3-coder:480b", true, 262_144, 65_536},
|
||||||
|
{"llama3.2", false, 0, 0},
|
||||||
|
{"unknown-model:cloud", false, 0, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l, ok := lookupCloudModelLimit(tt.name)
|
||||||
|
if ok != tt.wantOK {
|
||||||
|
t.Errorf("lookupCloudModelLimit(%q) ok = %v, want %v", tt.name, ok, tt.wantOK)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
if l.Context != tt.wantContext {
|
||||||
|
t.Errorf("context = %d, want %d", l.Context, tt.wantContext)
|
||||||
|
}
|
||||||
|
if l.Output != tt.wantOutput {
|
||||||
|
t.Errorf("output = %d, want %d", l.Output, tt.wantOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOpenCodeModels_NoConfig(t *testing.T) {
|
func TestOpenCodeModels_NoConfig(t *testing.T) {
|
||||||
o := &OpenCode{}
|
o := &OpenCode{}
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|||||||
Reference in New Issue
Block a user