agent: an agent whose own config keeps the reasoning effort its sessions start with gets an effort field, as Codex, Grok and Pi have — Claude Code's effortLevel (low to xhigh; max lasts a session only), Hermes's agent.reasoning_effort, omp's defaultThinkingLevel, Goose's GOOSE_THINKING_EFFORT, Copilot's effortLevel (the model's levels when its list says), Crush's large model reasoning_effort, Command Code's reasoningEffort for the current model (it keeps one per model) and dsh's llm-deepseek reasoningEffort (off, high or max, kept when the model changes or the catalog syncs); OpenCode, Gemini CLI, Cursor, ZCode, Devin and Alma keep none of their own in a file, so they get none. Asked on Discord

This commit is contained in:
yetone committed 2026-09-26 23:29:40 +08:00
1 parent 45a5041a20
commit 6694108bf2
8 files changed
+430 -7

No files matched your search

+45
View File
@@ -419,6 +419,20 @@ func goose(home, cfg string) *Agent {
Options: func(cur map[string]string) []Option {
return ownOptions("", cur["model"], "anthropic", "openai", "google", "openrouter")
},
}, {
// GOOSE_THINKING_EFFORT, the effort goose asks of a model that
// thinks, for every provider
Key: "effort", Label: "effort",
Get: func() string { v, _ := get("GOOSE_THINKING_EFFORT"); return v },
Set: func(v string) error {
if v == "" {
return edit.DelYAMLTop(path, "GOOSE_THINKING_EFFORT")
}
return set(edit.KV{Path: "GOOSE_THINKING_EFFORT", Value: v})
},
Options: func(map[string]string) []Option {
return static("off", "low", "medium", "high", "max")
},
}},
}
}
@@ -470,6 +484,21 @@ func copilot(home string) *Agent {
}
return out
},
}, {
// effortLevel, which Copilot saves beside the model and clears
// when its own /model changes the model; the levels are the
// model's, low to xhigh when Copilot's list does not say
Key: "effort", Label: "effort",
Get: jsonGet(path, "effortLevel"),
Set: jsonSet(path, "effortLevel"),
Options: func(cur map[string]string) []Option {
if live, _, ok := catalog.Live("copilot"); ok {
if e := catalog.Efforts(live, cur["model"]); len(e) > 0 {
return static(e...)
}
}
return static("low", "medium", "high", "xhigh")
},
}},
}
}
@@ -541,6 +570,22 @@ func crush(home, cfg string) *Agent {
Fields: []Field{
{Key: "model", Label: "large", Get: pairGet(get, "models.large.provider", "models.large.model"), Set: setter("models.large.provider", "models.large.model"), Options: opts("model")},
{Key: "small", Label: "small", Get: pairGet(get, "models.small.provider", "models.small.model"), Set: setter("models.small.provider", "models.small.model"), Options: opts("small")},
{
// the large model's reasoning_effort, which Crush's schema
// takes as low, medium or high (for OpenAI-style models)
Key: "effort", Label: "effort",
Get: func() string { v, _ := get("models.large.reasoning_effort"); return v },
Set: func(v string) error {
if v == "" {
return edit.DelJSON(path, "models.large.reasoning_effort")
}
if m, _ := get("models.large.model"); m == "" {
return fmt.Errorf("pick Crush's large model first; the effort is kept with it")
}
return set(edit.KV{Path: "models.large.reasoning_effort", Value: v})
},
Options: func(map[string]string) []Option { return static("low", "medium", "high") },
},
},
}
}
+17
View File
@@ -30,6 +30,9 @@ var claudeEnv = []string{
// A tier that has none follows the main model.
var claudeTiers = []string{"opus", "sonnet", "haiku", "fable"}
// claudeEfforts are the levels settings.json's effortLevel takes.
var claudeEfforts = []string{"low", "medium", "high", "xhigh"}
func tierEnv(tier string) string { return "ANTHROPIC_DEFAULT_" + strings.ToUpper(tier) + "_MODEL" }
func claude(home string) *Agent {
@@ -148,6 +151,20 @@ func claude(home string) *Agent {
// go stale.
return append(group(name, own), claudeViaMagpie()...)
},
}, {
// the effort Claude Code starts with, as its /effort saves it;
// settings.json keeps low to xhigh (max lasts a session only)
Key: "effort", Label: "effort",
Get: jsonGet(path, "effortLevel"),
Set: func(v string) error {
if v != "" && !contains(claudeEfforts, v) {
return fmt.Errorf("Claude Code keeps an effort of %s, not %q", strings.Join(claudeEfforts, ", "), v)
}
return jsonSet(path, "effortLevel")(v)
},
Options: func(map[string]string) []Option {
return static(claudeEfforts...)
},
}}
for _, tier := range claudeTiers {
fields = append(fields, Field{
+64
View File
@@ -8,6 +8,8 @@ package agent
// at the first "/". Command Code still wants its own sign-in for these.
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
@@ -91,6 +93,47 @@ func commandCode(home string) *Agent {
}
return append(out, viaMagpie("commandcode", magpieID+"/")...)
},
}, {
// settings.json's reasoningEffort keeps an effort for each model
// (Command Code's /effort saves it); this is the current model's
Key: "effort", Label: "effort",
Get: func() string { return ccEffortMap(path)[get("model")] },
Set: func(v string) error {
model := get("model")
if model == "" {
return fmt.Errorf("pick Command Code's model first; it keeps an effort for each model")
}
efforts := ccEffortMap(path)
if v == "" {
delete(efforts, model)
} else {
efforts[model] = v
}
if len(efforts) == 0 {
return edit.DelJSON(path, "reasoningEffort")
}
// written whole, as model ids hold dots
return edit.SetJSON(path, edit.KV{Path: "reasoningEffort", Value: efforts})
},
Options: func(cur map[string]string) []Option {
if ref, ok := strings.CutPrefix(cur["model"], magpieID+"/"); ok {
for _, m := range magpieModels("commandcode") {
if m.ID != ref {
continue
}
var efforts []string
for _, x := range ccEfforts {
if slices.Contains(m.Efforts, x) {
efforts = append(efforts, x)
}
}
if len(efforts) > 0 {
return static(efforts...)
}
}
}
return static(ccEfforts...)
},
}},
}
}
@@ -115,3 +158,24 @@ func ccProviderJSON() any {
}
return map[string]any{"name": "magpie", "api": "openai-completions", "baseURL": gatewayV1(), "apiKey": false, "models": ms}
}
// ccEffortMap is settings.json's reasoningEffort, model id to effort.
func ccEffortMap(path string) map[string]string {
out := map[string]string{}
b, err := os.ReadFile(path)
if err != nil {
return out
}
var c struct {
Efforts map[string]any `json:"reasoningEffort"`
}
if json.Unmarshal(b, &c) != nil {
return out
}
for k, v := range c.Efforts {
if s, ok := v.(string); ok {
out[k] = s
}
}
return out
}
+90 -6
View File
@@ -74,6 +74,13 @@ func dsh(home string) *Agent {
Options: func(map[string]string) []Option {
return append(append([]Option{}, dshModels...), viaMagpie("dsh", magpieID+"/")...)
},
}, {
// llm-deepseek's reasoningEffort, the thinking effort sessions
// start with (off, 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(map[string]string) []Option { return static(dshEfforts...) },
}},
}
}
@@ -320,13 +327,13 @@ func dshSetFile(path, v string, modern bool) error {
drop("agent-loop")
drop("llm-deepseek")
case modern && viaGateway:
put("llm-deepseek", dshProviderLines(true))
put("llm-deepseek", dshProviderLines(true, dshEffortIn(items)))
put("agent-default-model", dshDefaultLines(ref))
case modern:
drop("llm-deepseek")
put("agent-default-model", dshDefaultLines(v))
case viaGateway:
put("llm-deepseek", dshProviderLines(false))
put("llm-deepseek", dshProviderLines(false, dshEffortIn(items)))
put("agent-loop", dshLoopLines(ref))
put("api-gateway", dshRouteLines(ref))
default:
@@ -356,19 +363,23 @@ func yamlQuote(s string) string {
// dshProviderLines is the llm-deepseek entry pointing dsh at the gateway,
// with the catalog as the models its /model offers. Since 0.1.5 the key is a
// credential the entry names rather than holds.
func dshProviderLines(modern bool) []string {
// credential the entry names rather than holds. effort is the thinking
// effort sessions start with, high (dsh's own default) when "".
func dshProviderLines(modern bool, effort string) []string {
key := " apiKey: " + yamlQuote(gateway.Token)
if modern {
key = " apiKeyEnv: " + dshKeyRef
}
if effort == "" {
effort = "high"
}
lines := []string{
"- id: llm-deepseek " + dshMark,
" config:",
key,
" baseURL: " + yamlQuote(gatewayV1()),
" thinking: enabled",
" reasoningEffort: high",
" reasoningEffort: " + effort,
" models:",
}
ms := magpieModels("dsh")
@@ -447,7 +458,7 @@ func dshSync(dir string) error {
for _, l := range items[i].lines {
modern = modern || strings.HasPrefix(strings.TrimSpace(l), "apiKeyEnv:")
}
lines := dshProviderLines(modern)
lines := dshProviderLines(modern, dshEffortIn(items))
if strings.Join(lines, "\n") == strings.Join(items[i].lines, "\n") {
continue
}
@@ -463,6 +474,79 @@ func dshSync(dir string) error {
return nil
}
// dshEfforts are the thinking efforts llm-deepseek takes.
var dshEfforts = []string{"off", "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 ""
}
// 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")}
}
// dshGetEffort reads the effort of magpie's llm-deepseek entry.
func dshGetEffort(dir string) string {
_, items, err := dshRead(dshFiles(dir)[0])
if err != nil {
return ""
}
return dshEffortIn(items)
}
// dshSetEffort writes v into each llm-deepseek entry magpie wrote. dsh's
// own row is used whole when there is none, so the effort goes with a
// model through magpie.
func dshSetEffort(dir, v string) error {
if v != "" && !contains(dshEfforts, v) {
return fmt.Errorf("DeepSeek Harness takes an effort of %s, not %q", strings.Join(dshEfforts, ", "), v)
}
done := false
for _, f := range dshFiles(dir) {
head, items, err := dshRead(f)
if err != nil {
continue
}
i := dshFind(items, "llm-deepseek")
if i < 0 || !items[i].magpie {
continue
}
modern := false
for _, l := range items[i].lines {
modern = modern || strings.HasPrefix(strings.TrimSpace(l), "apiKeyEnv:")
}
items[i].lines = dshProviderLines(modern, v)
out := append([]string{}, head...)
for _, it := range items {
out = append(out, it.lines...)
}
if err := edit.WriteAtomic(f, []byte(strings.Join(out, "\n")+"\n")); err != nil {
return err
}
done = true
}
if !done && 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.
+1 -1
View File
@@ -185,7 +185,7 @@ func TestDshModelLimits(t *testing.T) {
}); err != nil {
t.Fatal(err)
}
s := strings.Join(dshProviderLines(true), "\n")
s := strings.Join(dshProviderLines(true, ""), "\n")
want := ` - id: "v/see"
name: "see · V"
contextWindow: 400000
+187
View File
@@ -0,0 +1,187 @@
package agent
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/provider"
)
// effortHome is a sandbox HOME with a provider for magpie to serve.
func effortHome(t *testing.T) string {
t.Helper()
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
t.Setenv("HERMES_HOME", "")
t.Setenv("DSH_HOME", "")
if err := provider.Save(provider.Provider{ID: "deepseek", Name: "DeepSeek", Chat: "https://api.deepseek.com/v1", Key: "k", Models: []string{"pro", "flash"}}); err != nil {
t.Fatal(err)
}
return home
}
// setEffort sets, reads back and resets an agent's effort, checking what
// else is in its file stays.
func setEffort(t *testing.T, a *Agent, v, keep string) {
t.Helper()
f := a.Field("effort")
if f == nil {
t.Fatalf("%s has no effort field", a.ID)
}
if err := f.Set(v); err != nil {
t.Fatalf("%s: %v", a.ID, err)
}
if got := f.Get(); got != v {
t.Fatalf("%s: effort %q, want %q\n%s", a.ID, got, v, readFile(a.Path))
}
found := false
for _, o := range f.Options(map[string]string{}) {
found = found || o.Value == v
}
if !found {
t.Fatalf("%s: %q not among the options", a.ID, v)
}
if !strings.Contains(readFile(a.Path), keep) {
t.Fatalf("%s lost %q:\n%s", a.ID, keep, readFile(a.Path))
}
if err := f.Set(""); err != nil {
t.Fatal(err)
}
if got := f.Get(); got != "" {
t.Fatalf("%s: reset left %q\n%s", a.ID, got, readFile(a.Path))
}
if !strings.Contains(readFile(a.Path), keep) {
t.Fatalf("%s lost %q on reset:\n%s", a.ID, keep, readFile(a.Path))
}
}
func TestEffortFields(t *testing.T) {
home := effortHome(t)
cfg := filepath.Join(home, ".config")
writeFile(t, filepath.Join(home, ".claude", "settings.json"), `{"theme":"dark","model":"opus"}`)
c := claude(home)
setEffort(t, c, "xhigh", `"theme"`)
// max lasts a session in Claude Code; settings.json drops it
if err := c.Field("effort").Set("max"); err == nil {
t.Fatal("claude took max")
}
writeFile(t, filepath.Join(home, ".hermes", "config.yaml"), "model:\n default: x\nagent:\n max_turns: 9\n")
h := hermes(home)
setEffort(t, h, "none", "max_turns: 9")
if v, _ := edit.GetYAML(h.Path, "model.default"); v != "x" {
t.Fatalf("hermes model: %q", v)
}
writeFile(t, filepath.Join(home, ".omp", "agent", "config.yml"), "modelRoles:\n default: a/b\n")
setEffort(t, omp(home), "xhigh", "default: a/b")
writeFile(t, filepath.Join(cfg, "goose", "config.yaml"), "GOOSE_MODEL: m\nGOOSE_PROVIDER: p\n")
g := goose(home, cfg)
setEffort(t, g, "max", "GOOSE_MODEL: m")
if v, _ := edit.GetYAMLTop(g.Path, "GOOSE_PROVIDER"); v != "p" {
t.Fatalf("goose provider: %q", v)
}
writeFile(t, filepath.Join(home, ".copilot", "settings.json"), `{"model":"gpt-5.5","contextTier":"long"}`)
setEffort(t, copilot(home), "xhigh", `"contextTier"`)
cr := crush(home, cfg)
os.MkdirAll(filepath.Dir(cr.Path), 0o755)
os.WriteFile(cr.Path, []byte(`{"options":{"debug":true}}`), 0o644)
// Crush keeps the effort with the large model
if err := cr.Field("effort").Set("high"); err == nil {
t.Fatal("crush took an effort without a large model")
}
if err := cr.Field("model").Set("magpie/deepseek/pro"); err != nil {
t.Fatal(err)
}
setEffort(t, cr, "medium", `"debug"`)
if cr.Field("model").Get() != "magpie/deepseek/pro" {
t.Fatalf("crush model: %s", readFile(cr.Path))
}
}
func TestCommandCodeEffort(t *testing.T) {
home := effortHome(t)
path := filepath.Join(home, ".commandcode", "settings.json")
writeFile(t, path, `{"theme":"dark","reasoningEffort":{"gpt-5.5":"low"}}`)
a := commandCode(home)
f := a.Field("effort")
if err := f.Set("high"); err == nil {
t.Fatal("took an effort without a model")
}
if err := a.Field("model").Set("magpie/deepseek/pro"); err != nil {
t.Fatal(err)
}
setEffort(t, a, "max", `"theme"`)
// each model keeps its own; the other one's stays
if m := ccEffortMap(path); m["gpt-5.5"] != "low" || len(m) != 1 {
t.Fatalf("efforts: %v\n%s", m, readFile(path))
}
// a model id with dots is one key, not a path
if err := a.Field("model").Set("anthropic/claude-4.5"); err != nil {
t.Fatal(err)
}
if err := f.Set("xhigh"); err != nil {
t.Fatal(err)
}
if m := ccEffortMap(path); m["anthropic/claude-4.5"] != "xhigh" || m["gpt-5.5"] != "low" {
t.Fatalf("efforts: %v\n%s", m, readFile(path))
}
// the last one gone, the map goes too
os.WriteFile(path, []byte(`{"model":"x","reasoningEffort":{"x":"low"}}`), 0o644)
if err := f.Set(""); err != nil {
t.Fatal(err)
}
if strings.Contains(readFile(path), "reasoningEffort") {
t.Fatalf("left the map:\n%s", readFile(path))
}
}
func TestDshEffort(t *testing.T) {
home := effortHome(t)
patch := filepath.Join(home, ".dsh", "profiles", "web", "cordis.patch.yml")
writeFile(t, patch, "[]\n")
a := dsh(home)
f := a.Field("effort")
// dsh's own row is used whole; the effort goes with magpie's
if err := f.Set("max"); err == nil {
t.Fatal("took an effort on dsh's own row")
}
if err := a.Field("model").Set("magpie/deepseek/pro"); err != nil {
t.Fatal(err)
}
if f.Get() != "high" {
t.Fatalf("effort: %q\n%s", f.Get(), readFile(patch))
}
if err := f.Set("max"); err != nil {
t.Fatal(err)
}
if err := f.Set("low"); err == nil {
t.Fatal("took low")
}
// kept when the model changes and when the catalog is synced
if err := a.Field("model").Set("magpie/deepseek/flash"); err != nil {
t.Fatal(err)
}
if err := a.Sync(); err != nil {
t.Fatal(err)
}
raw := readFile(patch)
if f.Get() != "max" || strings.Count(raw, "reasoningEffort:") != 1 || !strings.Contains(raw, `model: "deepseek/flash"`) {
t.Fatalf("patch:\n%s", raw)
}
if err := f.Set(""); err != nil {
t.Fatal(err)
}
if f.Get() != "high" {
t.Fatalf("reset: %q", f.Get())
}
}
+14
View File
@@ -108,6 +108,20 @@ func hermes(home string) *Agent {
Options: func(cur map[string]string) []Option {
return append(ownOptions("", cur["model"]), viaMagpie("hermes", magpieID+"/")...)
},
}, {
// agent.reasoning_effort, which Hermes's /reasoning saves; none
// turns reasoning off, and unset Hermes asks for medium
Key: "effort", Label: "effort",
Get: func() string { return getKey("agent.reasoning_effort") },
Set: func(v string) error {
if v == "" {
return edit.DelYAML(path, "agent.reasoning_effort")
}
return edit.SetYAML(path, edit.KV{Path: "agent.reasoning_effort", Value: v})
},
Options: func(map[string]string) []Option {
return static("none", "minimal", "low", "medium", "high", "xhigh")
},
}},
}
}
+12
View File
@@ -98,6 +98,18 @@ func omp(home string) *Agent {
Options: func(cur map[string]string) []Option {
return append(ownOptions("", cur["model"]), viaMagpie("omp", magpieID+"/")...)
},
}, {
// the thinking level sessions start with, as omp's settings save
// it; unset omp takes high (it also knows auto, its own pick)
Key: "effort", Label: "thinking",
Get: func() string { v, _ := edit.GetYAML(path, "defaultThinkingLevel"); return v },
Set: func(v string) error {
if v == "" {
return edit.DelYAML(path, "defaultThinkingLevel")
}
return edit.SetYAML(path, edit.KV{Path: "defaultThinkingLevel", Value: v})
},
Options: func(map[string]string) []Option { return static(ompEfforts...) },
}},
}
}