effort: Claude Code can start at max — settings.json's effortLevel keeps up to xhigh (/effort max lasts a session), so magpie keeps max as CLAUDE_CODE_EFFORT_LEVEL in its env, which every session asks for (tested: Claude Code then sends output_config.effort max), and another level takes it away. A model whose reasoning levels aren't known (one models.dev doesn't list, a custom provider's) can be given some of none…max on the Providers page or with magpie model efforts, which the lists agents get and the gateway then take it to have: a request's effort is fitted to them, max sent as max
This commit is contained in:
10 files changed
+151
-23
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -30,8 +31,13 @@ var claudeEnv = []string{
|
|||||||
// A tier that has none follows the main model.
|
// A tier that has none follows the main model.
|
||||||
var claudeTiers = []string{"opus", "sonnet", "haiku", "fable"}
|
var claudeTiers = []string{"opus", "sonnet", "haiku", "fable"}
|
||||||
|
|
||||||
// claudeEfforts are the levels settings.json's effortLevel takes.
|
// claudeEfforts are the levels Claude Code starts with: settings.json's
|
||||||
var claudeEfforts = []string{"low", "medium", "high", "xhigh"}
|
// effortLevel takes the first four, max is claudeEffortEnv.
|
||||||
|
var claudeEfforts = []string{"low", "medium", "high", "xhigh", "max"}
|
||||||
|
|
||||||
|
// claudeEffortEnv is the effort every Claude Code session asks for, max
|
||||||
|
// among them.
|
||||||
|
const claudeEffortEnv = "CLAUDE_CODE_EFFORT_LEVEL"
|
||||||
|
|
||||||
func tierEnv(tier string) string { return "ANTHROPIC_DEFAULT_" + strings.ToUpper(tier) + "_MODEL" }
|
func tierEnv(tier string) string { return "ANTHROPIC_DEFAULT_" + strings.ToUpper(tier) + "_MODEL" }
|
||||||
|
|
||||||
@@ -153,13 +159,23 @@ func claude(home string) *Agent {
|
|||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
// the effort Claude Code starts with, as its /effort saves it;
|
// the effort Claude Code starts with, as its /effort saves it;
|
||||||
// settings.json keeps low to xhigh (max lasts a session only)
|
// settings.json keeps low to xhigh (/effort max lasts a session
|
||||||
|
// only), so max is CLAUDE_CODE_EFFORT_LEVEL in its env, which every
|
||||||
|
// session starts with and asks for
|
||||||
Key: "effort", Label: "effort",
|
Key: "effort", Label: "effort",
|
||||||
Get: jsonGet(path, "effortLevel"),
|
Get: func() string { return cmp.Or(env(claudeEffortEnv), jsonGet(path, "effortLevel")()) },
|
||||||
Set: func(v string) error {
|
Set: func(v string) error {
|
||||||
if v != "" && !contains(claudeEfforts, v) {
|
if v != "" && !contains(claudeEfforts, v) {
|
||||||
return fmt.Errorf("Claude Code keeps an effort of %s, not %q", strings.Join(claudeEfforts, ", "), v)
|
return fmt.Errorf("Claude Code keeps an effort of %s, not %q", strings.Join(claudeEfforts, ", "), v)
|
||||||
}
|
}
|
||||||
|
if v == "max" {
|
||||||
|
return edit.SetJSON(path, edit.KV{Path: "env." + claudeEffortEnv, Value: v})
|
||||||
|
}
|
||||||
|
if env(claudeEffortEnv) != "" {
|
||||||
|
if err := edit.DelJSON(path, "env."+claudeEffortEnv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return jsonSet(path, "effortLevel")(v)
|
return jsonSet(path, "effortLevel")(v)
|
||||||
},
|
},
|
||||||
Options: func(map[string]string) []Option {
|
Options: func(map[string]string) []Option {
|
||||||
|
|||||||
@@ -72,3 +72,41 @@ func TestClaudeTiers(t *testing.T) {
|
|||||||
t.Fatal("theme lost")
|
t.Fatal("theme lost")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Claude Code's settings keep an effort up to xhigh; max is its
|
||||||
|
// CLAUDE_CODE_EFFORT_LEVEL, which another level takes away again.
|
||||||
|
func TestClaudeEffortMax(t *testing.T) {
|
||||||
|
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"))
|
||||||
|
path := filepath.Join(home, ".claude", "settings.json")
|
||||||
|
os.MkdirAll(filepath.Dir(path), 0o755)
|
||||||
|
os.WriteFile(path, []byte(`{"effortLevel": "high", "env": {"FOO": "1"}}`), 0o644)
|
||||||
|
e := claude(home).Field("effort")
|
||||||
|
get := func(k string) string { v, _ := edit.GetJSON(path, k); return v }
|
||||||
|
if e.Get() != "high" {
|
||||||
|
t.Fatalf("get %q", e.Get())
|
||||||
|
}
|
||||||
|
if err := e.Set("max"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e.Get() != "max" || get("env.CLAUDE_CODE_EFFORT_LEVEL") != "max" || get("env.FOO") != "1" {
|
||||||
|
b, _ := os.ReadFile(path)
|
||||||
|
t.Fatalf("max:\n%s", b)
|
||||||
|
}
|
||||||
|
if err := e.Set("xhigh"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e.Get() != "xhigh" || get("env.CLAUDE_CODE_EFFORT_LEVEL") != "" || get("effortLevel") != "xhigh" || get("env.FOO") != "1" {
|
||||||
|
b, _ := os.ReadFile(path)
|
||||||
|
t.Fatalf("xhigh:\n%s", b)
|
||||||
|
}
|
||||||
|
e.Set("max")
|
||||||
|
if err := e.Set(""); err != nil || e.Get() != "" {
|
||||||
|
t.Fatalf("reset: %v %q", err, e.Get())
|
||||||
|
}
|
||||||
|
if e.Set("ultra") == nil {
|
||||||
|
t.Fatal("ultra taken")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,10 +67,9 @@ func TestEffortFields(t *testing.T) {
|
|||||||
writeFile(t, filepath.Join(home, ".claude", "settings.json"), `{"theme":"dark","model":"opus"}`)
|
writeFile(t, filepath.Join(home, ".claude", "settings.json"), `{"theme":"dark","model":"opus"}`)
|
||||||
c := claude(home)
|
c := claude(home)
|
||||||
setEffort(t, c, "xhigh", `"theme"`)
|
setEffort(t, c, "xhigh", `"theme"`)
|
||||||
// max lasts a session in Claude Code; settings.json drops it
|
// max lasts a session in Claude Code's /effort; magpie keeps it as its
|
||||||
if err := c.Field("effort").Set("max"); err == nil {
|
// env (TestClaudeEffortMax)
|
||||||
t.Fatal("claude took max")
|
setEffort(t, c, "max", `"theme"`)
|
||||||
}
|
|
||||||
|
|
||||||
writeFile(t, filepath.Join(home, ".hermes", "config.yaml"), "model:\n default: x\nagent:\n max_turns: 9\n")
|
writeFile(t, filepath.Join(home, ".hermes", "config.yaml"), "model:\n default: x\nagent:\n max_turns: 9\n")
|
||||||
h := hermes(home)
|
h := hermes(home)
|
||||||
|
|||||||
@@ -2833,16 +2833,19 @@ function renderModels(p) {
|
|||||||
who.append(name, el("code", "", m.id));
|
who.append(name, el("code", "", m.id));
|
||||||
row.append(who);
|
row.append(who);
|
||||||
const levels = m.efforts || [];
|
const levels = m.efforts || [];
|
||||||
|
// a model whose levels aren't known (m.given) can be given any
|
||||||
|
// of them, and none again
|
||||||
if (levels.length > 1) {
|
if (levels.length > 1) {
|
||||||
const lv = el("div", "mlevels");
|
const lv = el("div", "mlevels");
|
||||||
lv.title = t("Reasoning levels agents are offered");
|
lv.title = t(m.given ? "Its reasoning levels aren't known: tick the ones it takes" : "Reasoning levels agents are offered");
|
||||||
const kept = m.kept?.length ? m.kept : levels;
|
const kept = m.kept?.length || m.given ? m.kept || [] : levels;
|
||||||
for (const l of levels) {
|
for (const l of levels) {
|
||||||
const [tk, cb] = tick(t(l), kept.includes(l));
|
const [tk, cb] = tick(t(l), kept.includes(l));
|
||||||
cb.onchange = () => {
|
cb.onchange = () => {
|
||||||
const next = levels.filter((x) => x === l ? cb.checked : kept.includes(x));
|
const next = levels.filter((x) => x === l ? cb.checked : kept.includes(x));
|
||||||
if (!next.length) { cb.checked = true; status(t("Keep at least one level"), "err"); return; }
|
if (!next.length && !m.given) { cb.checked = true; status(t("Keep at least one level"), "err"); return; }
|
||||||
accountAction("provider/efforts", { id: p.id, model: m.id, efforts: next.length === levels.length ? [] : next }, t("{id}: {levels}", { id: m.id, levels: next.map((x) => t(x)).join(", ") }));
|
accountAction("provider/efforts", { id: p.id, model: m.id, efforts: next.length === levels.length && !m.given ? [] : next },
|
||||||
|
next.length ? t("{id}: {levels}", { id: m.id, levels: next.map((x) => t(x)).join(", ") }) : t("{id} is as its provider has it again", { id: m.id }));
|
||||||
};
|
};
|
||||||
lv.append(tk);
|
lv.append(tk);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ const I18N = {
|
|||||||
"{id} is called {name}": "{id} 已改名为 {name}",
|
"{id} is called {name}": "{id} 已改名为 {name}",
|
||||||
"{id} has its own name again": "{id} 已恢复默认名称",
|
"{id} has its own name again": "{id} 已恢复默认名称",
|
||||||
"Reasoning levels agents are offered": "提供给 Agent 的推理档位",
|
"Reasoning levels agents are offered": "提供给 Agent 的推理档位",
|
||||||
|
"Its reasoning levels aren't known: tick the ones it takes": "未知这个模型的推理档位:勾选它支持的档位",
|
||||||
"Keep at least one level": "至少保留一个档位",
|
"Keep at least one level": "至少保留一个档位",
|
||||||
"{id}: {levels}": "{id}:{levels}",
|
"{id}: {levels}": "{id}:{levels}",
|
||||||
"Restore default": "恢复默认",
|
"Restore default": "恢复默认",
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ type modelJSON struct {
|
|||||||
Default string `json:"default,omitempty"` // its own name, when the user gave it another
|
Default string `json:"default,omitempty"` // its own name, when the user gave it another
|
||||||
Kept []string `json:"kept,omitempty"` // the reasoning levels the user keeps of Efforts, when not all
|
Kept []string `json:"kept,omitempty"` // the reasoning levels the user keeps of Efforts, when not all
|
||||||
Efforts []string `json:"efforts,omitempty"`
|
Efforts []string `json:"efforts,omitempty"`
|
||||||
On bool `json:"on"` // exposed to agents
|
Given bool `json:"given,omitempty"` // its levels aren't known: Efforts are those it can be given, Kept those it was
|
||||||
|
On bool `json:"on"` // exposed to agents
|
||||||
}
|
}
|
||||||
|
|
||||||
type providerJSON struct {
|
type providerJSON struct {
|
||||||
@@ -219,6 +220,9 @@ func providerInfo(p provider.Provider, agents []agentUse) providerJSON {
|
|||||||
j.Default = cmp.Or(m.Name, m.ID)
|
j.Default = cmp.Or(m.Name, m.ID)
|
||||||
j.Name = n
|
j.Name = n
|
||||||
}
|
}
|
||||||
|
if len(j.Efforts) == 0 {
|
||||||
|
j.Efforts, j.Given = provider.Levels, true
|
||||||
|
}
|
||||||
if k, ok := kept[m.ID]; ok {
|
if k, ok := kept[m.ID]; ok {
|
||||||
j.Kept = slices.DeleteFunc(slices.Clone(j.Efforts), func(e string) bool { return !slices.Contains(k, e) })
|
j.Kept = slices.DeleteFunc(slices.Clone(j.Efforts), func(e string) bool { return !slices.Contains(k, e) })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ import (
|
|||||||
// (low, medium and high of its six): settings' ModelEfforts, kept the same
|
// (low, medium and high of its six): settings' ModelEfforts, kept the same
|
||||||
// way. The lists magpie hands out — the gateway's, and those it writes into
|
// way. The lists magpie hands out — the gateway's, and those it writes into
|
||||||
// the agents' files — offer only those, in the vendor's order; a request
|
// the agents' files — offer only those, in the vendor's order; a request
|
||||||
// for another still reaches the vendor as it did.
|
// for another still reaches the vendor as it did. A model whose levels
|
||||||
|
// aren't known (one models.dev doesn't list, a custom provider's) can be
|
||||||
|
// given some of Levels the same way, which it is then taken to have.
|
||||||
|
|
||||||
// ModelName is the name the user gave a provider's model, if any.
|
// ModelName is the name the user gave a provider's model, if any.
|
||||||
func ModelName(pid, model string) (string, bool) {
|
func ModelName(pid, model string) (string, bool) {
|
||||||
@@ -88,25 +90,31 @@ func SetModelName(ref, name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Levels are the reasoning levels a model whose own aren't known can be
|
||||||
|
// given.
|
||||||
|
var Levels = []string{"none", "minimal", "low", "medium", "high", "xhigh", "max"}
|
||||||
|
|
||||||
// SetModelEfforts keeps only these of a provider's model's reasoning
|
// SetModelEfforts keeps only these of a provider's model's reasoning
|
||||||
// levels in the lists magpie hands out; none, or all it has, offers them
|
// levels in the lists magpie hands out; none, or all it has, offers them
|
||||||
// all again. They must be levels the model has.
|
// all again. They must be levels the model has — or, for a model whose
|
||||||
|
// levels aren't known, any of Levels, which it is given; none takes them
|
||||||
|
// away.
|
||||||
func SetModelEfforts(ref string, efforts []string) error {
|
func SetModelEfforts(ref string, efforts []string) error {
|
||||||
p, model, err := splitRef(ref)
|
p, model, err := splitRef(ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
all := p.Efforts(model)
|
all := p.Known(model)
|
||||||
var keep []string
|
var keep []string
|
||||||
for _, e := range efforts {
|
for _, e := range efforts {
|
||||||
e = strings.ToLower(strings.TrimSpace(e))
|
e = strings.ToLower(strings.TrimSpace(e))
|
||||||
if e == "" {
|
if e == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !slices.Contains(all, e) {
|
if len(all) == 0 && !slices.Contains(Levels, e) {
|
||||||
if len(all) == 0 {
|
return fmt.Errorf("%q is not a reasoning level (they are %s)", e, strings.Join(Levels, ", "))
|
||||||
return fmt.Errorf("%s/%s has no reasoning levels to choose from", p.ID, model)
|
}
|
||||||
}
|
if len(all) > 0 && !slices.Contains(all, e) {
|
||||||
return fmt.Errorf("%s/%s has no reasoning level %q (it has %s)", p.ID, model, e, strings.Join(all, ", "))
|
return fmt.Errorf("%s/%s has no reasoning level %q (it has %s)", p.ID, model, e, strings.Join(all, ", "))
|
||||||
}
|
}
|
||||||
if !slices.Contains(keep, e) {
|
if !slices.Contains(keep, e) {
|
||||||
@@ -151,11 +159,15 @@ func (p Provider) ModelEfforts() map[string][]string {
|
|||||||
|
|
||||||
// effortsKept is all without the levels the user didn't keep, in its own
|
// effortsKept is all without the levels the user didn't keep, in its own
|
||||||
// order; all of them when none of those kept is among them any more (the
|
// order; all of them when none of those kept is among them any more (the
|
||||||
// vendor's list changed).
|
// vendor's list changed). A model with none known has those the user gave
|
||||||
|
// it.
|
||||||
func effortsKept(all, kept []string) []string {
|
func effortsKept(all, kept []string) []string {
|
||||||
if len(kept) == 0 {
|
if len(kept) == 0 {
|
||||||
return all
|
return all
|
||||||
}
|
}
|
||||||
|
if len(all) == 0 {
|
||||||
|
return slices.DeleteFunc(slices.Clone(Levels), func(e string) bool { return !slices.Contains(kept, e) })
|
||||||
|
}
|
||||||
out := slices.DeleteFunc(slices.Clone(all), func(e string) bool { return !slices.Contains(kept, e) })
|
out := slices.DeleteFunc(slices.Clone(all), func(e string) bool { return !slices.Contains(kept, e) })
|
||||||
if len(out) == 0 {
|
if len(out) == 0 {
|
||||||
return all
|
return all
|
||||||
|
|||||||
@@ -179,3 +179,36 @@ func TestModelEfforts(t *testing.T) {
|
|||||||
t.Fatal(got)
|
t.Fatal(got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A model whose levels aren't known can be given some, which the catalog,
|
||||||
|
// the lists agents get and the gateway then take it to have; none takes
|
||||||
|
// them away, and a model with levels of its own takes no others.
|
||||||
|
func TestModelEffortsGiven(t *testing.T) {
|
||||||
|
prefsHome(t)
|
||||||
|
if err := Save(Provider{ID: "c", Name: "C", Key: "k", Chat: "http://127.0.0.1:1/v1", Models: []string{"mystery-7"}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
p, _, _ := Resolve("c/mystery-7")
|
||||||
|
if e := entry(t, "c/mystery-7"); len(e.Efforts) != 0 || len(p.Efforts("mystery-7")) != 0 {
|
||||||
|
t.Fatalf("unknown model has levels %v", e.Efforts)
|
||||||
|
}
|
||||||
|
if err := SetModelEfforts("c/mystery-7", []string{"max", "low", "high"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []string{"low", "high", "max"}
|
||||||
|
if e := entry(t, "c/mystery-7"); !slices.Equal(e.Efforts, want) || !slices.Equal(p.Efforts("mystery-7"), want) {
|
||||||
|
t.Fatalf("given: %v %v", e.Efforts, p.Efforts("mystery-7"))
|
||||||
|
}
|
||||||
|
if err := SetModelEfforts("c/mystery-7", []string{"turbo"}); err == nil {
|
||||||
|
t.Fatal("turbo taken")
|
||||||
|
}
|
||||||
|
if err := SetModelEfforts("c/mystery-7", nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e := entry(t, "c/mystery-7"); len(e.Efforts) != 0 || settings.Load().ModelEfforts["c/mystery-7"] != nil {
|
||||||
|
t.Fatalf("taken away: %v", e.Efforts)
|
||||||
|
}
|
||||||
|
if err := SetModelEfforts("a/sol", []string{"xhigh"}); err == nil {
|
||||||
|
t.Fatal("sol took xhigh")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,8 +274,17 @@ func (p Provider) RejectsTemperature(model string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Efforts are the reasoning levels the model takes, when known.
|
// Efforts are the reasoning levels the model takes, when known: its own,
|
||||||
|
// or those the user gave it when it has none known.
|
||||||
func (p Provider) Efforts(model string) []string {
|
func (p Provider) Efforts(model string) []string {
|
||||||
|
if all := p.Known(model); len(all) > 0 {
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
return effortsKept(nil, settings.Load().ModelEfforts[p.ID+"/"+model])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Known are the model's own reasoning levels, when known.
|
||||||
|
func (p Provider) Known(model string) []string {
|
||||||
for _, m := range p.Available() {
|
for _, m := range p.Available() {
|
||||||
if m.ID == model {
|
if m.ID == model {
|
||||||
return effortsOf(m)
|
return effortsOf(m)
|
||||||
|
|||||||
+14
-1
@@ -115,9 +115,14 @@ func modelEfforts(args []string) error {
|
|||||||
id := p.ID + "/" + model
|
id := p.ID + "/" + model
|
||||||
all := p.Efforts(model)
|
all := p.Efforts(model)
|
||||||
rest := args[1:]
|
rest := args[1:]
|
||||||
|
given := len(p.Known(model)) == 0 // levels aren't known: those it has are the user's
|
||||||
if len(rest) == 0 {
|
if len(rest) == 0 {
|
||||||
if len(all) == 0 {
|
if len(all) == 0 {
|
||||||
fmt.Println(muted.Render(id + " has no reasoning levels"))
|
fmt.Println(muted.Render(id+" has no reasoning levels known"), faint.Render("· give it some of "+strings.Join(provider.Levels, ", ")))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if given {
|
||||||
|
fmt.Println(bold.Render(strings.Join(all, " ")), muted.Render("· given it · --reset takes them away"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
kept, narrowed := p.ModelEfforts()[model]
|
kept, narrowed := p.ModelEfforts()[model]
|
||||||
@@ -146,6 +151,14 @@ func modelEfforts(args []string) error {
|
|||||||
if err := provider.SetModelEfforts(id, levels); err != nil {
|
if err := provider.SetModelEfforts(id, levels); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if given {
|
||||||
|
if all = p.Efforts(model); len(all) > 0 {
|
||||||
|
fmt.Println(green.Render("✓"), id, muted.Render("offers"), bold.Render(strings.Join(all, ", ")))
|
||||||
|
} else {
|
||||||
|
fmt.Println(green.Render("✓"), id, muted.Render("has no reasoning levels again"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if kept, ok := p.ModelEfforts()[model]; ok {
|
if kept, ok := p.ModelEfforts()[model]; ok {
|
||||||
fmt.Println(green.Render("✓"), id, muted.Render("offers"), bold.Render(strings.Join(kept, ", ")))
|
fmt.Println(green.Render("✓"), id, muted.Render("offers"), bold.Render(strings.Join(kept, ", ")))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in new issue
Block a user