Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion internal/gateway/search_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,17 @@ 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
}

// AutoSearcher names the provider and model magpie picks to search with
// 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 ""
}
Expand Down
21 changes: 21 additions & 0 deletions internal/gateway/search_choice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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", "")
Expand Down
20 changes: 13 additions & 7 deletions internal/gui/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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()}
Expand All @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/gui/assets/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
};
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 10 additions & 10 deletions internal/gui/tests/searcher-pick.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 Named:它已关闭", relays: "标为能搜索的中转站(MyRelay)不会被自动选择:它们会为别的模型的搜索花掉中转站的额度;如果它拒绝 magpie 自己发出的请求,magpie 会退回其他选择",
own: "Kimi Code 套餐(Kimi Code)的模型先用套餐自带的联网搜索;别的模型只有在这里选了它才用", web: "它自带的联网搜索" },
};

Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
Loading