174 lines
5.9 KiB
Go
174 lines
5.9 KiB
Go
package agent
|
|
|
|
// Mister Morph (morph) keeps its settings in ~/.morph/config.yaml, or the
|
|
// file $MISTER_MORPH_CONFIG names: the main model under llm.model, asked of
|
|
// the vendor llm.inference_provider names at llm.endpoint with llm.api_key.
|
|
// It has no list of providers to add one to, so magpie takes llm over:
|
|
// inference_provider openai_response_compatible at the gateway's /v1 (morph
|
|
// adds no second /v1 to a base ending in one), spoken to on OpenAI's Responses
|
|
// API, morph's own default, which the gateway serves for every vendor; its
|
|
// token as the key, and the catalog id as the model; a model through magpie
|
|
// is "magpie/<provider>/<model>" here. llm.provider is left as it is: morph
|
|
// derives it from inference_provider. Morph knows a model's window only from a list of its own, by the id
|
|
// after its last slash, so magpie hands it the window it knows in
|
|
// llm.context_window_tokens. What the user had under those keys is stashed
|
|
// and put back when magpie steps out. llm.profiles and llm.routes are the
|
|
// user's: a route that sends the main loop to another profile is left so.
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/yetone/magpie/internal/edit"
|
|
)
|
|
|
|
// morphUA is the User-Agent magpie has morph send, in llm.headers: its
|
|
// requests otherwise carry the OpenAI SDK's, which tells them from no one's.
|
|
const morphUA = "mistermorph"
|
|
|
|
// morphKeys are the keys under llm magpie writes, in the order they are put
|
|
// back.
|
|
var morphKeys = []string{"inference_provider", "endpoint", "api_key", "model", "context_window_tokens", "headers.User-Agent"}
|
|
|
|
func morph(home string) *Agent { return morphIn(here(home)) }
|
|
|
|
// morphIn is Mister Morph at a place: this machine's home, or a WSL distro's
|
|
// (see wsl.go), where MISTER_MORPH_CONFIG isn't read.
|
|
func morphIn(at place) *Agent {
|
|
dir := filepath.Join(at.home, ".morph")
|
|
path := at.getenv("MISTER_MORPH_CONFIG")
|
|
if path == "" {
|
|
path = filepath.Join(dir, "config.yaml")
|
|
}
|
|
key := "morph:" + path + ":"
|
|
getKey := func(k string) string { v, _ := edit.GetYAML(path, "llm."+k); return v }
|
|
// the token is magpie's alone, where the endpoint moves with the
|
|
// gateway's port
|
|
onMagpie := func() bool { return ourKey(getKey("api_key")) }
|
|
// restore puts back what the user had under llm before magpie
|
|
restore := func() error {
|
|
var del []string
|
|
var kvs []edit.KV
|
|
for _, k := range morphKeys {
|
|
v := unstash(key + k)
|
|
switch {
|
|
case v == "":
|
|
del = append(del, "llm."+k)
|
|
case k == "context_window_tokens":
|
|
if n, err := strconv.Atoi(v); err == nil {
|
|
kvs = append(kvs, edit.KV{Path: "llm." + k, Value: n})
|
|
break
|
|
}
|
|
fallthrough
|
|
default:
|
|
kvs = append(kvs, edit.KV{Path: "llm." + k, Value: v})
|
|
}
|
|
}
|
|
if err := edit.DelYAML(path, del...); err != nil {
|
|
return err
|
|
}
|
|
// the headers magpie's User-Agent was alone in
|
|
if m := edit.GetYAMLMap(path, "llm.headers"); m != nil && len(m) == 0 {
|
|
if err := edit.DelYAML(path, "llm.headers"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(kvs) == 0 {
|
|
return nil
|
|
}
|
|
return edit.SetYAML(path, kvs...)
|
|
}
|
|
// window is the context magpie knows ref to take, 0 when unknown
|
|
window := func(ref string) int {
|
|
for _, m := range magpieModels("morph") {
|
|
if m.ID == ref {
|
|
return m.Context
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
return &Agent{
|
|
ID: "morph", Name: "Mister Morph", Icon: "mistermorph-color", Aliases: []string{"mistermorph", "mister-morph"}, Spelled: prefixed,
|
|
UA: []string{morphUA},
|
|
// no Bin: morph is other tools' name too; ~/.morph is made by its
|
|
// first run, `morph install` or the desktop app's setup
|
|
Dir: dir, Path: path,
|
|
Notice: func() string {
|
|
if Running(`(^|/)morph( |$)`, `(^|/)mistermorph( |$)`) {
|
|
return "Mister Morph's Console uses this for new tasks; restart open morph chats to use it there."
|
|
}
|
|
return ""
|
|
},
|
|
Check: func() string {
|
|
if !onMagpie() {
|
|
return ""
|
|
}
|
|
return wiringOff("Mister Morph", path, func(k string) (string, bool) { return edit.GetYAML(path, "llm."+k) },
|
|
"endpoint", at.v1(), "inference_provider", "openai_response_compatible")
|
|
},
|
|
Fields: []Field{{
|
|
Key: "model", Label: "model",
|
|
Get: func() string {
|
|
v := getKey("model")
|
|
if v != "" && onMagpie() {
|
|
return magpieID + "/" + v
|
|
}
|
|
return v
|
|
},
|
|
Set: func(v string) error {
|
|
if v == "" {
|
|
if onMagpie() {
|
|
return restore()
|
|
}
|
|
return edit.DelYAML(path, "llm.model")
|
|
}
|
|
if ref, ok := strings.CutPrefix(v, magpieID+"/"); ok && isMagpie(ref) {
|
|
if !onMagpie() {
|
|
kv := map[string]string{}
|
|
for _, k := range morphKeys {
|
|
kv[key+k] = getKey(k)
|
|
}
|
|
stash(kv)
|
|
}
|
|
kvs := []edit.KV{
|
|
{Path: "llm.inference_provider", Value: "openai_response_compatible"},
|
|
{Path: "llm.endpoint", Value: at.v1()},
|
|
{Path: "llm.api_key", Value: at.gwKey()},
|
|
{Path: "llm.model", Value: ref},
|
|
{Path: "llm.headers.User-Agent", Value: morphUA},
|
|
}
|
|
if n := window(ref); n > 0 {
|
|
kvs = append(kvs, edit.KV{Path: "llm.context_window_tokens", Value: n})
|
|
} else if err := edit.DelYAML(path, "llm.context_window_tokens"); err != nil {
|
|
return err
|
|
}
|
|
return edit.SetYAML(path, kvs...)
|
|
}
|
|
if onMagpie() {
|
|
if err := restore(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return edit.SetYAML(path, edit.KV{Path: "llm.model", Value: v})
|
|
},
|
|
Options: func(cur map[string]string) []Option {
|
|
return append(ownOptions("", cur["model"]), viaMagpie("morph", magpieID+"/")...)
|
|
},
|
|
}, {
|
|
// llm.reasoning_effort; unset, morph sends none
|
|
Key: "effort", Label: "effort",
|
|
Get: func() string { return getKey("reasoning_effort") },
|
|
Set: func(v string) error {
|
|
if v == "" {
|
|
return edit.DelYAML(path, "llm.reasoning_effort")
|
|
}
|
|
return edit.SetYAML(path, edit.KV{Path: "llm.reasoning_effort", Value: v})
|
|
},
|
|
Options: func(map[string]string) []Option {
|
|
return static("none", "minimal", "low", "medium", "high", "xhigh", "max")
|
|
},
|
|
}},
|
|
}
|
|
}
|