mirror of
https://github.com/ollama/ollama.git
synced 2026-04-25 10:16:00 +02:00
Compare commits
2 Commits
codex/fix-
...
hoyyeva/vs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a2306087b | ||
|
|
8b8bcf0952 |
@@ -361,9 +361,11 @@ func (v *VSCode) statePath() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ShowInModelPicker ensures the given models are visible in VS Code's Copilot
|
// ShowInModelPicker ensures the given models are visible in VS Code's Copilot
|
||||||
// Chat model picker. It sets the configured models to true in the picker
|
// Chat model picker and sets the primary model as the active selection. It sets
|
||||||
// preferences so they appear in the dropdown. Models use the VS Code identifier
|
// the configured models to true in the picker preferences so they appear in the
|
||||||
// format "ollama/Ollama/<name>".
|
// dropdown, and writes the first model as the selected model for both the panel
|
||||||
|
// and editor chat views. Models use the VS Code identifier format
|
||||||
|
// "ollama/Ollama/<name>".
|
||||||
func (v *VSCode) ShowInModelPicker(models []string) error {
|
func (v *VSCode) ShowInModelPicker(models []string) error {
|
||||||
if len(models) == 0 {
|
if len(models) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -400,22 +402,25 @@ func (v *VSCode) ShowInModelPicker(models []string) error {
|
|||||||
// Build name→ID map from VS Code's cached model list.
|
// Build name→ID map from VS Code's cached model list.
|
||||||
// VS Code uses numeric IDs like "ollama/Ollama/4", not "ollama/Ollama/kimi-k2.5:cloud".
|
// VS Code uses numeric IDs like "ollama/Ollama/4", not "ollama/Ollama/kimi-k2.5:cloud".
|
||||||
nameToID := make(map[string]string)
|
nameToID := make(map[string]string)
|
||||||
|
var cached []map[string]any
|
||||||
var cacheJSON string
|
var cacheJSON string
|
||||||
if err := db.QueryRow("SELECT value FROM ItemTable WHERE key = 'chat.cachedLanguageModels.v2'").Scan(&cacheJSON); err == nil {
|
if err := db.QueryRow("SELECT value FROM ItemTable WHERE key = 'chat.cachedLanguageModels.v2'").Scan(&cacheJSON); err == nil {
|
||||||
var cached []map[string]any
|
_ = json.Unmarshal([]byte(cacheJSON), &cached)
|
||||||
if json.Unmarshal([]byte(cacheJSON), &cached) == nil {
|
}
|
||||||
for _, entry := range cached {
|
cachedNames := make(map[string]bool)
|
||||||
meta, _ := entry["metadata"].(map[string]any)
|
for _, entry := range cached {
|
||||||
if meta == nil {
|
meta, _ := entry["metadata"].(map[string]any)
|
||||||
continue
|
if meta == nil {
|
||||||
}
|
continue
|
||||||
if vendor, _ := meta["vendor"].(string); vendor == "ollama" {
|
}
|
||||||
name, _ := meta["name"].(string)
|
if vendor, _ := meta["vendor"].(string); vendor == "ollama" {
|
||||||
id, _ := entry["identifier"].(string)
|
name, _ := meta["name"].(string)
|
||||||
if name != "" && id != "" {
|
id, _ := entry["identifier"].(string)
|
||||||
nameToID[name] = id
|
if name != "" && id != "" {
|
||||||
}
|
nameToID[name] = id
|
||||||
}
|
}
|
||||||
|
if name != "" {
|
||||||
|
cachedNames[name] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,10 +445,68 @@ func (v *VSCode) ShowInModelPicker(models []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the primary model as the active selection in Copilot Chat so it
|
||||||
|
// doesn't default to "auto" or whatever the user last picked manually.
|
||||||
|
primaryID := v.modelVSCodeIDs(models[0], nameToID)[0]
|
||||||
|
for _, key := range []string{"chat.currentLanguageModel.panel", "chat.currentLanguageModel.editor"} {
|
||||||
|
if _, err := db.Exec("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)", key, primaryID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)", key+".isDefault", "false"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure configured models exist in the cached model list so VS Code can
|
||||||
|
// restore the selection immediately on startup, before extensions load.
|
||||||
|
// Without this, a model that was never previously used won't be in the
|
||||||
|
// cache, and VS Code falls back to "auto" until the Ollama BYOK provider
|
||||||
|
// discovers it via the API (which is slow).
|
||||||
|
cacheChanged := false
|
||||||
|
for _, m := range models {
|
||||||
|
if cachedNames[m] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.Contains(m, ":") && cachedNames[m+":latest"] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cacheID := m
|
||||||
|
if !strings.Contains(m, ":") {
|
||||||
|
cacheID = m + ":latest"
|
||||||
|
}
|
||||||
|
cached = append(cached, map[string]any{
|
||||||
|
"identifier": "ollama/Ollama/" + cacheID,
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"extension": map[string]any{"value": "github.copilot-chat"},
|
||||||
|
"name": m,
|
||||||
|
"id": m,
|
||||||
|
"vendor": "ollama",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"family": m,
|
||||||
|
"detail": "Ollama",
|
||||||
|
"maxInputTokens": 4096,
|
||||||
|
"maxOutputTokens": 4096,
|
||||||
|
"isDefaultForLocation": map[string]any{},
|
||||||
|
"isUserSelectable": true,
|
||||||
|
"capabilities": map[string]any{"toolCalling": true},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
cacheChanged = true
|
||||||
|
}
|
||||||
|
if cacheChanged {
|
||||||
|
cacheData, _ := json.Marshal(cached)
|
||||||
|
if _, err := db.Exec("INSERT OR REPLACE INTO ItemTable (key, value) VALUES ('chat.cachedLanguageModels.v2', ?)", string(cacheData)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// modelVSCodeIDs returns all possible VS Code picker IDs for a model name.
|
// modelVSCodeIDs returns all possible VS Code picker IDs for a model name.
|
||||||
|
// The primary (first) ID should match the live identifier that VS Code assigns
|
||||||
|
// at runtime via toModelIdentifier(vendor, group, m.id), where m.id comes from
|
||||||
|
// /api/tags and always includes the tag (e.g. "llama3.2:latest").
|
||||||
func (v *VSCode) modelVSCodeIDs(model string, nameToID map[string]string) []string {
|
func (v *VSCode) modelVSCodeIDs(model string, nameToID map[string]string) []string {
|
||||||
var ids []string
|
var ids []string
|
||||||
if id, ok := nameToID[model]; ok {
|
if id, ok := nameToID[model]; ok {
|
||||||
@@ -453,10 +516,13 @@ func (v *VSCode) modelVSCodeIDs(model string, nameToID map[string]string) []stri
|
|||||||
ids = append(ids, id)
|
ids = append(ids, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ids = append(ids, "ollama/Ollama/"+model)
|
// For untagged models, the live identifier includes :latest
|
||||||
|
// (e.g. ollama/Ollama/llama3.2:latest), so prefer that format
|
||||||
|
// to avoid a mismatch that causes VS Code to reset to "auto".
|
||||||
if !strings.Contains(model, ":") {
|
if !strings.Contains(model, ":") {
|
||||||
ids = append(ids, "ollama/Ollama/"+model+":latest")
|
ids = append(ids, "ollama/Ollama/"+model+":latest")
|
||||||
}
|
}
|
||||||
|
ids = append(ids, "ollama/Ollama/"+model)
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -388,6 +388,71 @@ func TestShowInModelPicker(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// helper to read a string value from the state DB
|
||||||
|
readValue := func(t *testing.T, dbPath, key string) string {
|
||||||
|
t.Helper()
|
||||||
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
var val string
|
||||||
|
if err := db.QueryRow("SELECT value FROM ItemTable WHERE key = ?", key).Scan(&val); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("sets primary model as active selection", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", "")
|
||||||
|
setupDB(t, testVSCodePath(t, tmpDir, ""), nil, nil)
|
||||||
|
|
||||||
|
err := v.ShowInModelPicker([]string{"llama3.2", "qwen3:8b"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := testVSCodePath(t, tmpDir, filepath.Join("globalStorage", "state.vscdb"))
|
||||||
|
panelModel := readValue(t, dbPath, "chat.currentLanguageModel.panel")
|
||||||
|
if panelModel != "ollama/Ollama/llama3.2:latest" {
|
||||||
|
t.Errorf("expected panel model ollama/Ollama/llama3.2:latest, got %q", panelModel)
|
||||||
|
}
|
||||||
|
editorModel := readValue(t, dbPath, "chat.currentLanguageModel.editor")
|
||||||
|
if editorModel != "ollama/Ollama/llama3.2:latest" {
|
||||||
|
t.Errorf("expected editor model ollama/Ollama/llama3.2:latest, got %q", editorModel)
|
||||||
|
}
|
||||||
|
panelDefault := readValue(t, dbPath, "chat.currentLanguageModel.panel.isDefault")
|
||||||
|
if panelDefault != "false" {
|
||||||
|
t.Errorf("expected panel isDefault false, got %q", panelDefault)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets cached numeric ID as active selection", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", "")
|
||||||
|
cache := []map[string]any{
|
||||||
|
{
|
||||||
|
"identifier": "ollama/Ollama/4",
|
||||||
|
"metadata": map[string]any{"vendor": "ollama", "name": "llama3.2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
setupDB(t, testVSCodePath(t, tmpDir, ""), nil, cache)
|
||||||
|
|
||||||
|
err := v.ShowInModelPicker([]string{"llama3.2"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := testVSCodePath(t, tmpDir, filepath.Join("globalStorage", "state.vscdb"))
|
||||||
|
panelModel := readValue(t, dbPath, "chat.currentLanguageModel.panel")
|
||||||
|
if panelModel != "ollama/Ollama/4" {
|
||||||
|
t.Errorf("expected panel model to use cached numeric ID ollama/Ollama/4, got %q", panelModel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("previously hidden model is re-shown when configured", func(t *testing.T) {
|
t.Run("previously hidden model is re-shown when configured", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
setTestHome(t, tmpDir)
|
setTestHome(t, tmpDir)
|
||||||
@@ -408,6 +473,111 @@ func TestShowInModelPicker(t *testing.T) {
|
|||||||
t.Error("expected llama3.2 to be re-shown")
|
t.Error("expected llama3.2 to be re-shown")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// helper to read and parse the cached models from the state DB
|
||||||
|
readCache := func(t *testing.T, dbPath string) []map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
var raw string
|
||||||
|
if err := db.QueryRow("SELECT value FROM ItemTable WHERE key = 'chat.cachedLanguageModels.v2'").Scan(&raw); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var result []map[string]any
|
||||||
|
_ = json.Unmarshal([]byte(raw), &result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("adds uncached model to cache for instant startup display", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", "")
|
||||||
|
// No seed cache — model has never been used in VS Code before
|
||||||
|
dbPath := setupDB(t, testVSCodePath(t, tmpDir, ""), nil, nil)
|
||||||
|
|
||||||
|
err := v.ShowInModelPicker([]string{"qwen3:8b"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cache := readCache(t, dbPath)
|
||||||
|
if len(cache) != 1 {
|
||||||
|
t.Fatalf("expected 1 cached entry, got %d", len(cache))
|
||||||
|
}
|
||||||
|
entry := cache[0]
|
||||||
|
if id, _ := entry["identifier"].(string); id != "ollama/Ollama/qwen3:8b" {
|
||||||
|
t.Errorf("expected identifier ollama/Ollama/qwen3:8b, got %q", id)
|
||||||
|
}
|
||||||
|
meta, _ := entry["metadata"].(map[string]any)
|
||||||
|
if meta == nil {
|
||||||
|
t.Fatal("expected metadata in cache entry")
|
||||||
|
}
|
||||||
|
if v, _ := meta["vendor"].(string); v != "ollama" {
|
||||||
|
t.Errorf("expected vendor ollama, got %q", v)
|
||||||
|
}
|
||||||
|
if sel, ok := meta["isUserSelectable"].(bool); !ok || !sel {
|
||||||
|
t.Error("expected isUserSelectable to be true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("does not duplicate already-cached model", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", "")
|
||||||
|
cache := []map[string]any{
|
||||||
|
{
|
||||||
|
"identifier": "ollama/Ollama/4",
|
||||||
|
"metadata": map[string]any{"vendor": "ollama", "name": "llama3.2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "copilot/copilot/auto",
|
||||||
|
"metadata": map[string]any{"vendor": "copilot", "name": "Auto"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dbPath := setupDB(t, testVSCodePath(t, tmpDir, ""), nil, cache)
|
||||||
|
|
||||||
|
err := v.ShowInModelPicker([]string{"llama3.2"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache should still have exactly 2 entries (no duplicate added)
|
||||||
|
result := readCache(t, dbPath)
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("expected 2 cached entries (no duplicate), got %d", len(result))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adds only missing models to existing cache", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", "")
|
||||||
|
cache := []map[string]any{
|
||||||
|
{
|
||||||
|
"identifier": "ollama/Ollama/4",
|
||||||
|
"metadata": map[string]any{"vendor": "ollama", "name": "llama3.2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dbPath := setupDB(t, testVSCodePath(t, tmpDir, ""), nil, cache)
|
||||||
|
|
||||||
|
// llama3.2 is cached, qwen3:8b is not
|
||||||
|
err := v.ShowInModelPicker([]string{"llama3.2", "qwen3:8b"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := readCache(t, dbPath)
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Fatalf("expected 2 cached entries, got %d", len(result))
|
||||||
|
}
|
||||||
|
// Second entry should be the newly added qwen3:8b
|
||||||
|
if id, _ := result[1]["identifier"].(string); id != "ollama/Ollama/qwen3:8b" {
|
||||||
|
t.Errorf("expected new entry ollama/Ollama/qwen3:8b, got %q", id)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseCopilotChatVersion(t *testing.T) {
|
func TestParseCopilotChatVersion(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user