Files
magpie/search_cli.go
T
yetone f654bec468 gateway: a model's web search goes to a search API of the user's own when no provider can search (#419 by HwFee: let me set up Tavily, Brave or SearXNG with my key rather than lose the search tool) — with no provider that searches by itself and no provider magpie could search with, the gateway took the client's web search tool away. Tavily, Brave Search, Exa, Firecrawl and SearXNG (an address of one's own, a key only if it asks for one) can now be set up, in an order: the injected web_search tool is then offered, and a search goes to the searcher's model as before, or, with none or when it fails, to each search API in turn, called directly with no model, its results handed back as the same hits and text the searcher path gives, so clients see the same web_search_tool_result blocks. The order is provider searches itself, searcher provider, search APIs, strip. They are kept in providers.json (0600) like provider keys; a backup carries them, their keys only when it carries keys, and one restored without keys keeps the key here; sync does the same, and a backup or sync from a magpie before them leaves the ones here alone. magpie search lists them, magpie search add <api> [key] [url=…] adds or updates one, magpie search rm <api> removes one; Settings has a Web search section (联网搜索) to add and remove them. Prompts, identity lines and headers to model providers are unchanged. TestWebSearchBySearchAPI, TestNoSearchAPINoTool, TestBackupSearchAPIs, TestSyncSearchAPIs and search-api.test.cjs fail without the change; TestSearchAPIsEachAsked checks each vendor against a fake, TestSearchCLI the command. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; search-api, provider-search, click-scroll, old-webkit pass.
2026-10-01 19:44:44 +08:00

90 lines
2.7 KiB
Go

package main
import (
"fmt"
"slices"
"strings"
"github.com/yetone/magpie/internal/provider"
)
// The web search APIs, from the terminal: magpie search (#419).
const searchUsage = `usage:
magpie search the search APIs a model's web search goes to
magpie search add <api> <key> [url=<address>] add one, or give one a new key
magpie search add searxng url=<address> [<key>]
magpie search rm <api> take one away
apis: tavily, brave, exa, firecrawl, searxng
A client's web search (Claude Code's WebSearch, Codex's web_search) is done by the
model's provider when it can search, then by a provider that can (a Claude or ChatGPT
account, OpenAI, Anthropic, OpenRouter…), and then by these, in the order they were
added: what they find goes to the model as it is, and no model is asked to read it.
Without any of them, the client's search is left out.
e.g. magpie search add tavily tvly-…
magpie search add searxng url=https://searx.example.com`
func searchCmd(args []string) error {
if len(args) == 0 {
as := provider.StoredSearchAPIs()
if len(as) == 0 {
fmt.Println(muted.Render(" no search API · magpie search add <tavily|brave|exa|firecrawl|searxng> <key>"))
return nil
}
for i, a := range as {
what := provider.Mask(a.Key)
if a.Key == "" {
what = "no key"
}
if a.URL != "" {
what += " " + a.URL
}
line := fmt.Sprintf(" %d. %s %s", i+1, pad(a.Name(), 14), muted.Render(what))
if !a.Ready() {
line += " " + faint.Render("needs its key")
}
fmt.Println(line)
}
return nil
}
switch args[0] {
case "help", "-h", "--help":
fmt.Println(searchUsage)
return nil
case "add", "set":
if len(args) < 2 {
return fmt.Errorf("magpie search add <api> <key> [url=<address>]")
}
a := provider.SearchAPI{Vendor: strings.ToLower(args[1])}
for _, v := range args[2:] {
if u, ok := strings.CutPrefix(v, "url="); ok {
a.URL = u
} else if a.Key == "" {
a.Key = v
} else {
return fmt.Errorf("magpie search add <api> <key> [url=<address>]")
}
}
if err := provider.SetSearchAPI(a); err != nil {
return err
}
as := provider.StoredSearchAPIs()
i := slices.IndexFunc(as, func(x provider.SearchAPI) bool { return x.Vendor == a.Vendor })
fmt.Println(green.Render("✓"), as[i].Name(), muted.Render(fmt.Sprintf("search API %d of %d", i+1, len(as))))
return nil
case "rm", "remove":
if len(args) != 2 {
return fmt.Errorf("magpie search rm <api>")
}
if err := provider.RemoveSearchAPI(strings.ToLower(args[1])); err != nil {
return err
}
fmt.Println(green.Render("✓"), "removed")
return nil
}
return fmt.Errorf("magpie search [add|rm] (magpie search help)")
}