models: Provider in model names has a third way, a name the user gave used just as they wrote it (#92 marsxxl: v0.1.591 的 Codex 下拉里自定义名称又带上了 · WorkBuddy / · Claude Code) — 559ce021 (#203) had a custom name keep " · <provider>" after it like any other, so "Opus 5.5" read "Opus 5.5 · Claude Code" in every list Labels feeds (Codex's model_catalog_json and the gateway's Codex /models, Pi's, OpenCode's, ZCode's, OMP's, Muse's), while #92 had been promised the name as written. Both are kept: the setting is now Off / Not on names I set (自定义名称不带供应商) / On. On stays the default and keeps #203's behaviour, so nobody's list changes unasked; own leaves a name the user gave as it is and a vendor's name with its provider's; off is #335's plain names. Stored as plainOwnNames beside plainNames, so a settings.json of before reads as it did (plainNames: true is off). POST /api/settings/plain-names takes mode on|own|off (a body of on alone still works), magpie model suffix on|own|off, a Settings save keeps it, and /v1/models' magpie_label now follows the setting through Labels as the agents' lists do. The row's hint is one line that fits (en, zh), and its zh name says 供应商 as the rest of the app. Prompts and headers are unchanged. TestSuffixMode, TestSuffixModesReachAgentFiles (Codex model_catalog_json and Pi models.json), TestModelsListSuffixModes (/v1/models magpie_label) and model-suffix-own.test.cjs (Chromium and WebKit, en and zh) fail without the change; TestModelName and TestModelNameAndLevelsReachAgentFiles keep #203's assertions. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestConfiguredModelsAdvertiseImageInput load-flaky, passes alone); plain-names, click-scroll, old-webkit pass.
This commit is contained in:
17 files changed
+496
-49
No files matched your search
@@ -0,0 +1,82 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
)
|
||||
|
||||
// The lists magpie writes into Codex's model_catalog_json and Pi's
|
||||
// models.json name models by the Provider in model names setting, and are
|
||||
// written again as it changes: on, as by default, a renamed model too has
|
||||
// its provider's after it (#203); own, the name the user gave is just as
|
||||
// they wrote it while a vendor's keeps its provider's (#92); off, none has
|
||||
// it (#335).
|
||||
func TestSuffixModesReachAgentFiles(t *testing.T) {
|
||||
home := syncHome(t)
|
||||
os.WriteFile(catalog.CachePath(), []byte(`{"zai":{"models":{"glm-4.6":{"id":"glm-4.6","name":"GLM-4.6"},"glm-5":{"id":"glm-5","name":"GLM-5"}}}}`), 0o644)
|
||||
catalog.Reset()
|
||||
if err := provider.Save(provider.Provider{ID: "relay", Name: "Relay", Catalog: "zai", Key: "k", Chat: "http://127.0.0.1:1/v1", Models: []string{"glm-4.6", "glm-5"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
piModels := filepath.Join(home, ".pi", "agent", "models.json")
|
||||
writeFile(t, piModels, `{"providers":{"magpie":{"name":"magpie","models":[]}}}`)
|
||||
codexDir := filepath.Join(home, ".codex")
|
||||
codexCat := filepath.Join(codexDir, "magpie-models.json")
|
||||
writeFile(t, filepath.Join(codexDir, "config.toml"), "model = \"relay/glm-4.6\"\nmodel_provider = \"magpie\"\nmodel_catalog_json = \""+codexCat+"\"\n")
|
||||
writeFile(t, codexCat, `{"models":[]}`)
|
||||
catalog.Changed = SyncCatalog
|
||||
t.Cleanup(func() { catalog.Changed = nil })
|
||||
if err := provider.SetModelName("relay/glm-4.6", "GLM 5.3"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
names := func() (pi, codex map[string]string) {
|
||||
var p struct {
|
||||
Providers map[string]struct {
|
||||
Models []map[string]any `json:"models"`
|
||||
} `json:"providers"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(readFile(piModels)), &p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pi = map[string]string{}
|
||||
for _, m := range p.Providers["magpie"].Models {
|
||||
pi[m["id"].(string)], _ = m["name"].(string)
|
||||
}
|
||||
var cx struct {
|
||||
Models []struct {
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"display_name"`
|
||||
} `json:"models"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(readFile(codexCat)), &cx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codex = map[string]string{}
|
||||
for _, m := range cx.Models {
|
||||
codex[m.Slug] = m.Name
|
||||
}
|
||||
return pi, codex
|
||||
}
|
||||
for _, c := range []struct{ mode, named, own string }{
|
||||
{provider.SuffixOn, "GLM 5.3 · Relay", "GLM-5 · Relay"},
|
||||
{provider.SuffixOwn, "GLM 5.3", "GLM-5 · Relay"},
|
||||
{provider.SuffixOff, "GLM 5.3", "GLM-5"},
|
||||
{provider.SuffixOn, "GLM 5.3 · Relay", "GLM-5 · Relay"},
|
||||
} {
|
||||
if err := provider.SetSuffixMode(c.mode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pi, codex := names()
|
||||
for what, got := range map[string]map[string]string{"pi": pi, "codex": codex} {
|
||||
if got["relay/glm-4.6"] != c.named || got["relay/glm-5"] != c.own {
|
||||
t.Fatalf("%s, %s: %v", c.mode, what, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
)
|
||||
|
||||
// /v1/models' magpie_label names a model as the agents' lists do, by the
|
||||
// Provider in model names setting: on, as by default, a name the user gave
|
||||
// a model has its provider's after it like any other (#203); own, that
|
||||
// name is just as they wrote it and the vendor's keep theirs (#92: "Opus
|
||||
// 5.5", not "Opus 5.5 · Claude Code"); off, none has it (#335).
|
||||
// display_name is the name alone throughout.
|
||||
func TestModelsListSuffixModes(t *testing.T) {
|
||||
fresh(t)
|
||||
os.MkdirAll(filepath.Dir(catalog.CachePath()), 0o755)
|
||||
data := `{"a":{"models":{"sol":{"id":"sol","name":"Sol"}}},"b":{"models":{"luna":{"id":"luna","name":"Luna"}}}}`
|
||||
if err := os.WriteFile(catalog.CachePath(), []byte(data), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog.Reset()
|
||||
t.Cleanup(catalog.Reset)
|
||||
for id, m := range map[string]string{"a": "sol", "b": "luna"} {
|
||||
if err := provider.Save(provider.Provider{ID: id, Name: "Vendor " + strings.ToUpper(id), Catalog: id, Key: "k", Chat: "http://127.0.0.1:1/v1", Models: []string{m}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := provider.SetModelName("a/sol", "Sol 5"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
list := func() map[string]string {
|
||||
rec := httptest.NewRecorder()
|
||||
New().Handler().ServeHTTP(rec, httptest.NewRequest("GET", "/v1/models", nil))
|
||||
var out struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"display_name"`
|
||||
Label string `json:"magpie_label"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := map[string]string{}
|
||||
for _, m := range out.Data {
|
||||
got[m.ID] = m.Name + "|" + m.Label
|
||||
}
|
||||
return got
|
||||
}
|
||||
for _, c := range []struct{ mode, sol, luna string }{
|
||||
{provider.SuffixOn, "Sol 5|Sol 5 · Vendor A", "Luna|Luna · Vendor B"},
|
||||
{provider.SuffixOwn, "Sol 5|Sol 5", "Luna|Luna · Vendor B"},
|
||||
{provider.SuffixOff, "Sol 5|Sol 5", "Luna|Luna"},
|
||||
} {
|
||||
if err := provider.SetSuffixMode(c.mode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := list(); got["a/sol"] != c.sol || got["b/luna"] != c.luna {
|
||||
t.Fatalf("%s: %v", c.mode, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,12 +504,14 @@ func (s *Server) models(w http.ResponseWriter, r *http.Request) {
|
||||
if agentOf(r) == "claude-desktop" {
|
||||
data = desktopModels(shown)
|
||||
} else {
|
||||
for _, e := range shown {
|
||||
labels := provider.Labels(shown)
|
||||
for i, e := range shown {
|
||||
m := modelObject(e)
|
||||
// for another magpie: its name with its provider here after
|
||||
// it, so two providers' models of one name are told apart
|
||||
// for another magpie: its name as the agents' lists here call
|
||||
// it, with its provider's after it unless the user wants it
|
||||
// plain, so two providers' models of one name are told apart
|
||||
// there as they are here
|
||||
m["magpie_label"] = e.Label()
|
||||
m["magpie_label"] = labels[i]
|
||||
data = append(data, m)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (s *Server) museModels(w http.ResponseWriter, r *http.Request) {
|
||||
"attachment": e.Images, "reasoning": len(e.Efforts) > 0, "temperature": false, "tool_call": true,
|
||||
"modalities": map[string]any{"input": input, "output": []string{"text"}},
|
||||
"options": map[string]any{"include": []string{}}, "variants": map[string]any{},
|
||||
"description": e.Label() + " via magpie"}
|
||||
"description": labels[i] + " via magpie"}
|
||||
// the window and the most a reply may hold: Muse hides a model
|
||||
// whose limit lacks either, and without a limit asks for replies
|
||||
// of 128K tokens, more than most models give; one magpie doesn't
|
||||
|
||||
+12
-3
@@ -591,7 +591,7 @@ func Handler(w Windows, gw *gateway.Server) http.Handler {
|
||||
// used or left is the Usage page's toggle as much as Settings', set on its own
|
||||
in.QuotaLeft = cur.QuotaLeft
|
||||
// how agents' lists name models, set on its own for the agents to be told
|
||||
in.PlainNames = cur.PlainNames
|
||||
in.PlainNames, in.PlainOwnNames = cur.PlainNames, cur.PlainOwnNames
|
||||
// which Codex accounts spend a reset by themselves, set on the Usage card
|
||||
in.CodexAutoReset = cur.CodexAutoReset
|
||||
// and the text size, which the keyboard changes too (text-size below)
|
||||
@@ -681,12 +681,21 @@ func Handler(w Windows, gw *gateway.Server) http.Handler {
|
||||
// whether the agents' lists name a model with its provider's after it or
|
||||
// alone (#335): their files are written again, and Codex asks again
|
||||
mux.HandleFunc("POST /api/settings/plain-names", func(rw http.ResponseWriter, r *http.Request) {
|
||||
var in struct{ On bool }
|
||||
// Mode is on, own (#92: not on the names the user gave) or off; a
|
||||
// body of On alone is the two-way switch's, On meaning plain
|
||||
var in struct {
|
||||
On bool
|
||||
Mode string
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
fail(rw, err)
|
||||
return
|
||||
}
|
||||
if err := provider.SetPlainNames(in.On); err != nil {
|
||||
set := func() error { return provider.SetPlainNames(in.On) }
|
||||
if in.Mode != "" {
|
||||
set = func() error { return provider.SetSuffixMode(in.Mode) }
|
||||
}
|
||||
if err := set(); err != nil {
|
||||
fail(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10613,10 +10613,12 @@ function renderTrayUsage(s, keep) {
|
||||
$("#unitsSegs").replaceChildren(segs([[false, t("万 / 亿")], [true, t("K / M / B")]], !!s.westernUnits,
|
||||
(v) => savePrefs({ ...keep, westernUnits: v })));
|
||||
renderAlerts(s, keep);
|
||||
// the agents' lists name a model with its provider's after it, or alone
|
||||
// (#335): set on its own, so the agents are told
|
||||
$("#plainNamesSegs").replaceChildren(segs([["off", t("Off")], ["on", t("On")]], s.plainNames ? "off" : "on", (v) =>
|
||||
writingPrefs(api("settings/plain-names", { on: v === "off" })).then((ns) => { prefs = ns; renderSettings(); }).catch((e) => { status(t(e.message), "err"); renderSettings(); })));
|
||||
// the agents' lists name a model with its provider's after it, all but
|
||||
// the names the user gave (#92), or none (#335): set on its own, so the
|
||||
// agents are told
|
||||
const suffix = s.plainNames ? "off" : s.plainOwnNames ? "own" : "on";
|
||||
$("#plainNamesSegs").replaceChildren(segs([["off", t("Off")], ["own", t("Not on names I set")], ["on", t("On")]], suffix, (v) =>
|
||||
writingPrefs(api("settings/plain-names", { mode: v })).then((ns) => { prefs = ns; renderSettings(); }).catch((e) => { status(t(e.message), "err"); renderSettings(); })));
|
||||
const rate = s.fx?.rate;
|
||||
const currencySub = $("#currencySub");
|
||||
currencySub.textContent = t("What a cost — the Usage page's, the tray panel's, the TUI's and the CLI's — is shown as; a vendor's own balance, already in its own currency, is never converted");
|
||||
|
||||
@@ -204,8 +204,9 @@ const I18N = {
|
||||
"Notifications can't be shown on this system": "此系统无法显示通知",
|
||||
"Share used": "已用比例",
|
||||
"Amount": "金额",
|
||||
"Provider in model names": "模型名带提供商",
|
||||
"Agents’ model lists name each model with its provider or “routing group” after it. Off, the name alone, unless two would read the same": "写给 agent 的模型列表里,每个模型名后面带上提供商或“routing group”。关闭后只用模型名,除非有两个会重名",
|
||||
"Provider in model names": "模型名带供应商",
|
||||
"Agents’ lists put each model’s provider after its name, or not after names you set": "写给 agent 的模型列表在模型名后带上供应商,或自定义的名称不带",
|
||||
"Not on names I set": "自定义名称不带供应商",
|
||||
"$ USD": "$ 美元",
|
||||
"¥ CNY": "¥ 人民币",
|
||||
"What a cost — the Usage page's, the tray panel's, the TUI's and the CLI's — is shown as; a vendor's own balance, already in its own currency, is never converted": "费用——用量页、托盘面板、TUI 和命令行里的——按此显示;厂商自己给出的余额本就是它自己的货币,不会被换算",
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
<div id="balanceAlertSegs" class="warm-at"></div>
|
||||
</div>
|
||||
<div class="row pref">
|
||||
<div class="who"><div class="name" data-t>Provider in model names</div><div class="sub" data-t>Agents’ model lists name each model with its provider or “routing group” after it. Off, the name alone, unless two would read the same</div></div>
|
||||
<div class="who"><div class="name" data-t>Provider in model names</div><div class="sub" data-t>Agents’ lists put each model’s provider after its name, or not after names you set</div></div>
|
||||
<div id="plainNamesSegs"></div>
|
||||
</div>
|
||||
<div class="row pref">
|
||||
|
||||
@@ -84,6 +84,7 @@ func TestSettingsSaveKeepsWhatItDoesNotSend(t *testing.T) {
|
||||
RequestArchive: true,
|
||||
QuotaLeft: true,
|
||||
PlainNames: true,
|
||||
PlainOwnNames: true,
|
||||
CodexAutoReset: []string{"me@example.com"},
|
||||
TextSize: 125,
|
||||
UpdateSkip: "0.1.500",
|
||||
|
||||
@@ -245,10 +245,16 @@ Routing tab alike; the other keeps "agent · model → provider". English and
|
||||
Chinese, Chromium and WebKit (#337).
|
||||
|
||||
`plain-names.test.cjs` checks the Settings page's "Provider in model names"
|
||||
row (#335): on by default, Off posts `settings/plain-names` with `on: true`
|
||||
row (#335): on by default, Off posts `settings/plain-names` with `mode: "off"`
|
||||
on its own and lights Off, On posts it back, and neither click scrolls the
|
||||
page. English and Chinese, Chromium and WebKit.
|
||||
|
||||
`model-suffix-own.test.cjs` checks that row's third way (#92): "Not on names
|
||||
I set" (自定义名称不带供应商) between Off and On, lit by `plainOwnNames`; On and
|
||||
then it post `settings/plain-names` with `mode: "on"` and `mode: "own"`,
|
||||
neither click scrolls the page, the hint isn't cut short and the three fit
|
||||
the row at 900px and 560px. English and Chinese, Chromium and WebKit.
|
||||
|
||||
`routing-steady.test.cjs` scrolls the Routing page down to its routing groups,
|
||||
opens one in its editor and types into its name, then streams six requests in
|
||||
from the trace (new agents and accounts on the stage, failures and retries in
|
||||
@@ -983,7 +989,7 @@ reads shows none of it. English and Chinese, Chromium and WebKit, with
|
||||
With Node.js and Playwright available:
|
||||
|
||||
```sh
|
||||
node --test internal/gui/tests/menu-scroll.test.cjs internal/gui/tests/click-scroll.test.cjs internal/gui/tests/panel-fold.test.cjs internal/gui/tests/rollup-short.test.cjs internal/gui/tests/panel-routing.test.cjs internal/gui/tests/gateway-fold.test.cjs internal/gui/tests/routing-kind.test.cjs internal/gui/tests/balance-fix.test.cjs internal/gui/tests/cli-update.test.cjs internal/gui/tests/login-import.test.cjs internal/gui/tests/agy-launch.test.cjs internal/gui/tests/currency.test.cjs internal/gui/tests/usage-ledger.test.cjs internal/gui/tests/update-check.test.cjs internal/gui/tests/omarchy.test.cjs internal/gui/tests/brand.test.cjs internal/gui/tests/library-loading.test.cjs internal/gui/tests/provider-typed.test.cjs node --test internal/gui/tests/signin-callback.test.cjs internal/gui/tests/routing-steady.test.cjs internal/gui/tests/old-webkit.test.cjs internal/gui/tests/session-terminal.test.cjs internal/gui/tests/text-size.test.cjs internal/gui/tests/zcode-site.test.cjs internal/gui/tests/rail-tip.test.cjs internal/gui/tests/sync-update.test.cjs internal/gui/tests/plugin-signin.test.cjs internal/gui/tests/plugin-market.test.cjs internal/gui/tests/s3-sync.test.cjs internal/gui/tests/routing-wb-refused.test.cjs internal/gui/tests/routing-blocked.test.cjs internal/gui/tests/routing-zcode-blocked.test.cjs internal/gui/tests/routing-shape.test.cjs internal/gui/tests/market-have.test.cjs internal/gui/tests/sessions-today.test.cjs internal/gui/tests/routing-side-calls.test.cjs internal/gui/tests/import-all-skills.test.cjs internal/gui/tests/routing-flood.test.cjs internal/gui/tests/routing-busy.test.cjs internal/gui/tests/own-skill.test.cjs internal/gui/tests/routing-manual.test.cjs internal/gui/tests/tray-usages.test.cjs internal/gui/tests/provider-remove.test.cjs internal/gui/tests/signin-link.test.cjs internal/gui/tests/tray-inuse.test.cjs internal/gui/tests/codex-auto-reset.test.cjs internal/gui/tests/routing-auto-reset.test.cjs internal/gui/tests/claude-usage-asked.test.cjs internal/gui/tests/claude-risk.test.cjs internal/gui/tests/library-toggle-status.test.cjs internal/gui/tests/library-all.test.cjs internal/gui/tests/routing-reroute-title.test.cjs internal/gui/tests/plain-names.test.cjs internal/gui/tests/panel-effort.test.cjs internal/gui/tests/agent-models-all-hidden.test.cjs internal/gui/tests/omp-roles.test.cjs internal/gui/tests/claude-effort.test.cjs internal/gui/tests/remote-magpie.test.cjs internal/gui/tests/qoder-cn-tile.test.cjs internal/gui/tests/provider-levels-scroll.test.cjs internal/gui/tests/signin-paste-codex.test.cjs internal/gui/tests/signin-paste-key.test.cjs internal/gui/tests/usage-alert.test.cjs internal/gui/tests/quota-families.test.cjs internal/gui/tests/provider-search.test.cjs internal/gui/tests/webdav-settings-refresh.test.cjs internal/gui/tests/lan-docker.test.cjs internal/gui/tests/plugin-key-hint.test.cjs internal/gui/tests/update-hide.test.cjs internal/gui/tests/tray-logos.test.cjs internal/gui/tests/routing-proxy.test.cjs internal/gui/tests/sse-body.test.cjs internal/gui/tests/remote-group-served.test.cjs internal/gui/tests/plugin-move-overflow.test.cjs internal/gui/tests/routing-time.test.cjs internal/gui/tests/unlisted-models.test.cjs internal/gui/tests/plugin-updates.test.cjs internal/gui/tests/request-archive.test.cjs internal/gui/tests/group-member-fast.test.cjs internal/gui/tests/providers-off-fold.test.cjs internal/gui/tests/account-return.test.cjs internal/gui/tests/usage-ledger-detail.test.cjs internal/gui/tests/usage-ledger-chart.test.cjs internal/gui/tests/usage-chart-axis.test.cjs internal/gui/tests/panel-usage.test.cjs internal/gui/tests/usage-refresh.test.cjs internal/gui/tests/usage-ledger-content.test.cjs internal/gui/tests/claude-direct.test.cjs internal/gui/tests/signin-again.test.cjs internal/gui/tests/sessions-calendar-fill.test.cjs internal/gui/tests/editor-short-window.test.cjs internal/gui/tests/group-from-model-add.test.cjs internal/gui/tests/sync-other-kind.test.cjs internal/gui/tests/web-page.test.cjs internal/gui/tests/balance-parts.test.cjs internal/gui/tests/search-api.test.cjs internal/gui/tests/provider-file-error.test.cjs internal/gui/tests/sessions-manage.test.cjs internal/gui/tests/number-units.test.cjs internal/gui/tests/sessions-bar-names.test.cjs
|
||||
node --test internal/gui/tests/menu-scroll.test.cjs internal/gui/tests/click-scroll.test.cjs internal/gui/tests/panel-fold.test.cjs internal/gui/tests/rollup-short.test.cjs internal/gui/tests/panel-routing.test.cjs internal/gui/tests/gateway-fold.test.cjs internal/gui/tests/routing-kind.test.cjs internal/gui/tests/balance-fix.test.cjs internal/gui/tests/cli-update.test.cjs internal/gui/tests/login-import.test.cjs internal/gui/tests/agy-launch.test.cjs internal/gui/tests/currency.test.cjs internal/gui/tests/usage-ledger.test.cjs internal/gui/tests/update-check.test.cjs internal/gui/tests/omarchy.test.cjs internal/gui/tests/brand.test.cjs internal/gui/tests/library-loading.test.cjs internal/gui/tests/provider-typed.test.cjs node --test internal/gui/tests/signin-callback.test.cjs internal/gui/tests/routing-steady.test.cjs internal/gui/tests/old-webkit.test.cjs internal/gui/tests/session-terminal.test.cjs internal/gui/tests/text-size.test.cjs internal/gui/tests/zcode-site.test.cjs internal/gui/tests/rail-tip.test.cjs internal/gui/tests/sync-update.test.cjs internal/gui/tests/plugin-signin.test.cjs internal/gui/tests/plugin-market.test.cjs internal/gui/tests/s3-sync.test.cjs internal/gui/tests/routing-wb-refused.test.cjs internal/gui/tests/routing-blocked.test.cjs internal/gui/tests/routing-zcode-blocked.test.cjs internal/gui/tests/routing-shape.test.cjs internal/gui/tests/market-have.test.cjs internal/gui/tests/sessions-today.test.cjs internal/gui/tests/routing-side-calls.test.cjs internal/gui/tests/import-all-skills.test.cjs internal/gui/tests/routing-flood.test.cjs internal/gui/tests/routing-busy.test.cjs internal/gui/tests/own-skill.test.cjs internal/gui/tests/routing-manual.test.cjs internal/gui/tests/tray-usages.test.cjs internal/gui/tests/provider-remove.test.cjs internal/gui/tests/signin-link.test.cjs internal/gui/tests/tray-inuse.test.cjs internal/gui/tests/codex-auto-reset.test.cjs internal/gui/tests/routing-auto-reset.test.cjs internal/gui/tests/claude-usage-asked.test.cjs internal/gui/tests/claude-risk.test.cjs internal/gui/tests/library-toggle-status.test.cjs internal/gui/tests/library-all.test.cjs internal/gui/tests/routing-reroute-title.test.cjs internal/gui/tests/plain-names.test.cjs internal/gui/tests/panel-effort.test.cjs internal/gui/tests/agent-models-all-hidden.test.cjs internal/gui/tests/omp-roles.test.cjs internal/gui/tests/claude-effort.test.cjs internal/gui/tests/remote-magpie.test.cjs internal/gui/tests/qoder-cn-tile.test.cjs internal/gui/tests/provider-levels-scroll.test.cjs internal/gui/tests/signin-paste-codex.test.cjs internal/gui/tests/signin-paste-key.test.cjs internal/gui/tests/usage-alert.test.cjs internal/gui/tests/quota-families.test.cjs internal/gui/tests/provider-search.test.cjs internal/gui/tests/webdav-settings-refresh.test.cjs internal/gui/tests/lan-docker.test.cjs internal/gui/tests/plugin-key-hint.test.cjs internal/gui/tests/update-hide.test.cjs internal/gui/tests/tray-logos.test.cjs internal/gui/tests/routing-proxy.test.cjs internal/gui/tests/sse-body.test.cjs internal/gui/tests/remote-group-served.test.cjs internal/gui/tests/plugin-move-overflow.test.cjs internal/gui/tests/routing-time.test.cjs internal/gui/tests/unlisted-models.test.cjs internal/gui/tests/plugin-updates.test.cjs internal/gui/tests/request-archive.test.cjs internal/gui/tests/group-member-fast.test.cjs internal/gui/tests/providers-off-fold.test.cjs internal/gui/tests/account-return.test.cjs internal/gui/tests/usage-ledger-detail.test.cjs internal/gui/tests/usage-ledger-chart.test.cjs internal/gui/tests/usage-chart-axis.test.cjs internal/gui/tests/panel-usage.test.cjs internal/gui/tests/usage-refresh.test.cjs internal/gui/tests/usage-ledger-content.test.cjs internal/gui/tests/claude-direct.test.cjs internal/gui/tests/signin-again.test.cjs internal/gui/tests/sessions-calendar-fill.test.cjs internal/gui/tests/editor-short-window.test.cjs internal/gui/tests/group-from-model-add.test.cjs internal/gui/tests/sync-other-kind.test.cjs internal/gui/tests/web-page.test.cjs internal/gui/tests/balance-parts.test.cjs internal/gui/tests/search-api.test.cjs internal/gui/tests/provider-file-error.test.cjs internal/gui/tests/sessions-manage.test.cjs internal/gui/tests/number-units.test.cjs internal/gui/tests/sessions-bar-names.test.cjs internal/gui/tests/model-suffix-own.test.cjs
|
||||
```
|
||||
|
||||
If Playwright is installed outside the repository, set `NODE_PATH` to the
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// The "Provider in model names" setting's third way (#92: 自定义名称后面又带上了
|
||||
// · 供应商): "Not on names I set" between Off and On, lit when settings say
|
||||
// plainOwnNames; On and then it post settings/plain-names with mode on and
|
||||
// own on their own, neither click scrolls the Settings page, and the three
|
||||
// fit the row, wide and narrow. English and Chinese, Chromium and WebKit; no
|
||||
// backend, the API is faked here.
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { chromium, webkit } = require("playwright");
|
||||
|
||||
const assets = path.resolve(__dirname, "../assets");
|
||||
|
||||
function settingsPayload(over) {
|
||||
return {
|
||||
theme: "light", lang: "en", tray: "panel", quotaLeft: false, currency: "usd",
|
||||
dock: false, dockWindow: false, proxy: "", redact: false, redactPersonal: false, redactWords: [],
|
||||
codexWarmup: "", claudeWarmup: "", codexWarmAt: "", claudeWarmAt: "", workbuddyCheckin: false, noStats: false,
|
||||
trayUsage: "", trayUsageEvery: 3, vision: "", imageGen: "",
|
||||
version: "0.1.400", dir: "~/.config/magpie", gateway: "http://127.0.0.1:3425",
|
||||
proxyNow: "none", proxySource: "none", login: false,
|
||||
visionModels: [], imageGenModels: [], workbuddyCheckins: [], lanURLs: [],
|
||||
fx: { rate: 7.2, at: new Date().toISOString(), stale: false },
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
function server(lang, posted) {
|
||||
let cur = settingsPayload({ lang, plainOwnNames: true });
|
||||
return async (route) => {
|
||||
const req = route.request(), url = new URL(req.url());
|
||||
const json = (data) => route.fulfill({ json: data });
|
||||
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:false};` });
|
||||
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
||||
if (url.pathname === "/api/state") return json({ agents: [], profiles: [], settings: { lang, theme: "light" }, fx: cur.fx });
|
||||
if (url.pathname === "/api/settings/plain-names") {
|
||||
const body = req.postDataJSON();
|
||||
posted.push(body);
|
||||
cur = { ...cur, plainNames: body.mode === "off", plainOwnNames: body.mode === "own" };
|
||||
return json(cur);
|
||||
}
|
||||
if (url.pathname === "/api/settings") return json(cur);
|
||||
if (url.pathname === "/api/plugins") return json({ plugins: [] });
|
||||
if (url.pathname === "/api/usage/quotas") return json([]);
|
||||
if (url.pathname === "/api/groups") return json({ groups: [], models: [] });
|
||||
if (url.pathname.startsWith("/api/")) return json({});
|
||||
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
|
||||
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
|
||||
await route.fulfill({ body: await fs.readFile(file), contentType });
|
||||
};
|
||||
}
|
||||
|
||||
const want = {
|
||||
en: { labels: ["Off", "Not on names I set", "On"], sub: /^Agents’ lists put each model’s provider after its name, or not after names you set$/ },
|
||||
zh: { labels: ["关闭", "自定义名称不带供应商", "开启"], sub: /^写给 agent 的模型列表在模型名后带上供应商,或自定义的名称不带$/ },
|
||||
};
|
||||
const view = (page) => page.locator("#view-settings").evaluate((v) => v.scrollTop);
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
for (const lang of ["en", "zh"]) {
|
||||
test(`${engine} ${lang}: a name the user gave can go without its provider`, async (t) => {
|
||||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
||||
const context = await browser.newContext({ viewport: { width: 900, height: 480 }, reducedMotion: "reduce" });
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors = [], posted = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/*", server(lang, posted));
|
||||
t.after(async () => {
|
||||
if (process.env.ARTIFACT_DIR) {
|
||||
await fs.mkdir(process.env.ARTIFACT_DIR, { recursive: true });
|
||||
await page.screenshot({ path: path.join(process.env.ARTIFACT_DIR, `${engine}-${lang}-model-suffix-own.png`) });
|
||||
}
|
||||
await browser.close();
|
||||
});
|
||||
await page.goto("http://magpie.test/?view=settings");
|
||||
const segs = page.locator("#plainNamesSegs .opt");
|
||||
await segs.first().waitFor();
|
||||
const row = page.locator(".row.pref", { has: page.locator("#plainNamesSegs") });
|
||||
assert.match(await row.locator(".sub").textContent(), want[lang].sub);
|
||||
assert.equal(await row.locator(".sub").evaluate((e) => e.scrollWidth <= e.clientWidth), true, "the hint is cut short");
|
||||
assert.deepEqual((await segs.allTextContents()).map((s) => s.trim()), want[lang].labels);
|
||||
const own = segs.nth(1);
|
||||
assert.equal(await own.evaluate((b) => b.classList.contains("on")), true, "plainOwnNames lights the middle one");
|
||||
|
||||
// the three stay inside the row, wide and narrow
|
||||
for (const width of [900, 560]) {
|
||||
await page.setViewportSize({ width, height: 480 });
|
||||
const r = await row.boundingBox(), s = await page.locator("#plainNamesSegs .segs").boundingBox();
|
||||
assert(s.x >= r.x - 0.5 && s.x + s.width <= r.x + r.width + 0.5, `${width}px: the options overflow the row`);
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, `${width}px: the page scrolls sideways`);
|
||||
}
|
||||
await page.setViewportSize({ width: 900, height: 480 });
|
||||
|
||||
// scrolled by a wheel till the row is mid-view, On and then the
|
||||
// middle one move nothing and are posted on their own
|
||||
const box = await page.locator("#view-settings").boundingBox();
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
const top = () => page.locator("#plainNamesSegs").evaluate((e) => e.getBoundingClientRect().top);
|
||||
for (let i = 0; i < 40 && (await top()) > 240; i++) { await page.mouse.wheel(0, 100); await page.waitForTimeout(30); }
|
||||
const before = await view(page);
|
||||
assert(before > 0, "the settings list must scroll to the row");
|
||||
await segs.nth(2).click();
|
||||
await page.locator("#plainNamesSegs .opt.on", { hasText: want[lang].labels[2] }).waitFor();
|
||||
assert.deepEqual(posted, [{ mode: "on" }]);
|
||||
await page.locator("#plainNamesSegs .opt", { hasText: want[lang].labels[1] }).click();
|
||||
await page.locator("#plainNamesSegs .opt.on", { hasText: want[lang].labels[1] }).waitFor();
|
||||
await page.waitForTimeout(400);
|
||||
assert.deepEqual(posted, [{ mode: "on" }, { mode: "own" }]);
|
||||
assert.equal(await view(page), before, "a click scrolled the page");
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
// The "Provider in model names" setting (#335: 希望 Codex 模型列表里的显示名可以
|
||||
// 不带 · routing group / · 提供商 后缀): on by default; Off posts
|
||||
// settings/plain-names on its own (so the agents' lists are written again)
|
||||
// and lights Off, On posts it back, and neither click scrolls the Settings
|
||||
// page. English and Chinese, Chromium and WebKit; no backend, the API is faked here.
|
||||
// with mode off and lights Off, On posts it back, and neither click scrolls
|
||||
// the Settings page. English and Chinese, Chromium and WebKit; no backend, the API is faked here.
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
@@ -37,7 +37,7 @@ function server(lang, posted) {
|
||||
if (url.pathname === "/api/settings/plain-names") {
|
||||
const body = req.postDataJSON();
|
||||
posted.push(["plain-names", body]);
|
||||
cur = { ...cur, plainNames: body.on };
|
||||
cur = { ...cur, plainNames: body.mode === "off", plainOwnNames: body.mode === "own" };
|
||||
return json(cur);
|
||||
}
|
||||
if (url.pathname === "/api/settings") {
|
||||
@@ -59,8 +59,8 @@ function server(lang, posted) {
|
||||
}
|
||||
|
||||
const want = {
|
||||
en: { name: "Provider in model names", sub: /routing group/, off: "Off", on: "On" },
|
||||
zh: { name: "模型名带提供商", sub: /除非有两个会重名/, off: "关闭", on: "开启" },
|
||||
en: { name: "Provider in model names", sub: /provider after its name/, off: "Off", own: "Not on names I set", on: "On" },
|
||||
zh: { name: "模型名带供应商", sub: /自定义的名称不带/, off: "关闭", own: "自定义名称不带供应商", on: "开启" },
|
||||
};
|
||||
const view = (page) => page.locator("#view-settings").evaluate((v) => v.scrollTop);
|
||||
|
||||
@@ -87,8 +87,8 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
const row = page.locator(".row.pref", { has: page.locator("#plainNamesSegs") });
|
||||
assert.equal((await row.locator(".name").textContent()).trim(), want[lang].name);
|
||||
assert.match(await row.locator(".sub").textContent(), want[lang].sub);
|
||||
assert.deepEqual((await segs.allTextContents()).map((s) => s.trim()), [want[lang].off, want[lang].on]);
|
||||
assert.equal(await segs.nth(1).evaluate((b) => b.classList.contains("on")), true, "on by default");
|
||||
assert.deepEqual((await segs.allTextContents()).map((s) => s.trim()), [want[lang].off, want[lang].own, want[lang].on]);
|
||||
assert.equal(await segs.nth(2).evaluate((b) => b.classList.contains("on")), true, "on by default");
|
||||
|
||||
// scrolled by a wheel till the row is mid-view (a real wheel, so the
|
||||
// reader's-scroll guard lets it stick), Off moves nothing and is
|
||||
@@ -103,12 +103,12 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
await page.locator("#plainNamesSegs .opt.on", { hasText: want[lang].off }).waitFor();
|
||||
await page.waitForTimeout(400);
|
||||
assert.equal(await view(page), before, "the click scrolled the page");
|
||||
assert.deepEqual(posted, [["plain-names", { on: true }]]);
|
||||
assert.deepEqual(posted, [["plain-names", { mode: "off" }]]);
|
||||
|
||||
// back on
|
||||
await page.locator("#plainNamesSegs .opt", { hasText: want[lang].on }).click();
|
||||
await segs.nth(2).click();
|
||||
await page.locator("#plainNamesSegs .opt.on", { hasText: want[lang].on }).waitFor();
|
||||
assert.deepEqual(posted.at(-1), ["plain-names", { on: false }]);
|
||||
assert.deepEqual(posted.at(-1), ["plain-names", { mode: "on" }]);
|
||||
assert.equal(await view(page), before, "the click scrolled the page");
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
|
||||
@@ -414,10 +414,14 @@ func (e Entry) Label() string {
|
||||
// when the user wants names plain (settings' PlainNames, #335), by the
|
||||
// name alone — but for two or more the list would call the same, as a
|
||||
// routing group found for a model is called with that model left in the
|
||||
// list, which keep their provider's after it to tell them apart.
|
||||
// list, which keep their provider's after it to tell them apart. With
|
||||
// their own names plain (PlainOwnNames, #92), a name the user gave a model
|
||||
// is that name just as they wrote it, and the vendor's keep Label's.
|
||||
func Labels(es []Entry) []string {
|
||||
out := make([]string, len(es))
|
||||
plain := settings.Load().PlainNames
|
||||
s := settings.Load()
|
||||
plain := s.PlainNames
|
||||
own := !plain && s.PlainOwnNames
|
||||
same := map[string]int{}
|
||||
if plain {
|
||||
for _, e := range es {
|
||||
@@ -426,21 +430,53 @@ func Labels(es []Entry) []string {
|
||||
}
|
||||
for i, e := range es {
|
||||
out[i] = e.Label()
|
||||
if plain && e.Name != "" && same[strings.ToLower(e.Name)] == 1 {
|
||||
if plain && e.Name != "" && same[strings.ToLower(e.Name)] == 1 || own && e.Default != "" && e.Name != "" {
|
||||
out[i] = e.Name
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SetPlainNames has the agents' model lists name models by their names
|
||||
// alone (see Labels), or with their providers' again, and the agents told.
|
||||
func SetPlainNames(on bool) error {
|
||||
// The ways the agents' lists name models (SuffixMode): every name with its
|
||||
// provider's after it, as by default; all but the names the user gave
|
||||
// models (#92); or none (#335).
|
||||
const (
|
||||
SuffixOn = "on"
|
||||
SuffixOwn = "own"
|
||||
SuffixOff = "off"
|
||||
)
|
||||
|
||||
// SuffixMode is how the agents' lists name models: SuffixOn, SuffixOwn or
|
||||
// SuffixOff.
|
||||
func SuffixMode() string {
|
||||
s := settings.Load()
|
||||
if s.PlainNames == on {
|
||||
switch {
|
||||
case s.PlainNames:
|
||||
return SuffixOff
|
||||
case s.PlainOwnNames:
|
||||
return SuffixOwn
|
||||
}
|
||||
return SuffixOn
|
||||
}
|
||||
|
||||
// SetSuffixMode has the agents' model lists name models as mode says (see
|
||||
// Labels), and the agents told when it changes.
|
||||
func SetSuffixMode(mode string) error {
|
||||
plain, own := false, false
|
||||
switch mode {
|
||||
case SuffixOn:
|
||||
case SuffixOwn:
|
||||
own = true
|
||||
case SuffixOff:
|
||||
plain = true
|
||||
default:
|
||||
return fmt.Errorf("provider in model names: on, own or off, not %q", mode)
|
||||
}
|
||||
s := settings.Load()
|
||||
if s.PlainNames == plain && s.PlainOwnNames == own {
|
||||
return nil
|
||||
}
|
||||
s.PlainNames = on
|
||||
s.PlainNames, s.PlainOwnNames = plain, own
|
||||
if err := settings.Save(s); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -448,6 +484,15 @@ func SetPlainNames(on bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPlainNames has the agents' model lists name models by their names
|
||||
// alone (see Labels), or with their providers' again, and the agents told.
|
||||
func SetPlainNames(on bool) error {
|
||||
if on {
|
||||
return SetSuffixMode(SuffixOff)
|
||||
}
|
||||
return SetSuffixMode(SuffixOn)
|
||||
}
|
||||
|
||||
// ModelNames are the names the user gave the provider's models, by model id.
|
||||
func (p Provider) ModelNames() map[string]string {
|
||||
out := map[string]string{}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
)
|
||||
|
||||
// Provider in model names has three ways (#92): on, as by default and as a
|
||||
// settings.json from before has it, every name with its provider's after
|
||||
// it, one the user gave too (#203); own, a name the user gave just as they
|
||||
// wrote it, the vendor's with their provider's; off, none (#335), as an
|
||||
// older plainNames: true still reads. Each change tells the agents; one to
|
||||
// the way already set doesn't, and a way of no such name is refused.
|
||||
func TestSuffixMode(t *testing.T) {
|
||||
prefsHome(t)
|
||||
touched := 0
|
||||
catalog.Changed = func() { touched++ }
|
||||
t.Cleanup(func() { catalog.Changed = nil })
|
||||
if err := SetModelName("a/sol", "My Sol"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
touched = 0
|
||||
codex := func() []string {
|
||||
var out []string
|
||||
for _, m := range CodexListed() {
|
||||
out = append(out, m.ID+"="+m.Name)
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
if SuffixMode() != SuffixOn {
|
||||
t.Fatalf("default %q", SuffixMode())
|
||||
}
|
||||
if got := codex(); !slices.Equal(got, []string{"a/sol=My Sol · A", "b/sol=Sol · B", "group/auto-sol=Sol · routing group"}) {
|
||||
t.Fatalf("on: %q", got)
|
||||
}
|
||||
|
||||
if err := SetSuffixMode(SuffixOwn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s := settings.Load(); SuffixMode() != SuffixOwn || s.PlainNames || !s.PlainOwnNames || touched != 1 {
|
||||
t.Fatalf("own: %q %+v, told %d", SuffixMode(), s, touched)
|
||||
}
|
||||
if got := codex(); !slices.Equal(got, []string{"a/sol=My Sol", "b/sol=Sol · B", "group/auto-sol=Sol · routing group"}) {
|
||||
t.Fatalf("own: %q", got)
|
||||
}
|
||||
if err := SetSuffixMode(SuffixOwn); err != nil || touched != 1 {
|
||||
t.Fatal(err, touched)
|
||||
}
|
||||
|
||||
if err := SetSuffixMode(SuffixOff); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s := settings.Load(); SuffixMode() != SuffixOff || !s.PlainNames || s.PlainOwnNames || touched != 2 {
|
||||
t.Fatalf("off: %q %+v, told %d", SuffixMode(), s, touched)
|
||||
}
|
||||
// b's sol and the group found for it would both read "Sol": they keep theirs
|
||||
if got := codex(); !slices.Equal(got, []string{"a/sol=My Sol", "b/sol=Sol · B", "group/auto-sol=Sol · routing group"}) {
|
||||
t.Fatalf("off: %q", got)
|
||||
}
|
||||
|
||||
// the two-way switch of before: off is plain, on is on again
|
||||
if err := SetPlainNames(false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if SuffixMode() != SuffixOn || touched != 3 {
|
||||
t.Fatalf("plain names off: %q, told %d", SuffixMode(), touched)
|
||||
}
|
||||
// a settings.json of before, plainNames: true, reads as off
|
||||
s := settings.Load()
|
||||
s.PlainNames = true
|
||||
if err := settings.Save(s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if SuffixMode() != SuffixOff {
|
||||
t.Fatalf("plainNames: true reads %q", SuffixMode())
|
||||
}
|
||||
if err := SetSuffixMode("sometimes"); err == nil {
|
||||
t.Fatal("took sometimes")
|
||||
}
|
||||
}
|
||||
@@ -150,6 +150,11 @@ type Settings struct {
|
||||
// (#335) — but for two in one list that would read the same, which keep
|
||||
// it (see provider.Labels).
|
||||
PlainNames bool `json:"plainNames,omitempty"`
|
||||
// PlainOwnNames, with PlainNames off, has those lists name a model the
|
||||
// user gave a name of their own by that name alone, just as they wrote
|
||||
// it (#92: "Opus 5.5", not "Opus 5.5 · Claude Code"), the vendor's names
|
||||
// keeping their provider's after them (see provider.Labels).
|
||||
PlainOwnNames bool `json:"plainOwnNames,omitempty"`
|
||||
// TextSize is how large the window's and the tray panel's pages are
|
||||
// drawn, in percent (one of TextSizes): the webviews' own zoom, as a
|
||||
// browser's, so the text and everything around it grow together.
|
||||
|
||||
+24
-13
@@ -49,9 +49,11 @@ const modelUsage = `usage:
|
||||
and says when there was no name of its own to take away
|
||||
magpie model wires the names your vendors are asked for models by
|
||||
magpie model names the models you named or narrowed
|
||||
magpie model suffix [on|off] whether the agents' lists name each model with its provider
|
||||
magpie model suffix [on|own|off] whether the agents' lists name each model with its provider
|
||||
(or "routing group") after it: on, as by default, "Sol · OpenAI";
|
||||
off, "Sol" alone — but two a list would name the same keep it
|
||||
own, a name you gave a model just as you wrote it, "Opus 5.5",
|
||||
the others as on; off, "Sol" alone — but two a list would name
|
||||
the same keep it
|
||||
|
||||
Each is looked for in this order: this model, then <provider id>/*, then the provider's own
|
||||
list, then models.dev. --reset removes only the first, and says so when a <provider id>/* value
|
||||
@@ -474,30 +476,39 @@ func modelPrices() error {
|
||||
}
|
||||
|
||||
// modelSuffix says whether the agents' lists name models with their
|
||||
// providers' after them, or sets it (#335).
|
||||
// providers' after them, all but the names the user gave (#92), or none
|
||||
// (#335), or sets it.
|
||||
func modelSuffix(args []string) error {
|
||||
if len(args) == 0 {
|
||||
if settings.Load().PlainNames {
|
||||
fmt.Println("off", muted.Render("· agents' lists name a model alone, \"Sol\" · magpie model suffix on"))
|
||||
} else {
|
||||
fmt.Println("on", muted.Render("· agents' lists name a model with its provider, \"Sol · OpenAI\" · magpie model suffix off"))
|
||||
switch provider.SuffixMode() {
|
||||
case provider.SuffixOff:
|
||||
fmt.Println("off", muted.Render("· agents' lists name a model alone, \"Sol\" · magpie model suffix on|own"))
|
||||
case provider.SuffixOwn:
|
||||
fmt.Println("own", muted.Render("· a name you gave a model just as you wrote it, \"Opus 5.5\"; the others \"Sol · OpenAI\" · magpie model suffix on|off"))
|
||||
default:
|
||||
fmt.Println("on", muted.Render("· agents' lists name a model with its provider, \"Sol · OpenAI\" · magpie model suffix own|off"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var plain bool
|
||||
mode := provider.SuffixOn
|
||||
switch strings.ToLower(args[0]) {
|
||||
case "on", "yes", "true":
|
||||
case "own", "mine":
|
||||
mode = provider.SuffixOwn
|
||||
case "off", "no", "false":
|
||||
plain = true
|
||||
mode = provider.SuffixOff
|
||||
default:
|
||||
return fmt.Errorf("magpie model suffix on|off, not %q", args[0])
|
||||
return fmt.Errorf("magpie model suffix on|own|off, not %q", args[0])
|
||||
}
|
||||
if err := provider.SetPlainNames(plain); err != nil {
|
||||
if err := provider.SetSuffixMode(mode); err != nil {
|
||||
return err
|
||||
}
|
||||
if plain {
|
||||
switch mode {
|
||||
case provider.SuffixOff:
|
||||
fmt.Println(green.Render("✓"), "agents' lists name each model alone", muted.Render("· two that would read the same keep their provider's"))
|
||||
} else {
|
||||
case provider.SuffixOwn:
|
||||
fmt.Println(green.Render("✓"), "agents' lists name a model you named just as you wrote it", muted.Render("· the others with their provider"))
|
||||
default:
|
||||
fmt.Println(green.Render("✓"), "agents' lists name each model with its provider again")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
)
|
||||
|
||||
// magpie model suffix off has the agents' lists name models alone, on
|
||||
// names them with their providers again (#335); anything else is refused.
|
||||
// names them with their providers again (#335), own all but the names the
|
||||
// user gave (#92); anything else is refused.
|
||||
func TestModelSuffixCmd(t *testing.T) {
|
||||
groupsHome(t)
|
||||
catalog.Changed = nil // no agent's files are rewritten here
|
||||
@@ -24,6 +26,17 @@ func TestModelSuffixCmd(t *testing.T) {
|
||||
if err := modelCmd([]string{"suffix", "on"}); err != nil || settings.Load().PlainNames {
|
||||
t.Fatal(err, settings.Load().PlainNames)
|
||||
}
|
||||
// own: a name the user gave just as they wrote it, the others with
|
||||
// their provider's (#92)
|
||||
if err := modelCmd([]string{"suffix", "own"}); err != nil || provider.SuffixMode() != provider.SuffixOwn {
|
||||
t.Fatal(err, provider.SuffixMode())
|
||||
}
|
||||
if err := modelCmd([]string{"suffix"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := modelCmd([]string{"suffix", "on"}); err != nil || provider.SuffixMode() != provider.SuffixOn {
|
||||
t.Fatal(err, provider.SuffixMode())
|
||||
}
|
||||
if err := modelCmd([]string{"suffix", "maybe"}); err == nil {
|
||||
t.Fatal("took maybe")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user