From 12cc2089e42cb4e18d976802dccbc6acc31fd848 Mon Sep 17 00:00:00 2001 From: yetone Date: Sat, 3 Oct 2026 01:47:42 +0800 Subject: [PATCH] =?UTF-8?q?agents:=20each=20magpie=20model=20in=20T3=20Cod?= =?UTF-8?q?e=20has=20a=20Reasoning=20pick=20of=20the=20levels=20it=20takes?= =?UTF-8?q?=20(KevinXC=20on=20Discord:=20=E5=8A=A0=E8=BF=9B=E5=8E=BB?= =?UTF-8?q?=E7=9A=84=E9=82=A3=E4=BA=9B=E6=A8=A1=E5=9E=8B=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E4=BB=BB=E4=BD=95=20reason=20=E5=92=8C=20thinking=20=E7=9A=84?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=EF=BC=8C=E5=AF=BC=E8=87=B4=E7=8E=B0=E5=9C=A8?= =?UTF-8?q?=E6=89=80=E6=9C=89=E7=9A=84=E6=A8=A1=E5=9E=8B=E9=83=BD=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E6=98=AF=20medium)=20=E2=80=94=20magpie's=20custom=20?= =?UTF-8?q?models=20in=20T3=20Code's=20magpie=20instance=20were=20a=20slug?= =?UTF-8?q?=20and=20a=20name,=20and=20T3=20shows=20a=20custom=20model=20wi?= =?UTF-8?q?th=20no=20capabilities=20with=20no=20options=20and=20sends=20Cl?= =?UTF-8?q?aude=20Code=20no=20effort.=20T3=20Code's=20custom=20model=20ent?= =?UTF-8?q?ry=20takes=20capabilities.optionDescriptors=20(packages/contrac?= =?UTF-8?q?ts=20model.ts=20CustomModelEntry),=20which=20replace=20the=20dr?= =?UTF-8?q?iver's=20default=20in=20its=20picker,=20and=20its=20Claude=20ad?= =?UTF-8?q?apter=20hands=20the=20effort=20chosen=20to=20the=20Agent=20SDK?= =?UTF-8?q?=20as=20it=20is=20for=20a=20custom=20model=20(no=20effortMap),?= =?UTF-8?q?=20which=20Claude=20Code=20sends=20to=20the=20gateway.=20Each?= =?UTF-8?q?=20model=20now=20gets=20an=20"effort"=20select=20labelled=20Rea?= =?UTF-8?q?soning,=20as=20T3's=20own=20Claude=20models=20have=20it,=20of?= =?UTF-8?q?=20the=20levels=20it=20takes=20that=20Claude=20Code=20can=20sen?= =?UTF-8?q?d=20(low,=20medium,=20high,=20xhigh,=20max,=20within=20claudeEf?= =?UTF-8?q?fortsFor=20for=20Claude's=20own=20models),=20medium=20chosen=20?= =?UTF-8?q?first,=20else=20the=20lowest;=20a=20model=20with=20none=20(or?= =?UTF-8?q?=20only=20none/minimal)=20gets=20no=20pick.=20TestT3CodeEfforts?= =?UTF-8?q?=20is=20new,=20and=20t3Format=20now=20decodes=20a=20custom=20mo?= =?UTF-8?q?del's=20capabilities=20strictly=20as=20T3's=20schema=20has=20th?= =?UTF-8?q?em=20(one=20default=20per=20select).=20Not=20run=20against=20a?= =?UTF-8?q?=20live=20T3=20Code.=20go=20vet=20./internal/...,=20go=20build?= =?UTF-8?q?=20-tags=20nogui=20(darwin,=20GOOS=3Dlinux),=20GOOS=3Dwindows?= =?UTF-8?q?=20go=20build,=20go=20test=20-tags=20nogui=20-count=3D1=20./...?= =?UTF-8?q?=20under=20a=20temp=20HOME=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/agent/t3code.go | 45 ++++++++++++++++-- internal/agent/t3code_test.go | 88 ++++++++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/internal/agent/t3code.go b/internal/agent/t3code.go index 1ac9aa81..3145503e 100644 --- a/internal/agent/t3code.go +++ b/internal/agent/t3code.go @@ -9,7 +9,8 @@ package agent // // {"providerInstances":{"":{"driver":"claudeAgent","displayName":…, // "enabled":true,"environment":[{"name":…,"value":…,"sensitive":false}], -// "config":{"customModels":[{"slug":…,"name":…}]}}}} +// "config":{"customModels":[{"slug":…,"name":…,"capabilities": +// {"optionDescriptors":[{"id":"effort","type":"select",…}]}}]}}}} // // A claudeAgent instance runs Claude Code (Anthropic's Agent SDK, the // user's ~/.claude settings read as Claude Code reads them) with the @@ -18,7 +19,8 @@ package agent // own, "magpie": Claude Code pointed at the gateway (ANTHROPIC_BASE_URL and // the token Claude Code is routed with, so its requests are Claude Code's // to the gateway, tiers and all), every magpie model one of its custom -// models, a 1M one marked [1m] as magpie marks it for Claude Code. It is +// models, a 1M one marked [1m] as magpie marks it for Claude Code, with +// a Reasoning pick of the levels it takes (t3Capabilities). It is // magpie's alone: the user's own instances, their custom models and T3's // other keys stay as they are, and off takes only it out. A binary path or // Claude home the user gave T3's own Claude is carried over, so it runs the @@ -107,7 +109,11 @@ func t3InstanceJSON(path, gw string) map[string]any { mark := claude1MFor("t3code") models := []map[string]any{} for _, m := range magpieModels("t3code") { - models = append(models, map[string]any{"slug": mark(m.ID), "name": m.Name}) + e := map[string]any{"slug": mark(m.ID), "name": m.Name} + if c := t3Capabilities(m.ID, m.Efforts); c != nil { + e["capabilities"] = c + } + models = append(models, e) } config := map[string]any{"customModels": models} for _, k := range []string{"binaryPath", "homePath"} { @@ -130,3 +136,36 @@ func t3InstanceJSON(path, gw string) map[string]any { "config": config, } } + +// t3EffortLabels are T3 Code's names for Claude Code's levels, as its own +// Claude models show them. +var t3EffortLabels = map[string]string{"low": "Low", "medium": "Medium", "high": "High", "xhigh": "Extra High", "max": "Max"} + +// t3Capabilities is the pick T3 Code shows beside a custom model: without +// one it shows none and sends no effort, so every magpie model ran at +// whatever Claude Code took (KevinXC on Discord). It is Reasoning, the +// levels of Claude Code's the model takes, medium chosen first as on T3's +// own Claude models (else the lowest); T3 hands the level to Claude Code +// as it is, which sends it to the gateway. nil for a model with no level +// Claude Code can send. +func t3Capabilities(id string, efforts []string) map[string]any { + var options []map[string]any + for _, l := range claudeEffortsFor(id) { + if contains(efforts, l) { + options = append(options, map[string]any{"id": l, "label": t3EffortLabels[l]}) + } + } + if len(options) == 0 { + return nil + } + def := options[0] + for _, o := range options { + if o["id"] == "medium" { + def = o + } + } + def["isDefault"] = true + return map[string]any{"optionDescriptors": []map[string]any{ + {"id": "effort", "label": "Reasoning", "type": "select", "options": options}, + }} +} diff --git a/internal/agent/t3code_test.go b/internal/agent/t3code_test.go index 20ce26fd..4cc78c60 100644 --- a/internal/agent/t3code_test.go +++ b/internal/agent/t3code_test.go @@ -46,8 +46,20 @@ type t3Envelope struct { BinaryPath string `json:"binaryPath"` HomePath string `json:"homePath"` CustomModels []struct { - Slug string `json:"slug"` - Name string `json:"name"` + Slug string `json:"slug"` + Name string `json:"name"` + Capabilities *struct { + OptionDescriptors []struct { + ID string `json:"id"` + Label string `json:"label"` + Type string `json:"type"` + Options []struct { + ID string `json:"id"` + Label string `json:"label"` + IsDefault *bool `json:"isDefault"` + } `json:"options"` + } `json:"optionDescriptors"` + } `json:"capabilities"` } `json:"customModels"` } `json:"config"` } @@ -78,6 +90,23 @@ func t3Format(t *testing.T, raw []byte) t3Envelope { if m.Slug == "" || m.Name == "" { t.Errorf("custom model: %s", raw) } + if m.Capabilities == nil { + continue + } + for _, d := range m.Capabilities.OptionDescriptors { + defaults := 0 + for _, o := range d.Options { + if o.ID == "" || o.Label == "" { + t.Errorf("option: %s", raw) + } + if o.IsDefault != nil && *o.IsDefault { + defaults++ + } + } + if d.ID == "" || d.Label == "" || d.Type != "select" || len(d.Options) == 0 || defaults != 1 { + t.Errorf("descriptor: %s", raw) + } + } } return e } @@ -230,3 +259,58 @@ func TestT3CodeHomeAndInstance(t *testing.T) { t.Error("the user's Claude instance is gone") } } + +// Each magpie model in T3 Code has a Reasoning pick of the levels it takes +// that Claude Code can send, medium chosen first, else the lowest (KevinXC +// on Discord: every model ran at medium with no way to change it); none +// for a model with no such level. +func TestT3CodeEfforts(t *testing.T) { + type opt struct { + id string + def bool + } + pick := func(id string, efforts []string) []opt { + c := t3Capabilities(id, efforts) + if c == nil { + return nil + } + raw, _ := json.Marshal(c) + var caps struct { + OptionDescriptors []struct { + ID, Label, Type string + Options []struct { + ID, Label string + IsDefault bool + } + } + } + json.Unmarshal(raw, &caps) + if len(caps.OptionDescriptors) != 1 || caps.OptionDescriptors[0].ID != "effort" || caps.OptionDescriptors[0].Type != "select" { + t.Fatalf("%s: %s", id, raw) + } + var out []opt + for _, o := range caps.OptionDescriptors[0].Options { + if o.Label == "" { + t.Errorf("%s: no label: %s", id, raw) + } + out = append(out, opt{o.ID, o.IsDefault}) + } + return out + } + for _, c := range []struct { + id string + efforts []string + want []opt + }{ + {"codex/gpt-6", []string{"none", "minimal", "low", "medium", "high", "xhigh"}, []opt{{"low", false}, {"medium", true}, {"high", false}, {"xhigh", false}}}, + {"ds/deepseek-v4", []string{"high", "max"}, []opt{{"high", true}, {"max", false}}}, + {"claude/claude-opus-4-6", []string{"low", "medium", "high", "xhigh", "max"}, []opt{{"low", false}, {"medium", true}, {"high", false}, {"max", false}}}, + {"claude/claude-haiku-4-5", []string{"low", "medium", "high"}, nil}, + {"glm/glm-4.6", []string{"none", "minimal"}, nil}, + {"glm/glm-4.6", nil, nil}, + } { + if got := pick(c.id, c.efforts); !slices.Equal(got, c.want) { + t.Errorf("%s %v: %v, want %v", c.id, c.efforts, got, c.want) + } + } +}