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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-web-search
2
2
 
3
- Provider-native web search for [pi](https://github.com/earendil-works/pi-coding-agent), across Google Gemini, OpenAI, and Anthropic, plus Gemini-only URL Context analysis.
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 Codex | Codex Responses API web search |
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
- Supports passing up to 20 additional URLs to analyze alongside the query.
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.3.1",
4
- "description": "Provider-native web search for pi across Google Gemini, OpenAI, and Anthropic, plus Gemini URL Context",
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 (model.api === "openai-responses" || model.api === "openai-codex-responses") return "openai";
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 resolveOpenAIResponsesUrl(model: Model<Api>): string {
244
- const base = trimTrailingSlash(model.baseUrl);
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 response = await fetch(resolveOpenAIResponsesUrl(model), {
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 =