fix(tools): improve web search provider fallback (#2629)

- centralize web search provider readiness and resolution logic
- fall back when the configured provider is unavailable or invalid
- allow native-search-capable models to use built-in search without the client tool
- simplify the tools page and add direct access to web search settings
- add backend, agent, and integration tests for the new selection behavior
This commit is contained in:
wenjie
2026-04-23 15:39:16 +08:00
committed by GitHub
parent 451db2f5d8
commit cac4f21746
16 changed files with 633 additions and 222 deletions
+37 -78
View File
@@ -261,6 +261,8 @@ func buildToolSupport(cfg *config.Config) []toolSupportItem {
status, reasonCode = resolveDiscoveryToolSupport(cfg, cfg.Tools.MCP.Discovery.UseRegex)
case "tool_search_tool_bm25":
status, reasonCode = resolveDiscoveryToolSupport(cfg, cfg.Tools.MCP.Discovery.UseBM25)
case "web_search":
status, reasonCode = resolveWebSearchToolSupport(cfg)
case "i2c", "spi":
status, reasonCode = resolveHardwareToolSupport(cfg.Tools.IsToolEnabled(entry.ConfigKey))
default:
@@ -304,6 +306,13 @@ func resolveDiscoveryToolSupport(cfg *config.Config, methodEnabled bool) (string
return "enabled", ""
}
func resolveWebSearchToolSupport(cfg *config.Config) (string, string) {
if !cfg.Tools.IsToolEnabled("web") {
return "disabled", ""
}
return "enabled", ""
}
func applyToolState(cfg *config.Config, toolName string, enabled bool) error {
switch toolName {
case "read_file":
@@ -507,6 +516,7 @@ func normalizeWebSearchAPIKeys(apiKeys []string, apiKey string) ([]string, bool)
}
func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
opts := picotools.WebSearchToolOptionsFromConfig(cfg)
current := resolveCurrentWebSearchProvider(cfg)
settings := map[string]webSearchProviderConfig{
"sogou": {
@@ -563,59 +573,53 @@ func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
{
ID: "sogou",
Label: "Sogou",
Configured: cfg.Tools.Web.Sogou.Enabled,
Configured: picotools.WebSearchProviderReady(opts, "sogou"),
Current: current == "sogou",
},
{
ID: "duckduckgo",
Label: "DuckDuckGo",
Configured: cfg.Tools.Web.DuckDuckGo.Enabled,
Configured: picotools.WebSearchProviderReady(opts, "duckduckgo"),
Current: current == "duckduckgo",
},
{
ID: "brave",
Label: "Brave Search",
Configured: cfg.Tools.Web.Brave.Enabled &&
len(cfg.Tools.Web.Brave.APIKeys.Values()) > 0,
ID: "brave",
Label: "Brave Search",
Configured: picotools.WebSearchProviderReady(opts, "brave"),
Current: current == "brave",
RequiresAuth: true,
},
{
ID: "tavily",
Label: "Tavily",
Configured: cfg.Tools.Web.Tavily.Enabled &&
len(cfg.Tools.Web.Tavily.APIKeys.Values()) > 0,
ID: "tavily",
Label: "Tavily",
Configured: picotools.WebSearchProviderReady(opts, "tavily"),
Current: current == "tavily",
RequiresAuth: true,
},
{
ID: "perplexity",
Label: "Perplexity",
Configured: cfg.Tools.Web.Perplexity.Enabled &&
len(cfg.Tools.Web.Perplexity.APIKeys.Values()) > 0,
ID: "perplexity",
Label: "Perplexity",
Configured: picotools.WebSearchProviderReady(opts, "perplexity"),
Current: current == "perplexity",
RequiresAuth: true,
},
{
ID: "searxng",
Label: "SearXNG",
Configured: cfg.Tools.Web.SearXNG.Enabled &&
strings.TrimSpace(cfg.Tools.Web.SearXNG.BaseURL) != "",
Current: current == "searxng",
ID: "searxng",
Label: "SearXNG",
Configured: picotools.WebSearchProviderReady(opts, "searxng"),
Current: current == "searxng",
},
{
ID: "glm_search",
Label: "GLM Search",
Configured: cfg.Tools.Web.GLMSearch.Enabled &&
cfg.Tools.Web.GLMSearch.APIKey.String() != "",
ID: "glm_search",
Label: "GLM Search",
Configured: picotools.WebSearchProviderReady(opts, "glm_search"),
Current: current == "glm_search",
RequiresAuth: true,
},
{
ID: "baidu_search",
Label: "Baidu Search",
Configured: cfg.Tools.Web.BaiduSearch.Enabled &&
cfg.Tools.Web.BaiduSearch.APIKey.String() != "",
ID: "baidu_search",
Label: "Baidu Search",
Configured: picotools.WebSearchProviderReady(opts, "baidu_search"),
Current: current == "baidu_search",
RequiresAuth: true,
},
@@ -637,57 +641,12 @@ func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
}
func resolveCurrentWebSearchProvider(cfg *config.Config) string {
selected := normalizeWebSearchProvider(cfg.Tools.Web.Provider)
if selected != "" && selected != "auto" && webSearchProviderConfigured(cfg, selected) {
return selected
if cfg == nil || !cfg.Tools.IsToolEnabled("web") {
return ""
}
for _, name := range []string{"perplexity", "brave", "searxng", "tavily"} {
if webSearchProviderConfigured(cfg, name) {
return name
}
}
if webSearchProviderConfigured(cfg, "sogou") && webSearchProviderConfigured(cfg, "duckduckgo") {
if picotools.GetPreferredWebSearchLanguage() == "en" {
return "duckduckgo"
}
return "sogou"
}
if webSearchProviderConfigured(cfg, "sogou") {
return "sogou"
}
if webSearchProviderConfigured(cfg, "duckduckgo") {
return "duckduckgo"
}
for _, name := range []string{"baidu_search", "glm_search"} {
if webSearchProviderConfigured(cfg, name) {
return name
}
}
return ""
}
func webSearchProviderConfigured(cfg *config.Config, name string) bool {
switch name {
case "sogou":
return cfg.Tools.Web.Sogou.Enabled
case "duckduckgo":
return cfg.Tools.Web.DuckDuckGo.Enabled
case "brave":
return cfg.Tools.Web.Brave.Enabled && len(cfg.Tools.Web.Brave.APIKeys.Values()) > 0
case "tavily":
return cfg.Tools.Web.Tavily.Enabled && len(cfg.Tools.Web.Tavily.APIKeys.Values()) > 0
case "perplexity":
return cfg.Tools.Web.Perplexity.Enabled && len(cfg.Tools.Web.Perplexity.APIKeys.Values()) > 0
case "searxng":
return cfg.Tools.Web.SearXNG.Enabled && strings.TrimSpace(cfg.Tools.Web.SearXNG.BaseURL) != ""
case "glm_search":
return cfg.Tools.Web.GLMSearch.Enabled && cfg.Tools.Web.GLMSearch.APIKey.String() != ""
case "baidu_search":
return cfg.Tools.Web.BaiduSearch.Enabled && cfg.Tools.Web.BaiduSearch.APIKey.String() != ""
default:
return false
selected, err := picotools.ResolveWebSearchProviderName(picotools.WebSearchToolOptionsFromConfig(cfg), "")
if err != nil {
return ""
}
return selected
}
+143
View File
@@ -198,6 +198,66 @@ func TestHandleUpdateToolState(t *testing.T) {
}
}
func TestHandleListTools_ReportsWebSearchEnabledWhenToolIsOn(t *testing.T) {
tests := []struct {
name string
preferNative bool
}{
{name: "without prefer_native", preferNative: false},
{name: "with prefer_native", preferNative: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.Tools.Web.PreferNative = tt.preferNative
cfg.Tools.Web.Provider = "brave"
cfg.Tools.Web.Sogou.Enabled = false
cfg.Tools.Web.DuckDuckGo.Enabled = false
cfg.Tools.Web.Brave.Enabled = true
cfg.Tools.Web.Brave.SetAPIKeys(nil)
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/tools", nil)
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var resp toolSupportResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
for _, tool := range resp.Tools {
if tool.Name != "web_search" {
continue
}
if tool.Status != "enabled" || tool.ReasonCode != "" {
t.Fatalf("web_search = %#v, want enabled with no reason code", tool)
}
return
}
t.Fatal("expected web_search in response")
})
}
}
func TestHandleGetWebSearchConfig(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
@@ -206,6 +266,7 @@ func TestHandleGetWebSearchConfig(t *testing.T) {
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.Tools.Web.PreferNative = false
cfg.Tools.Web.Provider = "sogou"
cfg.Tools.Web.Sogou.Enabled = true
cfg.Tools.Web.Sogou.MaxResults = 6
@@ -242,6 +303,48 @@ func TestHandleGetWebSearchConfig(t *testing.T) {
}
}
func TestHandleGetWebSearchConfig_DoesNotExposeNativeAsCurrentService(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.Tools.Web.PreferNative = true
cfg.Tools.Web.Provider = "brave"
cfg.Tools.Web.Sogou.Enabled = false
cfg.Tools.Web.DuckDuckGo.Enabled = false
cfg.Tools.Web.Brave.Enabled = true
cfg.Tools.Web.Brave.SetAPIKeys(nil)
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/tools/web-search-config", nil)
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var resp webSearchConfigResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if !resp.PreferNative {
t.Fatal("prefer_native should remain true in response")
}
if resp.CurrentService != "" {
t.Fatalf("current_service = %q, want empty when no external provider is ready", resp.CurrentService)
}
}
func TestHandleUpdateWebSearchConfig(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
@@ -393,6 +496,27 @@ func TestResolveCurrentWebSearchProvider_PrefersConfiguredProvidersBeforeSogou(t
}
}
func TestResolveCurrentWebSearchProvider_FallsBackWhenExplicitProviderUnavailable(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Tools.Web.Provider = "brave"
cfg.Tools.Web.Brave.Enabled = true
cfg.Tools.Web.Sogou.Enabled = true
if got := resolveCurrentWebSearchProvider(cfg); got != "sogou" {
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got)
}
}
func TestResolveCurrentWebSearchProvider_FallsBackWhenProviderIsUnknown(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Tools.Web.Provider = "totally_unknown"
cfg.Tools.Web.Sogou.Enabled = true
if got := resolveCurrentWebSearchProvider(cfg); got != "sogou" {
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got)
}
}
func TestResolveCurrentWebSearchProvider_UsesPreferredLanguageForSogouAndDuckDuckGo(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Tools.Web.Provider = "auto"
@@ -413,3 +537,22 @@ func TestResolveCurrentWebSearchProvider_UsesPreferredLanguageForSogouAndDuckDuc
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got)
}
}
func TestResolveCurrentWebSearchProvider_IgnoresPreferNativeInConfigView(t *testing.T) {
cfg := config.DefaultConfig()
cfg.ModelList = []*config.ModelConfig{{
ModelName: "custom-default",
Model: "openai/gpt-4o",
APIKeys: config.SimpleSecureStrings("sk-default"),
}}
cfg.Agents.Defaults.ModelName = "custom-default"
cfg.Tools.Web.PreferNative = true
cfg.Tools.Web.Provider = "brave"
cfg.Tools.Web.Sogou.Enabled = false
cfg.Tools.Web.DuckDuckGo.Enabled = false
cfg.Tools.Web.Brave.Enabled = true
if got := resolveCurrentWebSearchProvider(cfg); got != "" {
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want empty when only native search would be available", got)
}
}
@@ -1,7 +1,8 @@
import { IconSearch } from "@tabler/icons-react"
import { IconSearch, IconSettings } from "@tabler/icons-react"
import { useTranslation } from "react-i18next"
import type { ToolSupportItem } from "@/api/tools"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import {
@@ -29,6 +30,7 @@ interface ToolLibraryTabProps {
pendingToolName: string | null
onSearchQueryChange: (value: string) => void
onStatusFilterChange: (value: ToolStatusFilter) => void
onOpenWebSearchSettings: () => void
onToggleTool: (name: string, enabled: boolean) => void
}
@@ -43,6 +45,7 @@ export function ToolLibraryTab({
pendingToolName,
onSearchQueryChange,
onStatusFilterChange,
onOpenWebSearchSettings,
onToggleTool,
}: ToolLibraryTabProps) {
const { t } = useTranslation()
@@ -131,6 +134,7 @@ export function ToolLibraryTab({
key={tool.name}
tool={tool}
isPending={pendingToolName === tool.name}
onOpenWebSearchSettings={onOpenWebSearchSettings}
onToggleTool={onToggleTool}
/>
))}
@@ -146,10 +150,12 @@ export function ToolLibraryTab({
function ToolCard({
tool,
isPending,
onOpenWebSearchSettings,
onToggleTool,
}: {
tool: ToolSupportItem
isPending: boolean
onOpenWebSearchSettings: () => void
onToggleTool: (name: string, enabled: boolean) => void
}) {
const { t } = useTranslation()
@@ -157,8 +163,10 @@ function ToolCard({
? t(`pages.agent.tools.reasons.${tool.reason_code}`)
: ""
const isEnabled = tool.status === "enabled"
const isToggledOn = tool.status !== "disabled"
const isDisabled = tool.status === "disabled"
const isBlocked = tool.status === "blocked"
const isWebSearchTool = tool.name === "web_search"
return (
<Card
@@ -170,23 +178,40 @@ function ToolCard({
isDisabled && "opacity-[0.80] hover:opacity-100",
)}
>
<CardContent className="flex h-full flex-col p-6">
<div className="mb-3 flex items-start justify-between gap-4">
<CardContent className="flex h-full flex-col px-5 py-1">
<div className="mb-0.5 flex items-start justify-between gap-4">
<div className="flex min-w-0 flex-1 items-center gap-3">
<h4 className="text-foreground/90 min-w-0 break-all font-mono text-sm font-semibold tracking-tight">
<h4 className="text-foreground/90 min-w-0 font-mono text-sm font-semibold tracking-tight break-all">
{tool.name}
</h4>
<ToolStatusBadge status={tool.status} />
</div>
<Switch
checked={isEnabled}
disabled={isPending}
onCheckedChange={(checked) => onToggleTool(tool.name, checked)}
className={cn(
"shrink-0",
isEnabled && "shadow-xs ring-1 ring-emerald-500/20",
<div className="flex h-8 shrink-0 items-center gap-2">
{isWebSearchTool && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={onOpenWebSearchSettings}
className="text-muted-foreground hover:text-foreground hover:bg-muted/60 size-8 rounded-lg"
aria-label={t(
"pages.agent.tools.web_search.open_settings",
"Open Settings",
)}
>
<IconSettings className="size-4" />
</Button>
)}
/>
<Switch
checked={isToggledOn}
disabled={isPending}
onCheckedChange={(checked) => onToggleTool(tool.name, checked)}
className={cn(
"shrink-0",
isEnabled && "shadow-xs ring-1 ring-emerald-500/20",
)}
/>
</div>
</div>
<p className="text-muted-foreground/80 flex-1 text-[14px] leading-relaxed">
@@ -1,3 +1,4 @@
import { useLayoutEffect, useRef } from "react"
import { useTranslation } from "react-i18next"
import { PageHeader } from "@/components/page-header"
@@ -8,9 +9,9 @@ import { WebSearchTab } from "./web-search-tab"
export function ToolsPage() {
const { t } = useTranslation()
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
const {
activeTab,
currentProviderLabel,
expandedProvider,
groupedTools,
pendingToolName,
@@ -34,12 +35,19 @@ export function ToolsPage() {
updateWebSearchDraft,
} = useToolsPage()
useLayoutEffect(() => {
scrollContainerRef.current?.scrollTo({ top: 0 })
}, [activeTab])
return (
<div className="bg-background flex h-full flex-col">
<PageHeader title={t("navigation.tools", "Tools")} />
<ToolsTabs activeTab={activeTab} onChange={setActiveTab} />
<div className="flex-1 overflow-auto px-6 py-6 pb-20">
<div
ref={scrollContainerRef}
className="flex-1 overflow-auto px-6 py-6 pb-20"
>
<div className="mx-auto w-full max-w-6xl">
{activeTab === "library" ? (
<ToolLibraryTab
@@ -53,12 +61,12 @@ export function ToolsPage() {
pendingToolName={pendingToolName}
onSearchQueryChange={setSearchQuery}
onStatusFilterChange={setStatusFilter}
onOpenWebSearchSettings={() => setActiveTab("web-search")}
onToggleTool={toggleTool}
/>
) : (
<WebSearchTab
draft={webSearchDraft}
currentProviderLabel={currentProviderLabel}
providerLabelMap={providerLabelMap}
expandedProvider={expandedProvider}
isLoading={isWebSearchLoading}
@@ -132,11 +132,6 @@ export function useToolsPage() {
return new Map(providers.map((provider) => [provider.id, provider.label]))
}, [webSearchDraft])
const currentProviderLabel = webSearchDraft?.current_service
? (providerLabelMap.get(webSearchDraft.current_service) ??
webSearchDraft.current_service)
: t("pages.agent.tools.web_search.none", "None")
const pendingToolName = toggleToolMutation.isPending
? (toggleToolMutation.variables?.name ?? null)
: null
@@ -168,7 +163,6 @@ export function useToolsPage() {
return {
activeTab,
currentProviderLabel,
expandedProvider,
groupedTools: groupedTools.groupedTools,
pendingToolName,
@@ -36,7 +36,7 @@ export function WebSearchGeneralSettings({
label={t("pages.agent.tools.web_search.provider", "Primary Provider")}
description={t(
"pages.agent.tools.web_search.provider_description",
"Select the default search engine that agents will fallback to.",
"Select the default provider to use when the web search tool handles a request.",
)}
>
<Select
@@ -95,7 +95,7 @@ export function WebSearchGeneralSettings({
)}
description={t(
"pages.agent.tools.web_search.prefer_native_hint",
"Bypass external providers if the agent inherently supports web search tools.",
"When enabled, the model may use its built-in search capability instead of the configured provider list.",
)}
>
<Switch
@@ -10,7 +10,6 @@ import { WebSearchProviderSettings } from "./web-search-provider-settings"
interface WebSearchTabProps {
draft: WebSearchConfigResponse | null
currentProviderLabel: string
providerLabelMap: Map<string, string>
expandedProvider: string | null
isLoading: boolean
@@ -23,7 +22,6 @@ interface WebSearchTabProps {
export function WebSearchTab({
draft,
currentProviderLabel,
providerLabelMap,
expandedProvider,
isLoading,
@@ -52,21 +50,16 @@ export function WebSearchTab({
<>
<div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
<div className="max-w-xl space-y-3">
<div className="flex items-center gap-3">
<h1 className="text-foreground/90 text-2xl font-semibold tracking-tight">
{t(
"pages.agent.tools.web_search.title",
"Web Search Configuration",
)}
</h1>
<div className="rounded-full bg-emerald-500/10 px-2.5 py-0.5 text-[11px] font-semibold tracking-wide text-emerald-600 uppercase dark:text-emerald-400">
{currentProviderLabel}
</div>
</div>
<h1 className="text-foreground/90 text-2xl font-semibold tracking-tight">
{t(
"pages.agent.tools.web_search.title",
"Web Search Configuration",
)}
</h1>
<p className="text-muted-foreground/80 text-[14px] leading-relaxed">
{t(
"pages.agent.tools.web_search.description",
"Provide web search capability for agents to find the latest real-world info. Automatically routes to the optimal active provider.",
"Configure how the web search tool behaves by default, including whether the model may use its built-in search capability.",
)}
</p>
</div>
+5 -5
View File
@@ -555,16 +555,15 @@
"providers_config": "Integrations",
"load_error": "Failed to load web search configuration.",
"save": "Save Changes",
"open_settings": "Open Settings",
"save_success": "Settings saved successfully.",
"save_error": "Failed to save settings.",
"current_active": "Active: ",
"current_service": "Current Service",
"provider": "Primary Provider",
"provider_description": "Select the default search engine that agents will fallback to.",
"provider_description": "Select the default provider to use when the web search tool handles a request.",
"proxy": "HTTPS Proxy",
"proxy_description": "Optional global HTTP/S proxy for underlying web requests.",
"prefer_native": "Prefer Native Search",
"prefer_native_hint": "Bypass external providers if the agent inherently supports web search tools.",
"prefer_native_hint": "When enabled, the model may use its built-in search capability instead of the configured provider list.",
"provider_hint": "Enable this provider and fill any required connection settings.",
"max_results": "Max Results",
"base_url": "Base URL",
@@ -592,7 +591,8 @@
"requires_linux": "This tool only works on Linux hosts with the required device files exposed.",
"requires_skills": "Enable `tools.skills` before this skill-registry tool can be used.",
"requires_subagent": "Enable `tools.subagent` before the spawn tool can delegate work.",
"requires_mcp_discovery": "Enable `tools.mcp.discovery` before MCP discovery tools become available."
"requires_mcp_discovery": "Enable `tools.mcp.discovery` before MCP discovery tools become available.",
"requires_web_search_provider": "Configure at least one ready external web-search provider."
}
}
},
+5 -5
View File
@@ -555,16 +555,15 @@
"providers_config": "集成",
"load_error": "加载 Web Search 配置失败。",
"save": "保存更改",
"open_settings": "打开设置",
"save_success": "设置保存成功。",
"save_error": "保存设置失败。",
"current_active": "活动: ",
"current_service": "当前服务",
"provider": "首选服务",
"provider_description": "选择智能体在默认情况下进行网络搜索的回退引擎。",
"provider_description": "选择在由 Web Search 工具处理请求时默认使用的搜索引擎。",
"proxy": "HTTPS 代理",
"proxy_description": "用于底层网页请求的可选全局代理配置。",
"prefer_native": "优先使用模型搜索",
"prefer_native_hint": "如果当前模型本身支持联网功能,则直接使用模型自带的搜索能力",
"prefer_native_hint": "启用后,模型在支持时可以直接使用自身搜索能力,而不必走已配置的搜索引擎列表。",
"provider_hint": "启用该服务后,可继续填写所需的连接参数。",
"max_results": "最大获取结果数",
"base_url": "API 请求地址",
@@ -592,7 +591,8 @@
"requires_linux": "该工具仅在 Linux 主机上可用,并且需要暴露对应的设备文件。",
"requires_skills": "需要先启用 `tools.skills`,该技能注册表工具才能使用。",
"requires_subagent": "需要先启用 `tools.subagent``spawn` 才能委派任务。",
"requires_mcp_discovery": "需要先启用 `tools.mcp.discovery`MCP 发现工具才会可用。"
"requires_mcp_discovery": "需要先启用 `tools.mcp.discovery`MCP 发现工具才会可用。",
"requires_web_search_provider": "请至少配置一个可用的外部网络搜索 provider。"
}
}
},