From af01a8d6474a4ccab365abd9b2bfd92c53c0c42f Mon Sep 17 00:00:00 2001 From: hazukisendai <251835060+hazukisendai@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:48:47 +0800 Subject: [PATCH 1/2] gui: searchers show provider model names Settings web search reads the names given in Providers from provider.Served, the same source as image recognition, so Automatic and a named searcher show those names instead of bare model IDs. Tests cover both. --- internal/gateway/search_api.go | 5 ++++- internal/gateway/search_choice_test.go | 21 +++++++++++++++++++++ internal/gui/api.go | 20 +++++++++++++------- internal/gui/assets/app.js | 4 ++-- internal/gui/tests/searcher-pick.test.cjs | 20 ++++++++++---------- 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/internal/gateway/search_api.go b/internal/gateway/search_api.go index 9968c8a28..31d4112f0 100644 --- a/internal/gateway/search_api.go +++ b/internal/gateway/search_api.go @@ -58,6 +58,9 @@ func searcherName(p provider.Provider, model string) string { if model == "" { return p.Name } + if e, ok := provider.ServedEntryOf(p.ID + "/" + model); ok && e.Name != "" { + model = e.Name + } return p.Name + " · " + model } @@ -65,7 +68,7 @@ func searcherName(p provider.Provider, model string) string { // when Settings names none, "" when none can. func AutoSearcher() string { if p, m, ok := autoSearcher(); ok { - return p.Name + " · " + m + return searcherName(p, m) } return "" } diff --git a/internal/gateway/search_choice_test.go b/internal/gateway/search_choice_test.go index e6288a9c8..5b5ebef64 100644 --- a/internal/gateway/search_choice_test.go +++ b/internal/gateway/search_choice_test.go @@ -94,6 +94,15 @@ func TestSearcherChosen(t *testing.T) { if rs := RelaysSaidToSearch(); len(rs) != 1 || rs[0].ID != "relay" { t.Errorf("relays = %v", rs) } + if err := provider.SetModelName("ant/claude-haiku-4-5", "Named Haiku"); err != nil { + t.Fatal(err) + } + if got := AutoSearcher(); got != "Anthropic · Named Haiku" { + t.Errorf("auto = %q", got) + } + if err := provider.SetModelName("ant/claude-haiku-4-5", ""); err != nil { + t.Fatal(err) + } // a provider, with its small model choose("oai") @@ -104,6 +113,18 @@ func TestSearcherChosen(t *testing.T) { // a provider and a model of it choose("oai/gpt-5.5") want("oai", "gpt-5.5", "") + if err := provider.SetModelName("oai/gpt-5.5", "GPT Five Five"); err != nil { + t.Fatal(err) + } + if got := Searcher(); got != "OpenAI · GPT Five Five" { + t.Errorf("Searcher() = %q", got) + } + if err := provider.SetModelName("oai/gpt-5.5", ""); err != nil { + t.Fatal(err) + } + if got := Searcher(); got != "OpenAI · gpt-5.5" { + t.Errorf("Searcher() = %q", got) + } // a model it no longer lists: its small model choose("oai/gpt-4") want("oai", "gpt-5-mini", "") diff --git a/internal/gui/api.go b/internal/gui/api.go index 43e917081..085179dc4 100644 --- a/internal/gui/api.go +++ b/internal/gui/api.go @@ -350,11 +350,12 @@ type searchAPIJSON struct { // searchChoiceJSON is a provider that can search for a model that can't, // with the model it searches with when none is named, and its models. type searchChoiceJSON struct { - ID string `json:"id"` - Name string `json:"name"` - Icon string `json:"icon,omitempty"` - Small string `json:"small"` - Models []modelRef `json:"models"` + ID string `json:"id"` + Name string `json:"name"` + Icon string `json:"icon,omitempty"` + Small string `json:"small"` + SmallName string `json:"smallName,omitempty"` + Models []modelRef `json:"models"` // Service is a Kimi Code plan, which searches by its search service: // named by itself, with no model Service bool `json:"service,omitempty"` @@ -368,6 +369,10 @@ type searchVendorJSON struct { } func searchState(s *settingsJSON) { + names := map[string]string{} + for _, e := range provider.Served() { + names[e.ID] = cmp.Or(e.Name, e.Model) + } s.SearchAPIs, s.SearchVendors = []searchAPIJSON{}, []searchVendorJSON{} for _, a := range provider.StoredSearchAPIs() { j := searchAPIJSON{Vendor: a.Vendor, Name: a.Name(), URL: a.URL, Ready: a.Ready()} @@ -384,9 +389,10 @@ func searchState(s *settingsJSON) { s.SearchChoices = []searchChoiceJSON{} for _, c := range gateway.Searchers() { p := c.Provider - j := searchChoiceJSON{ID: p.ID, Name: p.Name, Icon: p.Icon, Small: c.Small, Models: []modelRef{}, Service: c.Service} + j := searchChoiceJSON{ID: p.ID, Name: p.Name, Icon: p.Icon, Small: c.Small, SmallName: names[p.ID+"/"+c.Small], Models: []modelRef{}, Service: c.Service} for _, m := range c.Models { - j.Models = append(j.Models, modelRef{ID: p.ID + "/" + m.ID, Name: cmp.Or(m.Name, m.ID), Provider: p.ID, PName: p.Name, Icon: p.Icon}) + id := p.ID + "/" + m.ID + j.Models = append(j.Models, modelRef{ID: id, Name: cmp.Or(names[id], m.Name, m.ID), Provider: p.ID, PName: p.Name, Icon: p.Icon}) } s.SearchChoices = append(s.SearchChoices, j) } diff --git a/internal/gui/assets/app.js b/internal/gui/assets/app.js index ed2a37b6d..a2c07bca7 100644 --- a/internal/gui/assets/app.js +++ b/internal/gui/assets/app.js @@ -14622,7 +14622,7 @@ function renderSearcher(s, keep, box) { const c = choices.find((x) => x.id === pid); if (!c) return id; if (c.service) return `${c.name} · ${t("its web search")}`; - if (!rest.length) return `${c.name} · ${c.small}`; + if (!rest.length) return `${c.name} · ${c.smallName || c.small}`; const m = c.models.find((x) => x.id === id); return `${m ? m.name : rest.join("/")} · ${c.name}`; }; @@ -14654,7 +14654,7 @@ function renderSearcher(s, keep, box) { options.push({ value: c.id, label: t("its web search"), note: c.name, icon: c.icon, group: c.name }); continue; } - options.push({ value: c.id, label: t("{model}, its small model", { model: c.small }), note: c.name, icon: c.icon, group: c.name }); + options.push({ value: c.id, label: t("{model}, its small model", { model: c.smallName || c.small }), note: c.name, icon: c.icon, group: c.name }); for (const m of c.models) options.push({ value: m.id, label: m.name || m.id, note: c.name, icon: c.icon, group: c.name, ref: m.id }); } b.onclick = (ev) => openPicker({ id: "", name: "", fields: [] }, { key: "searcher", label: "model", value: v, options, diff --git a/internal/gui/tests/searcher-pick.test.cjs b/internal/gui/tests/searcher-pick.test.cjs index a754a668e..33daf4197 100644 --- a/internal/gui/tests/searcher-pick.test.cjs +++ b/internal/gui/tests/searcher-pick.test.cjs @@ -21,17 +21,17 @@ const choices = [ { id: "claude", name: "Claude", icon: "claude", small: "claude-haiku-4-5", models: [ { id: "claude/claude-haiku-4-5", name: "Claude Haiku 4.5", provider: "claude", providerName: "Claude" }, { id: "claude/claude-opus-4-5", name: "Claude Opus 4.5", provider: "claude", providerName: "Claude" }] }, - { id: "openai", name: "OpenAI", icon: "openai", small: "gpt-5-mini", models: [ + { id: "openai", name: "OpenAI", icon: "openai", small: "gpt-5-mini", smallName: "GPT-5 Mini Named", models: [ { id: "openai/gpt-5-mini", name: "GPT-5 mini", provider: "openai", providerName: "OpenAI" }, - { id: "openai/gpt-5.5", name: "GPT-5.5", provider: "openai", providerName: "OpenAI" }] }, + { id: "openai/gpt-5.5", name: "GPT Five Five", provider: "openai", providerName: "OpenAI" }] }, { id: "relay", name: "MyRelay", icon: "generic", small: "claude-haiku-4-5", models: [ { id: "relay/claude-haiku-4-5", name: "Claude Haiku 4.5", provider: "relay", providerName: "MyRelay" }] }, { id: "kimi", name: "Kimi Code", icon: "kimi", small: "", models: [], service: true }, ]; const words = { - en: { name: "Searches for other models", auto: "Automatic", small: "gpt-5-mini, its small model", unused: "isn't used: it is turned off", relays: "Relays said to search (MyRelay) are never picked automatically: they would spend the relay's quota on other models' searches; if one refuses magpie's own request, magpie falls back", + en: { name: "Searches for other models", auto: "Automatic", small: "GPT-5 Mini Named, its small model", unused: "isn't used: it is turned off", relays: "Relays said to search (MyRelay) are never picked automatically: they would spend the relay's quota on other models' searches; if one refuses magpie's own request, magpie falls back", own: "A Kimi Code plan (Kimi Code) searches for its own models first, with its web search; for other models only when named here", web: "its web search" }, - zh: { name: "代搜供应商", auto: "自动", small: "gpt-5-mini(它的小模型)", unused: "没有用 OpenAI · gpt-5-mini:它已关闭", relays: "标为能搜索的中转站(MyRelay)不会被自动选择:它们会为别的模型的搜索花掉中转站的额度;如果它拒绝 magpie 自己发出的请求,magpie 会退回其他选择", + zh: { name: "代搜供应商", auto: "自动", small: "GPT-5 Mini Named(它的小模型)", unused: "没有用 OpenAI · gpt-5-mini:它已关闭", relays: "标为能搜索的中转站(MyRelay)不会被自动选择:它们会为别的模型的搜索花掉中转站的额度;如果它拒绝 magpie 自己发出的请求,magpie 会退回其他选择", own: "Kimi Code 套餐(Kimi Code)的模型先用套餐自带的联网搜索;别的模型只有在这里选了它才用", web: "它自带的联网搜索" }, }; @@ -100,18 +100,18 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", await page.locator("#pop").waitFor({ state: "visible" }); const items = await page.locator("#list li:not(.group)").allInnerTexts(); assert(items[0].includes(w.auto), "Automatic comes first"); - assert(items.some((x) => x.includes("GPT-5.5")) && items.some((x) => x.includes("Claude Opus 4.5"))); + assert(items.some((x) => x.includes("GPT Five Five")) && items.some((x) => x.includes("Claude Opus 4.5"))); assert(items.some((x) => x.includes("MyRelay")), "a relay said to search can be named"); - await click(page.locator("#list li:not(.group)", { hasText: "GPT-5.5" })); - await page.waitForFunction(() => document.querySelector("#searchList button.searcher-pick")?.innerText.includes("GPT-5.5")); + await click(page.locator("#list li:not(.group)", { hasText: "GPT Five Five" })); + await page.waitForFunction(() => document.querySelector("#searchList button.searcher-pick")?.innerText.includes("GPT Five Five")); assert.equal(posted.at(-1).searcher, "openai/gpt-5.5"); - assert.equal(await row.locator("button.searcher-pick").innerText(), "GPT-5.5 · OpenAI"); + assert.equal(await row.locator("button.searcher-pick").innerText(), "GPT Five Five · OpenAI"); // a provider, by its small model await click(row.locator("button.searcher-pick")); await page.locator("#pop").waitFor({ state: "visible" }); await click(page.locator("#list li:not(.group)", { hasText: w.small })); - await page.waitForFunction(() => document.querySelector("#searchList button.searcher-pick")?.innerText === "OpenAI · gpt-5-mini"); + await page.waitForFunction(() => document.querySelector("#searchList button.searcher-pick")?.innerText === "OpenAI · GPT-5 Mini Named"); assert.equal(posted.at(-1).searcher, "openai"); // a Kimi Code plan, by its web search: no model of it is offered @@ -130,7 +130,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", await click(row.locator("button.searcher-pick")); await page.locator("#pop").waitFor({ state: "visible" }); await click(page.locator("#list li:not(.group)", { hasText: w.small })); - await page.waitForFunction(() => document.querySelector("#searchList button.searcher-pick")?.innerText === "OpenAI · gpt-5-mini"); + await page.waitForFunction(() => document.querySelector("#searchList button.searcher-pick")?.innerText === "OpenAI · GPT-5 Mini Named"); // another setting saved keeps the pick const n = posted.length; From 5c6d9a09e36d413bc3dad1f93b233183a7ac052a Mon Sep 17 00:00:00 2001 From: yetone Date: Sun, 4 Oct 2026 04:03:52 +0800 Subject: [PATCH 2/2] =?UTF-8?q?gui:=20searcher-pick's=20zh=20case=20names?= =?UTF-8?q?=20the=20searcher=20by=20its=20model=20name=20too,=20as=20the?= =?UTF-8?q?=20en=20one=20does=20=E2=80=94=20the=20turned-off=20note=20read?= =?UTF-8?q?s=20=E6=B2=A1=E6=9C=89=E7=94=A8=20OpenAI=20=C2=B7=20GPT-5=20Min?= =?UTF-8?q?i=20Named=20now=20that=20the=20small=20model=20goes=20by=20its?= =?UTF-8?q?=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gui/tests/searcher-pick.test.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/gui/tests/searcher-pick.test.cjs b/internal/gui/tests/searcher-pick.test.cjs index 33daf4197..432107c0e 100644 --- a/internal/gui/tests/searcher-pick.test.cjs +++ b/internal/gui/tests/searcher-pick.test.cjs @@ -31,7 +31,7 @@ const choices = [ const words = { en: { name: "Searches for other models", auto: "Automatic", small: "GPT-5 Mini Named, its small model", unused: "isn't used: it is turned off", relays: "Relays said to search (MyRelay) are never picked automatically: they would spend the relay's quota on other models' searches; if one refuses magpie's own request, magpie falls back", own: "A Kimi Code plan (Kimi Code) searches for its own models first, with its web search; for other models only when named here", web: "its web search" }, - zh: { name: "代搜供应商", auto: "自动", small: "GPT-5 Mini Named(它的小模型)", unused: "没有用 OpenAI · gpt-5-mini:它已关闭", relays: "标为能搜索的中转站(MyRelay)不会被自动选择:它们会为别的模型的搜索花掉中转站的额度;如果它拒绝 magpie 自己发出的请求,magpie 会退回其他选择", + zh: { name: "代搜供应商", auto: "自动", small: "GPT-5 Mini Named(它的小模型)", unused: "没有用 OpenAI · GPT-5 Mini Named:它已关闭", relays: "标为能搜索的中转站(MyRelay)不会被自动选择:它们会为别的模型的搜索花掉中转站的额度;如果它拒绝 magpie 自己发出的请求,magpie 会退回其他选择", own: "Kimi Code 套餐(Kimi Code)的模型先用套餐自带的联网搜索;别的模型只有在这里选了它才用", web: "它自带的联网搜索" }, };