pi-web-search 1.3.0 → 1.3.1
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 +1 -0
- package/package.json +5 -5
- package/src/api.ts +164 -77
- package/src/index.ts +3 -3
- package/src/utils.ts +6 -6
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ 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
16
|
| Anthropic | Messages API web search |
|
|
16
17
|
|
|
17
18
|
Supports passing up to 20 additional URLs to analyze alongside the query.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-search",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Provider-native web search for pi across Google Gemini, OpenAI, and Anthropic, plus Gemini URL Context",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"test:real:url-context": "node tests/real-url-context.mjs"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@earendil-works/pi-ai": "^0.
|
|
41
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
40
|
+
"@earendil-works/pi-ai": "^0.80.3",
|
|
41
|
+
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
42
42
|
"@types/node": "^25.9.1",
|
|
43
43
|
"typebox": "^1.1.38",
|
|
44
44
|
"typescript": "^6.0.3"
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@earendil-works/pi-ai": "
|
|
53
|
-
"@earendil-works/pi-coding-agent": "
|
|
52
|
+
"@earendil-works/pi-ai": ">=0.80.3",
|
|
53
|
+
"@earendil-works/pi-coding-agent": ">=0.80.3",
|
|
54
54
|
"typebox": "*"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/api.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ExtensionContext, AgentToolUpdateCallback } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import { getEnvApiKey } from "@earendil-works/pi-ai/compat";
|
|
3
4
|
import { TextEncoder, TextDecoder } from "util";
|
|
4
5
|
|
|
5
6
|
// --- Provider Configuration ---
|
|
6
7
|
|
|
7
8
|
type ProviderKind = "google" | "openai" | "anthropic" | "unsupported";
|
|
8
9
|
|
|
9
|
-
type GoogleRequestBuilder = (model: Model<
|
|
10
|
+
type GoogleRequestBuilder = (model: Model<Api>, body: any) => { url: string; headers: Record<string, string>; body: any };
|
|
10
11
|
|
|
11
12
|
type ProviderConfig = {
|
|
12
13
|
kind: ProviderKind;
|
|
@@ -31,14 +32,14 @@ const GOOGLE_PROVIDERS: Record<string, ProviderConfig> = {
|
|
|
31
32
|
}
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
export function getProviderKind(model: Model<
|
|
35
|
+
export function getProviderKind(model: Model<Api>): ProviderKind {
|
|
35
36
|
if (GOOGLE_PROVIDERS[model.provider] || GOOGLE_PROVIDERS[model.api]) return "google";
|
|
36
|
-
if (model.api === "openai-responses") return "openai";
|
|
37
|
+
if (model.api === "openai-responses" || model.api === "openai-codex-responses") return "openai";
|
|
37
38
|
if (model.api === "anthropic-messages") return "anthropic";
|
|
38
39
|
return "unsupported";
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
export function getConfig(model: Model<
|
|
42
|
+
export function getConfig(model: Model<Api>): ProviderConfig {
|
|
42
43
|
const googleConfig = GOOGLE_PROVIDERS[model.provider] || GOOGLE_PROVIDERS[model.api];
|
|
43
44
|
if (googleConfig) return googleConfig;
|
|
44
45
|
const kind = getProviderKind(model);
|
|
@@ -51,28 +52,34 @@ type ResolvedAuth =
|
|
|
51
52
|
| { ok: true; apiKey?: string; headers?: Record<string, string>; }
|
|
52
53
|
| { ok: false; error: string; };
|
|
53
54
|
|
|
55
|
+
function getEnvAuth(model: Model<Api>): Extract<ResolvedAuth, { ok: true }> | undefined {
|
|
56
|
+
const apiKey = getEnvApiKey(model.provider);
|
|
57
|
+
return apiKey ? { ok: true, apiKey } : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
/**
|
|
55
61
|
* Get API key and headers for a model.
|
|
56
|
-
* Compatible with both new pi versions (getApiKeyAndHeaders) and old versions (getApiKey).
|
|
57
62
|
*/
|
|
58
|
-
async function getAuth(ctx: ExtensionContext, model: Model<
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
async function getAuth(ctx: ExtensionContext, model: Model<Api>): Promise<ResolvedAuth> {
|
|
64
|
+
const resolved = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
65
|
+
if (!resolved.ok) return resolved;
|
|
66
|
+
|
|
67
|
+
// pi-coding-agent 0.80.1+ returns { ok: true } from getApiKeyAndHeaders()
|
|
68
|
+
// when auth only comes from provider env vars such as ANTHROPIC_API_KEY.
|
|
69
|
+
// The main agent still works because pi-ai's streamSimple() performs its own
|
|
70
|
+
// getEnvApiKey() fallback, but this extension calls fetch() directly, so it
|
|
71
|
+
// must mirror that fallback while preserving explicit model/auth headers.
|
|
72
|
+
const envAuth = !resolved.apiKey && !hasAuthHeader(resolved.headers) ? getEnvAuth(model) : undefined;
|
|
73
|
+
return envAuth ? { ...resolved, apiKey: envAuth.apiKey } : resolved;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hasAuthHeader(headers?: Record<string, string>): boolean {
|
|
77
|
+
if (!headers) return false;
|
|
78
|
+
return Object.entries(headers).some(([name, value]) => {
|
|
79
|
+
if (!value) return false;
|
|
80
|
+
const normalized = name.toLowerCase();
|
|
81
|
+
return normalized === "authorization" || normalized === "x-api-key" || normalized === "x-goog-api-key";
|
|
82
|
+
});
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
// --- Streaming API Call ---
|
|
@@ -126,7 +133,7 @@ type SseEvent = {
|
|
|
126
133
|
async function readSseEvents(
|
|
127
134
|
response: Response,
|
|
128
135
|
signal: AbortSignal | undefined,
|
|
129
|
-
onEvent: (event: SseEvent) => void | Promise<void>
|
|
136
|
+
onEvent: (event: SseEvent) => boolean | void | Promise<boolean | void>
|
|
130
137
|
): Promise<void> {
|
|
131
138
|
if (!response.body) {
|
|
132
139
|
throw new Error("No response body");
|
|
@@ -137,59 +144,78 @@ async function readSseEvents(
|
|
|
137
144
|
let buffer = "";
|
|
138
145
|
let currentEventData = "";
|
|
139
146
|
let currentEventName = "";
|
|
147
|
+
let stopRequested = false;
|
|
148
|
+
let reachedEof = false;
|
|
140
149
|
|
|
141
|
-
const flushEvent = async () => {
|
|
142
|
-
if (!currentEventData) return;
|
|
150
|
+
const flushEvent = async (): Promise<boolean> => {
|
|
151
|
+
if (!currentEventData) return false;
|
|
143
152
|
const raw = currentEventData.trim();
|
|
144
153
|
currentEventData = "";
|
|
145
154
|
const eventName = currentEventName;
|
|
146
155
|
currentEventName = "";
|
|
147
|
-
if (!raw || raw === "[DONE]") return;
|
|
156
|
+
if (!raw || raw === "[DONE]") return false;
|
|
148
157
|
|
|
149
158
|
let data: any;
|
|
150
159
|
try {
|
|
151
160
|
data = JSON.parse(raw);
|
|
152
161
|
} catch {
|
|
153
|
-
return;
|
|
162
|
+
return false;
|
|
154
163
|
}
|
|
155
|
-
await onEvent({ event: eventName, data });
|
|
164
|
+
return await onEvent({ event: eventName, data }) === true;
|
|
156
165
|
};
|
|
157
166
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
try {
|
|
168
|
+
readLoop: while (true) {
|
|
169
|
+
if (signal?.aborted) {
|
|
170
|
+
throw new Error("Request was aborted");
|
|
171
|
+
}
|
|
162
172
|
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
const { done, value } = await reader.read();
|
|
174
|
+
if (done) {
|
|
175
|
+
reachedEof = true;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
165
178
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
buffer += decoder.decode(value, { stream: true });
|
|
180
|
+
const lines = buffer.split("\n");
|
|
181
|
+
buffer = lines.pop() || "";
|
|
182
|
+
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
if (line === "" || line === "\r") {
|
|
185
|
+
if (await flushEvent()) {
|
|
186
|
+
stopRequested = true;
|
|
187
|
+
break readLoop;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
169
191
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
192
|
+
if (line.startsWith("data:")) {
|
|
193
|
+
const data = line.slice(5).trim();
|
|
194
|
+
currentEventData = currentEventData ? currentEventData + "\n" + data : data;
|
|
195
|
+
} else if (line.startsWith("event:")) {
|
|
196
|
+
currentEventName = line.slice(6).trim();
|
|
197
|
+
}
|
|
174
198
|
}
|
|
199
|
+
}
|
|
175
200
|
|
|
201
|
+
if (!stopRequested && buffer.trim()) {
|
|
202
|
+
const line = buffer.trim();
|
|
176
203
|
if (line.startsWith("data:")) {
|
|
177
204
|
const data = line.slice(5).trim();
|
|
178
205
|
currentEventData = currentEventData ? currentEventData + "\n" + data : data;
|
|
179
|
-
} else if (line.startsWith("event:")) {
|
|
180
|
-
currentEventName = line.slice(6).trim();
|
|
181
206
|
}
|
|
182
207
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
if (!stopRequested) stopRequested = await flushEvent();
|
|
209
|
+
} finally {
|
|
210
|
+
if (!reachedEof) {
|
|
211
|
+
try {
|
|
212
|
+
await reader.cancel();
|
|
213
|
+
} catch {
|
|
214
|
+
// Ignore cancellation failures while cleaning up an interrupted stream.
|
|
215
|
+
}
|
|
190
216
|
}
|
|
217
|
+
reader.releaseLock();
|
|
191
218
|
}
|
|
192
|
-
await flushEvent();
|
|
193
219
|
}
|
|
194
220
|
|
|
195
221
|
function extractPromptFromGeminiBody(body: any): string {
|
|
@@ -210,6 +236,34 @@ function trimTrailingSlash(value: string): string {
|
|
|
210
236
|
return value.replace(/\/+$/, "");
|
|
211
237
|
}
|
|
212
238
|
|
|
239
|
+
function isOpenAICodexModel(model: Model<Api>): boolean {
|
|
240
|
+
return model.api === "openai-codex-responses";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function resolveOpenAIResponsesUrl(model: Model<Api>): string {
|
|
244
|
+
const base = trimTrailingSlash(model.baseUrl);
|
|
245
|
+
if (!isOpenAICodexModel(model)) return `${base}/responses`;
|
|
246
|
+
if (base.endsWith("/codex/responses")) return base;
|
|
247
|
+
if (base.endsWith("/codex")) return `${base}/responses`;
|
|
248
|
+
return `${base}/codex/responses`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function extractOpenAICodexAccountId(token: string): string {
|
|
252
|
+
try {
|
|
253
|
+
const parts = token.split(".");
|
|
254
|
+
if (parts.length !== 3) throw new Error("Invalid token");
|
|
255
|
+
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
256
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
257
|
+
const bytes = Uint8Array.from(atob(padded), (char) => char.charCodeAt(0));
|
|
258
|
+
const payload = JSON.parse(new TextDecoder().decode(bytes));
|
|
259
|
+
const accountId = payload?.["https://api.openai.com/auth"]?.chatgpt_account_id;
|
|
260
|
+
if (typeof accountId !== "string" || !accountId) throw new Error("Missing account ID");
|
|
261
|
+
return accountId;
|
|
262
|
+
} catch {
|
|
263
|
+
throw new Error("Failed to extract ChatGPT account ID from openai-codex credentials");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
213
267
|
function resolveAnthropicMessagesUrl(baseUrl: string): string {
|
|
214
268
|
const base = trimTrailingSlash(baseUrl);
|
|
215
269
|
return base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
|
|
@@ -459,7 +513,7 @@ function extractGoogleSearchDetails(groundingMetadata: any): { searchQueries: st
|
|
|
459
513
|
|
|
460
514
|
async function callGoogleStream(
|
|
461
515
|
ctx: ExtensionContext,
|
|
462
|
-
model: Model<
|
|
516
|
+
model: Model<Api>,
|
|
463
517
|
body: any,
|
|
464
518
|
onUpdate?: AgentToolUpdateCallback,
|
|
465
519
|
signal?: AbortSignal
|
|
@@ -551,7 +605,7 @@ async function callGoogleStream(
|
|
|
551
605
|
|
|
552
606
|
async function callOpenAIStream(
|
|
553
607
|
ctx: ExtensionContext,
|
|
554
|
-
model: Model<
|
|
608
|
+
model: Model<Api>,
|
|
555
609
|
prompt: string,
|
|
556
610
|
onUpdate?: AgentToolUpdateCallback,
|
|
557
611
|
signal?: AbortSignal
|
|
@@ -561,31 +615,55 @@ async function callOpenAIStream(
|
|
|
561
615
|
throw new Error(auth.error || "Failed to get API key and headers");
|
|
562
616
|
}
|
|
563
617
|
|
|
564
|
-
const headers
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
|
|
618
|
+
const headers = new Headers();
|
|
619
|
+
for (const [name, value] of Object.entries(model.headers || {})) headers.set(name, value);
|
|
620
|
+
for (const [name, value] of Object.entries(auth.headers || {})) headers.set(name, value);
|
|
621
|
+
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
622
|
+
if (!headers.has("Accept")) headers.set("Accept", "text/event-stream");
|
|
623
|
+
if (auth.apiKey && !headers.has("Authorization")) headers.set("Authorization", `Bearer ${auth.apiKey}`);
|
|
624
|
+
|
|
625
|
+
const isCodex = isOpenAICodexModel(model);
|
|
626
|
+
if (isCodex) {
|
|
627
|
+
const authorization = headers.get("Authorization");
|
|
628
|
+
const hasBearerAuth = typeof authorization === "string" && /^Bearer\s+\S+/i.test(authorization);
|
|
629
|
+
if (!auth.apiKey && !hasBearerAuth) {
|
|
630
|
+
throw new Error("No OAuth credential configured for openai-codex model");
|
|
631
|
+
}
|
|
632
|
+
if (!headers.has("chatgpt-account-id")) {
|
|
633
|
+
if (!auth.apiKey) {
|
|
634
|
+
throw new Error("No ChatGPT account ID configured for openai-codex model");
|
|
635
|
+
}
|
|
636
|
+
headers.set("chatgpt-account-id", extractOpenAICodexAccountId(auth.apiKey));
|
|
637
|
+
}
|
|
638
|
+
if (!headers.has("originator")) headers.set("originator", "codex_cli_rs");
|
|
572
639
|
}
|
|
640
|
+
const requestHeaders = Object.fromEntries(headers.entries());
|
|
573
641
|
|
|
574
642
|
const requestBody: any = {
|
|
575
643
|
model: model.id,
|
|
576
|
-
input:
|
|
644
|
+
input: isCodex
|
|
645
|
+
? [{ role: "user", content: [{ type: "input_text", text: prompt }] }]
|
|
646
|
+
: prompt,
|
|
577
647
|
tools: [{ type: "web_search" }],
|
|
578
|
-
include:
|
|
648
|
+
include: isCodex
|
|
649
|
+
? ["web_search_call.action.sources"]
|
|
650
|
+
: ["web_search_call.action.sources", "web_search_call.results"],
|
|
579
651
|
stream: true,
|
|
580
652
|
store: false,
|
|
581
653
|
};
|
|
582
654
|
if (model.reasoning) {
|
|
583
655
|
requestBody.reasoning = { effort: "none" };
|
|
584
656
|
}
|
|
657
|
+
if (isCodex) {
|
|
658
|
+
requestBody.instructions = "Answer the user's request using web search when needed.";
|
|
659
|
+
requestBody.text = { verbosity: "low" };
|
|
660
|
+
requestBody.tool_choice = "required";
|
|
661
|
+
requestBody.parallel_tool_calls = true;
|
|
662
|
+
}
|
|
585
663
|
|
|
586
|
-
const response = await fetch(
|
|
664
|
+
const response = await fetch(resolveOpenAIResponsesUrl(model), {
|
|
587
665
|
method: "POST",
|
|
588
|
-
headers,
|
|
666
|
+
headers: requestHeaders,
|
|
589
667
|
body: JSON.stringify(requestBody),
|
|
590
668
|
signal
|
|
591
669
|
});
|
|
@@ -649,7 +727,14 @@ async function callOpenAIStream(
|
|
|
649
727
|
raw: action,
|
|
650
728
|
});
|
|
651
729
|
}
|
|
652
|
-
nativeSearchCalls.
|
|
730
|
+
const existingCall = call.id ? nativeSearchCalls.find((existing) => existing.id === call.id) : undefined;
|
|
731
|
+
if (existingCall) {
|
|
732
|
+
Object.assign(existingCall, Object.fromEntries(
|
|
733
|
+
Object.entries(call).filter(([, value]) => value !== undefined)
|
|
734
|
+
));
|
|
735
|
+
} else {
|
|
736
|
+
nativeSearchCalls.push(call);
|
|
737
|
+
}
|
|
653
738
|
};
|
|
654
739
|
|
|
655
740
|
const collectFromResponse = (response: any) => {
|
|
@@ -664,7 +749,10 @@ async function callOpenAIStream(
|
|
|
664
749
|
};
|
|
665
750
|
|
|
666
751
|
await readSseEvents(response, signal, ({ data: event }) => {
|
|
667
|
-
if (event.type === "response.
|
|
752
|
+
if (event.type === "error" || event.type === "response.failed") {
|
|
753
|
+
const message = event.message || event.error?.message || event.response?.error?.message;
|
|
754
|
+
throw new Error(message || JSON.stringify(event.error || event.response?.error || event));
|
|
755
|
+
} else if (event.type === "response.output_text.delta") {
|
|
668
756
|
accumulatedText += event.delta || "";
|
|
669
757
|
onUpdate?.({
|
|
670
758
|
content: [{ type: "text", text: accumulatedText }],
|
|
@@ -674,8 +762,12 @@ async function callOpenAIStream(
|
|
|
674
762
|
collectAnnotation(event.annotation);
|
|
675
763
|
} else if (event.type === "response.output_item.added" || event.type === "response.output_item.done") {
|
|
676
764
|
collectWebSearchCall(event.item);
|
|
677
|
-
} else if (event.type === "response.
|
|
765
|
+
} else if (event.type === "response.incomplete" || event.response?.status === "incomplete") {
|
|
678
766
|
collectFromResponse(event.response);
|
|
767
|
+
if (isCodex) return true;
|
|
768
|
+
} else if (event.type === "response.completed" || event.type === "response.done") {
|
|
769
|
+
collectFromResponse(event.response);
|
|
770
|
+
if (isCodex) return true;
|
|
679
771
|
} else if (event.type === "response.web_search_call.in_progress" || event.type === "response.web_search_call.searching" || event.type === "response.web_search_call.completed") {
|
|
680
772
|
pushNativeSearchEvent(nativeSearchEvents, event.type);
|
|
681
773
|
const call = nativeSearchCalls.find((item) => item.id === event.item_id);
|
|
@@ -687,11 +779,6 @@ async function callOpenAIStream(
|
|
|
687
779
|
details: { streaming: true, searching: true }
|
|
688
780
|
});
|
|
689
781
|
}
|
|
690
|
-
} else if (event.type === "response.failed") {
|
|
691
|
-
const error = event.response?.error;
|
|
692
|
-
throw new Error(error?.message || JSON.stringify(error || event.response || event));
|
|
693
|
-
} else if (event.type === "error") {
|
|
694
|
-
throw new Error(event.message || JSON.stringify(event));
|
|
695
782
|
}
|
|
696
783
|
});
|
|
697
784
|
|
|
@@ -725,7 +812,7 @@ async function callOpenAIStream(
|
|
|
725
812
|
|
|
726
813
|
async function callAnthropicStream(
|
|
727
814
|
ctx: ExtensionContext,
|
|
728
|
-
model: Model<
|
|
815
|
+
model: Model<Api>,
|
|
729
816
|
prompt: string,
|
|
730
817
|
onUpdate?: AgentToolUpdateCallback,
|
|
731
818
|
signal?: AbortSignal
|
|
@@ -891,7 +978,7 @@ async function callAnthropicStream(
|
|
|
891
978
|
|
|
892
979
|
export async function callApiStream(
|
|
893
980
|
ctx: ExtensionContext,
|
|
894
|
-
model: Model<
|
|
981
|
+
model: Model<Api>,
|
|
895
982
|
body: any,
|
|
896
983
|
onUpdate?: AgentToolUpdateCallback,
|
|
897
984
|
signal?: AbortSignal
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
3
|
import { getProviderKind } from "./api.ts";
|
|
4
4
|
import { webSearch, WebSearchSchema } from "./web_search.ts";
|
|
5
5
|
import { urlContext, UrlContextSchema } from "./url_context.ts";
|
|
@@ -7,7 +7,7 @@ import { urlContext, UrlContextSchema } from "./url_context.ts";
|
|
|
7
7
|
const WEB_SEARCH_TOOL = "web_search";
|
|
8
8
|
const URL_CONTEXT_TOOL = "url_context";
|
|
9
9
|
|
|
10
|
-
function supportsUrlContext(model: Model<
|
|
10
|
+
function supportsUrlContext(model: Model<Api> | undefined) {
|
|
11
11
|
return !!model && getProviderKind(model) === "google";
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -24,7 +24,7 @@ export function createModelScopedToolManager(pi: Pick<ExtensionAPI, "getActiveTo
|
|
|
24
24
|
let lastAppliedActiveTools: Set<string> | undefined;
|
|
25
25
|
let suppressedTools = new Set<string>();
|
|
26
26
|
|
|
27
|
-
const sync = (model: Model<
|
|
27
|
+
const sync = (model: Model<Api> | undefined) => {
|
|
28
28
|
const currentActiveTools = new Set(pi.getActiveTools());
|
|
29
29
|
|
|
30
30
|
if (!preferredActiveTools) {
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext, AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
3
|
import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
@@ -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", "anthropic-messages"];
|
|
21
|
+
const SUPPORTED_PROVIDERS = ["google-generative-ai", "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 =
|
|
@@ -26,12 +26,12 @@ type WebSearchModelConfig =
|
|
|
26
26
|
| { status: "configured"; path: string; provider: string; modelId: string; }
|
|
27
27
|
| { status: "invalid"; path: string; error: string; };
|
|
28
28
|
|
|
29
|
-
function isSupportedSearchModel(model: Model<
|
|
29
|
+
function isSupportedSearchModel(model: Model<Api> | undefined): model is Model<Api> {
|
|
30
30
|
if (!model) return false;
|
|
31
31
|
return getProviderKind(model) !== "unsupported";
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function describeModel(model: Model<
|
|
34
|
+
function describeModel(model: Model<Api>): string {
|
|
35
35
|
return `${model.id} (${model.provider}/${model.api})`;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -82,13 +82,13 @@ function getAvailableSupportedModels(ctx: ExtensionContext): string[] {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export async function getModel(ctx: ExtensionContext): Promise<Model<
|
|
85
|
+
export async function getModel(ctx: ExtensionContext): Promise<Model<Api> | undefined> {
|
|
86
86
|
// Only use the currently selected model. Do not silently fall back to another
|
|
87
87
|
// configured model, because that can surprise users with unexpected API costs.
|
|
88
88
|
return isSupportedSearchModel(ctx.model) ? ctx.model : undefined;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export async function getWebSearchModel(ctx: ExtensionContext): Promise<Model<
|
|
91
|
+
export async function getWebSearchModel(ctx: ExtensionContext): Promise<Model<Api> | undefined> {
|
|
92
92
|
const config = readWebSearchModelConfig();
|
|
93
93
|
if (config.status === "invalid") return undefined;
|
|
94
94
|
|