pi-web-search 1.0.2 → 1.1.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/dist/api.d.ts +1 -1
- package/dist/api.js +89 -39
- package/dist/url_context.js +1 -1
- package/dist/web_search.js +1 -1
- package/package.json +7 -5
- package/src/api.ts +122 -41
- package/src/url_context.ts +1 -1
- package/src/web_search.ts +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface StreamResult {
|
|
|
15
15
|
groundingMetadata?: any;
|
|
16
16
|
urlContextMetadata?: any;
|
|
17
17
|
}
|
|
18
|
-
export declare function callApiStream(ctx: ExtensionContext, model: Model<any>, body: any, onUpdate?: AgentToolUpdateCallback): Promise<StreamResult>;
|
|
18
|
+
export declare function callApiStream(ctx: ExtensionContext, model: Model<any>, body: any, onUpdate?: AgentToolUpdateCallback, signal?: AbortSignal): Promise<StreamResult>;
|
|
19
19
|
export declare function applyCitations(text: string, groundingMetadata: any): {
|
|
20
20
|
text: string;
|
|
21
21
|
sources: {
|
package/dist/api.js
CHANGED
|
@@ -53,27 +53,43 @@ const PROVIDERS = {
|
|
|
53
53
|
export function getConfig(model) {
|
|
54
54
|
return PROVIDERS[model.provider] || PROVIDERS[model.api] || PROVIDERS["google-generative-ai"];
|
|
55
55
|
}
|
|
56
|
-
export async function callApiStream(ctx, model, body, onUpdate) {
|
|
56
|
+
export async function callApiStream(ctx, model, body, onUpdate, signal) {
|
|
57
57
|
const config = getConfig(model);
|
|
58
|
-
const
|
|
58
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
59
|
+
if (!auth.ok) {
|
|
60
|
+
throw new Error(auth.error || "Failed to get API key and headers");
|
|
61
|
+
}
|
|
62
|
+
// Extract projectId from apiKey for internal Google APIs (gemini-cli, antigravity)
|
|
63
|
+
// These providers return apiKey as JSON: {projectId, token}
|
|
59
64
|
let projectId;
|
|
60
|
-
if (model.api !== "google-generative-ai") {
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
if (model.api !== "google-generative-ai" && auth.apiKey) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(auth.apiKey);
|
|
68
|
+
projectId = parsed.projectId;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Not a JSON string, ignore
|
|
72
|
+
}
|
|
63
73
|
}
|
|
64
74
|
const req = config.buildRequest(model, body, projectId);
|
|
65
75
|
// Handle auth
|
|
66
|
-
if (
|
|
67
|
-
req.headers
|
|
76
|
+
if (auth.headers) {
|
|
77
|
+
Object.assign(req.headers, auth.headers);
|
|
68
78
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
if (auth.apiKey) {
|
|
80
|
+
if (model.api === "google-generative-ai") {
|
|
81
|
+
req.headers["x-goog-api-key"] = auth.apiKey;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const parsed = JSON.parse(auth.apiKey);
|
|
85
|
+
req.headers["Authorization"] = `Bearer ${parsed.token}`;
|
|
86
|
+
}
|
|
72
87
|
}
|
|
73
88
|
const response = await fetch(req.url, {
|
|
74
89
|
method: "POST",
|
|
75
90
|
headers: req.headers,
|
|
76
|
-
body: JSON.stringify(req.body)
|
|
91
|
+
body: JSON.stringify(req.body),
|
|
92
|
+
signal
|
|
77
93
|
});
|
|
78
94
|
if (!response.ok) {
|
|
79
95
|
throw new Error(`API error (${response.status}): ${await response.text()}`);
|
|
@@ -88,7 +104,12 @@ export async function callApiStream(ctx, model, body, onUpdate) {
|
|
|
88
104
|
let accumulatedText = "";
|
|
89
105
|
let groundingMetadata;
|
|
90
106
|
let urlContextMetadata;
|
|
107
|
+
let currentEventData = "";
|
|
108
|
+
let currentEventName = "";
|
|
91
109
|
while (true) {
|
|
110
|
+
if (signal?.aborted) {
|
|
111
|
+
throw new Error("Request was aborted");
|
|
112
|
+
}
|
|
92
113
|
const { done, value } = await reader.read();
|
|
93
114
|
if (done)
|
|
94
115
|
break;
|
|
@@ -96,40 +117,69 @@ export async function callApiStream(ctx, model, body, onUpdate) {
|
|
|
96
117
|
const lines = buffer.split("\n");
|
|
97
118
|
buffer = lines.pop() || "";
|
|
98
119
|
for (const line of lines) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
// Empty line indicates end of an event
|
|
121
|
+
if (line === "" || line === "\r") {
|
|
122
|
+
if (currentEventData) {
|
|
123
|
+
// Process the complete event
|
|
124
|
+
let chunk;
|
|
125
|
+
try {
|
|
126
|
+
chunk = JSON.parse(currentEventData);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
currentEventData = "";
|
|
130
|
+
currentEventName = "";
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
// Check for error in chunk (API errors are sent via SSE stream)
|
|
134
|
+
if (chunk.error) {
|
|
135
|
+
const errorMsg = chunk.error.message || JSON.stringify(chunk.error);
|
|
136
|
+
throw new Error(`API error (${chunk.error.code || chunk.error.status || 'unknown'}): ${errorMsg}`);
|
|
137
|
+
}
|
|
138
|
+
// Unwrap response for internal APIs
|
|
139
|
+
const data = chunk.response || chunk;
|
|
140
|
+
const candidate = data.candidates?.[0];
|
|
141
|
+
if (candidate?.content?.parts) {
|
|
142
|
+
for (const part of candidate.content.parts) {
|
|
143
|
+
if (part.text) {
|
|
144
|
+
accumulatedText += part.text;
|
|
145
|
+
// Stream update
|
|
146
|
+
onUpdate?.({
|
|
147
|
+
content: [{ type: "text", text: accumulatedText }],
|
|
148
|
+
details: { streaming: true }
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Capture metadata from final chunk
|
|
154
|
+
if (candidate?.groundingMetadata) {
|
|
155
|
+
groundingMetadata = candidate.groundingMetadata;
|
|
156
|
+
}
|
|
157
|
+
// Handle both camelCase and snake_case
|
|
158
|
+
if (candidate?.urlContextMetadata || candidate?.url_context_metadata) {
|
|
159
|
+
urlContextMetadata = candidate.urlContextMetadata || candidate.url_context_metadata;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
currentEventData = "";
|
|
163
|
+
currentEventName = "";
|
|
103
164
|
continue;
|
|
104
|
-
let chunk;
|
|
105
|
-
try {
|
|
106
|
-
chunk = JSON.parse(jsonStr);
|
|
107
165
|
}
|
|
108
|
-
|
|
109
|
-
|
|
166
|
+
// Parse SSE field
|
|
167
|
+
if (line.startsWith("data:")) {
|
|
168
|
+
const data = line.slice(5).trim();
|
|
169
|
+
currentEventData = currentEventData ? currentEventData + "\n" + data : data;
|
|
110
170
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (part.text) {
|
|
117
|
-
accumulatedText += part.text;
|
|
118
|
-
// Stream update
|
|
119
|
-
onUpdate?.({
|
|
120
|
-
content: [{ type: "text", text: accumulatedText }],
|
|
121
|
-
details: { streaming: true }
|
|
122
|
-
});
|
|
123
|
-
}
|
|
171
|
+
else if (line.startsWith("event:")) {
|
|
172
|
+
currentEventName = line.slice(6).trim();
|
|
173
|
+
// Check for error event type
|
|
174
|
+
if (currentEventName === "error") {
|
|
175
|
+
// Next data line should contain error details
|
|
124
176
|
}
|
|
125
177
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
groundingMetadata = candidate.groundingMetadata;
|
|
178
|
+
else if (line.startsWith("id:")) {
|
|
179
|
+
// Event ID, can be ignored for now
|
|
129
180
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
urlContextMetadata = candidate.urlContextMetadata || candidate.url_context_metadata;
|
|
181
|
+
else if (line.startsWith(":")) {
|
|
182
|
+
// Comment line, ignore
|
|
133
183
|
}
|
|
134
184
|
}
|
|
135
185
|
}
|
package/dist/url_context.js
CHANGED
|
@@ -68,7 +68,7 @@ export async function urlContext(id, params, signal, onUpdate, ctx) {
|
|
|
68
68
|
const result = await callApiStream(ctx, model, {
|
|
69
69
|
contents,
|
|
70
70
|
tools
|
|
71
|
-
}, onUpdate);
|
|
71
|
+
}, onUpdate, signal);
|
|
72
72
|
const { text, sources } = applyCitations(result.text, result.groundingMetadata);
|
|
73
73
|
// Handle both camelCase and snake_case metadata
|
|
74
74
|
const urlMeta = result.urlContextMetadata?.urlMetadata
|
package/dist/web_search.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function webSearch(id, params, signal, onUpdate, ctx) {
|
|
|
36
36
|
const result = await callApiStream(ctx, model, {
|
|
37
37
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
38
38
|
tools
|
|
39
|
-
}, onUpdate);
|
|
39
|
+
}, onUpdate, signal);
|
|
40
40
|
const { text, sources } = applyCitations(result.text, result.groundingMetadata);
|
|
41
41
|
// Handle URL context metadata
|
|
42
42
|
const urlMeta = result.urlContextMetadata?.urlMetadata
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-search",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Web search and content analysis extension for pi, powered by Gemini API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,12 +25,14 @@
|
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://github.com/ttttmr/pi-web-search#readme",
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
28
|
+
"@mariozechner/pi-coding-agent": "^0.64.0",
|
|
29
29
|
"@sinclair/typebox": "^0.32.15",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"typescript": "^5.0.0"
|
|
32
32
|
},
|
|
33
33
|
"pi": {
|
|
34
|
-
"extensions": [
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./src/index.ts"
|
|
36
|
+
]
|
|
35
37
|
}
|
|
36
38
|
}
|
package/src/api.ts
CHANGED
|
@@ -66,6 +66,36 @@ export function getConfig(model: Model<any>): ProviderConfig {
|
|
|
66
66
|
return PROVIDERS[model.provider] || PROVIDERS[model.api] || PROVIDERS["google-generative-ai"];
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// --- Auth Compatibility Layer ---
|
|
70
|
+
|
|
71
|
+
type ResolvedAuth =
|
|
72
|
+
| { ok: true; apiKey?: string; headers?: Record<string, string>; }
|
|
73
|
+
| { ok: false; error: string; };
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get API key and headers for a model.
|
|
77
|
+
* Compatible with both new pi versions (getApiKeyAndHeaders) and old versions (getApiKey).
|
|
78
|
+
*/
|
|
79
|
+
async function getAuth(ctx: ExtensionContext, model: Model<any>): Promise<ResolvedAuth> {
|
|
80
|
+
const registry = ctx.modelRegistry as any;
|
|
81
|
+
|
|
82
|
+
// Try new API first (pi >= 0.63.0)
|
|
83
|
+
if (typeof registry.getApiKeyAndHeaders === 'function') {
|
|
84
|
+
return await registry.getApiKeyAndHeaders(model);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fallback to old API (pi < 0.63.0)
|
|
88
|
+
if (typeof registry.getApiKey === 'function') {
|
|
89
|
+
const apiKey = await registry.getApiKey(model);
|
|
90
|
+
if (apiKey === undefined || apiKey === null) {
|
|
91
|
+
return { ok: false, error: "No API key configured for model" };
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, apiKey };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { ok: false, error: "Model registry does not support API key retrieval" };
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
// --- Streaming API Call ---
|
|
70
100
|
|
|
71
101
|
export interface StreamResult {
|
|
@@ -78,31 +108,47 @@ export async function callApiStream(
|
|
|
78
108
|
ctx: ExtensionContext,
|
|
79
109
|
model: Model<any>,
|
|
80
110
|
body: any,
|
|
81
|
-
onUpdate?: AgentToolUpdateCallback
|
|
111
|
+
onUpdate?: AgentToolUpdateCallback,
|
|
112
|
+
signal?: AbortSignal
|
|
82
113
|
): Promise<StreamResult> {
|
|
83
114
|
const config = getConfig(model);
|
|
84
|
-
const
|
|
115
|
+
const auth = await getAuth(ctx, model);
|
|
116
|
+
if (!auth.ok) {
|
|
117
|
+
throw new Error(auth.error || "Failed to get API key and headers");
|
|
118
|
+
}
|
|
85
119
|
|
|
120
|
+
// Extract projectId from apiKey for internal Google APIs (gemini-cli, antigravity)
|
|
121
|
+
// These providers return apiKey as JSON: {projectId, token}
|
|
86
122
|
let projectId: string | undefined;
|
|
87
|
-
if (model.api !== "google-generative-ai") {
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
if (model.api !== "google-generative-ai" && auth.apiKey) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(auth.apiKey);
|
|
126
|
+
projectId = parsed.projectId;
|
|
127
|
+
} catch {
|
|
128
|
+
// Not a JSON string, ignore
|
|
129
|
+
}
|
|
90
130
|
}
|
|
91
131
|
|
|
92
132
|
const req = config.buildRequest(model, body, projectId);
|
|
93
133
|
|
|
94
134
|
// Handle auth
|
|
95
|
-
if (
|
|
96
|
-
req.headers
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
135
|
+
if (auth.headers) {
|
|
136
|
+
Object.assign(req.headers, auth.headers);
|
|
137
|
+
}
|
|
138
|
+
if (auth.apiKey) {
|
|
139
|
+
if (model.api === "google-generative-ai") {
|
|
140
|
+
req.headers["x-goog-api-key"] = auth.apiKey;
|
|
141
|
+
} else {
|
|
142
|
+
const parsed = JSON.parse(auth.apiKey);
|
|
143
|
+
req.headers["Authorization"] = `Bearer ${parsed.token}`;
|
|
144
|
+
}
|
|
100
145
|
}
|
|
101
146
|
|
|
102
147
|
const response = await fetch(req.url, {
|
|
103
148
|
method: "POST",
|
|
104
149
|
headers: req.headers,
|
|
105
|
-
body: JSON.stringify(req.body)
|
|
150
|
+
body: JSON.stringify(req.body),
|
|
151
|
+
signal
|
|
106
152
|
});
|
|
107
153
|
|
|
108
154
|
if (!response.ok) {
|
|
@@ -120,8 +166,14 @@ export async function callApiStream(
|
|
|
120
166
|
let accumulatedText = "";
|
|
121
167
|
let groundingMetadata: any;
|
|
122
168
|
let urlContextMetadata: any;
|
|
169
|
+
let currentEventData = "";
|
|
170
|
+
let currentEventName = "";
|
|
123
171
|
|
|
124
172
|
while (true) {
|
|
173
|
+
if (signal?.aborted) {
|
|
174
|
+
throw new Error("Request was aborted");
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
const { done, value } = await reader.read();
|
|
126
178
|
if (done) break;
|
|
127
179
|
|
|
@@ -130,41 +182,70 @@ export async function callApiStream(
|
|
|
130
182
|
buffer = lines.pop() || "";
|
|
131
183
|
|
|
132
184
|
for (const line of lines) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
185
|
+
// Empty line indicates end of an event
|
|
186
|
+
if (line === "" || line === "\r") {
|
|
187
|
+
if (currentEventData) {
|
|
188
|
+
// Process the complete event
|
|
189
|
+
let chunk: any;
|
|
190
|
+
try {
|
|
191
|
+
chunk = JSON.parse(currentEventData);
|
|
192
|
+
} catch {
|
|
193
|
+
currentEventData = "";
|
|
194
|
+
currentEventName = "";
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
143
197
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
198
|
+
// Check for error in chunk (API errors are sent via SSE stream)
|
|
199
|
+
if (chunk.error) {
|
|
200
|
+
const errorMsg = chunk.error.message || JSON.stringify(chunk.error);
|
|
201
|
+
throw new Error(`API error (${chunk.error.code || chunk.error.status || 'unknown'}): ${errorMsg}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Unwrap response for internal APIs
|
|
205
|
+
const data = chunk.response || chunk;
|
|
206
|
+
const candidate = data.candidates?.[0];
|
|
207
|
+
|
|
208
|
+
if (candidate?.content?.parts) {
|
|
209
|
+
for (const part of candidate.content.parts) {
|
|
210
|
+
if (part.text) {
|
|
211
|
+
accumulatedText += part.text;
|
|
212
|
+
// Stream update
|
|
213
|
+
onUpdate?.({
|
|
214
|
+
content: [{ type: "text", text: accumulatedText }],
|
|
215
|
+
details: { streaming: true }
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Capture metadata from final chunk
|
|
222
|
+
if (candidate?.groundingMetadata) {
|
|
223
|
+
groundingMetadata = candidate.groundingMetadata;
|
|
224
|
+
}
|
|
225
|
+
// Handle both camelCase and snake_case
|
|
226
|
+
if (candidate?.urlContextMetadata || candidate?.url_context_metadata) {
|
|
227
|
+
urlContextMetadata = candidate.urlContextMetadata || candidate.url_context_metadata;
|
|
157
228
|
}
|
|
158
229
|
}
|
|
230
|
+
currentEventData = "";
|
|
231
|
+
currentEventName = "";
|
|
232
|
+
continue;
|
|
159
233
|
}
|
|
160
234
|
|
|
161
|
-
//
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
235
|
+
// Parse SSE field
|
|
236
|
+
if (line.startsWith("data:")) {
|
|
237
|
+
const data = line.slice(5).trim();
|
|
238
|
+
currentEventData = currentEventData ? currentEventData + "\n" + data : data;
|
|
239
|
+
} else if (line.startsWith("event:")) {
|
|
240
|
+
currentEventName = line.slice(6).trim();
|
|
241
|
+
// Check for error event type
|
|
242
|
+
if (currentEventName === "error") {
|
|
243
|
+
// Next data line should contain error details
|
|
244
|
+
}
|
|
245
|
+
} else if (line.startsWith("id:")) {
|
|
246
|
+
// Event ID, can be ignored for now
|
|
247
|
+
} else if (line.startsWith(":")) {
|
|
248
|
+
// Comment line, ignore
|
|
168
249
|
}
|
|
169
250
|
}
|
|
170
251
|
}
|
package/src/url_context.ts
CHANGED
package/src/web_search.ts
CHANGED
|
@@ -51,7 +51,7 @@ export async function webSearch(
|
|
|
51
51
|
const result = await callApiStream(ctx, model, {
|
|
52
52
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
53
53
|
tools
|
|
54
|
-
}, onUpdate);
|
|
54
|
+
}, onUpdate, signal);
|
|
55
55
|
|
|
56
56
|
const { text, sources } = applyCitations(result.text, result.groundingMetadata);
|
|
57
57
|
|