@oh-my-pi/anthropic-websearch 0.9.2 → 1.3.371
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/package.json +1 -1
- package/tools/index.ts +356 -372
package/package.json
CHANGED
package/tools/index.ts
CHANGED
|
@@ -10,134 +10,130 @@
|
|
|
10
10
|
* 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL as final fallback
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import * as fs from
|
|
14
|
-
import * as os from
|
|
15
|
-
import * as path from
|
|
16
|
-
import {
|
|
17
|
-
import type
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import runtime from "./runtime.json";
|
|
23
|
-
|
|
24
|
-
const DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
25
|
-
const DEFAULT_MODEL = "claude-sonnet-4-5-20250514";
|
|
13
|
+
import * as fs from 'node:fs'
|
|
14
|
+
import * as os from 'node:os'
|
|
15
|
+
import * as path from 'node:path'
|
|
16
|
+
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
|
|
17
|
+
import { type TSchema, Type } from '@sinclair/typebox'
|
|
18
|
+
import runtime from './runtime.json'
|
|
19
|
+
|
|
20
|
+
const DEFAULT_BASE_URL = 'https://api.anthropic.com'
|
|
21
|
+
const DEFAULT_MODEL = 'claude-sonnet-4-5-20250514'
|
|
26
22
|
|
|
27
23
|
interface RuntimeConfig {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
options?: {
|
|
25
|
+
model?: string
|
|
26
|
+
apiKey?: string
|
|
27
|
+
baseUrl?: string
|
|
28
|
+
}
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
interface AuthConfig {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
apiKey: string
|
|
33
|
+
baseUrl: string
|
|
34
|
+
isOAuth: boolean
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
interface ModelsJson {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
providers?: Record<
|
|
39
|
+
string,
|
|
40
|
+
{
|
|
41
|
+
baseUrl?: string
|
|
42
|
+
apiKey?: string
|
|
43
|
+
api?: string
|
|
44
|
+
}
|
|
45
|
+
>
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
interface AuthJson {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
anthropic?: {
|
|
50
|
+
type: 'oauth'
|
|
51
|
+
access: string
|
|
52
|
+
refresh?: string
|
|
53
|
+
expires: number
|
|
54
|
+
}
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
/**
|
|
59
58
|
* Parse a .env file and return key-value pairs
|
|
60
59
|
*/
|
|
61
60
|
function parseEnvFile(filePath: string): Record<string, string> {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const eqIndex = trimmed.indexOf("=");
|
|
72
|
-
if (eqIndex === -1) continue;
|
|
73
|
-
|
|
74
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
75
|
-
let value = trimmed.slice(eqIndex + 1).trim();
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
79
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
80
|
-
) {
|
|
81
|
-
value = value.slice(1, -1);
|
|
82
|
-
}
|
|
61
|
+
const result: Record<string, string> = {}
|
|
62
|
+
if (!fs.existsSync(filePath)) return result
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
66
|
+
for (const line of content.split('\n')) {
|
|
67
|
+
const trimmed = line.trim()
|
|
68
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
} catch {
|
|
87
|
-
// Ignore read errors
|
|
88
|
-
}
|
|
70
|
+
const eqIndex = trimmed.indexOf('=')
|
|
71
|
+
if (eqIndex === -1) continue
|
|
89
72
|
|
|
90
|
-
|
|
73
|
+
const key = trimmed.slice(0, eqIndex).trim()
|
|
74
|
+
let value = trimmed.slice(eqIndex + 1).trim()
|
|
75
|
+
|
|
76
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
77
|
+
value = value.slice(1, -1)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
result[key] = value
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Ignore read errors
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
/**
|
|
94
90
|
* Get env var from process.env or .env files
|
|
95
91
|
*/
|
|
96
92
|
function getEnv(key: string): string | undefined {
|
|
97
|
-
|
|
93
|
+
if (process.env[key]) return process.env[key]
|
|
98
94
|
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
const localEnv = parseEnvFile(path.join(process.cwd(), '.env'))
|
|
96
|
+
if (localEnv[key]) return localEnv[key]
|
|
101
97
|
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
const homeEnv = parseEnvFile(path.join(os.homedir(), '.env'))
|
|
99
|
+
if (homeEnv[key]) return homeEnv[key]
|
|
104
100
|
|
|
105
|
-
|
|
101
|
+
return undefined
|
|
106
102
|
}
|
|
107
103
|
|
|
108
104
|
/**
|
|
109
105
|
* Read JSON file safely
|
|
110
106
|
*/
|
|
111
107
|
function readJson<T>(filePath: string): T | null {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.existsSync(filePath)) return null
|
|
110
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
111
|
+
return JSON.parse(content) as T
|
|
112
|
+
} catch {
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
/**
|
|
122
118
|
* Check if a token is an OAuth token
|
|
123
119
|
*/
|
|
124
120
|
function isOAuthToken(apiKey: string): boolean {
|
|
125
|
-
|
|
121
|
+
return apiKey.includes('sk-ant-oat')
|
|
126
122
|
}
|
|
127
123
|
|
|
128
124
|
/**
|
|
129
125
|
* Get runtime config value, falling back to env var
|
|
130
126
|
*/
|
|
131
|
-
function getConfig(key:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
127
|
+
function getConfig(key: 'model' | 'apiKey' | 'baseUrl'): string | undefined {
|
|
128
|
+
const cfg = (runtime as RuntimeConfig).options ?? {}
|
|
129
|
+
if (cfg[key]) return cfg[key]
|
|
130
|
+
|
|
131
|
+
const envMap = {
|
|
132
|
+
model: 'ANTHROPIC_SEARCH_MODEL',
|
|
133
|
+
apiKey: 'ANTHROPIC_SEARCH_API_KEY',
|
|
134
|
+
baseUrl: 'ANTHROPIC_SEARCH_BASE_URL',
|
|
135
|
+
}
|
|
136
|
+
return getEnv(envMap[key])
|
|
141
137
|
}
|
|
142
138
|
|
|
143
139
|
/**
|
|
@@ -148,343 +144,331 @@ function getConfig(key: "model" | "apiKey" | "baseUrl"): string | undefined {
|
|
|
148
144
|
* 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
|
|
149
145
|
*/
|
|
150
146
|
function findAuthConfig(): AuthConfig | null {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// 2. Provider with api="anthropic-messages" in models.json
|
|
165
|
-
const modelsJson = readJson<ModelsJson>(path.join(piAgentDir, "models.json"));
|
|
166
|
-
if (modelsJson?.providers) {
|
|
167
|
-
for (const [_name, provider] of Object.entries(modelsJson.providers)) {
|
|
168
|
-
if (provider.api === "anthropic-messages" && provider.apiKey && provider.apiKey !== "none") {
|
|
169
|
-
return {
|
|
170
|
-
apiKey: provider.apiKey,
|
|
171
|
-
baseUrl: provider.baseUrl ?? DEFAULT_BASE_URL,
|
|
172
|
-
isOAuth: isOAuthToken(provider.apiKey),
|
|
173
|
-
};
|
|
147
|
+
const piAgentDir = path.join(os.homedir(), '.pi', 'agent')
|
|
148
|
+
|
|
149
|
+
// 1. Explicit config or env vars
|
|
150
|
+
const searchApiKey = getConfig('apiKey')
|
|
151
|
+
const searchBaseUrl = getConfig('baseUrl')
|
|
152
|
+
if (searchApiKey) {
|
|
153
|
+
return {
|
|
154
|
+
apiKey: searchApiKey,
|
|
155
|
+
baseUrl: searchBaseUrl ?? DEFAULT_BASE_URL,
|
|
156
|
+
isOAuth: isOAuthToken(searchApiKey),
|
|
174
157
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 2. Provider with api="anthropic-messages" in models.json
|
|
161
|
+
const modelsJson = readJson<ModelsJson>(path.join(piAgentDir, 'models.json'))
|
|
162
|
+
if (modelsJson?.providers) {
|
|
163
|
+
for (const [_name, provider] of Object.entries(modelsJson.providers)) {
|
|
164
|
+
if (provider.api === 'anthropic-messages' && provider.apiKey && provider.apiKey !== 'none') {
|
|
165
|
+
return {
|
|
166
|
+
apiKey: provider.apiKey,
|
|
167
|
+
baseUrl: provider.baseUrl ?? DEFAULT_BASE_URL,
|
|
168
|
+
isOAuth: isOAuthToken(provider.apiKey),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
184
171
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
172
|
+
// Also check for providers with baseUrl but apiKey="none" (proxy)
|
|
173
|
+
for (const [_name, provider] of Object.entries(modelsJson.providers)) {
|
|
174
|
+
if (provider.api === 'anthropic-messages' && provider.baseUrl) {
|
|
175
|
+
return {
|
|
176
|
+
apiKey: provider.apiKey ?? '',
|
|
177
|
+
baseUrl: provider.baseUrl,
|
|
178
|
+
isOAuth: false,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 3. OAuth credentials in auth.json
|
|
185
|
+
const authJson = readJson<AuthJson>(path.join(piAgentDir, 'auth.json'))
|
|
186
|
+
if (authJson?.anthropic?.type === 'oauth' && authJson.anthropic.access) {
|
|
187
|
+
// Check if not expired (with 5 min buffer)
|
|
188
|
+
if (authJson.anthropic.expires > Date.now() + 5 * 60 * 1000) {
|
|
189
|
+
return {
|
|
190
|
+
apiKey: authJson.anthropic.access,
|
|
191
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
192
|
+
isOAuth: true,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 4. Generic ANTHROPIC_API_KEY fallback
|
|
198
|
+
const apiKey = getEnv('ANTHROPIC_API_KEY')
|
|
199
|
+
const baseUrl = getEnv('ANTHROPIC_BASE_URL')
|
|
200
|
+
if (apiKey) {
|
|
193
201
|
return {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// 4. Generic ANTHROPIC_API_KEY fallback
|
|
202
|
-
const apiKey = getEnv("ANTHROPIC_API_KEY");
|
|
203
|
-
const baseUrl = getEnv("ANTHROPIC_BASE_URL");
|
|
204
|
-
if (apiKey) {
|
|
205
|
-
return {
|
|
206
|
-
apiKey,
|
|
207
|
-
baseUrl: baseUrl ?? DEFAULT_BASE_URL,
|
|
208
|
-
isOAuth: isOAuthToken(apiKey),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return null;
|
|
202
|
+
apiKey,
|
|
203
|
+
baseUrl: baseUrl ?? DEFAULT_BASE_URL,
|
|
204
|
+
isOAuth: isOAuthToken(apiKey),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return null
|
|
213
209
|
}
|
|
214
210
|
|
|
215
211
|
/**
|
|
216
212
|
* Build headers for Anthropic API request
|
|
217
213
|
*/
|
|
218
214
|
function buildHeaders(auth: AuthConfig): Record<string, string> {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
"content-type": "application/json",
|
|
254
|
-
"anthropic-beta": betas.join(","),
|
|
255
|
-
};
|
|
256
|
-
}
|
|
215
|
+
const betas = ['web-search-2025-03-05']
|
|
216
|
+
|
|
217
|
+
if (auth.isOAuth) {
|
|
218
|
+
// OAuth requires additional beta headers and stainless telemetry
|
|
219
|
+
betas.push('oauth-2025-04-20', 'claude-code-20250219', 'prompt-caching-2024-07-31')
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
'anthropic-version': '2023-06-01',
|
|
223
|
+
authorization: `Bearer ${auth.apiKey}`,
|
|
224
|
+
accept: 'application/json',
|
|
225
|
+
'content-type': 'application/json',
|
|
226
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
227
|
+
'anthropic-beta': betas.join(','),
|
|
228
|
+
'user-agent': 'claude-cli/2.0.46 (external, cli)',
|
|
229
|
+
'x-app': 'cli',
|
|
230
|
+
// Stainless SDK telemetry headers (required for OAuth)
|
|
231
|
+
'x-stainless-arch': 'x64',
|
|
232
|
+
'x-stainless-lang': 'js',
|
|
233
|
+
'x-stainless-os': 'Linux',
|
|
234
|
+
'x-stainless-package-version': '0.60.0',
|
|
235
|
+
'x-stainless-retry-count': '1',
|
|
236
|
+
'x-stainless-runtime': 'node',
|
|
237
|
+
'x-stainless-runtime-version': 'v24.3.0',
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
// Standard API key auth
|
|
241
|
+
return {
|
|
242
|
+
'anthropic-version': '2023-06-01',
|
|
243
|
+
'x-api-key': auth.apiKey,
|
|
244
|
+
accept: 'application/json',
|
|
245
|
+
'content-type': 'application/json',
|
|
246
|
+
'anthropic-beta': betas.join(','),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
257
249
|
}
|
|
258
250
|
|
|
259
251
|
/**
|
|
260
252
|
* Build API URL (OAuth requires ?beta=true)
|
|
261
253
|
*/
|
|
262
254
|
function buildUrl(auth: AuthConfig): string {
|
|
263
|
-
|
|
264
|
-
|
|
255
|
+
const base = `${auth.baseUrl}/v1/messages`
|
|
256
|
+
return auth.isOAuth ? `${base}?beta=true` : base
|
|
265
257
|
}
|
|
266
258
|
|
|
267
259
|
// Response types
|
|
268
260
|
interface WebSearchResult {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
261
|
+
type: 'web_search_result'
|
|
262
|
+
title: string
|
|
263
|
+
url: string
|
|
264
|
+
encrypted_content: string
|
|
265
|
+
page_age: string | null
|
|
274
266
|
}
|
|
275
267
|
|
|
276
268
|
interface Citation {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
269
|
+
type: 'web_search_result_location'
|
|
270
|
+
url: string
|
|
271
|
+
title: string
|
|
272
|
+
cited_text: string
|
|
273
|
+
encrypted_index: string
|
|
282
274
|
}
|
|
283
275
|
|
|
284
276
|
interface ContentBlock {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
277
|
+
type: string
|
|
278
|
+
text?: string
|
|
279
|
+
citations?: Citation[]
|
|
280
|
+
name?: string
|
|
281
|
+
input?: { query: string }
|
|
282
|
+
content?: WebSearchResult[]
|
|
291
283
|
}
|
|
292
284
|
|
|
293
285
|
interface ApiResponse {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
286
|
+
id: string
|
|
287
|
+
model: string
|
|
288
|
+
content: ContentBlock[]
|
|
289
|
+
usage: {
|
|
290
|
+
input_tokens: number
|
|
291
|
+
output_tokens: number
|
|
292
|
+
cache_read_input_tokens?: number
|
|
293
|
+
cache_creation_input_tokens?: number
|
|
294
|
+
server_tool_use?: { web_search_requests: number }
|
|
295
|
+
}
|
|
304
296
|
}
|
|
305
297
|
|
|
306
298
|
/**
|
|
307
299
|
* Call Anthropic API with web search
|
|
308
300
|
*/
|
|
309
301
|
async function callWebSearch(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
302
|
+
auth: AuthConfig,
|
|
303
|
+
model: string,
|
|
304
|
+
query: string,
|
|
305
|
+
systemPrompt?: string,
|
|
306
|
+
maxTokens?: number
|
|
315
307
|
): Promise<ApiResponse> {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
308
|
+
const url = buildUrl(auth)
|
|
309
|
+
const headers = buildHeaders(auth)
|
|
310
|
+
|
|
311
|
+
// Build system blocks
|
|
312
|
+
const systemBlocks: Array<{ type: string; text: string; cache_control?: { type: string } }> = []
|
|
313
|
+
|
|
314
|
+
if (auth.isOAuth) {
|
|
315
|
+
// OAuth requires Claude Code identity with cache_control
|
|
316
|
+
systemBlocks.push({
|
|
317
|
+
type: 'text',
|
|
318
|
+
text: "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
319
|
+
cache_control: { type: 'ephemeral' },
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (systemPrompt) {
|
|
324
|
+
systemBlocks.push({
|
|
325
|
+
type: 'text',
|
|
326
|
+
text: systemPrompt,
|
|
327
|
+
...(auth.isOAuth ? { cache_control: { type: 'ephemeral' } } : {}),
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const body: Record<string, unknown> = {
|
|
332
|
+
model,
|
|
333
|
+
max_tokens: maxTokens ?? 4096,
|
|
334
|
+
messages: [{ role: 'user', content: query }],
|
|
335
|
+
tools: [{ type: 'web_search_20250305', name: 'web_search' }],
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (systemBlocks.length > 0) {
|
|
339
|
+
body.system = systemBlocks
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const response = await fetch(url, {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
headers,
|
|
345
|
+
body: JSON.stringify(body),
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
const errorText = await response.text()
|
|
350
|
+
throw new Error(`Anthropic API error (${response.status}): ${errorText}`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return response.json() as Promise<ApiResponse>
|
|
362
354
|
}
|
|
363
355
|
|
|
364
356
|
/**
|
|
365
357
|
* Format response for display
|
|
366
358
|
*/
|
|
367
359
|
function formatResponse(response: ApiResponse): { text: string; details: unknown } {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
360
|
+
const parts: string[] = []
|
|
361
|
+
const searchQueries: string[] = []
|
|
362
|
+
const sources: Array<{ title: string; url: string; age: string | null }> = []
|
|
363
|
+
const citations: Citation[] = []
|
|
364
|
+
|
|
365
|
+
for (const block of response.content) {
|
|
366
|
+
if (block.type === 'server_tool_use' && block.name === 'web_search') {
|
|
367
|
+
searchQueries.push(block.input?.query ?? '')
|
|
368
|
+
} else if (block.type === 'web_search_tool_result' && block.content) {
|
|
369
|
+
for (const result of block.content) {
|
|
370
|
+
if (result.type === 'web_search_result') {
|
|
371
|
+
sources.push({
|
|
372
|
+
title: result.title,
|
|
373
|
+
url: result.url,
|
|
374
|
+
age: result.page_age,
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} else if (block.type === 'text' && block.text) {
|
|
379
|
+
parts.push(block.text)
|
|
380
|
+
if (block.citations) {
|
|
381
|
+
citations.push(...block.citations)
|
|
382
|
+
}
|
|
385
383
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let text = parts.join('\n\n')
|
|
387
|
+
|
|
388
|
+
// Add sources
|
|
389
|
+
if (sources.length > 0) {
|
|
390
|
+
text += '\n\n## Sources'
|
|
391
|
+
for (const [i, src] of sources.entries()) {
|
|
392
|
+
const age = src.age ? ` (${src.age})` : ''
|
|
393
|
+
text += `\n[${i + 1}] ${src.title}${age}\n ${src.url}`
|
|
390
394
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
details: {
|
|
408
|
-
model: response.model,
|
|
409
|
-
usage: response.usage,
|
|
410
|
-
searchQueries,
|
|
411
|
-
sources,
|
|
412
|
-
citations: citations.map((c) => ({
|
|
413
|
-
url: c.url,
|
|
414
|
-
title: c.title,
|
|
415
|
-
citedText: c.cited_text,
|
|
416
|
-
})),
|
|
417
|
-
},
|
|
418
|
-
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
text,
|
|
399
|
+
details: {
|
|
400
|
+
model: response.model,
|
|
401
|
+
usage: response.usage,
|
|
402
|
+
searchQueries,
|
|
403
|
+
sources,
|
|
404
|
+
citations: citations.map(c => ({
|
|
405
|
+
url: c.url,
|
|
406
|
+
title: c.title,
|
|
407
|
+
citedText: c.cited_text,
|
|
408
|
+
})),
|
|
409
|
+
},
|
|
410
|
+
}
|
|
419
411
|
}
|
|
420
412
|
|
|
421
413
|
// Tool schema
|
|
422
414
|
const SearchSchema = Type.Object({
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
})
|
|
415
|
+
query: Type.String({
|
|
416
|
+
description: 'The search query or question to answer using web search',
|
|
417
|
+
}),
|
|
418
|
+
system_prompt: Type.Optional(
|
|
419
|
+
Type.String({
|
|
420
|
+
description: 'System prompt to guide the response style and focus',
|
|
421
|
+
})
|
|
422
|
+
),
|
|
423
|
+
max_tokens: Type.Optional(
|
|
424
|
+
Type.Number({
|
|
425
|
+
description: 'Maximum tokens in response (default: 4096)',
|
|
426
|
+
minimum: 1,
|
|
427
|
+
maximum: 16384,
|
|
428
|
+
})
|
|
429
|
+
),
|
|
430
|
+
})
|
|
439
431
|
|
|
440
432
|
type SearchParams = {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const factory: CustomToolFactory = async (
|
|
447
|
-
_toolApi: ToolAPI,
|
|
448
|
-
): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
|
449
|
-
const auth = findAuthConfig();
|
|
450
|
-
if (!auth) {
|
|
451
|
-
console.error("anthropic-websearch: No auth config found. Set ANTHROPIC_SEARCH_API_KEY or configure models.json/auth.json");
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const model = getConfig("model") ?? DEFAULT_MODEL;
|
|
456
|
-
|
|
457
|
-
const tool: CustomAgentTool<typeof SearchSchema, unknown> = {
|
|
458
|
-
name: "anthropic_web_search",
|
|
459
|
-
label: "Anthropic Web Search",
|
|
460
|
-
description: `Web search powered by Claude (${model}). Uses Claude's built-in web search capability to find current information and synthesize answers with citations. Best for questions requiring up-to-date information from the web.`,
|
|
461
|
-
parameters: SearchSchema,
|
|
462
|
-
async execute(_toolCallId, params) {
|
|
463
|
-
try {
|
|
464
|
-
const p = (params ?? {}) as SearchParams;
|
|
465
|
-
const response = await callWebSearch(
|
|
466
|
-
auth,
|
|
467
|
-
model,
|
|
468
|
-
p.query,
|
|
469
|
-
p.system_prompt,
|
|
470
|
-
p.max_tokens,
|
|
471
|
-
);
|
|
472
|
-
const { text, details } = formatResponse(response);
|
|
473
|
-
return {
|
|
474
|
-
content: [{ type: "text" as const, text }],
|
|
475
|
-
details,
|
|
476
|
-
};
|
|
477
|
-
} catch (error) {
|
|
478
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
479
|
-
return {
|
|
480
|
-
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
481
|
-
details: { error: message },
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
},
|
|
485
|
-
};
|
|
433
|
+
query: string
|
|
434
|
+
system_prompt?: string
|
|
435
|
+
max_tokens?: number
|
|
436
|
+
}
|
|
486
437
|
|
|
487
|
-
|
|
488
|
-
|
|
438
|
+
const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
|
439
|
+
const auth = findAuthConfig()
|
|
440
|
+
if (!auth) {
|
|
441
|
+
console.error('anthropic-websearch: No auth config found. Set ANTHROPIC_SEARCH_API_KEY or configure models.json/auth.json')
|
|
442
|
+
return null
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const model = getConfig('model') ?? DEFAULT_MODEL
|
|
446
|
+
|
|
447
|
+
const tool: CustomAgentTool<typeof SearchSchema, unknown> = {
|
|
448
|
+
name: 'anthropic_web_search',
|
|
449
|
+
label: 'Anthropic Web Search',
|
|
450
|
+
description: `Web search powered by Claude (${model}). Uses Claude's built-in web search capability to find current information and synthesize answers with citations. Best for questions requiring up-to-date information from the web.`,
|
|
451
|
+
parameters: SearchSchema,
|
|
452
|
+
async execute(_toolCallId, params) {
|
|
453
|
+
try {
|
|
454
|
+
const p = (params ?? {}) as SearchParams
|
|
455
|
+
const response = await callWebSearch(auth, model, p.query, p.system_prompt, p.max_tokens)
|
|
456
|
+
const { text, details } = formatResponse(response)
|
|
457
|
+
return {
|
|
458
|
+
content: [{ type: 'text' as const, text }],
|
|
459
|
+
details,
|
|
460
|
+
}
|
|
461
|
+
} catch (error) {
|
|
462
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
463
|
+
return {
|
|
464
|
+
content: [{ type: 'text' as const, text: `Error: ${message}` }],
|
|
465
|
+
details: { error: message },
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return [tool]
|
|
472
|
+
}
|
|
489
473
|
|
|
490
|
-
export default factory
|
|
474
|
+
export default factory
|