@oh-my-pi/anthropic-websearch 0.1.0 → 0.8.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 +31 -11
- package/package.json +7 -2
- package/tools/anthropic-websearch/index.ts +143 -57
- package/tools/anthropic-websearch/runtime.json +3 -0
package/README.md
CHANGED
|
@@ -16,14 +16,18 @@ The plugin checks for credentials in this order:
|
|
|
16
16
|
|
|
17
17
|
1. **Explicit override**: `ANTHROPIC_SEARCH_API_KEY` / `ANTHROPIC_SEARCH_BASE_URL`
|
|
18
18
|
2. **models.json**: Provider with `api: "anthropic-messages"` in `~/.pi/agent/models.json`
|
|
19
|
-
3. **OAuth**: Anthropic OAuth credentials in `~/.pi/agent/auth.json`
|
|
19
|
+
3. **OAuth**: Anthropic OAuth credentials in `~/.pi/agent/auth.json` (Claude Code tokens supported)
|
|
20
20
|
4. **Fallback**: `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL`
|
|
21
21
|
|
|
22
22
|
This ordering prevents accidentally charging your console account if you have a proxy or OAuth set up.
|
|
23
23
|
|
|
24
|
-
###
|
|
24
|
+
### Using Claude Code OAuth tokens
|
|
25
25
|
|
|
26
|
-
If your `~/.pi/agent/
|
|
26
|
+
If you're logged into Claude Code (`pi login`), the plugin will automatically use your OAuth token from `~/.pi/agent/auth.json`. OAuth tokens (`sk-ant-oat01-...`) are fully supported with proper Claude Code identity headers.
|
|
27
|
+
|
|
28
|
+
### Using a proxy
|
|
29
|
+
|
|
30
|
+
If your `~/.pi/agent/models.json` has a provider with `api: "anthropic-messages"`:
|
|
27
31
|
|
|
28
32
|
```json
|
|
29
33
|
{
|
|
@@ -38,12 +42,12 @@ If your `~/.pi/agent/models.json` has:
|
|
|
38
42
|
}
|
|
39
43
|
```
|
|
40
44
|
|
|
41
|
-
The plugin will automatically use `http://localhost:4000
|
|
45
|
+
The plugin will automatically use `http://localhost:4000`.
|
|
42
46
|
|
|
43
|
-
###
|
|
47
|
+
### Direct API key
|
|
44
48
|
|
|
45
49
|
```bash
|
|
46
|
-
export ANTHROPIC_SEARCH_API_KEY=sk-ant-xxx
|
|
50
|
+
export ANTHROPIC_SEARCH_API_KEY=sk-ant-api03-xxx
|
|
47
51
|
```
|
|
48
52
|
|
|
49
53
|
## Tools
|
|
@@ -66,11 +70,27 @@ Search the web using Claude's built-in web search capability.
|
|
|
66
70
|
|
|
67
71
|
## Configuration
|
|
68
72
|
|
|
69
|
-
| Variable | Env | Description
|
|
70
|
-
| --------- | --------------------------- |
|
|
71
|
-
| `apiKey` | `ANTHROPIC_SEARCH_API_KEY` | API key (optional if using proxy/oauth)
|
|
72
|
-
| `baseUrl` | `ANTHROPIC_SEARCH_BASE_URL` | Base URL override
|
|
73
|
-
| `model` | `ANTHROPIC_SEARCH_MODEL` | Model to use (default: `claude-sonnet-4-20250514`) |
|
|
73
|
+
| Variable | Env | Description |
|
|
74
|
+
| --------- | --------------------------- | ---------------------------------------------------- |
|
|
75
|
+
| `apiKey` | `ANTHROPIC_SEARCH_API_KEY` | API key (optional if using proxy/oauth) |
|
|
76
|
+
| `baseUrl` | `ANTHROPIC_SEARCH_BASE_URL` | Base URL override |
|
|
77
|
+
| `model` | `ANTHROPIC_SEARCH_MODEL` | Model to use (default: `claude-sonnet-4-5-20250514`) |
|
|
78
|
+
|
|
79
|
+
Configure via `omp config`:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Set a different model
|
|
83
|
+
omp config @oh-my-pi/anthropic-websearch model claude-opus-4-20250514
|
|
84
|
+
|
|
85
|
+
# View current config
|
|
86
|
+
omp config @oh-my-pi/anthropic-websearch
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or via environment variables:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export ANTHROPIC_SEARCH_MODEL=claude-opus-4-20250514
|
|
93
|
+
```
|
|
74
94
|
|
|
75
95
|
## License
|
|
76
96
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/anthropic-websearch",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Anthropic Claude web search tool for pi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"omp-plugin",
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
},
|
|
18
18
|
"omp": {
|
|
19
19
|
"install": [
|
|
20
|
+
{
|
|
21
|
+
"src": "tools/anthropic-websearch/runtime.json",
|
|
22
|
+
"dest": "agent/tools/anthropic-websearch/runtime.json",
|
|
23
|
+
"copy": true
|
|
24
|
+
},
|
|
20
25
|
{
|
|
21
26
|
"src": "tools/anthropic-websearch/index.ts",
|
|
22
27
|
"dest": "agent/tools/anthropic-websearch/index.ts"
|
|
@@ -39,7 +44,7 @@
|
|
|
39
44
|
"type": "string",
|
|
40
45
|
"env": "ANTHROPIC_SEARCH_MODEL",
|
|
41
46
|
"description": "Model to use for web search",
|
|
42
|
-
"default": "claude-sonnet-4-20250514"
|
|
47
|
+
"default": "claude-sonnet-4-5-20250514"
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
},
|
|
@@ -19,13 +19,23 @@ import type {
|
|
|
19
19
|
CustomToolFactory,
|
|
20
20
|
ToolAPI,
|
|
21
21
|
} from "@mariozechner/pi-coding-agent";
|
|
22
|
+
import runtime from "./runtime.json";
|
|
22
23
|
|
|
23
24
|
const DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
24
|
-
const DEFAULT_MODEL = "claude-sonnet-4-20250514";
|
|
25
|
+
const DEFAULT_MODEL = "claude-sonnet-4-5-20250514";
|
|
26
|
+
|
|
27
|
+
interface RuntimeConfig {
|
|
28
|
+
options?: {
|
|
29
|
+
model?: string;
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
25
34
|
|
|
26
35
|
interface AuthConfig {
|
|
27
36
|
apiKey: string;
|
|
28
37
|
baseUrl: string;
|
|
38
|
+
isOAuth: boolean;
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
interface ModelsJson {
|
|
@@ -40,6 +50,7 @@ interface AuthJson {
|
|
|
40
50
|
anthropic?: {
|
|
41
51
|
type: "oauth";
|
|
42
52
|
access: string;
|
|
53
|
+
refresh?: string;
|
|
43
54
|
expires: number;
|
|
44
55
|
};
|
|
45
56
|
}
|
|
@@ -107,9 +118,31 @@ function readJson<T>(filePath: string): T | null {
|
|
|
107
118
|
}
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Check if a token is an OAuth token
|
|
123
|
+
*/
|
|
124
|
+
function isOAuthToken(apiKey: string): boolean {
|
|
125
|
+
return apiKey.includes("sk-ant-oat");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get runtime config value, falling back to env var
|
|
130
|
+
*/
|
|
131
|
+
function getConfig(key: "model" | "apiKey" | "baseUrl"): string | undefined {
|
|
132
|
+
const cfg = (runtime as RuntimeConfig).options ?? {};
|
|
133
|
+
if (cfg[key]) return cfg[key];
|
|
134
|
+
|
|
135
|
+
const envMap = {
|
|
136
|
+
model: "ANTHROPIC_SEARCH_MODEL",
|
|
137
|
+
apiKey: "ANTHROPIC_SEARCH_API_KEY",
|
|
138
|
+
baseUrl: "ANTHROPIC_SEARCH_BASE_URL",
|
|
139
|
+
};
|
|
140
|
+
return getEnv(envMap[key]);
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
/**
|
|
111
144
|
* Find auth config using priority order:
|
|
112
|
-
* 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
|
|
145
|
+
* 1. Runtime config / ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
|
|
113
146
|
* 2. Provider with api="anthropic-messages" in models.json
|
|
114
147
|
* 3. OAuth in auth.json
|
|
115
148
|
* 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
|
|
@@ -117,13 +150,14 @@ function readJson<T>(filePath: string): T | null {
|
|
|
117
150
|
function findAuthConfig(): AuthConfig | null {
|
|
118
151
|
const piAgentDir = path.join(os.homedir(), ".pi", "agent");
|
|
119
152
|
|
|
120
|
-
// 1. Explicit
|
|
121
|
-
const searchApiKey =
|
|
122
|
-
const searchBaseUrl =
|
|
153
|
+
// 1. Explicit config or env vars
|
|
154
|
+
const searchApiKey = getConfig("apiKey");
|
|
155
|
+
const searchBaseUrl = getConfig("baseUrl");
|
|
123
156
|
if (searchApiKey) {
|
|
124
157
|
return {
|
|
125
158
|
apiKey: searchApiKey,
|
|
126
159
|
baseUrl: searchBaseUrl ?? DEFAULT_BASE_URL,
|
|
160
|
+
isOAuth: isOAuthToken(searchApiKey),
|
|
127
161
|
};
|
|
128
162
|
}
|
|
129
163
|
|
|
@@ -135,6 +169,7 @@ function findAuthConfig(): AuthConfig | null {
|
|
|
135
169
|
return {
|
|
136
170
|
apiKey: provider.apiKey,
|
|
137
171
|
baseUrl: provider.baseUrl ?? DEFAULT_BASE_URL,
|
|
172
|
+
isOAuth: isOAuthToken(provider.apiKey),
|
|
138
173
|
};
|
|
139
174
|
}
|
|
140
175
|
}
|
|
@@ -144,6 +179,7 @@ function findAuthConfig(): AuthConfig | null {
|
|
|
144
179
|
return {
|
|
145
180
|
apiKey: provider.apiKey ?? "",
|
|
146
181
|
baseUrl: provider.baseUrl,
|
|
182
|
+
isOAuth: false,
|
|
147
183
|
};
|
|
148
184
|
}
|
|
149
185
|
}
|
|
@@ -152,11 +188,12 @@ function findAuthConfig(): AuthConfig | null {
|
|
|
152
188
|
// 3. OAuth credentials in auth.json
|
|
153
189
|
const authJson = readJson<AuthJson>(path.join(piAgentDir, "auth.json"));
|
|
154
190
|
if (authJson?.anthropic?.type === "oauth" && authJson.anthropic.access) {
|
|
155
|
-
// Check if not expired
|
|
156
|
-
if (authJson.anthropic.expires > Date.now()) {
|
|
191
|
+
// Check if not expired (with 5 min buffer)
|
|
192
|
+
if (authJson.anthropic.expires > Date.now() + 5 * 60 * 1000) {
|
|
157
193
|
return {
|
|
158
194
|
apiKey: authJson.anthropic.access,
|
|
159
195
|
baseUrl: DEFAULT_BASE_URL,
|
|
196
|
+
isOAuth: true,
|
|
160
197
|
};
|
|
161
198
|
}
|
|
162
199
|
}
|
|
@@ -168,20 +205,66 @@ function findAuthConfig(): AuthConfig | null {
|
|
|
168
205
|
return {
|
|
169
206
|
apiKey,
|
|
170
207
|
baseUrl: baseUrl ?? DEFAULT_BASE_URL,
|
|
208
|
+
isOAuth: isOAuthToken(apiKey),
|
|
171
209
|
};
|
|
172
210
|
}
|
|
173
211
|
|
|
174
212
|
return null;
|
|
175
213
|
}
|
|
176
214
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Build headers for Anthropic API request
|
|
217
|
+
*/
|
|
218
|
+
function buildHeaders(auth: AuthConfig): Record<string, string> {
|
|
219
|
+
const betas = ["web-search-2025-03-05"];
|
|
220
|
+
|
|
221
|
+
if (auth.isOAuth) {
|
|
222
|
+
// OAuth requires additional beta headers and stainless telemetry
|
|
223
|
+
betas.push(
|
|
224
|
+
"oauth-2025-04-20",
|
|
225
|
+
"claude-code-20250219",
|
|
226
|
+
"prompt-caching-2024-07-31",
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"anthropic-version": "2023-06-01",
|
|
231
|
+
"authorization": `Bearer ${auth.apiKey}`,
|
|
232
|
+
"accept": "application/json",
|
|
233
|
+
"content-type": "application/json",
|
|
234
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
235
|
+
"anthropic-beta": betas.join(","),
|
|
236
|
+
"user-agent": "claude-cli/2.0.46 (external, cli)",
|
|
237
|
+
"x-app": "cli",
|
|
238
|
+
// Stainless SDK telemetry headers (required for OAuth)
|
|
239
|
+
"x-stainless-arch": "x64",
|
|
240
|
+
"x-stainless-lang": "js",
|
|
241
|
+
"x-stainless-os": "Linux",
|
|
242
|
+
"x-stainless-package-version": "0.60.0",
|
|
243
|
+
"x-stainless-retry-count": "1",
|
|
244
|
+
"x-stainless-runtime": "node",
|
|
245
|
+
"x-stainless-runtime-version": "v24.3.0",
|
|
246
|
+
};
|
|
247
|
+
} else {
|
|
248
|
+
// Standard API key auth
|
|
249
|
+
return {
|
|
250
|
+
"anthropic-version": "2023-06-01",
|
|
251
|
+
"x-api-key": auth.apiKey,
|
|
252
|
+
"accept": "application/json",
|
|
253
|
+
"content-type": "application/json",
|
|
254
|
+
"anthropic-beta": betas.join(","),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
183
257
|
}
|
|
184
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Build API URL (OAuth requires ?beta=true)
|
|
261
|
+
*/
|
|
262
|
+
function buildUrl(auth: AuthConfig): string {
|
|
263
|
+
const base = `${auth.baseUrl}/v1/messages`;
|
|
264
|
+
return auth.isOAuth ? `${base}?beta=true` : base;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Response types
|
|
185
268
|
interface WebSearchResult {
|
|
186
269
|
type: "web_search_result";
|
|
187
270
|
title: string;
|
|
@@ -190,12 +273,6 @@ interface WebSearchResult {
|
|
|
190
273
|
page_age: string | null;
|
|
191
274
|
}
|
|
192
275
|
|
|
193
|
-
interface WebSearchToolResult {
|
|
194
|
-
type: "web_search_tool_result";
|
|
195
|
-
tool_use_id: string;
|
|
196
|
-
content: WebSearchResult[];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
276
|
interface Citation {
|
|
200
277
|
type: "web_search_result_location";
|
|
201
278
|
url: string;
|
|
@@ -204,38 +281,60 @@ interface Citation {
|
|
|
204
281
|
encrypted_index: string;
|
|
205
282
|
}
|
|
206
283
|
|
|
207
|
-
interface
|
|
208
|
-
type:
|
|
209
|
-
text
|
|
284
|
+
interface ContentBlock {
|
|
285
|
+
type: string;
|
|
286
|
+
text?: string;
|
|
210
287
|
citations?: Citation[];
|
|
288
|
+
name?: string;
|
|
289
|
+
input?: { query: string };
|
|
290
|
+
content?: WebSearchResult[];
|
|
211
291
|
}
|
|
212
292
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
interface MessagesResponse {
|
|
293
|
+
interface ApiResponse {
|
|
216
294
|
id: string;
|
|
217
|
-
type: string;
|
|
218
|
-
role: string;
|
|
219
|
-
content: ContentBlock[];
|
|
220
295
|
model: string;
|
|
221
|
-
|
|
296
|
+
content: ContentBlock[];
|
|
222
297
|
usage: {
|
|
223
298
|
input_tokens: number;
|
|
224
299
|
output_tokens: number;
|
|
300
|
+
cache_read_input_tokens?: number;
|
|
301
|
+
cache_creation_input_tokens?: number;
|
|
302
|
+
server_tool_use?: { web_search_requests: number };
|
|
225
303
|
};
|
|
226
304
|
}
|
|
227
305
|
|
|
228
306
|
/**
|
|
229
|
-
* Call Anthropic
|
|
307
|
+
* Call Anthropic API with web search
|
|
230
308
|
*/
|
|
231
|
-
async function
|
|
309
|
+
async function callWebSearch(
|
|
232
310
|
auth: AuthConfig,
|
|
233
|
-
query: string,
|
|
234
311
|
model: string,
|
|
312
|
+
query: string,
|
|
235
313
|
systemPrompt?: string,
|
|
236
314
|
maxTokens?: number,
|
|
237
|
-
): Promise<
|
|
238
|
-
const url =
|
|
315
|
+
): Promise<ApiResponse> {
|
|
316
|
+
const url = buildUrl(auth);
|
|
317
|
+
const headers = buildHeaders(auth);
|
|
318
|
+
|
|
319
|
+
// Build system blocks
|
|
320
|
+
const systemBlocks: Array<{ type: string; text: string; cache_control?: { type: string } }> = [];
|
|
321
|
+
|
|
322
|
+
if (auth.isOAuth) {
|
|
323
|
+
// OAuth requires Claude Code identity with cache_control
|
|
324
|
+
systemBlocks.push({
|
|
325
|
+
type: "text",
|
|
326
|
+
text: "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
327
|
+
cache_control: { type: "ephemeral" },
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (systemPrompt) {
|
|
332
|
+
systemBlocks.push({
|
|
333
|
+
type: "text",
|
|
334
|
+
text: systemPrompt,
|
|
335
|
+
...(auth.isOAuth ? { cache_control: { type: "ephemeral" } } : {}),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
239
338
|
|
|
240
339
|
const body: Record<string, unknown> = {
|
|
241
340
|
model,
|
|
@@ -244,21 +343,8 @@ async function callAnthropicWebSearch(
|
|
|
244
343
|
tools: [{ type: "web_search_20250305", name: "web_search" }],
|
|
245
344
|
};
|
|
246
345
|
|
|
247
|
-
if (
|
|
248
|
-
body.system =
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const headers: Record<string, string> = {
|
|
252
|
-
"Content-Type": "application/json",
|
|
253
|
-
"anthropic-version": "2023-06-01",
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// Handle different auth types
|
|
257
|
-
if (auth.apiKey.startsWith("sk-ant-")) {
|
|
258
|
-
headers["x-api-key"] = auth.apiKey;
|
|
259
|
-
} else if (auth.apiKey && auth.apiKey !== "none") {
|
|
260
|
-
// OAuth token or other bearer token
|
|
261
|
-
headers["Authorization"] = `Bearer ${auth.apiKey}`;
|
|
346
|
+
if (systemBlocks.length > 0) {
|
|
347
|
+
body.system = systemBlocks;
|
|
262
348
|
}
|
|
263
349
|
|
|
264
350
|
const response = await fetch(url, {
|
|
@@ -272,13 +358,13 @@ async function callAnthropicWebSearch(
|
|
|
272
358
|
throw new Error(`Anthropic API error (${response.status}): ${errorText}`);
|
|
273
359
|
}
|
|
274
360
|
|
|
275
|
-
return response.json() as Promise<
|
|
361
|
+
return response.json() as Promise<ApiResponse>;
|
|
276
362
|
}
|
|
277
363
|
|
|
278
364
|
/**
|
|
279
365
|
* Format response for display
|
|
280
366
|
*/
|
|
281
|
-
function formatResponse(response:
|
|
367
|
+
function formatResponse(response: ApiResponse): { text: string; details: unknown } {
|
|
282
368
|
const parts: string[] = [];
|
|
283
369
|
const searchQueries: string[] = [];
|
|
284
370
|
const sources: Array<{ title: string; url: string; age: string | null }> = [];
|
|
@@ -286,8 +372,8 @@ function formatResponse(response: MessagesResponse): { text: string; details: un
|
|
|
286
372
|
|
|
287
373
|
for (const block of response.content) {
|
|
288
374
|
if (block.type === "server_tool_use" && block.name === "web_search") {
|
|
289
|
-
searchQueries.push(block.input
|
|
290
|
-
} else if (block.type === "web_search_tool_result") {
|
|
375
|
+
searchQueries.push(block.input?.query ?? "");
|
|
376
|
+
} else if (block.type === "web_search_tool_result" && block.content) {
|
|
291
377
|
for (const result of block.content) {
|
|
292
378
|
if (result.type === "web_search_result") {
|
|
293
379
|
sources.push({
|
|
@@ -297,7 +383,7 @@ function formatResponse(response: MessagesResponse): { text: string; details: un
|
|
|
297
383
|
});
|
|
298
384
|
}
|
|
299
385
|
}
|
|
300
|
-
} else if (block.type === "text") {
|
|
386
|
+
} else if (block.type === "text" && block.text) {
|
|
301
387
|
parts.push(block.text);
|
|
302
388
|
if (block.citations) {
|
|
303
389
|
citations.push(...block.citations);
|
|
@@ -366,7 +452,7 @@ const factory: CustomToolFactory = async (
|
|
|
366
452
|
return null;
|
|
367
453
|
}
|
|
368
454
|
|
|
369
|
-
const model =
|
|
455
|
+
const model = getConfig("model") ?? DEFAULT_MODEL;
|
|
370
456
|
|
|
371
457
|
const tool: CustomAgentTool<typeof SearchSchema, unknown> = {
|
|
372
458
|
name: "anthropic_web_search",
|
|
@@ -376,10 +462,10 @@ const factory: CustomToolFactory = async (
|
|
|
376
462
|
async execute(_toolCallId, params) {
|
|
377
463
|
try {
|
|
378
464
|
const p = (params ?? {}) as SearchParams;
|
|
379
|
-
const response = await
|
|
465
|
+
const response = await callWebSearch(
|
|
380
466
|
auth,
|
|
381
|
-
p.query,
|
|
382
467
|
model,
|
|
468
|
+
p.query,
|
|
383
469
|
p.system_prompt,
|
|
384
470
|
p.max_tokens,
|
|
385
471
|
);
|