@oh-my-pi/anthropic-websearch 0.1.0 → 0.8.2

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
@@ -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
- ### Example: Using a proxy
24
+ ### Using Claude Code OAuth tokens
25
25
 
26
- If your `~/.pi/agent/models.json` has:
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` with no API key.
45
+ The plugin will automatically use `http://localhost:4000`.
42
46
 
43
- ### Example: Direct API key
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.0",
3
+ "version": "0.8.2",
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 search-specific env vars
121
- const searchApiKey = getEnv("ANTHROPIC_SEARCH_API_KEY");
122
- const searchBaseUrl = getEnv("ANTHROPIC_SEARCH_BASE_URL");
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
- // Response types
178
- interface ServerToolUse {
179
- type: "server_tool_use";
180
- id: string;
181
- name: "web_search";
182
- input: { query: string };
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 TextBlock {
208
- type: "text";
209
- text: string;
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
- type ContentBlock = ServerToolUse | WebSearchToolResult | TextBlock;
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
- stop_reason: string;
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 Messages API with web search tool
307
+ * Call Anthropic API with web search
230
308
  */
231
- async function callAnthropicWebSearch(
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<MessagesResponse> {
238
- const url = `${auth.baseUrl}/v1/messages`;
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 (systemPrompt) {
248
- body.system = systemPrompt;
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<MessagesResponse>;
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: MessagesResponse): { text: string; details: unknown } {
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.query);
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 = getEnv("ANTHROPIC_SEARCH_MODEL") ?? DEFAULT_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 callAnthropicWebSearch(
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
  );
@@ -0,0 +1,3 @@
1
+ {
2
+ "options": {}
3
+ }