pi-web-search 1.4.0 → 1.5.0
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/README.md +3 -2
- package/package.json +4 -2
- package/src/api.ts +33 -18
- package/src/index.ts +1 -1
- package/src/utils.ts +3 -5
- package/src/web_search.ts +10 -8
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-web-search
|
|
2
2
|
|
|
3
|
-
Provider-native web search for [pi](https://pi.dev) with Gemini + URL Context, OpenAI Responses variants, and Anthropic.
|
|
3
|
+
Provider-native web search for [pi](https://pi.dev) with Gemini + URL Context, xAI Grok, OpenAI Responses variants, and Anthropic.
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ Search the web using your currently selected model. Automatically picks the righ
|
|
|
11
11
|
| Provider | API |
|
|
12
12
|
|---|---|
|
|
13
13
|
| Google Gemini | Grounding with Google Search |
|
|
14
|
+
| xAI Grok | Responses API `web_search` |
|
|
14
15
|
| OpenAI | Responses API web search |
|
|
15
16
|
| Azure OpenAI | Responses API web search (`azure-openai-responses`) |
|
|
16
17
|
| OpenAI Codex | Codex Responses API web search (`openai-codex-responses`) |
|
|
@@ -37,7 +38,7 @@ pi install npm:pi-web-search
|
|
|
37
38
|
|
|
38
39
|
No extra config needed. Select a supported current model in pi and the tools auto-detect the matching provider API.
|
|
39
40
|
|
|
40
|
-
`web_search` will not scan configured models and pick one automatically when the current model does not support native search. To use a dedicated search model, opt in explicitly with
|
|
41
|
+
`web_search` will not scan configured models and pick one automatically when the current model does not support native search. To use a dedicated search model, opt in explicitly with `web-search.json` in pi's agent directory (by default `~/.pi/agent/`; respects `PI_CODING_AGENT_DIR`):
|
|
41
42
|
|
|
42
43
|
```json
|
|
43
44
|
{
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-search",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Provider-native web search for pi: Gemini + URL Context, OpenAI Responses (Azure/Codex/Copilot), and Anthropic",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Provider-native web search for pi: Gemini + URL Context, xAI Grok, OpenAI Responses (Azure/Codex/Copilot), and Anthropic",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"keywords": [
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"web-search",
|
|
12
12
|
"gemini",
|
|
13
13
|
"google",
|
|
14
|
+
"xai",
|
|
15
|
+
"grok",
|
|
14
16
|
"openai",
|
|
15
17
|
"anthropic",
|
|
16
18
|
"url-context",
|
package/src/api.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { TextEncoder, TextDecoder } from "util";
|
|
|
5
5
|
|
|
6
6
|
// --- Provider Configuration ---
|
|
7
7
|
|
|
8
|
-
type ProviderKind = "google" | "openai" | "anthropic" | "unsupported";
|
|
8
|
+
type ProviderKind = "google" | "openai" | "xai" | "anthropic" | "unsupported";
|
|
9
9
|
|
|
10
10
|
type GoogleRequestBuilder = (model: Model<Api>, body: any) => { url: string; headers: Record<string, string>; body: any };
|
|
11
11
|
|
|
@@ -34,6 +34,7 @@ const GOOGLE_PROVIDERS: Record<string, ProviderConfig> = {
|
|
|
34
34
|
|
|
35
35
|
export function getProviderKind(model: Model<Api>): ProviderKind {
|
|
36
36
|
if (GOOGLE_PROVIDERS[model.provider] || GOOGLE_PROVIDERS[model.api]) return "google";
|
|
37
|
+
if (model.provider === "xai" && model.api === "openai-responses") return "xai";
|
|
37
38
|
if (
|
|
38
39
|
model.api === "openai-responses"
|
|
39
40
|
|| model.api === "azure-openai-responses"
|
|
@@ -464,6 +465,14 @@ async function resolveGoogleGroundingRedirectUrls(searchResults: SearchResultDet
|
|
|
464
465
|
}
|
|
465
466
|
}
|
|
466
467
|
|
|
468
|
+
function preserveInlineCitations(text: string, citations: Array<{ endIndex?: number; title: string; url: string }>): { text: string; sources: Source[] } {
|
|
469
|
+
const sources: Source[] = [];
|
|
470
|
+
for (const citation of citations) {
|
|
471
|
+
if (citation.url) pushUniqueSource(sources, { title: citation.title, url: citation.url });
|
|
472
|
+
}
|
|
473
|
+
return { text, sources };
|
|
474
|
+
}
|
|
475
|
+
|
|
467
476
|
function applyIndexCitations(text: string, citations: Array<{ endIndex?: number; title: string; url: string }>): { text: string; sources: Source[] } {
|
|
468
477
|
const sources: Source[] = [];
|
|
469
478
|
const insertions = citations
|
|
@@ -682,21 +691,25 @@ async function callOpenAIStream(
|
|
|
682
691
|
}
|
|
683
692
|
const requestHeaders = Object.fromEntries(headers.entries());
|
|
684
693
|
|
|
694
|
+
const isXai = getProviderKind(model) === "xai";
|
|
695
|
+
const searchProvider: ProviderKind = isXai ? "xai" : "openai";
|
|
696
|
+
const providerSourceName = isXai ? "xai" : "openai";
|
|
685
697
|
const requestBody: any = {
|
|
686
698
|
model: model.id,
|
|
687
699
|
input: isCodex
|
|
688
700
|
? [{ role: "user", content: [{ type: "input_text", text: prompt }] }]
|
|
689
|
-
:
|
|
701
|
+
: isXai
|
|
702
|
+
? [{ role: "user", content: prompt }]
|
|
703
|
+
: prompt,
|
|
690
704
|
tools: [{ type: "web_search" }],
|
|
691
|
-
|
|
692
|
-
? ["web_search_call.action.sources"]
|
|
693
|
-
: ["web_search_call.action.sources", "web_search_call.results"],
|
|
705
|
+
...(isCodex || isXai
|
|
706
|
+
? { include: ["web_search_call.action.sources"] }
|
|
707
|
+
: { include: ["web_search_call.action.sources", "web_search_call.results"] }),
|
|
694
708
|
stream: true,
|
|
695
709
|
store: false,
|
|
696
710
|
};
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
711
|
+
// Let the provider choose its supported reasoning default. Some reasoning
|
|
712
|
+
// models reject "none", and supported effort levels vary between models.
|
|
700
713
|
if (isCodex) {
|
|
701
714
|
requestBody.instructions = "Answer the user's request using web search when needed.";
|
|
702
715
|
requestBody.text = { verbosity: "low" };
|
|
@@ -713,7 +726,7 @@ async function callOpenAIStream(
|
|
|
713
726
|
});
|
|
714
727
|
|
|
715
728
|
if (!response.ok) {
|
|
716
|
-
throw new Error(
|
|
729
|
+
throw new Error(`${isXai ? "xAI" : "OpenAI"} API error (${response.status}): ${await response.text()}`);
|
|
717
730
|
}
|
|
718
731
|
|
|
719
732
|
let accumulatedText = "";
|
|
@@ -735,7 +748,7 @@ async function callOpenAIStream(
|
|
|
735
748
|
const action = item.action || {};
|
|
736
749
|
const call: NativeSearchCallDetail = {
|
|
737
750
|
id: item.id,
|
|
738
|
-
provider:
|
|
751
|
+
provider: searchProvider,
|
|
739
752
|
status: item.status,
|
|
740
753
|
actionType: action.type,
|
|
741
754
|
raw: item,
|
|
@@ -755,7 +768,7 @@ async function callOpenAIStream(
|
|
|
755
768
|
pushUniqueSearchResult(searchResults, {
|
|
756
769
|
title: source.title || source.display_name || source.name || titleFromUrl(source.url),
|
|
757
770
|
url: source.url,
|
|
758
|
-
source:
|
|
771
|
+
source: `${providerSourceName}.web_search_call.action.sources`,
|
|
759
772
|
type: source.type || "url",
|
|
760
773
|
raw: source,
|
|
761
774
|
});
|
|
@@ -766,7 +779,7 @@ async function callOpenAIStream(
|
|
|
766
779
|
pushUniqueSearchResult(searchResults, {
|
|
767
780
|
title: titleFromUrl(action.url),
|
|
768
781
|
url: action.url,
|
|
769
|
-
source:
|
|
782
|
+
source: `${providerSourceName}.web_search_call.action.${action.type}`,
|
|
770
783
|
type: action.type,
|
|
771
784
|
raw: action,
|
|
772
785
|
});
|
|
@@ -816,21 +829,23 @@ async function callOpenAIStream(
|
|
|
816
829
|
pushNativeSearchEvent(nativeSearchEvents, event.type);
|
|
817
830
|
const call = nativeSearchCalls.find((item) => item.id === event.item_id);
|
|
818
831
|
if (call) call.status = event.type.replace("response.web_search_call.", "");
|
|
819
|
-
else nativeSearchCalls.push({ id: event.item_id, provider:
|
|
832
|
+
else nativeSearchCalls.push({ id: event.item_id, provider: searchProvider, status: event.type.replace("response.web_search_call.", ""), raw: event });
|
|
820
833
|
if (event.type === "response.web_search_call.searching") {
|
|
821
834
|
onUpdate?.({
|
|
822
|
-
content: [{ type: "text", text: accumulatedText ||
|
|
835
|
+
content: [{ type: "text", text: accumulatedText || `Searching the web with ${isXai ? "xAI" : "OpenAI"}...` }],
|
|
823
836
|
details: { streaming: true, searching: true }
|
|
824
837
|
});
|
|
825
838
|
}
|
|
826
839
|
}
|
|
827
840
|
});
|
|
828
841
|
|
|
829
|
-
const cited =
|
|
842
|
+
const cited = isXai
|
|
843
|
+
? preserveInlineCitations(accumulatedText || "No answer available.", citations)
|
|
844
|
+
: applyIndexCitations(accumulatedText || "No answer available.", citations);
|
|
830
845
|
const citationDetails = citations.map((citation) => ({
|
|
831
846
|
title: citation.title,
|
|
832
847
|
url: citation.url,
|
|
833
|
-
source:
|
|
848
|
+
source: `${providerSourceName}.url_citation`,
|
|
834
849
|
type: "citation",
|
|
835
850
|
raw: citation,
|
|
836
851
|
}));
|
|
@@ -844,7 +859,7 @@ async function callOpenAIStream(
|
|
|
844
859
|
return {
|
|
845
860
|
text: cited.text,
|
|
846
861
|
sources: cited.sources.length ? cited.sources.map((source) => ({ ...source, url: normalizeSearchUrl(source.url) })).filter((source) => !isLikelyJunkSearchUrl(source.url)) : derivedSources,
|
|
847
|
-
providerKind:
|
|
862
|
+
providerKind: searchProvider,
|
|
848
863
|
nativeSearchUsed: nativeSearchEvents.length > 0 || nativeSearchCalls.length > 0 || sanitizedSearchResults.length > 0,
|
|
849
864
|
nativeSearchEvents,
|
|
850
865
|
nativeSearchCalls,
|
|
@@ -1037,7 +1052,7 @@ export async function callApiStream(
|
|
|
1037
1052
|
throw new Error("No prompt text found in request body");
|
|
1038
1053
|
}
|
|
1039
1054
|
|
|
1040
|
-
if (kind === "openai") {
|
|
1055
|
+
if (kind === "openai" || kind === "xai") {
|
|
1041
1056
|
return callOpenAIStream(ctx, model, prompt, onUpdate, signal);
|
|
1042
1057
|
}
|
|
1043
1058
|
if (kind === "anthropic") {
|
package/src/index.ts
CHANGED
|
@@ -63,7 +63,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
63
63
|
pi.registerTool({
|
|
64
64
|
name: WEB_SEARCH_TOOL,
|
|
65
65
|
label: "Web Search",
|
|
66
|
-
description: "Search the web using the current supported provider (Google Gemini, OpenAI, or Anthropic). Optionally include URLs to analyze alongside search results.",
|
|
66
|
+
description: "Search the web using the current supported provider (Google Gemini, xAI Grok, OpenAI, or Anthropic). Optionally include URLs to analyze alongside search results.",
|
|
67
67
|
parameters: WebSearchSchema,
|
|
68
68
|
execute: webSearch,
|
|
69
69
|
renderCall(args, theme) {
|
package/src/utils.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext, AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
-
import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
5
|
import { join } from "node:path";
|
|
7
6
|
import { getProviderKind } from "./api.ts";
|
|
8
7
|
|
|
@@ -18,8 +17,7 @@ export function formatResult(text: string, details: any): AgentToolResult<any> {
|
|
|
18
17
|
|
|
19
18
|
// --- Model Selection ---
|
|
20
19
|
|
|
21
|
-
const SUPPORTED_PROVIDERS = ["google-generative-ai", "openai-responses", "azure-openai-responses", "openai-codex-responses", "anthropic-messages"];
|
|
22
|
-
const WEB_SEARCH_CONFIG_PATH = join(homedir(), ".pi", "agent", "web-search.json");
|
|
20
|
+
const SUPPORTED_PROVIDERS = ["google-generative-ai", "xai", "openai-responses", "azure-openai-responses", "openai-codex-responses", "anthropic-messages"];
|
|
23
21
|
|
|
24
22
|
type WebSearchModelConfig =
|
|
25
23
|
| { status: "missing"; path: string; }
|
|
@@ -36,7 +34,7 @@ function describeModel(model: Model<Api>): string {
|
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
function getWebSearchConfigPath(): string {
|
|
39
|
-
return process.env.PI_WEB_SEARCH_CONFIG ||
|
|
37
|
+
return process.env.PI_WEB_SEARCH_CONFIG || join(getAgentDir(), "web-search.json");
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
function readWebSearchModelConfig(): WebSearchModelConfig {
|
package/src/web_search.ts
CHANGED
|
@@ -59,7 +59,14 @@ export async function webSearch(
|
|
|
59
59
|
const cited = applyCitations(result.text, result.groundingMetadata);
|
|
60
60
|
const text = cited.text;
|
|
61
61
|
const sources = result.sources?.length ? result.sources : cited.sources;
|
|
62
|
-
const
|
|
62
|
+
const seenAdditionalResults = new Set<string>();
|
|
63
|
+
const extraSearchResults = (result.searchResults || []).filter((item) => {
|
|
64
|
+
if (!item.url || sources.some((source) => source.url === item.url)) return false;
|
|
65
|
+
const key = `${item.title || ""}\t${item.url}`;
|
|
66
|
+
if (seenAdditionalResults.has(key)) return false;
|
|
67
|
+
seenAdditionalResults.add(key);
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
63
70
|
|
|
64
71
|
// Handle URL context metadata
|
|
65
72
|
const urlMeta = result.urlContextMetadata?.urlMetadata
|
|
@@ -90,16 +97,11 @@ export async function webSearch(
|
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
if (extraSearchResults.length) {
|
|
93
|
-
|
|
94
|
-
summary += `\n\n## Additional Search Results\n${visibleResults.map((r, i) => {
|
|
100
|
+
summary += `\n\n## Additional Search Results\n${extraSearchResults.map((r, i) => {
|
|
95
101
|
const label = r.title || r.url || `Result ${i + 1}`;
|
|
96
102
|
const url = r.url ? ` - ${r.url}` : "";
|
|
97
|
-
|
|
98
|
-
return `${i + 1}. ${label}${url}${meta ? ` (${meta})` : ""}`;
|
|
103
|
+
return `${i + 1}. ${label}${url}`;
|
|
99
104
|
}).join("\n")}`;
|
|
100
|
-
if (extraSearchResults.length > visibleResults.length) {
|
|
101
|
-
summary += `\n... and ${extraSearchResults.length - visibleResults.length} more results in tool details.`;
|
|
102
|
-
}
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
return formatResult(summary, {
|