@konbakuyomu/smart-search-linux-x64 0.1.32 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/smart-search/_internal/base_library.zip +0 -0
- package/runtime/smart-search/_internal/smart_search/assets/i18n/messages.json +96 -4
- package/runtime/smart-search/_internal/smart_search/assets/skills/smart-search-cli/SKILL.md +1 -1
- package/runtime/smart-search/_internal/smart_search/assets/skills/smart-search-cli/references/cli-core.md +1 -1
- package/runtime/smart-search/_internal/smart_search/assets/skills/smart-search-cli/references/command-patterns.md +2 -0
- package/runtime/smart-search/_internal/smart_search/assets/skills/smart-search-cli/references/provider-routing.md +2 -1
- package/runtime/smart-search/_internal/smart_search/assets/skills/smart-search-cli/references/setup-config.md +1 -0
- package/runtime/smart-search/_internal/smart_search/cli.py +27 -14
- package/runtime/smart-search/_internal/smart_search/config.py +59 -8
- package/runtime/smart-search/_internal/smart_search/jev_search.py +3 -0
- package/runtime/smart-search/_internal/smart_search/providers/openai_compatible.py +32 -2
- package/runtime/smart-search/_internal/smart_search/providers/xai_responses.py +14 -1
- package/runtime/smart-search/_internal/smart_search/service.py +51 -17
- package/runtime/smart-search/_internal/smart_search/sources.py +12 -0
- package/runtime/smart-search/_internal/smart_search/ui_metadata.py +8 -2
- package/runtime/smart-search/_internal/smart_search/utils.py +7 -3
- package/runtime/smart-search/_internal/{smart_search-0.1.32.dist-info → smart_search-0.1.33.dist-info}/METADATA +1 -1
- package/runtime/smart-search/_internal/{smart_search-0.1.32.dist-info → smart_search-0.1.33.dist-info}/RECORD +24 -24
- package/runtime/smart-search/package.json +7 -7
- package/runtime/smart-search/smart-search +0 -0
- /package/runtime/smart-search/_internal/{smart_search-0.1.32.dist-info → smart_search-0.1.33.dist-info}/INSTALLER +0 -0
- /package/runtime/smart-search/_internal/{smart_search-0.1.32.dist-info → smart_search-0.1.33.dist-info}/REQUESTED +0 -0
- /package/runtime/smart-search/_internal/{smart_search-0.1.32.dist-info → smart_search-0.1.33.dist-info}/WHEEL +0 -0
- /package/runtime/smart-search/_internal/{smart_search-0.1.32.dist-info → smart_search-0.1.33.dist-info}/direct_url.json +0 -0
- /package/runtime/smart-search/_internal/{smart_search-0.1.32.dist-info → smart_search-0.1.33.dist-info}/entry_points.txt +0 -0
- /package/runtime/smart-search/_internal/{smart_search-0.1.32.dist-info → smart_search-0.1.33.dist-info}/licenses/LICENSE +0 -0
- /package/runtime/smart-search/_internal/{smart_search-0.1.32.dist-info → smart_search-0.1.33.dist-info}/top_level.txt +0 -0
package/package.json
CHANGED
|
Binary file
|
|
@@ -1447,6 +1447,54 @@
|
|
|
1447
1447
|
"TinyFish 搜索与抓取 API 均可用",
|
|
1448
1448
|
"Both TinyFish search and fetch APIs are available"
|
|
1449
1449
|
],
|
|
1450
|
+
"Invalid OPENAI_COMPATIBLE_TOOLS: {0}. Supported values: {1}": [
|
|
1451
|
+
"无效的 OPENAI_COMPATIBLE_TOOLS:{0}。支持:{1}",
|
|
1452
|
+
"Invalid OPENAI_COMPATIBLE_TOOLS: {0}. Supported values: {1}"
|
|
1453
|
+
],
|
|
1454
|
+
"OPENAI_COMPATIBLE_TOOLS only works with OPENAI_COMPATIBLE_API_MODE=responses. Set the API mode to responses, or clear OPENAI_COMPATIBLE_TOOLS.": [
|
|
1455
|
+
"OPENAI_COMPATIBLE_TOOLS 只在 OPENAI_COMPATIBLE_API_MODE=responses 时生效。请把接口模式改成 responses,或清空 OPENAI_COMPATIBLE_TOOLS。",
|
|
1456
|
+
"OPENAI_COMPATIBLE_TOOLS only works with OPENAI_COMPATIBLE_API_MODE=responses. Set the API mode to responses, or clear OPENAI_COMPATIBLE_TOOLS."
|
|
1457
|
+
],
|
|
1458
|
+
"OPENAI_COMPATIBLE_TOOLS 只在 OPENAI_COMPATIBLE_API_MODE=responses 时生效。请把接口模式改成 responses,或清空 OPENAI_COMPATIBLE_TOOLS。": [
|
|
1459
|
+
"OPENAI_COMPATIBLE_TOOLS 只在 OPENAI_COMPATIBLE_API_MODE=responses 时生效。请把接口模式改成 responses,或清空 OPENAI_COMPATIBLE_TOOLS。",
|
|
1460
|
+
"OPENAI_COMPATIBLE_TOOLS only works with OPENAI_COMPATIBLE_API_MODE=responses. Set the API mode to responses, or clear OPENAI_COMPATIBLE_TOOLS."
|
|
1461
|
+
],
|
|
1462
|
+
"OpenAI 兼容联网工具": [
|
|
1463
|
+
"OpenAI 兼容联网工具",
|
|
1464
|
+
"openai-compatible tools"
|
|
1465
|
+
],
|
|
1466
|
+
"OpenAI-compatible tools are invalid.": [
|
|
1467
|
+
"OpenAI-compatible 联网工具配置无效。",
|
|
1468
|
+
"OpenAI-compatible tools are invalid."
|
|
1469
|
+
],
|
|
1470
|
+
"OpenAI-compatible was rate limited upstream (HTTP 429), so this diagnosis cannot tell whether the route works.": [
|
|
1471
|
+
"OpenAI-compatible 被上游限流(HTTP 429),这次诊断判断不了链路是否正常。",
|
|
1472
|
+
"OpenAI-compatible was rate limited upstream (HTTP 429), so this diagnosis cannot tell whether the route works."
|
|
1473
|
+
],
|
|
1474
|
+
"OpenAI-compatible 联网工具配置无效。": [
|
|
1475
|
+
"OpenAI-compatible 联网工具配置无效。",
|
|
1476
|
+
"OpenAI-compatible tools are invalid."
|
|
1477
|
+
],
|
|
1478
|
+
"OpenAI-compatible 被上游限流(HTTP 429),这次诊断判断不了链路是否正常。": [
|
|
1479
|
+
"OpenAI-compatible 被上游限流(HTTP 429),这次诊断判断不了链路是否正常。",
|
|
1480
|
+
"OpenAI-compatible was rate limited upstream (HTTP 429), so this diagnosis cannot tell whether the route works."
|
|
1481
|
+
],
|
|
1482
|
+
"Run this diagnosis again in a minute; if it keeps happening, lower concurrency, switch models, or raise the account's rate limit. The URL, key and model name are not necessarily wrong.": [
|
|
1483
|
+
"等一分钟再运行本诊断;经常出现就降低并发、换模型,或提高账户的速率额度。地址、key 和模型名不一定有问题。",
|
|
1484
|
+
"Run this diagnosis again in a minute; if it keeps happening, lower concurrency, switch models, or raise the account's rate limit. The URL, key and model name are not necessarily wrong."
|
|
1485
|
+
],
|
|
1486
|
+
"Save OPENAI_COMPATIBLE_TOOLS (web_search or web_search_preview; needs --openai-compatible-api-mode responses).": [
|
|
1487
|
+
"保存 OPENAI_COMPATIBLE_TOOLS(web_search 或 web_search_preview;需要 --openai-compatible-api-mode responses)。",
|
|
1488
|
+
"Save OPENAI_COMPATIBLE_TOOLS (web_search or web_search_preview; needs --openai-compatible-api-mode responses)."
|
|
1489
|
+
],
|
|
1490
|
+
"Strict validation needs sources that a search service retrieved on its own; this run only has the search model's own citations, which are not independent evidence. Configure a web_search provider (for example Tavily, Zhipu or Yuanbao Search) before using strict, or switch to balanced.": [
|
|
1491
|
+
"strict 校验需要搜索服务独立检索到的来源;这次只有搜索模型自带的引用,不算独立证据。配置一个 web_search 提供方(例如 Tavily、智谱、元宝搜索)后再用 strict,或改用 balanced。",
|
|
1492
|
+
"Strict validation needs sources that a search service retrieved on its own; this run only has the search model's own citations, which are not independent evidence. Configure a web_search provider (for example Tavily, Zhipu or Yuanbao Search) before using strict, or switch to balanced."
|
|
1493
|
+
],
|
|
1494
|
+
"Use OPENAI_COMPATIBLE_TOOLS=web_search (or web_search_preview) only with OPENAI_COMPATIBLE_API_MODE=responses; otherwise leave it empty.": [
|
|
1495
|
+
"只在 OPENAI_COMPATIBLE_API_MODE=responses 时把 OPENAI_COMPATIBLE_TOOLS 设为 web_search(或 web_search_preview);其他情况留空。",
|
|
1496
|
+
"Use OPENAI_COMPATIBLE_TOOLS=web_search (or web_search_preview) only with OPENAI_COMPATIBLE_API_MODE=responses; otherwise leave it empty."
|
|
1497
|
+
],
|
|
1450
1498
|
"Yuanbao Search API is available": [
|
|
1451
1499
|
"元宝搜索 API 可用",
|
|
1452
1500
|
"Yuanbao Search API is available"
|
|
@@ -4663,6 +4711,14 @@
|
|
|
4663
4711
|
"OpenAI 兼容 API 模式",
|
|
4664
4712
|
"openai-compatible API mode"
|
|
4665
4713
|
],
|
|
4714
|
+
"openai-compatible tools": [
|
|
4715
|
+
"OpenAI 兼容联网工具",
|
|
4716
|
+
"openai-compatible tools"
|
|
4717
|
+
],
|
|
4718
|
+
"strict 校验需要搜索服务独立检索到的来源;这次只有搜索模型自带的引用,不算独立证据。配置一个 web_search 提供方(例如 Tavily、智谱、元宝搜索)后再用 strict,或改用 balanced。": [
|
|
4719
|
+
"strict 校验需要搜索服务独立检索到的来源;这次只有搜索模型自带的引用,不算独立证据。配置一个 web_search 提供方(例如 Tavily、智谱、元宝搜索)后再用 strict,或改用 balanced。",
|
|
4720
|
+
"Strict validation needs sources that a search service retrieved on its own; this run only has the search model's own citations, which are not independent evidence. Configure a web_search provider (for example Tavily, Zhipu or Yuanbao Search) before using strict, or switch to balanced."
|
|
4721
|
+
],
|
|
4666
4722
|
"xai-responses 流式": [
|
|
4667
4723
|
"xai-responses 流式",
|
|
4668
4724
|
"xai-responses stream"
|
|
@@ -7423,6 +7479,34 @@
|
|
|
7423
7479
|
"TinyFish 搜索与抓取 API 均可用",
|
|
7424
7480
|
"Both TinyFish search and fetch APIs are available"
|
|
7425
7481
|
],
|
|
7482
|
+
"{0} Also, the web search capability check did not pass: {1}. If this model can only search through the Responses web_search tool (for example OpenAI's gpt-6.1-sol), set OPENAI_COMPATIBLE_API_MODE=responses and OPENAI_COMPATIBLE_TOOLS=web_search; use web_search_preview for relays that only know the older name.": [
|
|
7483
|
+
"{0} 另外,联网搜索能力检查未通过:{1}。如果这个模型只能通过 Responses 的 web_search 工具联网(例如 OpenAI 官方的 gpt-6.1-sol),请设置 OPENAI_COMPATIBLE_API_MODE=responses 和 OPENAI_COMPATIBLE_TOOLS=web_search;中转站只认旧名时改用 web_search_preview。",
|
|
7484
|
+
"{0} Also, the web search capability check did not pass: {1}. If this model can only search through the Responses web_search tool (for example OpenAI's gpt-6.1-sol), set OPENAI_COMPATIBLE_API_MODE=responses and OPENAI_COMPATIBLE_TOOLS=web_search; use web_search_preview for relays that only know the older name."
|
|
7485
|
+
],
|
|
7486
|
+
"{0} Also, the web search capability check did not pass: {1}. The request already carried the {2} tool, so this model or relay may not support Responses web search tools; switch to a model that does, or clear OPENAI_COMPATIBLE_TOOLS and use a model that searches on its own.": [
|
|
7487
|
+
"{0} 另外,联网搜索能力检查未通过:{1}。请求里已经带了 {2} 工具,可能是这个模型或中转站不支持 Responses 的联网工具;换一个支持的模型,或清空 OPENAI_COMPATIBLE_TOOLS 改用自带搜索的模型。",
|
|
7488
|
+
"{0} Also, the web search capability check did not pass: {1}. The request already carried the {2} tool, so this model or relay may not support Responses web search tools; switch to a model that does, or clear OPENAI_COMPATIBLE_TOOLS and use a model that searches on its own."
|
|
7489
|
+
],
|
|
7490
|
+
"{0} does not support x_search in XAI_TOOLS (HTTP 400). Only the official xAI API serves x_search; set XAI_TOOLS to web_search and try again.": [
|
|
7491
|
+
"{0} 不支持 XAI_TOOLS 里的 x_search(HTTP 400)。x_search 只有 xAI 官方接口能用;把 XAI_TOOLS 改成 web_search 再试。",
|
|
7492
|
+
"{0} does not support x_search in XAI_TOOLS (HTTP 400). Only the official xAI API serves x_search; set XAI_TOOLS to web_search and try again."
|
|
7493
|
+
],
|
|
7494
|
+
"{0} 不支持 XAI_TOOLS 里的 x_search(HTTP 400)。x_search 只有 xAI 官方接口能用;把 XAI_TOOLS 改成 web_search 再试。": [
|
|
7495
|
+
"{0} 不支持 XAI_TOOLS 里的 x_search(HTTP 400)。x_search 只有 xAI 官方接口能用;把 XAI_TOOLS 改成 web_search 再试。",
|
|
7496
|
+
"{0} does not support x_search in XAI_TOOLS (HTTP 400). Only the official xAI API serves x_search; set XAI_TOOLS to web_search and try again."
|
|
7497
|
+
],
|
|
7498
|
+
"{0} 另外,联网搜索能力检查未通过:{1}。如果这个模型只能通过 Responses 的 web_search 工具联网(例如 OpenAI 官方的 gpt-6.1-sol),请设置 OPENAI_COMPATIBLE_API_MODE=responses 和 OPENAI_COMPATIBLE_TOOLS=web_search;中转站只认旧名时改用 web_search_preview。": [
|
|
7499
|
+
"{0} 另外,联网搜索能力检查未通过:{1}。如果这个模型只能通过 Responses 的 web_search 工具联网(例如 OpenAI 官方的 gpt-6.1-sol),请设置 OPENAI_COMPATIBLE_API_MODE=responses 和 OPENAI_COMPATIBLE_TOOLS=web_search;中转站只认旧名时改用 web_search_preview。",
|
|
7500
|
+
"{0} Also, the web search capability check did not pass: {1}. If this model can only search through the Responses web_search tool (for example OpenAI's gpt-6.1-sol), set OPENAI_COMPATIBLE_API_MODE=responses and OPENAI_COMPATIBLE_TOOLS=web_search; use web_search_preview for relays that only know the older name."
|
|
7501
|
+
],
|
|
7502
|
+
"{0} 另外,联网搜索能力检查未通过:{1}。请求里已经带了 {2} 工具,可能是这个模型或中转站不支持 Responses 的联网工具;换一个支持的模型,或清空 OPENAI_COMPATIBLE_TOOLS 改用自带搜索的模型。": [
|
|
7503
|
+
"{0} 另外,联网搜索能力检查未通过:{1}。请求里已经带了 {2} 工具,可能是这个模型或中转站不支持 Responses 的联网工具;换一个支持的模型,或清空 OPENAI_COMPATIBLE_TOOLS 改用自带搜索的模型。",
|
|
7504
|
+
"{0} Also, the web search capability check did not pass: {1}. The request already carried the {2} tool, so this model or relay may not support Responses web search tools; switch to a model that does, or clear OPENAI_COMPATIBLE_TOOLS and use a model that searches on its own."
|
|
7505
|
+
],
|
|
7506
|
+
"保存 OPENAI_COMPATIBLE_TOOLS(web_search 或 web_search_preview;需要 --openai-compatible-api-mode responses)。": [
|
|
7507
|
+
"保存 OPENAI_COMPATIBLE_TOOLS(web_search 或 web_search_preview;需要 --openai-compatible-api-mode responses)。",
|
|
7508
|
+
"Save OPENAI_COMPATIBLE_TOOLS (web_search or web_search_preview; needs --openai-compatible-api-mode responses)."
|
|
7509
|
+
],
|
|
7426
7510
|
"元宝搜索 API 可用": [
|
|
7427
7511
|
"元宝搜索 API 可用",
|
|
7428
7512
|
"Yuanbao Search API is available"
|
|
@@ -8783,6 +8867,10 @@
|
|
|
8783
8867
|
"可选;设置后在 xAI Responses 请求中发送 reasoning.effort,默认不发送。",
|
|
8784
8868
|
"Optional; when set, sends reasoning.effort in xAI Responses requests; omitted by default."
|
|
8785
8869
|
],
|
|
8870
|
+
"只在 OPENAI_COMPATIBLE_API_MODE=responses 时把 OPENAI_COMPATIBLE_TOOLS 设为 web_search(或 web_search_preview);其他情况留空。": [
|
|
8871
|
+
"只在 OPENAI_COMPATIBLE_API_MODE=responses 时把 OPENAI_COMPATIBLE_TOOLS 设为 web_search(或 web_search_preview);其他情况留空。",
|
|
8872
|
+
"Use OPENAI_COMPATIBLE_TOOLS=web_search (or web_search_preview) only with OPENAI_COMPATIBLE_API_MODE=responses; otherwise leave it empty."
|
|
8873
|
+
],
|
|
8786
8874
|
"可选;设置后在 xAI Responses 请求中发送 reasoning.effort,默认不发送。": [
|
|
8787
8875
|
"可选;设置后在 xAI Responses 请求中发送 reasoning.effort,默认不发送。",
|
|
8788
8876
|
"Optional; when set, sends reasoning.effort in xAI Responses requests; omitted by default."
|
|
@@ -11703,6 +11791,10 @@
|
|
|
11703
11791
|
"无效的 --timeout:{0}。需要有限的正数。",
|
|
11704
11792
|
"Invalid --timeout: {0}. Expected a positive finite number."
|
|
11705
11793
|
],
|
|
11794
|
+
"无效的 OPENAI_COMPATIBLE_TOOLS:{0}。支持:{1}": [
|
|
11795
|
+
"无效的 OPENAI_COMPATIBLE_TOOLS:{0}。支持:{1}",
|
|
11796
|
+
"Invalid OPENAI_COMPATIBLE_TOOLS: {0}. Supported values: {1}"
|
|
11797
|
+
],
|
|
11706
11798
|
"无效的 OpenAI 兼容 API 模式:{0}。支持:{1}": [
|
|
11707
11799
|
"无效的 OpenAI 兼容 API 模式:{0}。支持:{1}",
|
|
11708
11800
|
"Invalid OpenAI-compatible API mode: {0}. Supported values: {1}"
|
|
@@ -13655,6 +13747,10 @@
|
|
|
13655
13747
|
"站点地图",
|
|
13656
13748
|
"Site map"
|
|
13657
13749
|
],
|
|
13750
|
+
"等一分钟再运行本诊断;经常出现就降低并发、换模型,或提高账户的速率额度。地址、key 和模型名不一定有问题。": [
|
|
13751
|
+
"等一分钟再运行本诊断;经常出现就降低并发、换模型,或提高账户的速率额度。地址、key 和模型名不一定有问题。",
|
|
13752
|
+
"Run this diagnosis again in a minute; if it keeps happening, lower concurrency, switch models, or raise the account's rate limit. The URL, key and model name are not necessarily wrong."
|
|
13753
|
+
],
|
|
13658
13754
|
"等多久、失败之后怎么办、冷却多长时间。默认值适合大多数人。": [
|
|
13659
13755
|
"等多久、失败之后怎么办、冷却多长时间。默认值适合大多数人。",
|
|
13660
13756
|
"How long to wait, what happens after a failure, how long a dead provider stays out. The defaults suit most people."
|
|
@@ -17943,10 +18039,6 @@
|
|
|
17943
18039
|
"尚未配置 Jev 独立汇总模型",
|
|
17944
18040
|
"Jev synthesis model is not configured"
|
|
17945
18041
|
],
|
|
17946
|
-
"{0} 另外,联网搜索能力检查未通过:{1}。OpenAI-compatible 不会发送搜索工具;如果该模型只能通过 Responses 的 web_search 工具联网,请把 XAI_API_URL、XAI_API_KEY、XAI_MODEL 指向支持 /v1/responses 的中转站。": [
|
|
17947
|
-
"{0} 另外,联网搜索能力检查未通过:{1}。OpenAI-compatible 不会发送搜索工具;如果该模型只能通过 Responses 的 web_search 工具联网,请把 XAI_API_URL、XAI_API_KEY、XAI_MODEL 指向支持 /v1/responses 的中转站。",
|
|
17948
|
-
"{0} Also, the web search capability check did not pass: {1}. OpenAI-compatible sends no search tool; if this model can only search through the Responses web_search tool, point XAI_API_URL, XAI_API_KEY and XAI_MODEL at a relay that supports /v1/responses."
|
|
17949
|
-
],
|
|
17950
18042
|
"DeepSeek search response is not a JSON object": [
|
|
17951
18043
|
"DeepSeek 搜索响应不是 JSON 对象",
|
|
17952
18044
|
"DeepSeek search response is not a JSON object"
|
|
@@ -23,7 +23,7 @@ Use the local `smart-search` command as the default execution layer for web rese
|
|
|
23
23
|
1. Run `smart-search doctor --format json` when configuration or availability is uncertain.
|
|
24
24
|
2. If `doctor` reports missing configuration, use `smart-search setup` or `smart-search config set KEY VALUE` when the user provides keys. Do not ask users to edit global environment variables by default.
|
|
25
25
|
3. If OpenAI-compatible `search` hangs or times out after `doctor` succeeds, run `smart-search diagnose openai-compatible --format markdown` and use its summary.
|
|
26
|
-
4. Keep `OPENAI_COMPATIBLE_API_MODE=chat-completions` unless a named relay has passed the official Responses subset probes; set `responses` only for that relay and keep the diagnostic evidence.
|
|
26
|
+
4. Keep `OPENAI_COMPATIBLE_API_MODE=chat-completions` unless a named relay has passed the official Responses subset probes; set `responses` only for that relay and keep the diagnostic evidence. For a model that searches only through the Responses `web_search` tool (for example `gpt-6.1-sol` with an official OpenAI key), also set `OPENAI_COMPATIBLE_TOOLS=web_search`.
|
|
27
27
|
5. If `doctor` returns `ok: true`, use only `smart-search` CLI subcommands for web research. Do not call Codex native web search in the same task.
|
|
28
28
|
6. Use `smart-search skills status --targets codex --format json` when the installed global skill may be stale; use `smart-search skills update --targets codex --format json` to refresh it without rerunning setup.
|
|
29
29
|
7. Use `smart-search smoke --mock --format json` after CLI/provider architecture changes. Use `--live` only when real keys are available and the user expects live checks.
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
- `smart-search map URL [--instructions TEXT] [--max-depth N] [--max-breadth N] [--limit N] [--timeout SECONDS] [--format json|markdown|content] [--output PATH]`
|
|
49
49
|
- `smart-search doctor [--format json|markdown|content] [--output PATH]`
|
|
50
50
|
- `smart-search diagnose openai-compatible [--timeout SECONDS] [--format json|markdown] [--output PATH]`
|
|
51
|
-
- `smart-search setup [--lang zh|en] [--advanced] [--non-interactive] [--skip-skills] [--install-skills CSV] [--skills-root PATH] [--xai-api-url URL] [--xai-api-key KEY] [--xai-model ID] [--xai-tools-explicit CSV] [--xai-stream true|false] [--openai-compatible-api-url URL] [--openai-compatible-api-key KEY] [--openai-compatible-model ID] [--openai-compatible-fallback-models CSV] [--openai-compatible-api-mode chat-completions|responses] [--openai-compatible-stream true|false] [--validation-level fast|balanced|strict] [--fallback-mode auto|off] [--minimum-profile standard|off] [--intent-router jev|local] [--search-timeout SECONDS] [--provider-cooldown SECONDS] [--provider-failure-threshold N] [--exa-key KEY] [--context7-key KEY] [--zhipu-key KEY] [--zhipu-api-url URL] [--zhipu-search-engine ENGINE] [--zhipu-mcp-key KEY] [--zhipu-mcp-search-api-url URL] [--zhipu-mcp-reader-api-url URL] [--zhipu-mcp-zread-api-url URL] [--zhipu-mcp-timeout SECONDS] [--jina-key KEY] [--jina-reader-api-url URL] [--jina-respond-with MODE] [--jina-timeout SECONDS] [--tavily-api-url URL] [--tavily-key KEY] [--firecrawl-api-url URL] [--firecrawl-key KEY] [--tinyfish-key KEY] [--tinyfish-search-api-url URL] [--tinyfish-fetch-api-url URL] [--tinyfish-timeout SECONDS] [--yuanbao-secret-id ID] [--yuanbao-secret-key KEY] [--yuanbao-api-url URL] [--yuanbao-timeout SECONDS] [--deepseek-key KEY] [--deepseek-api-url URL] [--deepseek-timeout SECONDS] [--anysearch-api-url URL] [--anysearch-key KEY] [--anysearch-timeout SECONDS] [--sciverse-api-url URL] [--sciverse-token TOKEN] [--sciverse-timeout SECONDS] [--format json|markdown|content] [--output PATH]`
|
|
51
|
+
- `smart-search setup [--lang zh|en] [--advanced] [--non-interactive] [--skip-skills] [--install-skills CSV] [--skills-root PATH] [--xai-api-url URL] [--xai-api-key KEY] [--xai-model ID] [--xai-tools-explicit CSV] [--xai-stream true|false] [--openai-compatible-api-url URL] [--openai-compatible-api-key KEY] [--openai-compatible-model ID] [--openai-compatible-fallback-models CSV] [--openai-compatible-api-mode chat-completions|responses] [--openai-compatible-tools web_search|web_search_preview] [--openai-compatible-stream true|false] [--validation-level fast|balanced|strict] [--fallback-mode auto|off] [--minimum-profile standard|off] [--intent-router jev|local] [--search-timeout SECONDS] [--provider-cooldown SECONDS] [--provider-failure-threshold N] [--exa-key KEY] [--context7-key KEY] [--zhipu-key KEY] [--zhipu-api-url URL] [--zhipu-search-engine ENGINE] [--zhipu-mcp-key KEY] [--zhipu-mcp-search-api-url URL] [--zhipu-mcp-reader-api-url URL] [--zhipu-mcp-zread-api-url URL] [--zhipu-mcp-timeout SECONDS] [--jina-key KEY] [--jina-reader-api-url URL] [--jina-respond-with MODE] [--jina-timeout SECONDS] [--tavily-api-url URL] [--tavily-key KEY] [--firecrawl-api-url URL] [--firecrawl-key KEY] [--tinyfish-key KEY] [--tinyfish-search-api-url URL] [--tinyfish-fetch-api-url URL] [--tinyfish-timeout SECONDS] [--yuanbao-secret-id ID] [--yuanbao-secret-key KEY] [--yuanbao-api-url URL] [--yuanbao-timeout SECONDS] [--deepseek-key KEY] [--deepseek-api-url URL] [--deepseek-timeout SECONDS] [--anysearch-api-url URL] [--anysearch-key KEY] [--anysearch-timeout SECONDS] [--sciverse-api-url URL] [--sciverse-token TOKEN] [--sciverse-timeout SECONDS] [--format json|markdown|content] [--output PATH]`
|
|
52
52
|
- `smart-search config path|list|set|unset ... [--format json|markdown|content] [--output PATH]`
|
|
53
53
|
- `smart-search model set MODEL [--format json|markdown|content] [--output PATH]`
|
|
54
54
|
- `smart-search model current [--format json|markdown|content] [--output PATH]`
|
|
@@ -58,6 +58,7 @@ smart-search skills update --all --format json
|
|
|
58
58
|
smart-search route "React useEffect API docs" --format markdown
|
|
59
59
|
smart-search setup --non-interactive --zhipu-api-url "https://open.bigmodel.cn/api" --zhipu-search-engine "search_std"
|
|
60
60
|
smart-search setup --non-interactive --openai-compatible-api-mode responses
|
|
61
|
+
smart-search setup --non-interactive --openai-compatible-api-mode responses --openai-compatible-tools web_search
|
|
61
62
|
smart-search setup --non-interactive --openai-compatible-stream true
|
|
62
63
|
smart-search setup --non-interactive --openai-compatible-fallback-models "model-a,model-b"
|
|
63
64
|
smart-search setup --non-interactive --anysearch-api-url "https://api.anysearch.com/mcp" --anysearch-key "key"
|
|
@@ -77,6 +78,7 @@ smart-search config set OPENAI_COMPATIBLE_API_URL "https://api.openai.com/v1" --
|
|
|
77
78
|
smart-search config set OPENAI_COMPATIBLE_API_KEY "key" --format json
|
|
78
79
|
smart-search config set OPENAI_COMPATIBLE_MODEL "model-id" --format json
|
|
79
80
|
smart-search config set OPENAI_COMPATIBLE_API_MODE "responses" --format json
|
|
81
|
+
smart-search config set OPENAI_COMPATIBLE_TOOLS "web_search" --format json
|
|
80
82
|
smart-search config set OPENAI_COMPATIBLE_FALLBACK_MODELS "model-a,model-b" --format json
|
|
81
83
|
smart-search config set OPENAI_COMPATIBLE_STREAM "true" --format json
|
|
82
84
|
smart-search config set SMART_SEARCH_TIMEOUT_SECONDS "180" --format json
|
|
@@ -27,7 +27,7 @@ For research, `SMART_SEARCH_JEV_SYNTHESIZE` accepts `true`, `false`, and `auto`.
|
|
|
27
27
|
- Legacy `SMART_SEARCH_API_URL`, `SMART_SEARCH_API_KEY`, `SMART_SEARCH_API_MODE`, `SMART_SEARCH_MODEL`, and `SMART_SEARCH_XAI_TOOLS` are unsupported config keys.
|
|
28
28
|
- xAI Responses mode may use only `XAI_TOOLS=web_search,x_search` and a subset of those tools.
|
|
29
29
|
- `XAI_REASONING_EFFORT` accepts `low`, `medium`, or `high` (case-insensitive, stored lowercase). Optional; when set, sends reasoning.effort in xAI Responses requests; omitted by default. This also applies to Grok search selected by Jev. Unsupported reasoning values reported by the upstream API remain errors; the field is not silently removed and retried. The xAI connection test in `doctor` and the App sends the same `reasoning.effort` and `stream` as a search, without tools, and names them in its message and under `sent`; a stream that fails after HTTP 200 fails the test.
|
|
30
|
-
-
|
|
30
|
+
- OpenAI-compatible sends no search tools by default. `OPENAI_COMPATIBLE_TOOLS` (`web_search` or `web_search_preview`, comma-separated) attaches Responses tools to `search()` only; it requires `OPENAI_COMPATIBLE_API_MODE=responses`, and any other mode is a `parameter_error` on save and before a request. It never applies to `fetch()`, `describe_url()`, `rank_sources()`, synthesis, or the `doctor` connection test. `x_search` and legacy `search_parameters` are never sent; xAI Chat Completions Live Search is deprecated.
|
|
31
31
|
- AnySearch is reported only as optional experimental `vertical_search`; it is not part of the `web_search` fallback and is not required by the `standard` minimum profile.
|
|
32
32
|
- Sciverse is reported only as optional experimental explicit-only `vertical_search`; it is not `docs_search`, not part of default `search` / `research` fallback, and not required by the `standard` minimum profile.
|
|
33
33
|
- Jina Reader is `web_fetch` only, not a general search provider. `JINA_API_KEY` is required before Jina satisfies the standard minimum profile; anonymous `r.jina.ai` is explicit/experimental fetch behavior.
|
|
@@ -138,6 +138,7 @@ OpenAI-compatible streaming:
|
|
|
138
138
|
|
|
139
139
|
- `OPENAI_COMPATIBLE_API_MODE` defaults to `chat-completions` and accepts only `chat-completions` or `responses`. The selected protocol applies consistently to `search()`, provider-side `fetch()`, `describe_url()`, `rank_sources()`, `doctor`, and `diagnose openai-compatible`.
|
|
140
140
|
- Responses mode handles the official `model` + `instructions`/`input` subset, heterogeneous `output` text parts, URL citations, typed stream deltas, and terminal failures. It does not promise `/responses` support from every relay marketed as OpenAI-compatible; accept each named relay with both diagnose stream probes.
|
|
141
|
+
- Models that search only through the Responses `web_search` tool, such as `gpt-6.1-sol` on the official OpenAI API, need `OPENAI_COMPATIBLE_API_MODE=responses` plus `OPENAI_COMPATIBLE_TOOLS=web_search`. Chat Completions search models such as `gpt-5-search-api` search on their own; their `url_citation` annotations become structured sources. `diagnose openai-compatible` sends the configured tools with its search-shape and web-search probes.
|
|
141
142
|
- `OPENAI_COMPATIBLE_STREAM` defaults to `false` and accepts `true`, `1`, or `yes` as true.
|
|
142
143
|
- For OpenAI-compatible search, `search --stream` means "prefer stream first"; stream empty/timeout/retryable protocol failures fall back to the same provider/model with `stream=false`.
|
|
143
144
|
- `search --no-stream` forces `stream=false` for the current invocation.
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
- Use `smart-search setup --non-interactive --jina-key "key"` to let Jina satisfy `web_fetch`; `JINA_RESPOND_WITH=readerlm-v2` also requires `JINA_API_KEY`.
|
|
85
85
|
- Use `smart-search setup --non-interactive --zhipu-mcp-key "key"` only when the user explicitly wants Coding Plan Remote MCP quota.
|
|
86
86
|
- Use `smart-search setup --non-interactive --openai-compatible-api-mode responses` only when a named relay requires `/responses`. The default is `chat-completions` and uses `/chat/completions`; invalid modes fail before setup saves any field.
|
|
87
|
+
- Use `smart-search setup --non-interactive --openai-compatible-api-mode responses --openai-compatible-tools web_search` when the model searches only through the Responses `web_search` tool, such as `gpt-6.1-sol` with an official OpenAI key. Use `web_search_preview` for a relay that only knows the older name. Tools without `responses` mode fail before setup saves any field.
|
|
87
88
|
- Use `smart-search setup --non-interactive --openai-compatible-stream true` only when an OpenAI-compatible relay benefits from SSE streaming for long requests. Default remains false.
|
|
88
89
|
- Use `smart-search setup --non-interactive --openai-compatible-fallback-models "model-a,model-b"` to save ordered OpenAI-compatible backup models for primary model hard failure. These models do not receive a reserved time slice; the primary model keeps the remaining shared main-search budget. `--fallback off` and `search --model MODEL` disable this model fallback for one invocation.
|
|
89
90
|
- Use `smart-search setup --non-interactive --search-timeout 300` or `smart-search config set SMART_SEARCH_TIMEOUT_SECONDS 300` to persist the normal search budget. Invalid non-positive values fail before provider work.
|
|
@@ -869,6 +869,8 @@ def _format_model_markdown(data: dict[str, Any]) -> str:
|
|
|
869
869
|
rows.append(["openai-compatible", data.get("openai_compatible_model")])
|
|
870
870
|
if data.get("openai_compatible_api_mode"):
|
|
871
871
|
rows.append([tr('openai-compatible API mode'), data.get("openai_compatible_api_mode")])
|
|
872
|
+
if data.get("openai_compatible_tools"):
|
|
873
|
+
rows.append([tr('openai-compatible tools'), ", ".join(data["openai_compatible_tools"])])
|
|
872
874
|
fallback_models = data.get("openai_compatible_fallback_models") or []
|
|
873
875
|
if fallback_models:
|
|
874
876
|
rows.append([tr('openai-compatible fallback'), ", ".join(fallback_models)])
|
|
@@ -2337,6 +2339,7 @@ def _run_advanced_setup_prompts(values: dict[str, str], current: dict[str, str],
|
|
|
2337
2339
|
("OPENAI_COMPATIBLE_MODEL", "OpenAI-compatible model", True),
|
|
2338
2340
|
("OPENAI_COMPATIBLE_FALLBACK_MODELS", "OpenAI-compatible fallback models (comma-separated)", True),
|
|
2339
2341
|
("OPENAI_COMPATIBLE_API_MODE", "OpenAI-compatible API mode (chat-completions/responses)", True),
|
|
2342
|
+
("OPENAI_COMPATIBLE_TOOLS", "OpenAI-compatible Responses tools (web_search; needs API mode responses)", True),
|
|
2340
2343
|
("OPENAI_COMPATIBLE_STREAM", "OpenAI-compatible stream mode (true/false)", True),
|
|
2341
2344
|
("SMART_SEARCH_JEV_SYNTHESIS_API_URL", "JEV synthesis API URL", True),
|
|
2342
2345
|
("SMART_SEARCH_JEV_SYNTHESIS_API_KEY", "JEV synthesis API key", True),
|
|
@@ -2770,6 +2773,7 @@ def _run_setup(args: argparse.Namespace) -> int:
|
|
|
2770
2773
|
"OPENAI_COMPATIBLE_MODEL": args.openai_compatible_model,
|
|
2771
2774
|
"OPENAI_COMPATIBLE_FALLBACK_MODELS": args.openai_compatible_fallback_models,
|
|
2772
2775
|
"OPENAI_COMPATIBLE_API_MODE": args.openai_compatible_api_mode,
|
|
2776
|
+
"OPENAI_COMPATIBLE_TOOLS": args.openai_compatible_tools,
|
|
2773
2777
|
"OPENAI_COMPATIBLE_STREAM": args.openai_compatible_stream,
|
|
2774
2778
|
"SMART_SEARCH_JEV_SYNTHESIS_API_URL": args.jev_synthesis_api_url,
|
|
2775
2779
|
"SMART_SEARCH_JEV_SYNTHESIS_API_KEY": args.jev_synthesis_api_key,
|
|
@@ -2844,20 +2848,28 @@ def _run_setup(args: argparse.Namespace) -> int:
|
|
|
2844
2848
|
|
|
2845
2849
|
# Validate all local enum/numeric constraints before persisting any setup
|
|
2846
2850
|
# field, so an invalid API mode cannot leave a partial provider profile.
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
"
|
|
2855
|
-
"
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2851
|
+
def validate_before_saving() -> None:
|
|
2852
|
+
for key, value in values.items():
|
|
2853
|
+
if value:
|
|
2854
|
+
service.config._validate_config_value(key, value)
|
|
2855
|
+
# Tools and API mode are saved one key at a time below, so check the pair up front.
|
|
2856
|
+
if values.get("OPENAI_COMPATIBLE_TOOLS") or values.get("OPENAI_COMPATIBLE_API_MODE"):
|
|
2857
|
+
service.config.check_openai_compatible_tools_mode(
|
|
2858
|
+
values.get("OPENAI_COMPATIBLE_TOOLS") or service.config.openai_compatible_tools_raw,
|
|
2859
|
+
values.get("OPENAI_COMPATIBLE_API_MODE") or service.config._get_config_value("OPENAI_COMPATIBLE_API_MODE", "") or "",
|
|
2860
|
+
)
|
|
2861
|
+
|
|
2862
|
+
try:
|
|
2863
|
+
validate_before_saving()
|
|
2864
|
+
except ValueError as e:
|
|
2865
|
+
data = {
|
|
2866
|
+
"ok": False,
|
|
2867
|
+
"error_type": "parameter_error",
|
|
2868
|
+
"error": str(e),
|
|
2869
|
+
"config_file": service.config_path()["config_file"],
|
|
2870
|
+
"saved": {},
|
|
2871
|
+
}
|
|
2872
|
+
return _print_result("setup", data, args.format, args.output)
|
|
2861
2873
|
|
|
2862
2874
|
saved: dict[str, str] = {}
|
|
2863
2875
|
for key, value in values.items():
|
|
@@ -3469,6 +3481,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3469
3481
|
setup_parser.add_argument("--openai-compatible-model", default="", help="Save OPENAI_COMPATIBLE_MODEL.")
|
|
3470
3482
|
setup_parser.add_argument("--openai-compatible-fallback-models", default="", help="Save OPENAI_COMPATIBLE_FALLBACK_MODELS.")
|
|
3471
3483
|
setup_parser.add_argument("--openai-compatible-api-mode", default="", help="Save OPENAI_COMPATIBLE_API_MODE (chat-completions or responses).")
|
|
3484
|
+
setup_parser.add_argument("--openai-compatible-tools", default="", help="Save OPENAI_COMPATIBLE_TOOLS (web_search or web_search_preview; needs --openai-compatible-api-mode responses).")
|
|
3472
3485
|
setup_parser.add_argument("--openai-compatible-stream", default="", help="Save OPENAI_COMPATIBLE_STREAM.")
|
|
3473
3486
|
setup_parser.add_argument("--jev-synthesis-api-url", default="", help="Save SMART_SEARCH_JEV_SYNTHESIS_API_URL.")
|
|
3474
3487
|
setup_parser.add_argument("--jev-synthesis-api-key", default="", help="Save SMART_SEARCH_JEV_SYNTHESIS_API_KEY.")
|
|
@@ -37,6 +37,8 @@ class Config:
|
|
|
37
37
|
_ALLOWED_XAI_TOOLS = {"web_search", "x_search"}
|
|
38
38
|
_ALLOWED_XAI_REASONING_EFFORTS = {"low", "medium", "high"}
|
|
39
39
|
_ALLOWED_OPENAI_COMPATIBLE_API_MODES = {"chat-completions", "responses"}
|
|
40
|
+
# Responses tools the OpenAI-compatible channel may attach to search(); empty sends none.
|
|
41
|
+
_ALLOWED_OPENAI_COMPATIBLE_TOOLS = {"web_search", "web_search_preview"}
|
|
40
42
|
_ALLOWED_VALIDATION_LEVELS = {"fast", "balanced", "strict"}
|
|
41
43
|
_ALLOWED_FALLBACK_MODES = {"auto", "off"}
|
|
42
44
|
_ALLOWED_MINIMUM_PROFILES = {"standard", "off"}
|
|
@@ -77,6 +79,7 @@ class Config:
|
|
|
77
79
|
"OPENAI_COMPATIBLE_MODEL",
|
|
78
80
|
"OPENAI_COMPATIBLE_FALLBACK_MODELS",
|
|
79
81
|
"OPENAI_COMPATIBLE_API_MODE",
|
|
82
|
+
"OPENAI_COMPATIBLE_TOOLS",
|
|
80
83
|
"OPENAI_COMPATIBLE_STREAM",
|
|
81
84
|
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
82
85
|
"SMART_SEARCH_FALLBACK_MODE",
|
|
@@ -166,6 +169,7 @@ class Config:
|
|
|
166
169
|
"OPENAI_COMPATIBLE_MODEL",
|
|
167
170
|
"OPENAI_COMPATIBLE_FALLBACK_MODELS",
|
|
168
171
|
"OPENAI_COMPATIBLE_API_MODE",
|
|
172
|
+
"OPENAI_COMPATIBLE_TOOLS",
|
|
169
173
|
"OPENAI_COMPATIBLE_STREAM",
|
|
170
174
|
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
171
175
|
"SMART_SEARCH_FALLBACK_MODE",
|
|
@@ -521,6 +525,15 @@ class Config:
|
|
|
521
525
|
for old_key, new_key in self._LEGACY_CONFIG_KEYS.items():
|
|
522
526
|
if new_key == key:
|
|
523
527
|
config_data.pop(old_key, None)
|
|
528
|
+
if {"OPENAI_COMPATIBLE_TOOLS", "OPENAI_COMPATIBLE_API_MODE"} & (set(normalized_set) | set(normalized_unset)):
|
|
529
|
+
try:
|
|
530
|
+
self.check_openai_compatible_tools_mode(
|
|
531
|
+
str(config_data.get("OPENAI_COMPATIBLE_TOOLS") or ""),
|
|
532
|
+
str(config_data.get("OPENAI_COMPATIBLE_API_MODE") or ""),
|
|
533
|
+
)
|
|
534
|
+
except ValueError as e:
|
|
535
|
+
return {"ok": False, "errors": [{"key": "OPENAI_COMPATIBLE_TOOLS", "error": str(e)}],
|
|
536
|
+
"saved": [], "unset": []}
|
|
524
537
|
self._save_config_file(config_data)
|
|
525
538
|
except OSError:
|
|
526
539
|
raise ValueError(tr('无法锁定或保存配置文件,请检查权限或稍后重试。')) from None
|
|
@@ -642,27 +655,55 @@ class Config:
|
|
|
642
655
|
def openai_compatible_stream(self) -> bool:
|
|
643
656
|
return (self._get_config_value("OPENAI_COMPATIBLE_STREAM", "false") or "false").lower() in ("true", "1", "yes")
|
|
644
657
|
|
|
645
|
-
|
|
646
|
-
|
|
658
|
+
@property
|
|
659
|
+
def openai_compatible_tools_raw(self) -> str:
|
|
660
|
+
return self._get_config_value("OPENAI_COMPATIBLE_TOOLS", "") or ""
|
|
661
|
+
|
|
662
|
+
@property
|
|
663
|
+
def openai_compatible_tools(self) -> list[str]:
|
|
664
|
+
"""Tools attached to Responses search(); raises when they could never be sent."""
|
|
665
|
+
tools = self.parse_openai_compatible_tools(self.openai_compatible_tools_raw)
|
|
666
|
+
self.check_openai_compatible_tools_mode(",".join(tools), self.openai_compatible_api_mode)
|
|
667
|
+
return tools
|
|
668
|
+
|
|
669
|
+
def check_openai_compatible_tools_mode(self, tools_raw: str, api_mode: str) -> None:
|
|
670
|
+
"""Tools ride on Responses requests only, so any other mode would silently drop them."""
|
|
671
|
+
tools, _ = self._split_tool_list(tools_raw, self._ALLOWED_OPENAI_COMPATIBLE_TOOLS)
|
|
672
|
+
if tools and (api_mode or self._DEFAULT_OPENAI_COMPATIBLE_API_MODE).strip().lower() != "responses":
|
|
673
|
+
raise ValueError(tr(
|
|
674
|
+
'OPENAI_COMPATIBLE_TOOLS only works with OPENAI_COMPATIBLE_API_MODE=responses. '
|
|
675
|
+
'Set the API mode to responses, or clear OPENAI_COMPATIBLE_TOOLS.'
|
|
676
|
+
))
|
|
677
|
+
|
|
678
|
+
@staticmethod
|
|
679
|
+
def _split_tool_list(raw: str, allowed: set[str]) -> tuple[list[str], list[str]]:
|
|
647
680
|
tools: list[str] = []
|
|
648
681
|
invalid: list[str] = []
|
|
649
|
-
|
|
650
|
-
for item in raw.split(","):
|
|
682
|
+
for item in (raw or "").split(","):
|
|
651
683
|
tool = item.strip().lower()
|
|
652
684
|
if not tool:
|
|
653
685
|
continue
|
|
654
|
-
if tool not in
|
|
686
|
+
if tool not in allowed:
|
|
655
687
|
invalid.append(tool)
|
|
656
|
-
|
|
657
|
-
if tool not in seen:
|
|
658
|
-
seen.add(tool)
|
|
688
|
+
elif tool not in tools:
|
|
659
689
|
tools.append(tool)
|
|
690
|
+
return tools, invalid
|
|
691
|
+
|
|
692
|
+
def parse_xai_tools(self, raw: str | None = None) -> list[str]:
|
|
693
|
+
tools, invalid = self._split_tool_list(raw or self.xai_tools_raw, self._ALLOWED_XAI_TOOLS)
|
|
660
694
|
if invalid:
|
|
661
695
|
allowed = ", ".join(sorted(self._ALLOWED_XAI_TOOLS))
|
|
662
696
|
invalid_text = ", ".join(invalid)
|
|
663
697
|
raise ValueError(tr('Invalid XAI_TOOLS: {0}. Supported values: {1}', invalid_text, allowed))
|
|
664
698
|
return tools
|
|
665
699
|
|
|
700
|
+
def parse_openai_compatible_tools(self, raw: str | None) -> list[str]:
|
|
701
|
+
tools, invalid = self._split_tool_list(raw or "", self._ALLOWED_OPENAI_COMPATIBLE_TOOLS)
|
|
702
|
+
if invalid:
|
|
703
|
+
allowed = ", ".join(sorted(self._ALLOWED_OPENAI_COMPATIBLE_TOOLS))
|
|
704
|
+
raise ValueError(tr('Invalid OPENAI_COMPATIBLE_TOOLS: {0}. Supported values: {1}', ", ".join(invalid), allowed))
|
|
705
|
+
return tools
|
|
706
|
+
|
|
666
707
|
def _validated_enum(self, key: str, default: str, allowed: set[str]) -> str:
|
|
667
708
|
return self._validate_enum_value(key, self._get_config_value(key, default) or default, allowed)
|
|
668
709
|
|
|
@@ -742,6 +783,9 @@ class Config:
|
|
|
742
783
|
if key == "XAI_TOOLS":
|
|
743
784
|
self.parse_xai_tools(value or self._DEFAULT_XAI_TOOLS)
|
|
744
785
|
return
|
|
786
|
+
if key == "OPENAI_COMPATIBLE_TOOLS":
|
|
787
|
+
self.parse_openai_compatible_tools(value)
|
|
788
|
+
return
|
|
745
789
|
# Every getter below falls back to its default on an empty value, so an
|
|
746
790
|
# empty string is how a key is cleared rather than a value to check.
|
|
747
791
|
if not str(value).strip():
|
|
@@ -1225,6 +1269,12 @@ class Config:
|
|
|
1225
1269
|
self._DEFAULT_OPENAI_COMPATIBLE_API_MODE,
|
|
1226
1270
|
self._ALLOWED_OPENAI_COMPATIBLE_API_MODES,
|
|
1227
1271
|
)
|
|
1272
|
+
openai_compatible_tools = self.openai_compatible_tools_raw
|
|
1273
|
+
try:
|
|
1274
|
+
openai_compatible_tools = ",".join(self.openai_compatible_tools)
|
|
1275
|
+
except ValueError as exc:
|
|
1276
|
+
if not openai_compatible_api_mode_error:
|
|
1277
|
+
config_parameter_errors.append(str(exc))
|
|
1228
1278
|
primary_api_mode = tr('未配置')
|
|
1229
1279
|
if xai_configured:
|
|
1230
1280
|
primary_api_mode = "xai-responses"
|
|
@@ -1273,6 +1323,7 @@ class Config:
|
|
|
1273
1323
|
"OPENAI_COMPATIBLE_MODEL": self.openai_compatible_model,
|
|
1274
1324
|
"OPENAI_COMPATIBLE_FALLBACK_MODELS": ",".join(self.openai_compatible_fallback_models),
|
|
1275
1325
|
"OPENAI_COMPATIBLE_API_MODE": openai_compatible_api_mode,
|
|
1326
|
+
"OPENAI_COMPATIBLE_TOOLS": openai_compatible_tools,
|
|
1276
1327
|
"OPENAI_COMPATIBLE_STREAM": self.openai_compatible_stream,
|
|
1277
1328
|
"SMART_SEARCH_VALIDATION_LEVEL": validation_level,
|
|
1278
1329
|
"SMART_SEARCH_FALLBACK_MODE": fallback_mode,
|
|
@@ -678,6 +678,9 @@ async def search(
|
|
|
678
678
|
error_type = "" if ok else (failure.error_type if failure else "timeout" if stopped == "deadline" else "evidence_error")
|
|
679
679
|
error = "" if ok else (failure.error if failure else "Search deadline exhausted" if stopped == "deadline"
|
|
680
680
|
else "Search did not obtain enough verified useful evidence")
|
|
681
|
+
if not ok and not failure and stopped != "deadline" and validation == "strict" and evidence and not has_source_evidence(evidence):
|
|
682
|
+
# Only a search model's own citations came back; strict needs pages a search provider retrieved.
|
|
683
|
+
error = source_message('strict 校验需要搜索服务独立检索到的来源;这次只有搜索模型自带的引用,不算独立证据。配置一个 web_search 提供方(例如 Tavily、智谱、元宝搜索)后再用 strict,或改用 balanced。')
|
|
681
684
|
if not ok and not evidence and terminal_error:
|
|
682
685
|
error_type, error = terminal_error["error_type"], terminal_error.get("error", error)
|
|
683
686
|
if stopped == "cancelled":
|
|
@@ -173,6 +173,7 @@ class OpenAICompatibleSearchProvider(BaseSearchProvider):
|
|
|
173
173
|
model: str = "grok-4-fast",
|
|
174
174
|
stream: bool = False,
|
|
175
175
|
api_mode: str = "chat-completions",
|
|
176
|
+
tools: list[str] | None = None,
|
|
176
177
|
):
|
|
177
178
|
super().__init__(normalize_openai_compatible_api_url(api_url), api_key)
|
|
178
179
|
self.model = model
|
|
@@ -181,6 +182,8 @@ class OpenAICompatibleSearchProvider(BaseSearchProvider):
|
|
|
181
182
|
if self.api_mode not in OPENAI_COMPATIBLE_API_MODES:
|
|
182
183
|
allowed = ", ".join(sorted(OPENAI_COMPATIBLE_API_MODES))
|
|
183
184
|
raise ValueError(source_message('Invalid OpenAI-compatible API mode: {0}. Supported values: {1}', self.api_mode, allowed))
|
|
185
|
+
# Responses tools such as web_search, sent with search() only (OPENAI_COMPATIBLE_TOOLS).
|
|
186
|
+
self.tools = list(tools or [])
|
|
184
187
|
self.last_transport_attempts: list[dict[str, Any]] = []
|
|
185
188
|
self._search_deadline_monotonic: float | None = None
|
|
186
189
|
|
|
@@ -263,6 +266,13 @@ class OpenAICompatibleSearchProvider(BaseSearchProvider):
|
|
|
263
266
|
]
|
|
264
267
|
return payload
|
|
265
268
|
|
|
269
|
+
def _build_search_payload(self, instructions: str, user_content: str, *, stream: bool) -> dict[str, Any]:
|
|
270
|
+
"""The search request; the only one that may carry Responses tools."""
|
|
271
|
+
payload = self._build_request_payload(instructions, user_content, stream=stream)
|
|
272
|
+
if self.api_mode == "responses" and self.tools:
|
|
273
|
+
payload["tools"] = [{"type": tool} for tool in self.tools]
|
|
274
|
+
return payload
|
|
275
|
+
|
|
266
276
|
async def search(self, query: str, platform: str = "", ctx=None) -> List[SearchResult]:
|
|
267
277
|
headers = self._build_api_headers()
|
|
268
278
|
platform_prompt = ""
|
|
@@ -272,7 +282,7 @@ class OpenAICompatibleSearchProvider(BaseSearchProvider):
|
|
|
272
282
|
|
|
273
283
|
time_context = get_local_time_info() + "\n"
|
|
274
284
|
|
|
275
|
-
payload = self.
|
|
285
|
+
payload = self._build_search_payload(
|
|
276
286
|
search_prompt,
|
|
277
287
|
time_context + query + platform_prompt,
|
|
278
288
|
stream=self.stream,
|
|
@@ -487,6 +497,7 @@ class OpenAICompatibleSearchProvider(BaseSearchProvider):
|
|
|
487
497
|
return await self._parse_responses_streaming_response(response, ctx)
|
|
488
498
|
|
|
489
499
|
content = ""
|
|
500
|
+
sources: list[dict] = []
|
|
490
501
|
full_body_buffer = []
|
|
491
502
|
|
|
492
503
|
async for line in response.aiter_lines():
|
|
@@ -507,6 +518,8 @@ class OpenAICompatibleSearchProvider(BaseSearchProvider):
|
|
|
507
518
|
delta = choices[0].get("delta", {})
|
|
508
519
|
if "content" in delta:
|
|
509
520
|
content += delta["content"]
|
|
521
|
+
if isinstance(delta, dict) and delta.get("annotations"):
|
|
522
|
+
sources = self._merge_citations(sources, self._normalize_chat_annotations(delta["annotations"]))
|
|
510
523
|
except (json.JSONDecodeError, IndexError):
|
|
511
524
|
continue
|
|
512
525
|
|
|
@@ -517,9 +530,11 @@ class OpenAICompatibleSearchProvider(BaseSearchProvider):
|
|
|
517
530
|
if "choices" in data and len(data["choices"]) > 0:
|
|
518
531
|
message = data["choices"][0].get("message", {})
|
|
519
532
|
content = message.get("content", "")
|
|
533
|
+
sources = self._merge_citations(sources, self._extract_citations(data))
|
|
520
534
|
except json.JSONDecodeError:
|
|
521
535
|
pass
|
|
522
536
|
|
|
537
|
+
content = self._append_sources(content, sources)
|
|
523
538
|
await log_info(ctx, f"content: {content}", config.debug_enabled)
|
|
524
539
|
|
|
525
540
|
return content
|
|
@@ -857,9 +872,24 @@ class OpenAICompatibleSearchProvider(BaseSearchProvider):
|
|
|
857
872
|
continue
|
|
858
873
|
message = choice.get("message")
|
|
859
874
|
if isinstance(message, dict):
|
|
860
|
-
sources = self._merge_citations(
|
|
875
|
+
sources = self._merge_citations(
|
|
876
|
+
sources,
|
|
877
|
+
self._normalize_citations(message.get("citations")),
|
|
878
|
+
self._normalize_chat_annotations(message.get("annotations")),
|
|
879
|
+
)
|
|
861
880
|
return sources
|
|
862
881
|
|
|
882
|
+
def _normalize_chat_annotations(self, annotations: Any) -> list[dict]:
|
|
883
|
+
"""Chat Completions search models nest each citation: {"type": "url_citation", "url_citation": {...}}."""
|
|
884
|
+
if not isinstance(annotations, list):
|
|
885
|
+
return []
|
|
886
|
+
citations = [
|
|
887
|
+
item.get("url_citation") if isinstance(item.get("url_citation"), dict) else item
|
|
888
|
+
for item in annotations
|
|
889
|
+
if isinstance(item, dict) and item.get("type") == "url_citation"
|
|
890
|
+
]
|
|
891
|
+
return self._normalize_citations(citations)
|
|
892
|
+
|
|
863
893
|
def _normalize_responses_citations(self, citations: Any) -> list[dict]:
|
|
864
894
|
if not citations:
|
|
865
895
|
return []
|
|
@@ -14,6 +14,7 @@ from .base import BaseSearchProvider
|
|
|
14
14
|
from .openai_compatible import OpenAICompatibleSearchProvider, _StopAfterSlowFailure, _StopAtDeadline, _WaitWithRetryAfter, _is_retryable_exception, get_local_time_info
|
|
15
15
|
from ..config import config
|
|
16
16
|
from ..logger import log_info
|
|
17
|
+
from ..provider_errors import ProviderCallError
|
|
17
18
|
from ..utils import search_prompt
|
|
18
19
|
|
|
19
20
|
|
|
@@ -115,7 +116,19 @@ class XAIResponsesSearchProvider(BaseSearchProvider):
|
|
|
115
116
|
async def search(self, query: str, platform: str = "", ctx=None) -> str:
|
|
116
117
|
payload = self._build_search_payload(query, platform)
|
|
117
118
|
await log_info(ctx, f"platform_prompt: {query}", config.debug_enabled)
|
|
118
|
-
|
|
119
|
+
try:
|
|
120
|
+
return await self._execute_response_with_retry(self._build_api_headers(), payload, ctx)
|
|
121
|
+
except httpx.HTTPStatusError as exc:
|
|
122
|
+
# The default XAI_TOOLS includes x_search, which only xAI serves; other Responses
|
|
123
|
+
# services (OpenAI, most relays) answer 400. Say which setting to change.
|
|
124
|
+
if exc.response.status_code == 400 and "x_search" in self.tools and "x_search" in (exc.response.text or ""):
|
|
125
|
+
host = urlsplit(self.api_url).netloc or self.api_url
|
|
126
|
+
raise ProviderCallError(
|
|
127
|
+
"parameter_error",
|
|
128
|
+
source_message('{0} 不支持 XAI_TOOLS 里的 x_search(HTTP 400)。x_search 只有 xAI 官方接口能用;把 XAI_TOOLS 改成 web_search 再试。', host),
|
|
129
|
+
additional_secrets=(self.api_key,),
|
|
130
|
+
) from exc
|
|
131
|
+
raise
|
|
119
132
|
|
|
120
133
|
async def _execute_response_with_retry(self, headers: dict, payload: dict, ctx=None) -> str:
|
|
121
134
|
timeout = self._request_timeout()
|
|
@@ -1656,7 +1656,7 @@ def _main_search_provider_configs(model_override: str = "", providers: str = "au
|
|
|
1656
1656
|
"model": model_override or config.openai_compatible_model,
|
|
1657
1657
|
"fallback_models": [] if model_override else config.openai_compatible_fallback_models,
|
|
1658
1658
|
"stream": config.openai_compatible_stream,
|
|
1659
|
-
"tools":
|
|
1659
|
+
"tools": config.openai_compatible_tools,
|
|
1660
1660
|
"source": "OPENAI_COMPATIBLE_*",
|
|
1661
1661
|
}
|
|
1662
1662
|
|
|
@@ -1690,6 +1690,7 @@ def _main_search_providers(provider_configs: list[dict[str, Any]], fallback: str
|
|
|
1690
1690
|
provider_config["model"],
|
|
1691
1691
|
provider_config.get("stream", False),
|
|
1692
1692
|
provider_config.get("api_mode", provider_config.get("mode", "chat-completions")),
|
|
1693
|
+
tools=provider_config.get("tools") or [],
|
|
1693
1694
|
)
|
|
1694
1695
|
)
|
|
1695
1696
|
return providers
|
|
@@ -2841,6 +2842,10 @@ def _diagnose_check_result(
|
|
|
2841
2842
|
return result
|
|
2842
2843
|
|
|
2843
2844
|
|
|
2845
|
+
def _rate_limited(*checks: dict[str, Any]) -> bool:
|
|
2846
|
+
return any(check.get("http_status") == 429 for check in checks)
|
|
2847
|
+
|
|
2848
|
+
|
|
2844
2849
|
def _openai_compatible_diagnosis(quick: dict[str, Any], no_stream: dict[str, Any], stream: dict[str, Any]) -> tuple[bool, str, str]:
|
|
2845
2850
|
quick_ok = quick.get("status") == "ok"
|
|
2846
2851
|
no_stream_ok = no_stream.get("status") == "ok"
|
|
@@ -2853,6 +2858,12 @@ def _openai_compatible_diagnosis(quick: dict[str, Any], no_stream: dict[str, Any
|
|
|
2853
2858
|
source_message('OpenAI-compatible 主链路正常。'),
|
|
2854
2859
|
source_message('真实 search 形态的 stream=false 和 stream=true 都能返回。若用户仍卡住,更可能是调用方、PATH、超时设置或上游偶发波动。'),
|
|
2855
2860
|
)
|
|
2861
|
+
if _rate_limited(quick, no_stream, stream):
|
|
2862
|
+
return (
|
|
2863
|
+
False,
|
|
2864
|
+
source_message('OpenAI-compatible 被上游限流(HTTP 429),这次诊断判断不了链路是否正常。'),
|
|
2865
|
+
source_message('等一分钟再运行本诊断;经常出现就降低并发、换模型,或提高账户的速率额度。地址、key 和模型名不一定有问题。'),
|
|
2866
|
+
)
|
|
2856
2867
|
if stream_ok and not no_stream_ok:
|
|
2857
2868
|
return (
|
|
2858
2869
|
False,
|
|
@@ -2892,11 +2903,12 @@ async def _probe_openai_compatible_search_shape(
|
|
|
2892
2903
|
stream: bool,
|
|
2893
2904
|
timeout_seconds: float,
|
|
2894
2905
|
api_mode: str = "chat-completions",
|
|
2906
|
+
tools: list[str] | None = None,
|
|
2895
2907
|
) -> dict[str, Any]:
|
|
2896
2908
|
name = f"真实 {api_mode} search 请求 (stream={'true' if stream else 'false'})"
|
|
2897
2909
|
start = time.time()
|
|
2898
|
-
provider = OpenAICompatibleSearchProvider(api_url, api_key, model, stream=stream, api_mode=api_mode)
|
|
2899
|
-
payload = provider.
|
|
2910
|
+
provider = OpenAICompatibleSearchProvider(api_url, api_key, model, stream=stream, api_mode=api_mode, tools=tools)
|
|
2911
|
+
payload = provider._build_search_payload(search_prompt, get_local_time_info() + "\nping", stream=stream)
|
|
2900
2912
|
headers = provider._build_api_headers()
|
|
2901
2913
|
headers["User-Agent"] = "smart-search/diagnose"
|
|
2902
2914
|
timeout = httpx.Timeout(connect=6.0, read=timeout_seconds, write=10.0, pool=None)
|
|
@@ -2996,10 +3008,11 @@ async def _probe_openai_compatible_web_search(
|
|
|
2996
3008
|
api_mode: str,
|
|
2997
3009
|
stream: bool,
|
|
2998
3010
|
timeout_seconds: float,
|
|
3011
|
+
tools: list[str] | None = None,
|
|
2999
3012
|
) -> dict:
|
|
3000
3013
|
name = "联网搜索能力"
|
|
3001
3014
|
start = time.time()
|
|
3002
|
-
provider = OpenAICompatibleSearchProvider(api_url, api_key, model, stream=stream, api_mode=api_mode)
|
|
3015
|
+
provider = OpenAICompatibleSearchProvider(api_url, api_key, model, stream=stream, api_mode=api_mode, tools=tools)
|
|
3003
3016
|
answer = ""
|
|
3004
3017
|
try:
|
|
3005
3018
|
content = await asyncio.wait_for(provider.search("What is the latest stable Python release? Cite the source URL."), timeout_seconds)
|
|
@@ -3026,16 +3039,15 @@ async def diagnose_openai_compatible(timeout_seconds: float = 30.0) -> dict[str,
|
|
|
3026
3039
|
api_key = config.openai_compatible_api_key
|
|
3027
3040
|
model = config.openai_compatible_model
|
|
3028
3041
|
info = config.config_path_info()
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
except ValueError as e:
|
|
3042
|
+
|
|
3043
|
+
def invalid_config(error: Exception, configured_api_mode: str, summary: str, recommendation: str) -> dict[str, Any]:
|
|
3032
3044
|
return {
|
|
3033
3045
|
"ok": False,
|
|
3034
3046
|
"provider": "openai-compatible",
|
|
3035
3047
|
"api_url": api_url or "未配置",
|
|
3036
3048
|
"api_key": config._mask_api_key(api_key) if api_key else "未配置",
|
|
3037
3049
|
"model": model,
|
|
3038
|
-
"configured_api_mode":
|
|
3050
|
+
"configured_api_mode": configured_api_mode,
|
|
3039
3051
|
"configured_stream": config.openai_compatible_stream,
|
|
3040
3052
|
"timeout_seconds": timeout_seconds,
|
|
3041
3053
|
"config_file": info.get("config_file", ""),
|
|
@@ -3043,11 +3055,23 @@ async def diagnose_openai_compatible(timeout_seconds: float = 30.0) -> dict[str,
|
|
|
3043
3055
|
"checks": [],
|
|
3044
3056
|
"next_command": OPENAI_COMPATIBLE_DIAGNOSE_COMMAND,
|
|
3045
3057
|
"error_type": "parameter_error",
|
|
3046
|
-
"error": str(
|
|
3047
|
-
"summary":
|
|
3048
|
-
"recommendation":
|
|
3058
|
+
"error": str(error),
|
|
3059
|
+
"summary": summary,
|
|
3060
|
+
"recommendation": recommendation,
|
|
3049
3061
|
"elapsed_ms": _elapsed_ms(start),
|
|
3050
3062
|
}
|
|
3063
|
+
|
|
3064
|
+
try:
|
|
3065
|
+
api_mode = config.openai_compatible_api_mode
|
|
3066
|
+
except ValueError as e:
|
|
3067
|
+
return invalid_config(e, "invalid", source_message('OpenAI-compatible API mode is invalid.'),
|
|
3068
|
+
source_message('Set OPENAI_COMPATIBLE_API_MODE to chat-completions or responses.'))
|
|
3069
|
+
try:
|
|
3070
|
+
tools = config.openai_compatible_tools
|
|
3071
|
+
except ValueError as e:
|
|
3072
|
+
return invalid_config(e, api_mode, source_message('OpenAI-compatible tools are invalid.'),
|
|
3073
|
+
source_message('Use OPENAI_COMPATIBLE_TOOLS=web_search (or web_search_preview) only with '
|
|
3074
|
+
'OPENAI_COMPATIBLE_API_MODE=responses; otherwise leave it empty.'))
|
|
3051
3075
|
endpoint = openai_compatible_endpoint(api_url or "", api_mode)
|
|
3052
3076
|
result: dict[str, Any] = {
|
|
3053
3077
|
"ok": False,
|
|
@@ -3056,6 +3080,7 @@ async def diagnose_openai_compatible(timeout_seconds: float = 30.0) -> dict[str,
|
|
|
3056
3080
|
"api_key": config._mask_api_key(api_key) if api_key else "未配置",
|
|
3057
3081
|
"model": model,
|
|
3058
3082
|
"configured_api_mode": api_mode,
|
|
3083
|
+
"configured_tools": tools,
|
|
3059
3084
|
"endpoint": endpoint if api_url else "",
|
|
3060
3085
|
"configured_stream": config.openai_compatible_stream,
|
|
3061
3086
|
"timeout_seconds": timeout_seconds,
|
|
@@ -3104,18 +3129,21 @@ async def diagnose_openai_compatible(timeout_seconds: float = 30.0) -> dict[str,
|
|
|
3104
3129
|
"has_content": bool(quick.get("has_content", quick.get("status") == "ok")),
|
|
3105
3130
|
}
|
|
3106
3131
|
result["checks"].append(quick_check)
|
|
3107
|
-
|
|
3132
|
+
# Probes send what a real search sends, tools included; the quick check above never does.
|
|
3133
|
+
tool_kwargs = {"tools": tools} if tools else {}
|
|
3134
|
+
probe_kwargs = {"stream": False, "timeout_seconds": timeout_seconds, **tool_kwargs}
|
|
3108
3135
|
if api_mode != "chat-completions":
|
|
3109
3136
|
probe_kwargs["api_mode"] = api_mode
|
|
3110
3137
|
no_stream = await _probe_openai_compatible_search_shape(api_url, api_key, model, **probe_kwargs)
|
|
3111
3138
|
result["checks"].append(no_stream)
|
|
3112
|
-
probe_kwargs = {"stream": True, "timeout_seconds": timeout_seconds}
|
|
3139
|
+
probe_kwargs = {"stream": True, "timeout_seconds": timeout_seconds, **tool_kwargs}
|
|
3113
3140
|
if api_mode != "chat-completions":
|
|
3114
3141
|
probe_kwargs["api_mode"] = api_mode
|
|
3115
3142
|
stream = await _probe_openai_compatible_search_shape(api_url, api_key, model, **probe_kwargs)
|
|
3116
3143
|
result["checks"].append(stream)
|
|
3117
3144
|
web = await _probe_openai_compatible_web_search(api_url, api_key, model, api_mode=api_mode,
|
|
3118
|
-
stream=config.openai_compatible_stream, timeout_seconds=timeout_seconds
|
|
3145
|
+
stream=config.openai_compatible_stream, timeout_seconds=timeout_seconds,
|
|
3146
|
+
**tool_kwargs)
|
|
3119
3147
|
result["checks"].append(web)
|
|
3120
3148
|
result["web_search_capable"] = web["status"] == "ok"
|
|
3121
3149
|
|
|
@@ -3144,14 +3172,18 @@ async def diagnose_openai_compatible(timeout_seconds: float = 30.0) -> dict[str,
|
|
|
3144
3172
|
result["timeout_policy"] = OPENAI_COMPATIBLE_TIMEOUT_POLICY
|
|
3145
3173
|
|
|
3146
3174
|
ok, summary, recommendation = _openai_compatible_diagnosis(quick_check, no_stream, stream)
|
|
3147
|
-
|
|
3148
|
-
|
|
3175
|
+
# Under a rate limit the web probe fails for the same reason; its advice would mislead.
|
|
3176
|
+
rate_limited = not ok and _rate_limited(quick_check, no_stream, stream)
|
|
3177
|
+
if web["status"] != "ok" and not rate_limited and tools:
|
|
3178
|
+
recommendation = source_message('{0} 另外,联网搜索能力检查未通过:{1}。请求里已经带了 {2} 工具,可能是这个模型或中转站不支持 Responses 的联网工具;换一个支持的模型,或清空 OPENAI_COMPATIBLE_TOOLS 改用自带搜索的模型。', recommendation, web['message'], ", ".join(tools))
|
|
3179
|
+
elif web["status"] != "ok" and not rate_limited:
|
|
3180
|
+
recommendation = source_message('{0} 另外,联网搜索能力检查未通过:{1}。如果这个模型只能通过 Responses 的 web_search 工具联网(例如 OpenAI 官方的 gpt-6.1-sol),请设置 OPENAI_COMPATIBLE_API_MODE=responses 和 OPENAI_COMPATIBLE_TOOLS=web_search;中转站只认旧名时改用 web_search_preview。', recommendation, web['message'])
|
|
3149
3181
|
if inventory.get("status") == "warning":
|
|
3150
3182
|
recommendation = source_message('{0} 另外,{1}。兜底只在主模型硬失败后接力,不会再把主模型砍成 30 秒。', recommendation, inventory['message'])
|
|
3151
3183
|
result.update(
|
|
3152
3184
|
{
|
|
3153
3185
|
"ok": ok,
|
|
3154
|
-
"error_type": "" if ok else "network_error",
|
|
3186
|
+
"error_type": "" if ok else "rate_limited" if rate_limited else "network_error",
|
|
3155
3187
|
"error": "" if ok else summary,
|
|
3156
3188
|
"summary": summary,
|
|
3157
3189
|
"recommendation": recommendation,
|
|
@@ -3906,6 +3938,7 @@ async def doctor() -> dict[str, Any]:
|
|
|
3906
3938
|
def current_model() -> dict[str, Any]:
|
|
3907
3939
|
try:
|
|
3908
3940
|
api_mode = config.openai_compatible_api_mode
|
|
3941
|
+
tools = config.openai_compatible_tools
|
|
3909
3942
|
except ValueError as e:
|
|
3910
3943
|
return {
|
|
3911
3944
|
"ok": False,
|
|
@@ -3920,6 +3953,7 @@ def current_model() -> dict[str, Any]:
|
|
|
3920
3953
|
"openai_compatible_model": config.openai_compatible_model,
|
|
3921
3954
|
"openai_compatible_fallback_models": config.openai_compatible_fallback_models,
|
|
3922
3955
|
"openai_compatible_api_mode": api_mode,
|
|
3956
|
+
"openai_compatible_tools": tools,
|
|
3923
3957
|
"config_file": str(config.config_file),
|
|
3924
3958
|
}
|
|
3925
3959
|
|
|
@@ -13,6 +13,9 @@ from .utils import NO_WEB_SEARCH_MARKER, extract_unique_urls
|
|
|
13
13
|
|
|
14
14
|
_MD_LINK_PATTERN = re.compile(r"\[([^\]]+)\]\((https?://[^)]+)\)")
|
|
15
15
|
_INLINE_CITATION_LINK_PATTERN = re.compile(r"\[\[(\d+)\]\]\((https?://[^)]+)\)")
|
|
16
|
+
# How OpenAI search models cite at the end of a sentence or bullet: "... in 2026. ([python.org](https://...))"
|
|
17
|
+
_PARENTHESIZED_CITATION_PATTERN = re.compile(r"[((]\s*\[[^\]]+\]\(https?://[^)\s]+\)\s*[))]")
|
|
18
|
+
_BARE_OR_ANGLE_URL_PATTERN = re.compile(r"<?https?://[^\s>]+>?")
|
|
16
19
|
_SOURCES_HEADING_PATTERN = re.compile(
|
|
17
20
|
r"(?im)^"
|
|
18
21
|
r"(?:#{1,6}\s*)?"
|
|
@@ -338,11 +341,20 @@ def _is_link_only_line(line: str) -> bool:
|
|
|
338
341
|
return False
|
|
339
342
|
if stripped.startswith(("http://", "https://")):
|
|
340
343
|
return True
|
|
344
|
+
citation = _PARENTHESIZED_CITATION_PATTERN.search(stripped)
|
|
345
|
+
if citation and _has_prose(stripped[: citation.start()]):
|
|
346
|
+
# A sentence that ends with its citation belongs to the answer, not to a trailing source list.
|
|
347
|
+
return False
|
|
341
348
|
if _MD_LINK_PATTERN.search(stripped):
|
|
342
349
|
return True
|
|
343
350
|
return False
|
|
344
351
|
|
|
345
352
|
|
|
353
|
+
def _has_prose(text: str) -> bool:
|
|
354
|
+
text = _BARE_OR_ANGLE_URL_PATTERN.sub("", _MD_LINK_PATTERN.sub("", text))
|
|
355
|
+
return re.search(r"[^\W\d_]", text) is not None
|
|
356
|
+
|
|
357
|
+
|
|
346
358
|
def _parse_sources_payload(payload: str) -> list[dict]:
|
|
347
359
|
payload = (payload or "").strip().rstrip(";")
|
|
348
360
|
if not payload:
|
|
@@ -198,11 +198,17 @@ CONFIG_FIELDS: tuple[ConfigField, ...] = (
|
|
|
198
198
|
provider="openai-compatible", capabilities=("main_search",)),
|
|
199
199
|
_f(key="OPENAI_COMPATIBLE_API_MODE", section="getting_started", tier="advanced", kind="enum",
|
|
200
200
|
label_zh="接口模式", label_en="API mode",
|
|
201
|
-
help_zh="大多数服务用 chat-completions。",
|
|
202
|
-
help_en="Most services want chat-completions.",
|
|
201
|
+
help_zh="大多数服务用 chat-completions。要让 gpt-6.1-sol 这类只在 Responses 里联网的模型搜索,选 responses,再在下面的联网工具填 web_search。",
|
|
202
|
+
help_en="Most services want chat-completions. For models such as gpt-6.1-sol that only search through Responses, choose responses and set the tools below to web_search.",
|
|
203
203
|
default=Config._DEFAULT_OPENAI_COMPATIBLE_API_MODE,
|
|
204
204
|
choices=tuple(sorted(Config._ALLOWED_OPENAI_COMPATIBLE_API_MODES)),
|
|
205
205
|
provider="openai-compatible", capabilities=("main_search",)),
|
|
206
|
+
_f(key="OPENAI_COMPATIBLE_TOOLS", section="getting_started", tier="advanced", kind="csv",
|
|
207
|
+
label_zh="联网工具", label_en="Responses tools",
|
|
208
|
+
help_zh="只在接口模式为 responses 时可用,只随搜索请求发送。用 OpenAI 官方 key 跑 gpt-6.1-sol 这类模型要联网就填 web_search;中转站只认旧名时填 web_search_preview;模型自带搜索(如 gpt-5-search-api)就留空。",
|
|
209
|
+
help_en="Only with API mode responses, and sent with search requests only. Set web_search to let models such as gpt-6.1-sol search with an official OpenAI key; use web_search_preview for relays that only know the older name; leave empty for models that search on their own, such as gpt-5-search-api.",
|
|
210
|
+
choices=tuple(sorted(Config._ALLOWED_OPENAI_COMPATIBLE_TOOLS)), placeholder="web_search",
|
|
211
|
+
provider="openai-compatible", capabilities=("main_search",)),
|
|
206
212
|
_f(key="OPENAI_COMPATIBLE_STREAM", section="getting_started", tier="advanced", kind="bool",
|
|
207
213
|
label_zh="流式返回", label_en="Stream responses",
|
|
208
214
|
help_zh="部分服务只在流式下才肯长时间思考。", help_en="Some services only think for long under streaming.",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import TYPE_CHECKING, List
|
|
2
2
|
import re
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
# A runtime import would load providers/__init__.py, whose providers import this
|
|
6
|
+
# module back: importing smart_search.sources first then fails halfway.
|
|
7
|
+
from .providers.base import SearchResult
|
|
4
8
|
|
|
5
9
|
_URL_PATTERN = re.compile(r'https?://[^\s<>"\'`,。、;:!?》)】\)]+')
|
|
6
10
|
|
|
@@ -52,7 +56,7 @@ def format_extra_sources(tavily_results: list[dict] | None, firecrawl_results: l
|
|
|
52
56
|
return "\n\n".join(sections)
|
|
53
57
|
|
|
54
58
|
|
|
55
|
-
def format_search_results(results: List[SearchResult]) -> str:
|
|
59
|
+
def format_search_results(results: List["SearchResult"]) -> str:
|
|
56
60
|
if not results:
|
|
57
61
|
return "No results found."
|
|
58
62
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smart-search
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.33
|
|
4
4
|
Summary: CLI-first multi-source web research with xAI/OpenAI-compatible search, Exa, Context7, Zhipu, Tavily, Firecrawl, and Deep Research planning.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
../../../bin/smart-search,sha256=3Si0-mMx4MCMjCfKNF0xeBuTy8aXkDufWgG0psj8uJs,205
|
|
2
|
-
smart_search-0.1.
|
|
3
|
-
smart_search-0.1.
|
|
4
|
-
smart_search-0.1.
|
|
5
|
-
smart_search-0.1.
|
|
6
|
-
smart_search-0.1.
|
|
7
|
-
smart_search-0.1.
|
|
8
|
-
smart_search-0.1.
|
|
9
|
-
smart_search-0.1.
|
|
10
|
-
smart_search-0.1.
|
|
2
|
+
smart_search-0.1.33.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
3
|
+
smart_search-0.1.33.dist-info/METADATA,sha256=-ozSu6zVh4IbV38fr3DJwS4p7MS1CXgSepI5FXCSHw4,19724
|
|
4
|
+
smart_search-0.1.33.dist-info/RECORD,,
|
|
5
|
+
smart_search-0.1.33.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
smart_search-0.1.33.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
7
|
+
smart_search-0.1.33.dist-info/direct_url.json,sha256=lFRY6ow2tINE_t60B1wCO0WZXfSjjC9GUsp1Y2OjrAY,75
|
|
8
|
+
smart_search-0.1.33.dist-info/entry_points.txt,sha256=fiHguhz7hQ6ybeepO0OPpZm19XCi2J-Bkigf8DGBuKM,55
|
|
9
|
+
smart_search-0.1.33.dist-info/licenses/LICENSE,sha256=o_zbH827gkTiZfsNLJGQn1z8Z4eLAvb8heFaSqSGVv8,1067
|
|
10
|
+
smart_search-0.1.33.dist-info/top_level.txt,sha256=mBPprcb_b3tBApeRa2nlLaGj3UVNxXCdyz6QQvDzJpc,13
|
|
11
11
|
smart_search/__init__.py,sha256=da1PTClDMl-IBkrSvq6JC1lnS-K_BASzCvxVhNxN5Ls,13
|
|
12
12
|
smart_search/__pycache__/__init__.cpython-313.pyc,,
|
|
13
13
|
smart_search/__pycache__/activity.cpython-313.pyc,,
|
|
@@ -40,19 +40,19 @@ smart_search/__pycache__/ui_metadata.cpython-313.pyc,,
|
|
|
40
40
|
smart_search/__pycache__/ui_server.cpython-313.pyc,,
|
|
41
41
|
smart_search/__pycache__/utils.cpython-313.pyc,,
|
|
42
42
|
smart_search/activity.py,sha256=66yPPAfuaDSF4UTEuRRDefp2m1-l8eVnr8K5_ShoQCU,13368
|
|
43
|
-
smart_search/assets/i18n/messages.json,sha256=
|
|
44
|
-
smart_search/assets/skills/smart-search-cli/SKILL.md,sha256=
|
|
43
|
+
smart_search/assets/i18n/messages.json,sha256=4ovku0BT4KouTk0g58b_jX3RqXvBDFlWFxDo-DSGI38,653887
|
|
44
|
+
smart_search/assets/skills/smart-search-cli/SKILL.md,sha256=gQ5HpNRnZ7VVYCu_MnyfAla62vQA2m6wl6mlvjfNLo8,8736
|
|
45
45
|
smart_search/assets/skills/smart-search-cli/agents/openai.yaml,sha256=4S1VuEER7Puw9JvxDsyI__Tdrr1j2h6aF26yHho_x8E,162
|
|
46
46
|
smart_search/assets/skills/smart-search-cli/references/cli-contract.md,sha256=mZyQRioJ4_ySkxcl9WQBMGXk5fcWOEMBMnXXUGnRoEA,1843
|
|
47
|
-
smart_search/assets/skills/smart-search-cli/references/cli-core.md,sha256=
|
|
48
|
-
smart_search/assets/skills/smart-search-cli/references/command-patterns.md,sha256=
|
|
47
|
+
smart_search/assets/skills/smart-search-cli/references/cli-core.md,sha256=QusO29X5_psX-K2I67rUlz3cMqyuOyFZU6MnWIMMJ0Y,15240
|
|
48
|
+
smart_search/assets/skills/smart-search-cli/references/command-patterns.md,sha256=TC3HCEgTwrQf1w5AbmhhPqJRpCJHyyf9jTpCmfdy3WE,12902
|
|
49
49
|
smart_search/assets/skills/smart-search-cli/references/deep-research-mode.md,sha256=Yt1qHQnbpKgA6P5PTXwv4qFDZTJn7MqarZvCqlmCg-0,14485
|
|
50
|
-
smart_search/assets/skills/smart-search-cli/references/provider-routing.md,sha256=
|
|
50
|
+
smart_search/assets/skills/smart-search-cli/references/provider-routing.md,sha256=zSEOTKBpiIC_vTXCRTU5wiwcGmnGFzZbd_LgBjqdbko,30231
|
|
51
51
|
smart_search/assets/skills/smart-search-cli/references/regression-release.md,sha256=SkOUwxpPm_HeBGlpUil5ybTBiwa-wAzt-765zw9kYqk,5673
|
|
52
|
-
smart_search/assets/skills/smart-search-cli/references/setup-config.md,sha256=
|
|
52
|
+
smart_search/assets/skills/smart-search-cli/references/setup-config.md,sha256=lqXOSoIs0dGV7uamLeEvaLHX0MNQoMNmuKIyvFFEcuw,18964
|
|
53
53
|
smart_search/assets/ui/index.html,sha256=i3se6JX5iHnGXIEXhHvLKZgPzNh8JMJAeRqfIlqac3U,63800
|
|
54
|
-
smart_search/cli.py,sha256=
|
|
55
|
-
smart_search/config.py,sha256=
|
|
54
|
+
smart_search/cli.py,sha256=kA3-roJfQsIjPHj81aA-7qXcJLEDkdxPDrYBrTZP9-M,169589
|
|
55
|
+
smart_search/config.py,sha256=hcK15frDWVyHFCrTMaejfM3cSPNO1ntkP0AOCfNm-tI,61256
|
|
56
56
|
smart_search/desktop_backend.py,sha256=VYFc83YBllSljgbgx9oPPNbQLwGADZGVtN_CF08ShD8,42417
|
|
57
57
|
smart_search/desktop_catalog.py,sha256=VMIsLFAD98Czetwy-ka606QnyUSk0gDBhsfz7k4QDl8,11485
|
|
58
58
|
smart_search/desktop_cli.py,sha256=dpXLwf6b9Dgw9lKPTDZpKH1Z85OpIgrYyNJM2hXIu68,11069
|
|
@@ -64,7 +64,7 @@ smart_search/desktop_worker.py,sha256=Nkw_FMNNejrt8oJ6aNUmziSd7vVu_Xy9ijvfgstdaw
|
|
|
64
64
|
smart_search/i18n.py,sha256=-mDfpi-P4AQPCUpnuI--W9c1AFurN5QPhAGYMXB02kU,4869
|
|
65
65
|
smart_search/intent_router.py,sha256=UkFnv4R2xC6bF6QEn_lqSYqYOoWgoT_mXeG4CNcSJ3M,7931
|
|
66
66
|
smart_search/jev.py,sha256=C80OBG2RMOJCMtogltVDHUg0xoCHO_8uIT69736myq8,27169
|
|
67
|
-
smart_search/jev_search.py,sha256=
|
|
67
|
+
smart_search/jev_search.py,sha256=k2e5nkmB3tx9wSU0784yAR3QiwsbUubqWh8y-DJiW3A,49881
|
|
68
68
|
smart_search/logger.py,sha256=pwfjP50ywoTRirH4H7qYTNgt4TgoMhZlX2pb8dNvO9k,1203
|
|
69
69
|
smart_search/provider_descriptions.py,sha256=PISS5lhXG4nLtr8xW8Y2_Okcc21SwSWPPTmdLOhsOfY,11027
|
|
70
70
|
smart_search/provider_errors.py,sha256=IXyLo6Kc4WSP7faH5mMTQyAIVKR0BR0680qd0ZApKUo,5384
|
|
@@ -90,20 +90,20 @@ smart_search/providers/context7.py,sha256=zOImhEiKUVg60h9SbYdJRBSUfoVvS-8N6O62su
|
|
|
90
90
|
smart_search/providers/deepseek.py,sha256=br_XHY0lc5xvgppp_4dPd_g5UU9jzEVCronn_UvyoH4,4678
|
|
91
91
|
smart_search/providers/exa.py,sha256=6BLnSxDLM0PKXlkcqRKw49G728yBkRicDzQiJt2vIzo,6875
|
|
92
92
|
smart_search/providers/jina.py,sha256=hz8KAcFSstWT4DIBWKdEhymYjBjUvo_SFeZcwCdPkTs,4048
|
|
93
|
-
smart_search/providers/openai_compatible.py,sha256=
|
|
93
|
+
smart_search/providers/openai_compatible.py,sha256=NmrEjO8vj6R_SDdQFAvv320DX2MLQg6NoEAFfhOwRVc,43120
|
|
94
94
|
smart_search/providers/sciverse.py,sha256=kxhhkx4K08H7VGf4RWFdnB-dlmeEelDM2OcEwERTr8c,13567
|
|
95
95
|
smart_search/providers/tinyfish.py,sha256=rdEjmPK_dDe-aD5M5sfJM6n2MA_uBuE2NVR6i7lKBqI,8446
|
|
96
|
-
smart_search/providers/xai_responses.py,sha256=
|
|
96
|
+
smart_search/providers/xai_responses.py,sha256=PL_1SOMWKm6BWwWgdjMntdN_VoFckXly1ye8fr4gBQ8,11485
|
|
97
97
|
smart_search/providers/yuanbao.py,sha256=CsbL1iEson5lAbIRyuv2Rai0Nm6LiHPZTq8WhL3wUbE,6357
|
|
98
98
|
smart_search/providers/zhipu.py,sha256=PN0_Vc51rDYlxEmz2B1vgSxEt9jXf_RIsRobGp-LBPs,7088
|
|
99
99
|
smart_search/providers/zhipu_mcp.py,sha256=4XgckrHzwSdhUtMlTSvhVxrAA7Fr7QX_4BYWfSHmXUg,10976
|
|
100
100
|
smart_search/sciverse_schema.py,sha256=ml5iDXEpY7cnEMz74BGTo4yxX7UvPOe_i7w4Pi5t1wY,18178
|
|
101
|
-
smart_search/service.py,sha256=
|
|
101
|
+
smart_search/service.py,sha256=Viq6Of5jyB0XwQPAa_EOk8-J8gIpw7nyTGzaIgER4nk,197746
|
|
102
102
|
smart_search/skill_installer.py,sha256=CvM9GQShaTR8ttNhilx8Up0vVsHZfsnYD5uM7wZrzBs,21782
|
|
103
103
|
smart_search/skill_maintenance.py,sha256=2s2-1ZEEjRg2MfRKlaw6Y51OO86kOf9axl7fW6KMv0g,8818
|
|
104
|
-
smart_search/sources.py,sha256=
|
|
104
|
+
smart_search/sources.py,sha256=uGRPMZ9wnNrkkby4edQkPSwBxJfiyfgtaDdKsQ9rdTA,15067
|
|
105
105
|
smart_search/state_files.py,sha256=K02dlR3Xuj2vIkRiEdrFr3OSiEsPxN5_HSo4NTmz0sc,2063
|
|
106
106
|
smart_search/ui_api.py,sha256=p73zozirtzu7u8AQS4uWTCbsbomkbkxCExs_mE1_HW4,10043
|
|
107
|
-
smart_search/ui_metadata.py,sha256=
|
|
107
|
+
smart_search/ui_metadata.py,sha256=oIKurTP3gdHtOS5sHcOcr3L-kS38t1V_B5axO9cKiYg,55094
|
|
108
108
|
smart_search/ui_server.py,sha256=pcAPiYWmJXTmX1Xpy12v97JhkbRx-nJ3zRYefGW1Tc0,20508
|
|
109
|
-
smart_search/utils.py,sha256=
|
|
109
|
+
smart_search/utils.py,sha256=2b1Wy1Jk0loJ1MainS1N8WYjtMXwTQ9JlVOra3gYhEY,9193
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@konbakuyomu/smart-search",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "CLI-first multi-source web research with xAI/OpenAI-compatible search, Exa, Context7, Zhipu, Tavily, Firecrawl, and Deep Research planning.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/konbakuyomu/smartsearch#readme",
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
},
|
|
40
40
|
"smartSearchBinary": true,
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@konbakuyomu/smart-search-darwin-x64": "0.1.
|
|
43
|
-
"@konbakuyomu/smart-search-darwin-arm64": "0.1.
|
|
44
|
-
"@konbakuyomu/smart-search-win32-x64": "0.1.
|
|
45
|
-
"@konbakuyomu/smart-search-win32-arm64": "0.1.
|
|
46
|
-
"@konbakuyomu/smart-search-linux-x64": "0.1.
|
|
47
|
-
"@konbakuyomu/smart-search-linux-arm64": "0.1.
|
|
42
|
+
"@konbakuyomu/smart-search-darwin-x64": "0.1.33",
|
|
43
|
+
"@konbakuyomu/smart-search-darwin-arm64": "0.1.33",
|
|
44
|
+
"@konbakuyomu/smart-search-win32-x64": "0.1.33",
|
|
45
|
+
"@konbakuyomu/smart-search-win32-arm64": "0.1.33",
|
|
46
|
+
"@konbakuyomu/smart-search-linux-x64": "0.1.33",
|
|
47
|
+
"@konbakuyomu/smart-search-linux-arm64": "0.1.33"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|