Files
magpie/internal/agent/dsh.go
T

1211 lines
37 KiB
Go

package agent
// DeepSeek Harness (dsh) boots from its shipped rows and then applies a
// personal patch list: a YAML list of {id, config} entries, each replacing
// the whole config of the row with that id. Since 0.1.5 every profile has
// its own, ~/.dsh/profiles/<name>/cordis.patch.yml (web and desktop read it
// live); before that there was one, ~/.dsh/config.yaml.
//
// Since 0.1.5 magpie is one of dsh's custom model providers: a route named
// magpie in the llm-pi-ai entry's providers (what dsh's Models page writes
// for "Add model provider"), with the gateway as its endpoint, the catalog
// as its models and, as the key, a credential named in apiKeyEnv, which
// magpie puts in ~/.dsh/.env; agent-default-model names the model new
// sessions start on. dsh's own DeepSeek row (llm-deepseek) is left to dsh:
// taken over, it read as DeepSeek in dsh's Models page, and a DeepSeek key
// entered there went to the gateway under magpie's credential name, over
// magpie's own (Discord: 集成进 dsh 会被 dsh 覆写). A route of the user's in
// the same entry stays as it is, and so does one dsh adds beside magpie's.
//
// Before 0.1.5, which has no custom providers, magpie took over
// llm-deepseek, with the key in the entry, and set agent-loop's main agent
// (the TUI) and api-gateway's route (`dsh -p`, `dsh web`). Entries magpie
// writes whole are marked as magpie's, the user's own entries stay as they
// are, and one magpie replaces is stashed and put back when it steps out.
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"gopkg.in/yaml.v3"
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/gateway"
)
const dshMark = "# magpie"
// dshKeyRef is the credential dsh signs gateway requests with. It is not
// dshOldKeyRef, the one magpie's llm-deepseek entry named: dsh's Models page
// showed that entry as DeepSeek, so a DeepSeek key entered there may sit in
// dsh's own store under that name, which dsh reads over .env.
const (
dshKeyRef = "MAGPIE_GATEWAY_KEY"
dshOldKeyRef = "MAGPIE_API_KEY"
)
// dshRoute is magpie's route among dsh's custom providers, and the
// provider agent-default-model names for a model through magpie.
const dshRoute = magpieID
// dshPiRow is the row custom providers are configured on, and its plugin.
const (
dshPiRow = "llm-pi-ai"
dshPiPlugin = "@deepseek-ai/dsh-llm-pi-ai"
)
// dshModels are the models dsh reaches on its own, as it ships them — under
// its own name, not DeepSeek: magpie's DeepSeek provider is a group of that
// name too, and two would read as one listed twice (#115).
var dshModels = []Option{
{Value: "deepseek-flash", Label: "DeepSeek-V41-Flash", Icon: "deepseek-color", Group: "DeepSeek Harness"},
{Value: "deepseek-v4-pro", Label: "DeepSeek-V4-Pro", Icon: "deepseek-color", Group: "DeepSeek Harness"},
{Value: "deepseek-v4-flash", Label: "DeepSeek-V4-Flash", Icon: "deepseek-color", Group: "DeepSeek Harness"},
}
func dsh(home string) *Agent {
dir := os.Getenv("DSH_HOME")
if dir == "" {
dir = filepath.Join(home, ".dsh")
}
path := filepath.Join(dir, "config.yaml")
if files := dshProfiles(dir); len(files) > 0 {
path = files[0]
}
return &Agent{
ID: "dsh", Name: "DeepSeek Harness", Icon: "deepseek-color", Aliases: []string{"deepseek-harness"},
UA: []string{"deepseek-harness"},
Bin: "dsh", Dir: dir, Path: path,
Sync: func() error { return dshSync(dir) },
Notice: func() string {
var notes []string
if Running(`(^|/)dsh( |$)`) {
if len(dshProfiles(dir)) > 0 {
notes = append(notes, "New dsh sessions start on this; one already open keeps its model until you pick another in it. A dsh started with -p or in a terminal reads it at start-up.")
} else {
notes = append(notes, "dsh reads its config at start-up — restart open dsh sessions to use this.")
}
}
// only before 0.1.5 does magpie go through llm-deepseek
if len(dshProfiles(dir)) == 0 && dshSettingsEndpoint(filepath.Join(dir, "settings.yaml")) {
notes = append(notes, "~/.dsh/settings.yaml sets its own DeepSeek endpoint or key, which dsh puts over magpie's; clear it in dsh's Models page to go through magpie.")
}
return strings.Join(notes, " ")
},
Check: func() string { return dshCheck(dir) },
Fields: []Field{{
Key: "model", Label: "model",
Get: func() string { return dshGet(dir) },
Set: func(v string) error { return dshSet(dir, v) },
Options: func(map[string]string) []Option {
return append(append([]Option{}, dshModels...), viaMagpie("dsh", magpieID+"/")...)
},
}, {
// the thinking effort sessions start with: agent-default-model's
// reasoningEffort since 0.1.5 (a model through magpie offers its
// own levels), llm-deepseek's before (off, low, high or max;
// dsh's own default is high)
Key: "effort", Label: "thinking",
Get: func() string { return dshGetEffort(dir) },
Set: func(v string) error { return dshSetEffort(dir, v) },
Options: func(cur map[string]string) []Option {
// a model through magpie that lists no levels has none in
// dsh (its route gives it no reasoningEfforts), so none is
// offered: dsh's own four were, and each was turned away
if ref, ok := strings.CutPrefix(cur["model"], magpieID+"/"); ok && len(dshProfiles(dir)) > 0 {
return static(dshLevels(ref)...)
}
return static(dshEfforts...)
},
}},
}
}
// dshProfiles are the profiles' patch lists (dsh 0.1.5 on), web's first.
func dshProfiles(dir string) []string {
files, _ := filepath.Glob(filepath.Join(dir, "profiles", "*", "cordis.patch.yml"))
var web, rest []string
for _, f := range files {
if filepath.Base(filepath.Dir(f)) == "web" {
web = append(web, f)
} else {
rest = append(rest, f)
}
}
return append(web, rest...)
}
// dshItem is one entry of the patch list, as its lines.
type dshItem struct {
id string
magpie bool
lines []string
}
var dshIDLine = regexp.MustCompile(`^(?:- | )id:\s*['"]?([^'"#\s]+)['"]?\s*(#.*)?$`)
// dshParse splits the patch list at path into what comes before its first
// entry and the entries. A list written in flow style ([ {id: a} ], which
// dsh's own writers keep a profile's [] in) is read as the same list in block
// style, and written back so; a file that is no list is left alone.
func dshParse(path, raw string) (head []string, items []dshItem, err error) {
if b, ok := edit.BlockList(raw); ok {
raw = b
}
for _, l := range splitLinesKeep(raw) {
t := strings.TrimSpace(l)
switch {
case strings.HasPrefix(l, "- ") || l == "-":
items = append(items, dshItem{lines: []string{l}})
case len(items) == 0:
if t != "" && !strings.HasPrefix(t, "#") && t != "[]" {
return nil, nil, fmt.Errorf("%s is not a list of entries magpie can edit", path)
}
if t != "[]" {
head = append(head, l)
}
continue
case t != "" && !strings.HasPrefix(t, "#") && !strings.HasPrefix(l, " "):
return nil, nil, fmt.Errorf("%s is not a list of entries magpie can edit", path)
default:
items[len(items)-1].lines = append(items[len(items)-1].lines, l)
}
it := &items[len(items)-1]
if m := dshIDLine.FindStringSubmatch(l); m != nil && it.id == "" {
it.id = m[1]
it.magpie = strings.TrimSpace(m[2]) == dshMark
}
}
return head, items, nil
}
func splitLinesKeep(s string) []string {
s = strings.ReplaceAll(s, "\r\n", "\n")
s = strings.TrimRight(s, "\n")
if s == "" {
return nil
}
return strings.Split(s, "\n")
}
func dshRead(path string) ([]string, []dshItem, error) {
b, err := edit.Read(path)
if err != nil {
return nil, nil, err
}
return dshParse(path, string(b))
}
// dshWrite puts a patch list back together.
func dshWrite(path string, head []string, items []dshItem) error {
out := append([]string{}, head...)
for _, it := range items {
out = append(out, it.lines...)
}
if len(items) == 0 {
if _, err := os.Stat(path); err != nil {
return nil // nothing was there, nothing to write
}
out = append(out, "[]") // dsh wants a list, even an empty one
}
return edit.WriteAtomic(path, []byte(strings.Join(out, "\n")+"\n"))
}
func dshFind(items []dshItem, id string) int {
for i, it := range items {
if it.id == id {
return i
}
}
return -1
}
// dshFindLast is the entry of that id dsh goes by: the last, as its own
// config editor edits.
func dshFindLast(items []dshItem, id string) int {
for i := len(items) - 1; i >= 0; i-- {
if items[i].id == id {
return i
}
}
return -1
}
// dshNode reads one entry as YAML: its mapping ({id, name, config}).
func dshNode(it dshItem) (*yaml.Node, error) {
var doc yaml.Node
if err := yaml.Unmarshal([]byte(strings.Join(it.lines, "\n")), &doc); err != nil {
return nil, err
}
if len(doc.Content) == 0 || doc.Content[0].Kind != yaml.SequenceNode || len(doc.Content[0].Content) != 1 || doc.Content[0].Content[0].Kind != yaml.MappingNode {
return nil, fmt.Errorf("the %s entry is not one magpie can edit", it.id)
}
return doc.Content[0].Content[0], nil
}
// dshNodeLines writes an entry read with dshNode back as lines.
func dshNodeLines(n *yaml.Node) ([]string, error) {
var buf bytes.Buffer
enc := yaml.NewEncoder(&buf)
enc.SetIndent(2)
if err := enc.Encode(&yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{n}}); err != nil {
return nil, err
}
enc.Close()
return splitLinesKeep(buf.String()), nil
}
func yamlKey(n *yaml.Node, key string) *yaml.Node {
if n == nil || n.Kind != yaml.MappingNode {
return nil
}
for i := 0; i+1 < len(n.Content); i += 2 {
if n.Content[i].Value == key {
return n.Content[i+1]
}
}
return nil
}
func yamlSetKey(n *yaml.Node, key string, v *yaml.Node) {
for i := 0; i+1 < len(n.Content); i += 2 {
if n.Content[i].Value == key {
n.Content[i+1] = v
return
}
}
n.Content = append(n.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, v)
}
func yamlDelKey(n *yaml.Node, key string) {
for i := 0; i+1 < len(n.Content); i += 2 {
if n.Content[i].Value == key {
n.Content = append(n.Content[:i], n.Content[i+2:]...)
return
}
}
}
// yamlMapping is the mapping at key, made when there is none (or the value
// there is not one).
func yamlMapping(n *yaml.Node, key string) *yaml.Node {
if v := yamlKey(n, key); v != nil && v.Kind == yaml.MappingNode {
return v
}
v := &yaml.Node{Kind: yaml.MappingNode}
yamlSetKey(n, key, v)
return v
}
// dshConfig reads the scalar fields of an entry's config: dsh writes them
// as it likes, quoted or not.
func dshConfig(it dshItem) map[string]string {
n, err := dshNode(it)
if err != nil {
return nil
}
out := map[string]string{}
if c := yamlKey(n, "config"); c != nil && c.Kind == yaml.MappingNode {
for i := 0; i+1 < len(c.Content); i += 2 {
if v := c.Content[i+1]; v.Kind == yaml.ScalarNode {
out[c.Content[i].Value] = v.Value
}
}
}
return out
}
// dshRouteIn is magpie's route among the custom providers of a patch list,
// nil when there is none.
func dshRouteIn(items []dshItem) *yaml.Node {
i := dshFindLast(items, dshPiRow)
if i < 0 {
return nil
}
n, err := dshNode(items[i])
if err != nil {
return nil
}
return yamlKey(yamlKey(yamlKey(n, "config"), "providers"), dshRoute)
}
// dshOldWiring reports whether a profile still has what magpie wrote before
// it was a custom provider: dsh's own DeepSeek row taken over.
func dshOldWiring(items []dshItem) bool {
i := dshFind(items, "llm-deepseek")
return i >= 0 && items[i].magpie
}
// dshOurs reports whether an entry is magpie's: marked so, or — should dsh
// rewrite it without the mark — a model on magpie's route, which is gone
// with it.
func dshOurs(it dshItem) bool {
return it.magpie || it.id == "agent-default-model" && dshConfig(it)["provider"] == dshRoute
}
// dshWired reports whether a profile's patch list has magpie's route.
func dshWired(items []dshItem) bool { return dshRouteIn(items) != nil }
// dshServes reports whether magpie gives dsh this model now: the route
// written again would list it, where one naming a model magpie no longer
// gives dsh would not.
func dshServes(id string) bool {
for _, m := range magpieModels("dsh") {
if m.ID == id {
return true
}
}
return false
}
// dshRouteLists reports whether magpie's route carries a model with this id.
func dshRouteLists(route *yaml.Node, id string) bool {
models := yamlKey(route, "models")
if models == nil {
return false
}
for _, m := range models.Content {
if v := yamlKey(m, "id"); v != nil && v.Value == id {
return true
}
}
return false
}
var dshModelLine = regexp.MustCompile(`^\s+model:\s*(.+?)\s*$`)
// dshGet reads the model new sessions start on: the one last picked in dsh,
// saved in its settings (0.1.x), else the patch list's.
func dshGet(dir string) string {
files := dshProfiles(dir)
if len(files) == 0 {
return dshGetLegacy(filepath.Join(dir, "config.yaml"))
}
_, items, err := dshRead(files[0])
if err != nil {
return ""
}
return dshStart(dir, items)
}
// dshStart reads the model a profile's sessions start on: the one last picked
// in dsh, saved in its settings, which go over every profile, else the
// profile's own agent-default-model entry.
func dshStart(dir string, items []dshItem) string {
sel := edit.GetYAMLMap(filepath.Join(dir, "settings.yaml"), "agent-default-model")
if sel["model"] == "" {
if i := dshFindLast(items, "agent-default-model"); i >= 0 {
sel = dshConfig(items[i])
}
}
switch {
case sel["model"] == "":
return ""
case sel["provider"] == dshRoute,
// set before magpie was a provider of its own, not yet moved
sel["provider"] == "deepseek-official" && dshOldWiring(items):
return magpieID + "/" + sel["model"]
}
return sel["model"]
}
// dshGetLegacy reads agent-loop's model from a config.yaml before 0.1.5.
func dshGetLegacy(path string) string {
_, items, err := dshRead(path)
if err != nil {
return ""
}
model := ""
if i := dshFind(items, "agent-loop"); i >= 0 {
for _, l := range items[i].lines {
if m := dshModelLine.FindStringSubmatch(l); m != nil {
model = yamlScalar(m[1])
break
}
}
}
if model != "" && dshOldWiring(items) {
return magpieID + "/" + model
}
return model
}
// dshCheck says what keeps a dsh on one of magpie's models from reaching
// the gateway: magpie's route (its llm-deepseek entry before 0.1.5) pointed
// elsewhere, the model it starts on one the route doesn't list, or — since
// 0.1.5 — the key it names gone from .env, or another key under that name in
// dsh's own store, which it reads first.
func dshCheck(dir string) string {
if !usesMagpie(dshGet(dir)) {
return ""
}
files := dshProfiles(dir)
if len(files) == 0 {
path := filepath.Join(dir, "config.yaml")
_, items, _ := dshRead(path)
base := ""
if i := dshFind(items, "llm-deepseek"); i >= 0 {
for _, l := range items[i].lines {
if k, v, ok := strings.Cut(strings.TrimSpace(l), ":"); ok && k == "baseURL" {
base = yamlScalar(strings.TrimSpace(v))
}
}
}
get := func(string) (string, bool) { return base, base != "" }
return wiringOff("DeepSeek Harness", path, get, "baseURL", gatewayV1())
}
// a profile of dsh's (its desktop app's) without magpie's route:
// sessions there list dsh's own models alone
for _, f := range files[1:] {
if _, items, err := dshRead(f); err == nil && !dshWired(items) {
return "DeepSeek Harness's " + filepath.Base(filepath.Dir(f)) + " profile (" + f + ") has none of magpie's models, so sessions there list DeepSeek's own alone"
}
}
_, items, _ := dshRead(files[0])
route := dshRouteIn(items)
get := func(k string) (string, bool) {
if v := yamlKey(route, k); v != nil && v.Kind == yaml.ScalarNode {
return v.Value, true
}
return "", false
}
if route == nil && dshOldWiring(items) {
// not moved yet; the next sync does it
get = func(k string) (string, bool) {
v, ok := dshConfig(items[dshFind(items, "llm-deepseek")])[k]
return v, ok
}
}
if off := wiringOff("DeepSeek Harness", files[0], get, "baseURL", gatewayV1()); off != "" {
return off
}
// dsh counts a model its provider doesn't list as none at all and refuses
// the turn, so a model of magpie's a profile's route has not is as
// unusable there as a route pointed elsewhere: a catalog that moved on, a
// profile written elsewhere, or one dsh's Models page wrote leave it that
// way. Every profile carries a route of its own, and dsh runs whichever
// one its session names, so every profile is asked.
for _, f := range files {
_, it, err := dshRead(f)
if err != nil {
continue
}
r := dshRouteIn(it)
if r == nil {
continue // no route yet: the sync this asks for writes one
}
if ref, ok := strings.CutPrefix(dshStart(dir, it), magpieID+"/"); ok && !dshRouteLists(r, ref) {
// a route out of date is written again — Apply again, or picking
// a model of magpie's in dsh, does it; one naming a model magpie
// no longer gives dsh is not, so that one asks for another model
// rather than for a click that cannot help
if dshServes(ref) {
return "DeepSeek Harness starts on " + magpieID + "/" + ref + ", which magpie's route in " + f + " doesn't list: a session there fails with no such configured model until that route is written again (Apply again, or pick one of magpie's models in dsh)"
}
return "DeepSeek Harness starts on " + magpieID + "/" + ref + ", which magpie no longer gives dsh: a session there fails with no such configured model until another of magpie's is picked in dsh"
}
}
env := filepath.Join(dir, ".env")
if off := wiringOff("DeepSeek Harness", env, func(k string) (string, bool) { return edit.GetEnvFile(env, k) },
dshKeyRef, gateway.Token); off != "" {
return off
}
creds := filepath.Join(dir, ".credentials.yaml")
if v, ok := edit.GetYAML(creds, "refs."+dshKeyRef); ok && v != gateway.Token {
return "DeepSeek Harness's own key store (" + creds + ") holds another " + dshKeyRef + ", which it uses over magpie's; remove it there (dsh's Models page, Magpie's key) to go through magpie"
}
return ""
}
// yamlScalar reads a plain or quoted YAML scalar.
func yamlScalar(v string) string {
if i := strings.Index(v, " #"); i >= 0 {
v = strings.TrimSpace(v[:i])
}
if strings.HasPrefix(v, `"`) {
var s string
if json.Unmarshal([]byte(v), &s) == nil {
return s
}
}
return strings.Trim(v, `'"`)
}
func dshStashKey(path, id string) string { return "dsh:" + path + ":" + id }
// dshSet writes v as the model new sessions start on: a catalog model
// through the gateway, one of dsh's own models directly, or "" for dsh's own
// default. Every profile gets it; config.yaml only where there are none.
func dshSet(dir, v string) error {
ref, viaGateway := strings.CutPrefix(v, magpieID+"/")
if viaGateway && !isMagpie(ref) {
return fmt.Errorf("unknown model %q", v)
}
legacy := filepath.Join(dir, "config.yaml")
files := dshProfiles(dir)
if len(files) == 0 {
return dshSetFile(legacy, v, false)
}
for _, f := range files {
if err := dshSetFile(f, v, true); err != nil {
return err
}
}
// what an older dsh was given is no use now
if _, err := os.Stat(legacy); err == nil {
if err := dshSetFile(legacy, "", false); err != nil {
return err
}
}
if err := dshEnv(dir, viaGateway); err != nil {
return err
}
// a model picked in dsh 0.1.x is saved in its settings and goes over
// the patch list; picking one here takes over from it
settings := filepath.Join(dir, "settings.yaml")
if v != "" && edit.GetYAMLMap(settings, "agent-default-model") != nil {
return edit.DelYAML(settings, "agent-default-model")
}
return nil
}
// dshEnv puts the key for the gateway in $DSH_HOME/.env, or with on false
// takes it out; the name magpie's llm-deepseek entry used goes either way.
func dshEnv(dir string, on bool) error {
env := filepath.Join(dir, ".env")
if on {
if v, ok := edit.GetEnvFile(env, dshKeyRef); !ok || v != gateway.Token {
if err := edit.SetEnvFile(env, edit.KV{Path: dshKeyRef, Value: gateway.Token}); err != nil {
return err
}
}
}
for _, k := range []string{dshKeyRef, dshOldKeyRef} {
// the old name only as magpie left it
if v, ok := edit.GetEnvFile(env, k); ok && !(k == dshKeyRef && on) && (k == dshKeyRef || v == gateway.Token) {
if err := edit.DelEnvFile(env, k); err != nil {
return err
}
}
}
return nil
}
// dshSetFile writes v into one patch list; modern is the layout of dsh
// 0.1.5 on.
func dshSetFile(path, v string, modern bool) error {
head, items, err := dshRead(path)
if err != nil {
return err
}
ref, viaGateway := strings.CutPrefix(v, magpieID+"/")
effort := ""
if modern {
effort = dshEffortOf(items)
} else {
effort = dshEffortIn(items)
}
put := func(id string, lines []string) {
i := dshFind(items, id)
if i >= 0 && !dshOurs(items[i]) {
stash(map[string]string{dshStashKey(path, id): strings.Join(items[i].lines, "\n")})
}
it := dshItem{id: id, magpie: true, lines: lines}
if i >= 0 {
items[i] = it
} else {
items = append(items, it)
}
}
drop := func(id string) {
i := dshFind(items, id)
if i < 0 || !dshOurs(items[i]) {
return
}
if old := unstash(dshStashKey(path, id)); old != "" {
items[i] = dshItem{id: id, lines: strings.Split(old, "\n")}
return
}
items = append(items[:i], items[i+1:]...)
}
switch {
case v == "":
drop("agent-default-model")
drop("api-gateway")
drop("agent-loop")
drop("llm-deepseek")
if items, err = dshPutRoute(items, false); err != nil {
return err
}
case modern && viaGateway:
drop("llm-deepseek") // magpie's before it was a provider of its own
if items, err = dshPutRoute(items, true); err != nil {
return err
}
if !contains(dshLevels(ref), effort) {
effort = ""
}
put("agent-default-model", dshDefaultLines(dshRoute, ref, effort))
case modern:
drop("llm-deepseek")
if items, err = dshPutRoute(items, false); err != nil {
return err
}
if !contains(dshEfforts, effort) {
effort = ""
}
put("agent-default-model", dshDefaultLines("deepseek-official", v, effort))
case viaGateway:
put("llm-deepseek", dshProviderLines(effort))
put("agent-loop", dshLoopLines(ref))
put("api-gateway", dshRouteLines(ref))
default:
drop("api-gateway")
drop("llm-deepseek")
put("agent-loop", dshLoopLines(v))
}
return dshWrite(path, head, items)
}
// dshPutRoute puts magpie's route, with the catalog as it is now, among the
// custom providers of the llm-pi-ai entry, or with on false takes it out.
// Other routes there, the user's or ones dsh added beside magpie's, stay; an
// entry left with nothing goes.
func dshPutRoute(items []dshItem, on bool) ([]dshItem, error) {
i := dshFindLast(items, dshPiRow)
if i < 0 && !on {
return items, nil
}
var n *yaml.Node
if i >= 0 {
var err error
if n, err = dshNode(items[i]); err != nil {
return nil, err
}
} else {
items = append(items, dshItem{id: dshPiRow, magpie: true, lines: []string{"- id: " + dshPiRow + " " + dshMark, " name: " + yamlQuote(dshPiPlugin)}})
i = len(items) - 1
n, _ = dshNode(items[i])
}
config := yamlMapping(n, "config")
providers := yamlMapping(config, "providers")
if on {
var route yaml.Node
if err := route.Encode(dshRouteConfig()); err != nil {
return nil, err
}
// what dsh's Models page added to it (a default level, headers)
// stays; magpie's fields are put as they are now
if cur := yamlKey(providers, dshRoute); cur != nil && cur.Kind == yaml.MappingNode {
for k := 0; k+1 < len(route.Content); k += 2 {
yamlSetKey(cur, route.Content[k].Value, route.Content[k+1])
}
} else {
yamlSetKey(providers, dshRoute, &route)
}
} else {
yamlDelKey(providers, dshRoute)
if len(providers.Content) == 0 {
yamlDelKey(config, "providers")
}
if len(config.Content) == 0 {
yamlDelKey(n, "config")
}
if yamlKey(n, "config") == nil && len(n.Content) <= 4 { // id and name alone
return append(items[:i], items[i+1:]...), nil
}
}
lines, err := dshNodeLines(n)
if err != nil {
return nil, err
}
items[i] = dshItem{id: dshPiRow, magpie: items[i].magpie, lines: lines}
return items, nil
}
// dshPiRoute is magpie's route as llm-pi-ai takes a custom provider (the
// shape dsh's Models page writes): the gateway's Chat Completions API, the
// key a credential named here, the catalog as its models.
type dshPiRoute struct {
DisplayName string `yaml:"displayName"`
APIKeyEnv string `yaml:"apiKeyEnv"`
API string `yaml:"api"`
BaseURL string `yaml:"baseURL"`
Models []dshPiModel `yaml:"models"`
}
// dshPiModel is one model of the route. Without contextWindow and maxTokens
// dsh takes every model for a 128K one writing 16K at most, and without
// input for a text-only one; without reasoningEfforts it offers none.
type dshPiModel struct {
ID string `yaml:"id"`
Name string `yaml:"name,omitempty"`
ContextWindow int `yaml:"contextWindow,omitempty"`
MaxTokens int `yaml:"maxTokens,omitempty"`
Input []string `yaml:"input,omitempty,flow"`
ReasoningEfforts *dshPiLevels `yaml:"reasoningEfforts,omitempty"`
}
// dshPiLevels maps each of dsh's thinking levels a model takes to the
// reasoning_effort sent for it; a level left out is not offered.
type dshPiLevels struct {
Off string `yaml:"off,omitempty"`
Minimal string `yaml:"minimal,omitempty"`
Low string `yaml:"low,omitempty"`
Medium string `yaml:"medium,omitempty"`
High string `yaml:"high,omitempty"`
XHigh string `yaml:"xhigh,omitempty"`
Max string `yaml:"max,omitempty"`
}
// dshThinking are dsh's thinking levels (pi-ai's), in its order.
var dshThinking = []string{"off", "minimal", "low", "medium", "high", "xhigh", "max"}
// dshModelLevels are the levels of dsh's a catalog model takes, each with
// the effort sent for it: its own, and off for its none.
func dshModelLevels(efforts []string) ([]string, *dshPiLevels) {
var levels []string
var out dshPiLevels
for _, l := range dshThinking {
e := l
if l == "off" {
e = "none"
}
if !contains(efforts, e) {
continue
}
levels = append(levels, l)
switch l {
case "off":
out.Off = e
case "minimal":
out.Minimal = e
case "low":
out.Low = e
case "medium":
out.Medium = e
case "high":
out.High = e
case "xhigh":
out.XHigh = e
case "max":
out.Max = e
}
}
// dsh turns away a model offering no level but off
if len(levels) == 0 || len(levels) == 1 && levels[0] == "off" {
return nil, nil
}
return levels, &out
}
// dshLevels are the thinking levels dsh offers for a catalog model.
func dshLevels(ref string) []string {
for _, m := range magpieModels("dsh") {
if m.ID == ref {
levels, _ := dshModelLevels(m.Efforts)
return levels
}
}
return nil
}
func dshRouteConfig() dshPiRoute {
r := dshPiRoute{DisplayName: "Magpie", APIKeyEnv: dshKeyRef, API: "openai-completions", BaseURL: gatewayV1(), Models: []dshPiModel{}}
for _, m := range magpieModels("dsh") {
e := dshPiModel{ID: m.ID, Name: m.Name, ContextWindow: m.Context}
if m.Output > 0 {
e.MaxTokens = maxTokens(m)
}
switch {
case m.Images:
e.Input = []string{"text", "image"}
case m.ImageInput != nil:
e.Input = []string{"text"}
}
_, e.ReasoningEfforts = dshModelLevels(m.Efforts)
r.Models = append(r.Models, e)
}
return r
}
func yamlQuote(s string) string {
b, _ := json.Marshal(s)
return string(b)
}
// dshProviderLines is the llm-deepseek entry pointing a dsh before 0.1.5 at
// the gateway, with the catalog as the models its /model offers and the key
// in the entry. effort is the thinking effort sessions start with, high
// (dsh's own default) when "".
func dshProviderLines(effort string) []string {
if effort == "" {
effort = "high"
}
lines := []string{
"- id: llm-deepseek " + dshMark,
" config:",
" apiKey: " + yamlQuote(gateway.Token),
" baseURL: " + yamlQuote(gatewayV1()),
" thinking: enabled",
" reasoningEffort: " + effort,
" models:",
}
ms := magpieModels("dsh")
if len(ms) == 0 {
lines[len(lines)-1] = " models: []"
}
for _, m := range ms {
lines = append(lines, " - id: "+yamlQuote(m.ID), " name: "+yamlQuote(m.Name))
// what dsh would otherwise take for every model: a million tokens
// of context, 256K out, and text only
if m.Context > 0 {
lines = append(lines, fmt.Sprintf(" contextWindow: %d", m.Context))
}
if m.Output > 0 {
lines = append(lines, fmt.Sprintf(" maxTokens: %d", maxTokens(m)))
}
if m.Images {
lines = append(lines, " inputModalities: [text, image]")
}
}
return lines
}
// dshDefaultLines is the agent-default-model entry: the model new sessions
// start on, in every entry point, on provider's route, and the thinking
// effort they start with when effort isn't "".
func dshDefaultLines(provider, model, effort string) []string {
lines := []string{
"- id: agent-default-model " + dshMark,
" config:",
" provider: " + provider,
" model: " + yamlQuote(model),
}
if effort != "" {
lines = append(lines, " reasoningEffort: "+yamlQuote(effort))
}
return lines
}
// dshLoopLines is the agent-loop entry dsh ships, with model in place.
func dshLoopLines(model string) []string {
return []string{
"- id: agent-loop " + dshMark,
" config:",
" agents:",
" - id: main",
" provider: deepseek-official",
" model: " + yamlQuote(model),
" cwd: !!js process.cwd()",
}
}
// dshRouteLines is the api-gateway entry: the route headless and web
// sessions start on.
func dshRouteLines(model string) []string {
return []string{
"- id: api-gateway " + dshMark,
" config:",
" provider: deepseek-official",
" model: " + yamlQuote(model),
}
}
// dshSync puts the catalog as it is now into magpie's route (its
// llm-deepseek entry before 0.1.5), in every patch list that has it; a
// profile still on dsh's DeepSeek row taken over moves to the route.
// Nothing else in the patch lists changes.
func dshSync(dir string) error {
files := dshProfiles(dir)
if len(files) == 0 {
return dshSyncLegacy(filepath.Join(dir, "config.yaml"))
}
defer func() {
// the key under the name the route gives it, moved from the old one
if _, items, err := dshRead(files[0]); err == nil && dshWired(items) {
dshEnv(dir, true)
}
}()
for _, f := range files {
head, items, err := dshRead(f)
if err != nil {
continue
}
if dshOldWiring(items) {
if err := dshMove(f, items); err != nil {
return err
}
continue
}
if !dshWired(items) {
continue
}
i := dshFindLast(items, dshPiRow)
before := strings.Join(items[i].lines, "\n")
if items, err = dshPutRoute(items, true); err != nil {
continue
}
if strings.Join(items[i].lines, "\n") == before {
continue
}
if err := dshWrite(f, head, items); err != nil {
return err
}
}
return dshFillNewProfiles(files)
}
// dshMove moves a profile magpie wired by taking over dsh's DeepSeek row
// onto magpie's route, the model and effort kept.
func dshMove(f string, items []dshItem) error {
model := ""
if i := dshFindLast(items, "agent-default-model"); i >= 0 {
if c := dshConfig(items[i]); c["provider"] == "deepseek-official" {
model = c["model"]
}
}
if model == "" || !isMagpie(model) {
// nothing of magpie's to start on: dsh's own row back alone
head, items, err := dshRead(f)
if err != nil {
return err
}
if old := unstash(dshStashKey(f, "llm-deepseek")); old != "" {
items[dshFind(items, "llm-deepseek")] = dshItem{id: "llm-deepseek", lines: strings.Split(old, "\n")}
} else {
i := dshFind(items, "llm-deepseek")
items = append(items[:i], items[i+1:]...)
}
return dshWrite(f, head, items)
}
return dshSetFile(f, magpieID+"/"+model, true)
}
// dshSyncLegacy is dshSync on a config.yaml before 0.1.5.
func dshSyncLegacy(f string) error {
head, items, err := dshRead(f)
if err != nil {
return nil
}
i := dshFind(items, "llm-deepseek")
if i < 0 || !items[i].magpie {
return nil
}
lines := dshProviderLines(dshEffortIn(items))
if strings.Join(lines, "\n") == strings.Join(items[i].lines, "\n") {
return nil
}
items[i].lines = lines
return dshWrite(f, head, items)
}
// dshFillNewProfiles gives a profile dsh made after magpie set it up — the
// desktop app's, opened for the first time after the web's was wired —
// magpie's route, and the model magpie set in the others when it starts on
// none of the user's: until then it lists dsh's own models alone.
func dshFillNewProfiles(files []string) error {
model := ""
var bare []string
for _, f := range files {
_, items, err := dshRead(f)
if err != nil {
continue
}
if !dshWired(items) {
bare = append(bare, f)
continue
}
if i := dshFindLast(items, "agent-default-model"); i >= 0 && model == "" {
if c := dshConfig(items[i]); c["provider"] == dshRoute {
model = c["model"]
}
}
}
if model == "" {
return nil
}
for _, f := range bare {
head, items, err := dshRead(f)
if err != nil {
continue
}
if i := dshFind(items, "agent-default-model"); i < 0 || dshOurs(items[i]) {
if err := dshSetFile(f, magpieID+"/"+model, true); err != nil {
return err
}
continue
}
// a model of the user's: magpie's are listed beside it
if items, err = dshPutRoute(items, true); err != nil {
continue
}
if err := dshWrite(f, head, items); err != nil {
return err
}
}
return nil
}
// dshEfforts are the thinking efforts dsh's own DeepSeek row takes: its
// reasoningEffort is off, low, high or max (low since 0.1.1), sent as
// reasoning_effort before 0.1.7 and output_config.effort from it on.
var dshEfforts = []string{"off", "low", "high", "max"}
// dshEffortIn is the reasoningEffort of the llm-deepseek entry magpie
// wrote, "" when there is none.
func dshEffortIn(items []dshItem) string {
i := dshFind(items, "llm-deepseek")
if i < 0 || !items[i].magpie {
return ""
}
for _, l := range items[i].lines {
if k, v, ok := strings.Cut(strings.TrimSpace(l), ":"); ok && k == "reasoningEffort" {
return yamlScalar(strings.TrimSpace(v))
}
}
return ""
}
// dshEffortOf is the effort sessions start with in a profile (0.1.5 on):
// agent-default-model's, else, not yet moved, magpie's llm-deepseek's.
func dshEffortOf(items []dshItem) string {
if i := dshFindLast(items, "agent-default-model"); i >= 0 {
if e := dshConfig(items[i])["reasoningEffort"]; e != "" {
return e
}
}
return dshEffortIn(items)
}
// dshFiles are the patch lists magpie writes: every profile's, else
// config.yaml.
func dshFiles(dir string) []string {
if files := dshProfiles(dir); len(files) > 0 {
return files
}
return []string{filepath.Join(dir, "config.yaml")}
}
// DshPatchFiles are the patch lists of the dsh whose home is dir, as magpie
// writes them: every profile's (web's first), else config.yaml.
func DshPatchFiles(dir string) []string { return dshFiles(dir) }
// dshGetEffort reads the effort sessions start with.
func dshGetEffort(dir string) string {
files := dshProfiles(dir)
if len(files) == 0 {
_, items, err := dshRead(filepath.Join(dir, "config.yaml"))
if err != nil {
return ""
}
return dshEffortIn(items)
}
_, items, err := dshRead(files[0])
if err != nil {
return ""
}
return dshEffortOf(items)
}
// dshSetEffort writes v as the effort sessions start with: into
// agent-default-model (0.1.5 on), or into magpie's llm-deepseek entry
// before, dsh's own row being used whole when there is none.
func dshSetEffort(dir, v string) error {
files := dshProfiles(dir)
if len(files) > 0 {
model := dshGet(dir)
ref, viaGateway := strings.CutPrefix(model, magpieID+"/")
levels := dshEfforts
if viaGateway {
levels = dshLevels(ref)
}
if v != "" && len(levels) == 0 {
return fmt.Errorf("DeepSeek Harness has no thinking levels for %s: magpie knows of none the model takes", ref)
}
if v != "" && !contains(levels, v) {
return fmt.Errorf("DeepSeek Harness takes an effort of %s for this model, not %q", strings.Join(levels, ", "), v)
}
if model == "" {
if v == "" {
return nil
}
return fmt.Errorf("pick a model for DeepSeek Harness first; the effort is kept with it")
}
for _, f := range files {
head, items, err := dshRead(f)
if err != nil {
continue
}
// each profile keeps the model it starts on
p, m := "deepseek-official", ref
if viaGateway {
p = dshRoute
}
if i := dshFindLast(items, "agent-default-model"); i >= 0 {
if c := dshConfig(items[i]); c["model"] != "" && c["provider"] != "" {
p, m = c["provider"], c["model"]
}
}
i := dshFind(items, "agent-default-model")
if i >= 0 && !dshOurs(items[i]) {
stash(map[string]string{dshStashKey(f, "agent-default-model"): strings.Join(items[i].lines, "\n")})
}
it := dshItem{id: "agent-default-model", magpie: true, lines: dshDefaultLines(p, m, v)}
if i >= 0 {
items[i] = it
} else {
items = append(items, it)
}
if err := dshWrite(f, head, items); err != nil {
return err
}
}
return nil
}
if v != "" && !contains(dshEfforts, v) {
return fmt.Errorf("DeepSeek Harness takes an effort of %s, not %q", strings.Join(dshEfforts, ", "), v)
}
f := filepath.Join(dir, "config.yaml")
head, items, err := dshRead(f)
if err == nil {
if i := dshFind(items, "llm-deepseek"); i >= 0 && items[i].magpie {
items[i].lines = dshProviderLines(v)
return dshWrite(f, head, items)
}
}
if v != "" {
return fmt.Errorf("pick a model through magpie for DeepSeek Harness first; the effort is kept with magpie's DeepSeek entry")
}
return nil
}
// dshSettingsEndpoint reports whether dsh's own settings carry an
// llm-deepseek section with an endpoint or key, which dsh puts over the
// patch list.
func dshSettingsEndpoint(path string) bool {
b, err := os.ReadFile(path)
if err != nil {
return false
}
in := false
for _, l := range splitLinesKeep(string(b)) {
if l != "" && !strings.HasPrefix(l, " ") && !strings.HasPrefix(l, "#") {
in = strings.HasPrefix(l, "llm-deepseek:")
continue
}
t := strings.TrimSpace(l)
if in && (strings.HasPrefix(t, "baseURL:") || strings.HasPrefix(t, "apiKey:")) {
return true
}
}
return false
}