Files
magpie/internal/agent/opencode_variants_test.go
T
yetone 729f6bdbdf models: a model its maker lists with a thinking switch alone takes no borrowed levels, and the gateway sends it no more than high (#214 Moody-Max: opencode 的 mimo-v2.6 Flash 你这边设置的默认推理强度是 max 导致400报错)
A model not under its provider's own models.dev vendor (a relay or provider of the user's, a live id that doesn't match, or a preview/-exp or typed id Known looks up) took the levels most listed for it across all of models.dev. For mimo-v2.6-flash those are the resellers' (llmgateway, counted once per upstream host it lists, and requesty, up to max), while Xiaomi lists it with a thinking switch alone. OpenCode was offered variants up to max, Pi and Codex the same levels, and the gateway passed max on unchanged (fitEffort keeps an effort when no levels are known), which Xiaomi turned away with a 400. The same held for Zhipu's glm-5/5.1/5v-turbo, Kimi k2.6, MiniMax M2.x, gemma-4 and others whose makers give no levels while resellers give up to max.

Now catalog.ListedBy looks a model up in given catalogs (vendor prefix, case, a ':free' or '(…)' suffix aside) and gives the levels listed there, none for a switch or no options, and skips an entry with a token budget alone (Claude 4.5), which says nothing of efforts. Known looks in the provider's own catalogs first, and every borrow (Known, effortsOf) asks the makers' catalogs (vendor presets, less Groq and Ollama Cloud, marked Hosts as they serve others' models) before the crowd. The crowd vote counts a provider once per list, so an aggregator listing a model under several hosts no longer outvotes the rest. Provider.Levelless says a model is known to have no levels, and the gateway (fitFor, on the direct and fallback paths) sends such a model no more than high; a model whose levels are simply unknown is still sent what was asked.

Tests: TestEffortsOfOneVoteAProvider and TestListedBy (catalog); TestMakerLevelsBeforeResellers (provider: Xiaomi's preset, a preview left out of the list and a relay get no levels, an unlisted model and a budget-only Claude one still borrow, Levelless); TestOpenCodeNoResellerLevelsForSwitchModel (opencode.json has empty variants for Xiaomi's and a relay's mimo, three for a reseller-only model; Pi has no reasoning or thinkingLevelMap for mimo); TestSwitchModelSentNoMoreThanHigh (gateway: mimo asked max/xhigh sent high, low as asked, an unknown model max). The agent and gateway tests fail on main. An audit of every preset against the current models.dev snapshot found the cases above. Not tested with a real Xiaomi key or a running OpenCode. go build -tags nogui (and linux, windows), go test -tags nogui ./... pass.
2026-09-29 19:41:29 +08:00

212 lines
7.2 KiB
Go

package agent
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/provider"
)
func TestOpenCodeVariants(t *testing.T) {
for _, c := range []struct {
efforts []string
want map[string]any
}{
{[]string{"none", "high", "max"}, map[string]any{
"none": map[string]any{"reasoningEffort": "none"},
"high": map[string]any{"reasoningEffort": "high"},
"max": map[string]any{"reasoningEffort": "max"},
}},
{[]string{"low", "medium", "high", "xhigh"}, map[string]any{
"low": map[string]any{"reasoningEffort": "low"},
"medium": map[string]any{"reasoningEffort": "medium"},
"high": map[string]any{"reasoningEffort": "high"},
"xhigh": map[string]any{"reasoningEffort": "xhigh"},
}},
// none at all: an empty set, so OpenCode 2 doesn't make low, medium
// and high of its own
{nil, map[string]any{}},
} {
if got := openCodeVariants(c.efforts); !reflect.DeepEqual(got, c.want) {
t.Errorf("%v: got %v, want %v", c.efforts, got, c.want)
}
}
}
// TestOpenCodeWritesVariants: each model of magpie's in opencode.json names
// the reasoning levels it has as variants — OpenCode 2 otherwise offers low,
// medium and high for every one — and the rest of the entry is as before.
func TestOpenCodeWritesVariants(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"))
if err := provider.Save(provider.Provider{
ID: "think", Name: "Think", Chat: "https://example.test/v1", Key: "key",
Models: []string{"deep", "wide", "plain"},
}); err != nil {
t.Fatal(err)
}
if err := catalog.SaveLive("think", "https://example.test/v1", []catalog.Model{
{ID: "deep", Efforts: []string{"none", "high", "max"}, Context: 1000000, Output: 64000},
{ID: "wide", Efforts: []string{"low", "medium", "high", "xhigh", "max"}},
{ID: "plain"},
}); err != nil {
t.Fatal(err)
}
path := filepath.Join(home, ".config", "opencode", "opencode.json")
writeFile(t, path, `{"theme":"dark"}`)
if err := opencode(home, filepath.Join(home, ".config")).Field("model").Set("magpie/think/deep"); err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var cfg struct {
Theme string `json:"theme"`
Model string `json:"model"`
Provider map[string]struct {
NPM string `json:"npm"`
Models map[string]map[string]any `json:"models"`
} `json:"provider"`
}
if err := json.Unmarshal(b, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Theme != "dark" || cfg.Model != "magpie/think/deep" {
t.Fatalf("config: %s", b)
}
p := cfg.Provider["magpie"]
if p.NPM != "@ai-sdk/openai-compatible" {
t.Fatalf("npm %q", p.NPM)
}
levels := func(id string) []string {
t.Helper()
m, ok := p.Models[id]
if !ok {
t.Fatalf("%s missing: %s", id, b)
}
vs, ok := m["variants"].(map[string]any)
if !ok {
t.Fatalf("%s has no variants: %v", id, m)
}
var out []string
for _, e := range []string{"none", "minimal", "low", "medium", "high", "xhigh", "max"} {
if v, ok := vs[e]; ok {
if !reflect.DeepEqual(v, map[string]any{"reasoningEffort": e}) {
t.Fatalf("%s %s: %v", id, e, v)
}
out = append(out, e)
}
}
if len(out) != len(vs) {
t.Fatalf("%s: unexpected variants %v", id, vs)
}
return out
}
if got := levels("think/deep"); !reflect.DeepEqual(got, []string{"none", "high", "max"}) {
t.Fatalf("deep: %v", got)
}
if got := levels("think/wide"); !reflect.DeepEqual(got, []string{"low", "medium", "high", "xhigh", "max"}) {
t.Fatalf("wide: %v", got)
}
if got := levels("think/plain"); got != nil {
t.Fatalf("plain: %v", got)
}
if lim, _ := p.Models["think/deep"]["limit"].(map[string]any); lim["context"] != float64(1000000) {
t.Fatalf("deep limit: %v", p.Models["think/deep"])
}
}
// Xiaomi's mimo-v2.6-flash, which models.dev lists under Xiaomi with a
// thinking switch alone, gets no variants in opencode.json — under Xiaomi's
// preset or a relay of the user's alike — and no thinking levels in Pi:
// OpenCode offered it the resellers' levels up to max, which Xiaomi turns
// away with a 400 (#214). A model no maker lists keeps the resellers'.
func TestOpenCodeNoResellerLevelsForSwitchModel(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"))
os.MkdirAll(filepath.Dir(catalog.CachePath()), 0o755)
os.WriteFile(catalog.CachePath(), []byte(`{
"xiaomi": {"models": {"mimo-v2.6-flash": {"id":"mimo-v2.6-flash","reasoning":true,"reasoning_options":[{"type":"toggle"}]}}},
"llmgateway": {"models": {
"deepinfra/mimo-v2.6-flash": {"id":"deepinfra/mimo-v2.6-flash","reasoning_options":[{"type":"effort","values":["none","minimal","low","medium","high","xhigh","max"]}]},
"xiaomi/mimo-v2.6-flash": {"id":"xiaomi/mimo-v2.6-flash","reasoning_options":[{"type":"effort","values":["none","minimal","low","medium","high","xhigh","max"]}]},
"open-model": {"id":"open-model","reasoning_options":[{"type":"effort","values":["low","high","max"]}]}}}
}`), 0o644)
catalog.Reset()
t.Cleanup(catalog.Reset)
xiaomi, err := provider.FromPreset("xiaomi")
if err != nil {
t.Fatal(err)
}
xiaomi.Key, xiaomi.Models = "k", []string{"mimo-v2.6-flash"}
if err := provider.Save(xiaomi); err != nil {
t.Fatal(err)
}
if err := provider.Save(provider.Provider{ID: "relay", Name: "Relay", Chat: "https://relay.test/v1", Key: "k",
Models: []string{"mimo-v2.6-flash", "open-model"}}); err != nil {
t.Fatal(err)
}
path := filepath.Join(home, ".config", "opencode", "opencode.json")
writeFile(t, path, `{}`)
if err := opencode(home, filepath.Join(home, ".config")).Field("model").Set("magpie/xiaomi/mimo-v2.6-flash"); err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var cfg struct {
Provider map[string]struct {
Models map[string]struct {
Variants map[string]any `json:"variants"`
} `json:"models"`
} `json:"provider"`
}
if err := json.Unmarshal(b, &cfg); err != nil {
t.Fatal(err)
}
ms := cfg.Provider["magpie"].Models
for id, want := range map[string]int{"xiaomi/mimo-v2.6-flash": 0, "relay/mimo-v2.6-flash": 0, "relay/open-model": 3} {
m, ok := ms[id]
if !ok || m.Variants == nil || len(m.Variants) != want {
t.Errorf("%s: variants %v (listed %v), want %d", id, m.Variants, ok, want)
}
}
pi, _ := json.Marshal(magpieProviderJSON("pi"))
var piCfg struct {
Models []struct {
ID string `json:"id"`
Reasoning bool `json:"reasoning"`
Levels map[string]any `json:"thinkingLevelMap"`
} `json:"models"`
}
json.Unmarshal(pi, &piCfg)
seen := 0
for _, m := range piCfg.Models {
switch m.ID {
case "xiaomi/mimo-v2.6-flash", "relay/mimo-v2.6-flash":
seen++
if m.Reasoning || m.Levels != nil {
t.Errorf("pi %s: reasoning %v, levels %v", m.ID, m.Reasoning, m.Levels)
}
case "relay/open-model":
seen++
if !m.Reasoning || m.Levels["max"] != "max" {
t.Errorf("pi %s: reasoning %v, levels %v", m.ID, m.Reasoning, m.Levels)
}
}
}
if seen != 3 {
t.Errorf("pi models: %s", pi)
}
}