agents: Cline lists every magpie model — its built-in openai-compatible provider knows only gpt-4o, so /model offered that alone and the rest had to be typed in one by one. magpie now puts its models in models.json beside providers.json, as the entry Cline's own migration writes for openai-compatible (provider name, baseUrl, defaultModelId, and the models with their context, output, images and reasoning), which takes the place of the built-in list; the user's entry there is stashed and put back when magpie steps out, and Sync keeps the list current. Tested live with Cline 3.0.65's TUI against a sandbox gateway: /model shows provider magpie with every magpie model, picking one runs the turn on it, and magpie reads the pick back
This commit is contained in:
2 files changed
+92
-10
No files matched your search
+61
-10
@@ -13,9 +13,12 @@ package agent
|
||||
// refused when a session starts ("Unknown or disabled provider",
|
||||
// cline/cline#14180), so magpie takes Cline's built-in openai-compatible
|
||||
// provider, which reads the models from the gateway's /v1/models, and makes
|
||||
// it the one in use. What was there, and the provider in use, are stashed
|
||||
// and put back when magpie steps out. Cline's requests name only the AI SDK,
|
||||
// so the header says they are Cline's.
|
||||
// it the one in use. That provider lists only gpt-4o, so magpie's models
|
||||
// are put in models.json beside it, as the entry Cline's own migration writes
|
||||
// for it ({"provider":{"name","baseUrl","defaultModelId"},"models":{…}}),
|
||||
// which takes the place of the built-in list. What was there, and the
|
||||
// provider in use, are stashed and put back when magpie steps out. Cline's
|
||||
// requests name only the AI SDK, so the header says they are Cline's.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -37,6 +40,7 @@ func cline(home string) *Agent {
|
||||
dir = filepath.Join(home, ".cline")
|
||||
}
|
||||
path := filepath.Join(dir, "data", "settings", "providers.json")
|
||||
models := filepath.Join(dir, "data", "settings", "models.json")
|
||||
key := "cline:" + path + ":"
|
||||
slot := "providers." + clineSlot
|
||||
get := func(k string) string { v, _ := edit.GetJSON(path, k); return v }
|
||||
@@ -48,6 +52,13 @@ func cline(home string) *Agent {
|
||||
// use the user had before magpie
|
||||
restore := func() error {
|
||||
entry, last := unstash(key+"entry"), unstash(key+"lastUsedProvider")
|
||||
if list := unstash(key + "models"); list != "" {
|
||||
if err := edit.SetJSON(models, edit.KV{Path: "providers." + clineSlot, Value: json.RawMessage(list)}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := edit.DelJSON(models, "providers."+clineSlot); err != nil {
|
||||
return err
|
||||
}
|
||||
if entry != "" {
|
||||
if err := edit.SetJSON(path, edit.KV{Path: slot, Value: json.RawMessage(entry)}); err != nil {
|
||||
return err
|
||||
@@ -76,7 +87,10 @@ func cline(home string) *Agent {
|
||||
if !onMagpie() {
|
||||
return nil
|
||||
}
|
||||
return syncJSON(path, slot+".settings.baseUrl", func() any { return gatewayV1() })
|
||||
if err := syncJSON(path, slot+".settings.baseUrl", func() any { return gatewayV1() }); err != nil {
|
||||
return err
|
||||
}
|
||||
return syncJSON(models, "providers."+clineSlot, func() any { return clineModels(get(slot + ".settings.model")) })
|
||||
},
|
||||
Notice: func() string {
|
||||
if Running(`(^|/)cline( |$)`) {
|
||||
@@ -117,12 +131,18 @@ func cline(home string) *Agent {
|
||||
if ref, ok := cutMagpie(v); ok {
|
||||
effort := get("providers." + own() + ".settings.reasoning.effort")
|
||||
if !onMagpie() {
|
||||
list, _ := edit.GetJSON(models, "providers."+clineSlot)
|
||||
stash(map[string]string{
|
||||
key + "entry": get(slot),
|
||||
key + "lastUsedProvider": inUse(),
|
||||
key + "models": list,
|
||||
})
|
||||
}
|
||||
return clineWrite(path,
|
||||
if err := clineWrite(models, `{"version":1,"providers":{}}`,
|
||||
edit.KV{Path: "providers." + clineSlot, Value: clineModels(ref)}); err != nil {
|
||||
return err
|
||||
}
|
||||
return clineWrite(path, providersEmpty,
|
||||
edit.KV{Path: slot, Value: clineProvider(ref, effort)},
|
||||
edit.KV{Path: "lastUsedProvider", Value: clineSlot})
|
||||
}
|
||||
@@ -132,7 +152,7 @@ func cline(home string) *Agent {
|
||||
}
|
||||
}
|
||||
p := own()
|
||||
return clineWrite(path,
|
||||
return clineWrite(path, providersEmpty,
|
||||
edit.KV{Path: "providers." + p + ".settings.provider", Value: p},
|
||||
edit.KV{Path: "providers." + p + ".settings.model", Value: v})
|
||||
},
|
||||
@@ -155,7 +175,7 @@ func cline(home string) *Agent {
|
||||
if v == "" {
|
||||
return edit.DelJSON(path, k)
|
||||
}
|
||||
return clineWrite(path, edit.KV{Path: k + ".effort", Value: v})
|
||||
return clineWrite(path, providersEmpty, edit.KV{Path: k + ".effort", Value: v})
|
||||
},
|
||||
Options: func(map[string]string) []Option {
|
||||
return static("none", "low", "medium", "high", "xhigh")
|
||||
@@ -183,13 +203,44 @@ func clineProvider(model, effort string) map[string]any {
|
||||
return map[string]any{"settings": s, "updatedAt": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), "tokenSource": "manual"}
|
||||
}
|
||||
|
||||
// clineWrite sets kvs in providers.json, which Cline keeps private.
|
||||
func clineWrite(path string, kvs ...edit.KV) error {
|
||||
// clineModels is magpie's entry in models.json: every magpie model, model
|
||||
// the one the provider starts on.
|
||||
func clineModels(model string) map[string]any {
|
||||
ms := map[string]any{}
|
||||
for _, m := range magpieModels("cline") {
|
||||
caps := []string{"streaming", "tools"}
|
||||
if m.Images {
|
||||
caps = append(caps, "images")
|
||||
}
|
||||
if len(m.Efforts) > 0 {
|
||||
caps = append(caps, "reasoning")
|
||||
}
|
||||
e := map[string]any{"id": m.ID, "name": m.Name, "capabilities": caps}
|
||||
if m.Context > 0 {
|
||||
e["contextWindow"] = m.Context
|
||||
}
|
||||
if m.Output > 0 {
|
||||
e["maxTokens"] = m.Output
|
||||
}
|
||||
ms[m.ID] = e
|
||||
}
|
||||
return map[string]any{
|
||||
"provider": map[string]any{"name": "magpie", "baseUrl": gatewayV1(), "defaultModelId": model},
|
||||
"models": ms,
|
||||
}
|
||||
}
|
||||
|
||||
// providersEmpty is a providers.json with nothing in it.
|
||||
const providersEmpty = `{"version":1,"modes":{},"providers":{}}`
|
||||
|
||||
// clineWrite sets kvs in a file of Cline's settings, which it keeps
|
||||
// private, made from empty when there is none.
|
||||
func clineWrite(path, empty string, kvs ...edit.KV) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(path, []byte("{\"version\":1,\"modes\":{},\"providers\":{}}\n"), 0o600); err != nil {
|
||||
if err := os.WriteFile(path, []byte(empty+"\n"), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/gateway"
|
||||
@@ -34,7 +35,22 @@ func TestCline(t *testing.T) {
|
||||
json.Unmarshal(b, &f)
|
||||
return f, string(b)
|
||||
}
|
||||
mpath := filepath.Join(filepath.Dir(path), "models.json")
|
||||
type list struct {
|
||||
Provider map[string]any `json:"provider"`
|
||||
Models map[string]map[string]any `json:"models"`
|
||||
}
|
||||
models := func() (map[string]list, string) {
|
||||
var f struct {
|
||||
Providers map[string]list `json:"providers"`
|
||||
}
|
||||
b, _ := os.ReadFile(mpath)
|
||||
json.Unmarshal(b, &f)
|
||||
return f.Providers, string(b)
|
||||
}
|
||||
os.MkdirAll(filepath.Dir(path), 0o700)
|
||||
mine := `{"provider":{"name":"OpenAI Compatible","baseUrl":"https://x/v1","defaultModelId":"mine"},"models":{"mine":{"id":"mine","name":"mine"}}}`
|
||||
os.WriteFile(mpath, []byte(`{"version":1,"providers":{"openai-compatible":`+mine+`,"ollama":{"models":{"q":{"id":"q"}}}}}`), 0o600)
|
||||
os.WriteFile(path, []byte(`{"version":1,"lastUsedProvider":"anthropic","modes":{},"providers":{
|
||||
"anthropic":{"settings":{"provider":"anthropic","apiKey":"sk-a","model":"claude-opus-5","reasoning":{"effort":"high"}},"updatedAt":"2026-09-01T00:00:00.000Z","tokenSource":"manual"},
|
||||
"openai-compatible":{"settings":{"provider":"openai-compatible","apiKey":"sk-o","model":"mine","baseUrl":"https://x/v1"},"updatedAt":"2026-09-01T00:00:00.000Z","tokenSource":"manual"}}}`), 0o600)
|
||||
@@ -54,6 +70,13 @@ func TestCline(t *testing.T) {
|
||||
c.Providers["anthropic"].Settings["apiKey"] != "sk-a" {
|
||||
t.Fatalf("magpie:\n%s", raw)
|
||||
}
|
||||
// Cline's openai-compatible lists only gpt-4o: magpie's models take
|
||||
// its place in models.json, the user's other entries kept
|
||||
ml, mraw := models()
|
||||
if m := ml["openai-compatible"]; m.Provider["baseUrl"] != gatewayV1() || m.Provider["defaultModelId"] != "deepseek/pro" ||
|
||||
m.Models["deepseek/pro"] == nil || m.Models["deepseek/flash"] == nil || m.Models["mine"] != nil || ml["ollama"].Models["q"] == nil {
|
||||
t.Fatalf("models:\n%s", mraw)
|
||||
}
|
||||
if f.Get() != "magpie/deepseek/pro" || e.Get() != "high" || a.Check() != "" {
|
||||
t.Fatalf("get: %q %q %q", f.Get(), e.Get(), a.Check())
|
||||
}
|
||||
@@ -79,10 +102,15 @@ func TestCline(t *testing.T) {
|
||||
c.Providers["openai-compatible"].Settings["apiKey"] != "sk-o" || c.Providers["openai-compatible"].Settings["baseUrl"] != "https://x/v1" {
|
||||
t.Fatalf("own:\n%s", raw)
|
||||
}
|
||||
if ml, mraw := models(); ml["openai-compatible"].Models["mine"] == nil || ml["openai-compatible"].Models["deepseek/pro"] != nil ||
|
||||
ml["openai-compatible"].Provider["baseUrl"] != "https://x/v1" || ml["ollama"].Models["q"] == nil {
|
||||
t.Fatalf("own models:\n%s", mraw)
|
||||
}
|
||||
|
||||
// reset from magpie, with no Cline settings before it: nothing of
|
||||
// magpie's is left
|
||||
os.Remove(path)
|
||||
os.Remove(mpath)
|
||||
if f.Get() != "" {
|
||||
t.Fatalf("empty get: %q", f.Get())
|
||||
}
|
||||
@@ -103,4 +131,7 @@ func TestCline(t *testing.T) {
|
||||
if c.LastUsed != "" || len(c.Providers) != 0 || c.Version != 1 {
|
||||
t.Fatalf("reset:\n%s", raw)
|
||||
}
|
||||
if ml, mraw := models(); len(ml) != 0 || !strings.Contains(mraw, `"version"`) {
|
||||
t.Fatalf("reset models:\n%s", mraw)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user