pi-web-search 1.3.1 → 1.4.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 +7 -3
- package/package.json +4 -2
- package/src/api.ts +49 -5
- package/src/index.ts +23 -1
- package/src/utils.ts +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-web-search
|
|
2
2
|
|
|
3
|
-
Provider-native web search for [pi](https://
|
|
3
|
+
Provider-native web search for [pi](https://pi.dev) with Gemini + URL Context, OpenAI Responses variants, and Anthropic.
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
@@ -12,10 +12,14 @@ Search the web using your currently selected model. Automatically picks the righ
|
|
|
12
12
|
|---|---|
|
|
13
13
|
| Google Gemini | Grounding with Google Search |
|
|
14
14
|
| OpenAI | Responses API web search |
|
|
15
|
-
| OpenAI
|
|
15
|
+
| Azure OpenAI | Responses API web search (`azure-openai-responses`) |
|
|
16
|
+
| OpenAI Codex | Codex Responses API web search (`openai-codex-responses`) |
|
|
17
|
+
| GitHub Copilot | OpenAI Responses API web search via Copilot credentials |
|
|
16
18
|
| Anthropic | Messages API web search |
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
GitHub Copilot OpenAI Responses models are supported, including Business and Enterprise seats whose API endpoint is resolved from their authenticated Copilot credentials. This includes models such as `gpt-5.6-sol`.
|
|
21
|
+
|
|
22
|
+
Supports passing up to 20 additional URLs to analyze alongside the query. Successful `web_search` results are collapsed by default in pi; expand the tool call to inspect the full answer and source details.
|
|
19
23
|
|
|
20
24
|
### `url_context`
|
|
21
25
|
|
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
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Provider-native web search for pi: Gemini + URL Context, OpenAI Responses (Azure/Codex/Copilot), and Anthropic",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"keywords": [
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@earendil-works/pi-ai": "^0.80.3",
|
|
41
41
|
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
42
|
+
"@earendil-works/pi-tui": "^0.80.3",
|
|
42
43
|
"@types/node": "^25.9.1",
|
|
43
44
|
"typebox": "^1.1.38",
|
|
44
45
|
"typescript": "^6.0.3"
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"peerDependencies": {
|
|
52
53
|
"@earendil-works/pi-ai": ">=0.80.3",
|
|
53
54
|
"@earendil-works/pi-coding-agent": ">=0.80.3",
|
|
55
|
+
"@earendil-works/pi-tui": "*",
|
|
54
56
|
"typebox": "*"
|
|
55
57
|
}
|
|
56
58
|
}
|
package/src/api.ts
CHANGED
|
@@ -34,7 +34,11 @@ 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 (
|
|
37
|
+
if (
|
|
38
|
+
model.api === "openai-responses"
|
|
39
|
+
|| model.api === "azure-openai-responses"
|
|
40
|
+
|| model.api === "openai-codex-responses"
|
|
41
|
+
) return "openai";
|
|
38
42
|
if (model.api === "anthropic-messages") return "anthropic";
|
|
39
43
|
return "unsupported";
|
|
40
44
|
}
|
|
@@ -49,7 +53,7 @@ export function getConfig(model: Model<Api>): ProviderConfig {
|
|
|
49
53
|
// --- Auth Compatibility Layer ---
|
|
50
54
|
|
|
51
55
|
type ResolvedAuth =
|
|
52
|
-
| { ok: true; apiKey?: string; headers?: Record<string, string>; }
|
|
56
|
+
| { ok: true; apiKey?: string; headers?: Record<string, string>; baseUrl?: string; }
|
|
53
57
|
| { ok: false; error: string; };
|
|
54
58
|
|
|
55
59
|
function getEnvAuth(model: Model<Api>): Extract<ResolvedAuth, { ok: true }> | undefined {
|
|
@@ -240,8 +244,47 @@ function isOpenAICodexModel(model: Model<Api>): boolean {
|
|
|
240
244
|
return model.api === "openai-codex-responses";
|
|
241
245
|
}
|
|
242
246
|
|
|
243
|
-
function
|
|
244
|
-
|
|
247
|
+
function resolveGitHubCopilotBaseUrl(
|
|
248
|
+
model: Model<Api>,
|
|
249
|
+
auth: Extract<ResolvedAuth, { ok: true }>,
|
|
250
|
+
): string {
|
|
251
|
+
if (model.provider !== "github-copilot") return model.baseUrl;
|
|
252
|
+
|
|
253
|
+
// Modern pi versions expose the credential-specific Copilot endpoint
|
|
254
|
+
// resolved by the provider. Prefer it so GitHub Enterprise Server and any
|
|
255
|
+
// future provider-owned routing continue to work without token parsing here.
|
|
256
|
+
if (typeof auth.baseUrl === "string" && auth.baseUrl.trim()) {
|
|
257
|
+
return auth.baseUrl;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Compatibility fallback for older pi versions whose extension auth API
|
|
261
|
+
// returned the Copilot token but not its resolved base URL.
|
|
262
|
+
if (!auth.apiKey) return model.baseUrl;
|
|
263
|
+
const proxyEndpoints = auth.apiKey
|
|
264
|
+
.split(";")
|
|
265
|
+
.filter((field) => field.startsWith("proxy-ep="))
|
|
266
|
+
.map((field) => field.slice("proxy-ep=".length));
|
|
267
|
+
if (proxyEndpoints.length !== 1) return model.baseUrl;
|
|
268
|
+
|
|
269
|
+
const proxyHost = proxyEndpoints[0].toLowerCase();
|
|
270
|
+
const labels = proxyHost.split(".");
|
|
271
|
+
const isValidLabel = (label: string) =>
|
|
272
|
+
label.length > 0
|
|
273
|
+
&& label.length <= 63
|
|
274
|
+
&& /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label);
|
|
275
|
+
const isCopilotProxyHost = proxyHost.length <= 253
|
|
276
|
+
&& labels.length >= 4
|
|
277
|
+
&& labels[0] === "proxy"
|
|
278
|
+
&& labels.at(-2) === "githubcopilot"
|
|
279
|
+
&& labels.at(-1) === "com"
|
|
280
|
+
&& labels.every(isValidLabel);
|
|
281
|
+
if (!isCopilotProxyHost) return model.baseUrl;
|
|
282
|
+
|
|
283
|
+
return `https://api.${labels.slice(1).join(".")}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function resolveOpenAIResponsesUrl(model: Model<Api>, baseUrl = model.baseUrl): string {
|
|
287
|
+
const base = trimTrailingSlash(baseUrl);
|
|
245
288
|
if (!isOpenAICodexModel(model)) return `${base}/responses`;
|
|
246
289
|
if (base.endsWith("/codex/responses")) return base;
|
|
247
290
|
if (base.endsWith("/codex")) return `${base}/responses`;
|
|
@@ -661,7 +704,8 @@ async function callOpenAIStream(
|
|
|
661
704
|
requestBody.parallel_tool_calls = true;
|
|
662
705
|
}
|
|
663
706
|
|
|
664
|
-
const
|
|
707
|
+
const baseUrl = resolveGitHubCopilotBaseUrl(model, auth);
|
|
708
|
+
const response = await fetch(resolveOpenAIResponsesUrl(model, baseUrl), {
|
|
665
709
|
method: "POST",
|
|
666
710
|
headers: requestHeaders,
|
|
667
711
|
body: JSON.stringify(requestBody),
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
4
|
import { getProviderKind } from "./api.ts";
|
|
4
5
|
import { webSearch, WebSearchSchema } from "./web_search.ts";
|
|
5
6
|
import { urlContext, UrlContextSchema } from "./url_context.ts";
|
|
@@ -64,7 +65,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
64
65
|
label: "Web Search",
|
|
65
66
|
description: "Search the web using the current supported provider (Google Gemini, OpenAI, or Anthropic). Optionally include URLs to analyze alongside search results.",
|
|
66
67
|
parameters: WebSearchSchema,
|
|
67
|
-
execute: webSearch
|
|
68
|
+
execute: webSearch,
|
|
69
|
+
renderCall(args, theme) {
|
|
70
|
+
const query = args.query || "…";
|
|
71
|
+
const urlCount = args.urls?.length ?? 0;
|
|
72
|
+
const urls = urlCount > 0 ? theme.fg("muted", ` + ${urlCount} URL${urlCount === 1 ? "" : "s"}`) : "";
|
|
73
|
+
return new Text(
|
|
74
|
+
`${theme.fg("toolTitle", theme.bold("web_search"))} ${theme.fg("accent", query)}${urls}`,
|
|
75
|
+
0,
|
|
76
|
+
0,
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
renderResult(result, { expanded }, theme) {
|
|
80
|
+
const output = result.content
|
|
81
|
+
.filter((part) => part.type === "text")
|
|
82
|
+
.map((part) => part.text)
|
|
83
|
+
.join("\n");
|
|
84
|
+
|
|
85
|
+
const isError = Boolean(result.details?.error);
|
|
86
|
+
if (!expanded && !isError) return new Text("", 0, 0);
|
|
87
|
+
|
|
88
|
+
return new Text(theme.fg(isError ? "error" : "toolOutput", output), 0, 0);
|
|
89
|
+
}
|
|
68
90
|
});
|
|
69
91
|
|
|
70
92
|
pi.registerTool({
|
package/src/utils.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function formatResult(text: string, details: any): AgentToolResult<any> {
|
|
|
18
18
|
|
|
19
19
|
// --- Model Selection ---
|
|
20
20
|
|
|
21
|
-
const SUPPORTED_PROVIDERS = ["google-generative-ai", "openai-responses", "openai-codex-responses", "anthropic-messages"];
|
|
21
|
+
const SUPPORTED_PROVIDERS = ["google-generative-ai", "openai-responses", "azure-openai-responses", "openai-codex-responses", "anthropic-messages"];
|
|
22
22
|
const WEB_SEARCH_CONFIG_PATH = join(homedir(), ".pi", "agent", "web-search.json");
|
|
23
23
|
|
|
24
24
|
type WebSearchModelConfig =
|