gateway: a model Claude Code names that magpie doesn't serve goes to the one it is set to use for that tier — claude-haiku-4-5-… for a title or a small task goes to its haiku tier's model, opus and sonnet to theirs, a name of no tier to its main model, rather than to some provider that happens to list the name and wasn't picked for it. Other agents, and ids magpie shows, are sent as asked

This commit is contained in:
yetone committed 2026-09-28 13:25:25 +08:00
1 parent 41cbe54a14
commit 4cbd96a3e4
5 files changed
+147

No files matched your search

+39
View File
@@ -3,6 +3,7 @@ package agent
import (
"cmp"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
@@ -276,3 +277,41 @@ var claudeManaged = func() string {
}
return "/etc/claude-code/managed-settings.json"
}
// StandIn is the model Claude Code is set to use in place of one it named
// that magpie doesn't serve: claude-haiku-4-5-… for a title or a small
// task goes to its haiku tier's model, and a name of no tier to its main
// model. "" when Claude Code isn't routed through magpie or asked for
// isn't Claude Code. For gateway.StandIn.
func StandIn(agent, model string) string {
if agent != "claude" {
return ""
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return claudeStandIn(filepath.Join(home, ".claude", "settings.json"), model)
}
func claudeStandIn(path, model string) string {
env := func(k string) string { v, _ := edit.GetJSON(path, "env."+k); return v }
if env("ANTHROPIC_BASE_URL") != gateway.URL() {
return ""
}
m := strings.ToLower(model)
for _, t := range claudeTiers {
if strings.Contains(m, t) {
if v := env(tierEnv(t)); v != "" {
return v
}
if t == "haiku" {
if v := env("ANTHROPIC_SMALL_FAST_MODEL"); v != "" {
return v
}
}
break
}
}
return env("ANTHROPIC_MODEL")
}
+37
View File
@@ -0,0 +1,37 @@
package agent
import (
"os"
"path/filepath"
"testing"
"github.com/yetone/magpie/internal/gateway"
)
func TestClaudeStandIn(t *testing.T) {
path := filepath.Join(t.TempDir(), "settings.json")
write := func(s string) {
if err := os.WriteFile(path, []byte(s), 0o644); err != nil {
t.Fatal(err)
}
}
write(`{"env":{"ANTHROPIC_BASE_URL":"` + gateway.URL() + `","ANTHROPIC_MODEL":"a/main","ANTHROPIC_DEFAULT_HAIKU_MODEL":"a/small","ANTHROPIC_DEFAULT_OPUS_MODEL":"a/big"}}`)
for asked, want := range map[string]string{
"claude-haiku-4-5-20251001": "a/small",
"claude-opus-5-5": "a/big",
"claude-sonnet-5": "a/main", // a tier without a model follows the main one
"gpt-5": "a/main",
} {
if got := claudeStandIn(path, asked); got != want {
t.Errorf("%s: %q, want %q", asked, got, want)
}
}
// Claude Code on Anthropic's own endpoint: nothing stands in
write(`{"env":{"ANTHROPIC_MODEL":"a/main"}}`)
if got := claudeStandIn(path, "claude-haiku-4-5"); got != "" {
t.Errorf("not routed: %q", got)
}
if got := StandIn("codex", "claude-haiku-4-5"); got != "" {
t.Errorf("another agent: %q", got)
}
}
+33
View File
@@ -50,6 +50,37 @@ func agentOf(r *http.Request) string {
return usage.AgentOf(r.Header.Get("User-Agent"))
}
// StandIn is the model an agent is set to use in place of one it named that
// magpie doesn't serve it: Claude Code asks for claude-haiku-… by name for
// its small tasks, whatever its haiku tier is set to, and that goes to the
// tier's model rather than to some provider that happens to list the name.
// "" leaves the model as asked. Set by main.
var StandIn func(agent, model string) string
// standIn is StandIn's model for one magpie shows no entry for: not a
// catalog id or model, nor a ready provider's "provider/model".
func standIn(agent, asked string) string {
if StandIn == nil || asked == "" || strings.HasPrefix(asked, provider.GroupPrefix) {
return ""
}
id := strings.TrimSuffix(asked, "[1m]")
for _, e := range provider.Catalog() {
if e.ID == id || e.Model == id {
return ""
}
}
if pid, _, ok := strings.Cut(id, "/"); ok {
if p, err := provider.Find(pid); err == nil && p.Ready() {
return ""
}
}
m := StandIn(agent, asked)
if m == asked {
return ""
}
return m
}
// Version is set by main.
var Version = "dev"
@@ -589,6 +620,8 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request, from provider.Pro
asked := call.Model
if id, ok := provider.GroupFor(asked); ok {
asked = id
} else if m := standIn(call.Agent, asked); m != "" {
asked = m
}
p, model, ok := provider.Resolve(asked)
if !ok {
+35
View File
@@ -0,0 +1,35 @@
package gateway
import (
"strings"
"testing"
"github.com/yetone/magpie/internal/provider"
)
// a model the agent names that magpie doesn't serve goes to the one the
// agent is set to use for it; one magpie serves is sent as asked
func TestStandIn(t *testing.T) {
t.Setenv("HOME", t.TempDir())
f := &fake{t: t, ctype: "application/json", reply: `{"id":"c1","choices":[{"message":{"role":"assistant","content":"OK"},"finish_reason":"stop"}]}`}
setup(t, provider.Chat, f)
var asked []string
StandIn = func(agent, model string) string { asked = append(asked, model); return "fake/m1" }
t.Cleanup(func() { StandIn = nil })
code, body := post(t, "/v1/chat/completions", `{"model":"claude-haiku-4-5-20251001","messages":[{"role":"user","content":"hi"}]}`)
if code != 200 || modelOf(f.got) != "m1" || !strings.Contains(body, "OK") {
t.Fatalf("stood in: %d %s, upstream %s", code, body, f.got)
}
for _, m := range []string{"m1", "fake/m1", "fake/other"} {
asked = nil
if code, body := post(t, "/v1/chat/completions", `{"model":"`+m+`","messages":[{"role":"user","content":"hi"}]}`); code != 200 || len(asked) != 0 {
t.Fatalf("%s: %d %s, stand-in asked for %v", m, code, body, asked)
}
}
// no stand-in: unknown as before
StandIn = func(string, string) string { return "" }
if code, _ := post(t, "/v1/chat/completions", `{"model":"claude-haiku-4-5-20251001","messages":[{"role":"user","content":"hi"}]}`); code != 404 {
t.Fatalf("without a stand-in: %d", code)
}
}
+3
View File
@@ -103,6 +103,9 @@ func run(args []string) error {
// a provider added, edited or removed, or a list fetched anew, reaches
// the model lists agents keep in files of their own
catalog.Changed = agent.SyncCatalog
// a model Claude Code names that magpie doesn't serve goes to the one
// it is set to use for that tier
gateway.StandIn = agent.StandIn
// the setup kept the same on every computer, by whichever serves
gateway.WhileServing = append(gateway.WhileServing, davsync.Run)
if len(args) == 0 {