@sparkleideas/providers 3.5.2-patch.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.
@@ -0,0 +1,435 @@
1
+ /**
2
+ * V3 Anthropic (Claude) Provider
3
+ *
4
+ * Supports Claude 3.5, 3 Opus, Sonnet, and Haiku models.
5
+ *
6
+ * @module @sparkleideas/providers/anthropic-provider
7
+ */
8
+
9
+ import { BaseProvider, BaseProviderOptions } from './base-provider.js';
10
+ import {
11
+ LLMProvider,
12
+ LLMModel,
13
+ LLMRequest,
14
+ LLMResponse,
15
+ LLMStreamEvent,
16
+ ModelInfo,
17
+ ProviderCapabilities,
18
+ HealthCheckResult,
19
+ AuthenticationError,
20
+ RateLimitError,
21
+ LLMProviderError,
22
+ } from './types.js';
23
+
24
+ interface AnthropicRequest {
25
+ model: string;
26
+ messages: Array<{
27
+ role: 'user' | 'assistant';
28
+ content: string | Array<{ type: string; text?: string; source?: unknown }>;
29
+ }>;
30
+ system?: string;
31
+ max_tokens: number;
32
+ temperature?: number;
33
+ top_p?: number;
34
+ top_k?: number;
35
+ stop_sequences?: string[];
36
+ stream?: boolean;
37
+ tools?: Array<{
38
+ name: string;
39
+ description: string;
40
+ input_schema: unknown;
41
+ }>;
42
+ }
43
+
44
+ interface AnthropicResponse {
45
+ id: string;
46
+ type: string;
47
+ role: string;
48
+ model: string;
49
+ content: Array<{ type: string; text?: string; name?: string; input?: unknown }>;
50
+ stop_reason: string;
51
+ usage: {
52
+ input_tokens: number;
53
+ output_tokens: number;
54
+ };
55
+ }
56
+
57
+ export class AnthropicProvider extends BaseProvider {
58
+ readonly name: LLMProvider = 'anthropic';
59
+ readonly capabilities: ProviderCapabilities = {
60
+ supportedModels: [
61
+ 'claude-3-5-sonnet-20241022',
62
+ 'claude-3-5-sonnet-latest',
63
+ 'claude-3-opus-20240229',
64
+ 'claude-3-sonnet-20240229',
65
+ 'claude-3-haiku-20240307',
66
+ ],
67
+ maxContextLength: {
68
+ 'claude-3-5-sonnet-20241022': 200000,
69
+ 'claude-3-5-sonnet-latest': 200000,
70
+ 'claude-3-opus-20240229': 200000,
71
+ 'claude-3-sonnet-20240229': 200000,
72
+ 'claude-3-haiku-20240307': 200000,
73
+ },
74
+ maxOutputTokens: {
75
+ 'claude-3-5-sonnet-20241022': 8192,
76
+ 'claude-3-5-sonnet-latest': 8192,
77
+ 'claude-3-opus-20240229': 4096,
78
+ 'claude-3-sonnet-20240229': 4096,
79
+ 'claude-3-haiku-20240307': 4096,
80
+ },
81
+ supportsStreaming: true,
82
+ supportsToolCalling: true,
83
+ supportsSystemMessages: true,
84
+ supportsVision: true,
85
+ supportsAudio: false,
86
+ supportsFineTuning: false,
87
+ supportsEmbeddings: false,
88
+ supportsBatching: true,
89
+ rateLimit: {
90
+ requestsPerMinute: 1000,
91
+ tokensPerMinute: 100000,
92
+ concurrentRequests: 100,
93
+ },
94
+ pricing: {
95
+ 'claude-3-5-sonnet-20241022': {
96
+ promptCostPer1k: 0.003,
97
+ completionCostPer1k: 0.015,
98
+ currency: 'USD',
99
+ },
100
+ 'claude-3-5-sonnet-latest': {
101
+ promptCostPer1k: 0.003,
102
+ completionCostPer1k: 0.015,
103
+ currency: 'USD',
104
+ },
105
+ 'claude-3-opus-20240229': {
106
+ promptCostPer1k: 0.015,
107
+ completionCostPer1k: 0.075,
108
+ currency: 'USD',
109
+ },
110
+ 'claude-3-sonnet-20240229': {
111
+ promptCostPer1k: 0.003,
112
+ completionCostPer1k: 0.015,
113
+ currency: 'USD',
114
+ },
115
+ 'claude-3-haiku-20240307': {
116
+ promptCostPer1k: 0.00025,
117
+ completionCostPer1k: 0.00125,
118
+ currency: 'USD',
119
+ },
120
+ },
121
+ };
122
+
123
+ private baseUrl: string = 'https://api.anthropic.com/v1';
124
+ private headers: Record<string, string> = {};
125
+
126
+ constructor(options: BaseProviderOptions) {
127
+ super(options);
128
+ }
129
+
130
+ protected async doInitialize(): Promise<void> {
131
+ if (!this.config.apiKey) {
132
+ throw new AuthenticationError('Anthropic API key is required', 'anthropic');
133
+ }
134
+
135
+ this.baseUrl = this.config.apiUrl || 'https://api.anthropic.com/v1';
136
+ this.headers = {
137
+ 'x-api-key': this.config.apiKey,
138
+ 'anthropic-version': '2023-06-01',
139
+ 'Content-Type': 'application/json',
140
+ };
141
+ }
142
+
143
+ protected async doComplete(request: LLMRequest): Promise<LLMResponse> {
144
+ const anthropicRequest = this.buildRequest(request);
145
+
146
+ const controller = new AbortController();
147
+ const timeout = setTimeout(() => controller.abort(), this.config.timeout || 60000);
148
+
149
+ try {
150
+ const response = await fetch(`${this.baseUrl}/messages`, {
151
+ method: 'POST',
152
+ headers: this.headers,
153
+ body: JSON.stringify(anthropicRequest),
154
+ signal: controller.signal,
155
+ });
156
+
157
+ clearTimeout(timeout);
158
+
159
+ if (!response.ok) {
160
+ await this.handleErrorResponse(response);
161
+ }
162
+
163
+ const data = await response.json() as AnthropicResponse;
164
+ return this.transformResponse(data, request);
165
+ } catch (error) {
166
+ clearTimeout(timeout);
167
+ throw this.transformError(error);
168
+ }
169
+ }
170
+
171
+ protected async *doStreamComplete(request: LLMRequest): AsyncIterable<LLMStreamEvent> {
172
+ const anthropicRequest = this.buildRequest(request, true);
173
+
174
+ const controller = new AbortController();
175
+ const timeout = setTimeout(() => controller.abort(), (this.config.timeout || 60000) * 2);
176
+
177
+ try {
178
+ const response = await fetch(`${this.baseUrl}/messages`, {
179
+ method: 'POST',
180
+ headers: this.headers,
181
+ body: JSON.stringify(anthropicRequest),
182
+ signal: controller.signal,
183
+ });
184
+
185
+ if (!response.ok) {
186
+ await this.handleErrorResponse(response);
187
+ }
188
+
189
+ const reader = response.body!.getReader();
190
+ const decoder = new TextDecoder();
191
+ let buffer = '';
192
+ let totalOutputTokens = 0;
193
+ let inputTokens = 0;
194
+
195
+ while (true) {
196
+ const { done, value } = await reader.read();
197
+ if (done) break;
198
+
199
+ buffer += decoder.decode(value, { stream: true });
200
+ const lines = buffer.split('\n');
201
+ buffer = lines.pop() || '';
202
+
203
+ for (const line of lines) {
204
+ if (line.startsWith('data: ')) {
205
+ const data = line.slice(6);
206
+ if (data === '[DONE]') continue;
207
+
208
+ try {
209
+ const event = JSON.parse(data);
210
+
211
+ if (event.type === 'content_block_delta' && event.delta?.text) {
212
+ yield {
213
+ type: 'content',
214
+ delta: { content: event.delta.text },
215
+ };
216
+ } else if (event.type === 'message_delta' && event.usage) {
217
+ totalOutputTokens = event.usage.output_tokens;
218
+ } else if (event.type === 'message_start' && event.message?.usage) {
219
+ inputTokens = event.message.usage.input_tokens;
220
+ } else if (event.type === 'message_stop') {
221
+ const model = request.model || this.config.model;
222
+ const pricing = this.capabilities.pricing[model];
223
+
224
+ const promptCost = (inputTokens / 1000) * pricing.promptCostPer1k;
225
+ const completionCost = (totalOutputTokens / 1000) * pricing.completionCostPer1k;
226
+
227
+ yield {
228
+ type: 'done',
229
+ usage: {
230
+ promptTokens: inputTokens,
231
+ completionTokens: totalOutputTokens,
232
+ totalTokens: inputTokens + totalOutputTokens,
233
+ },
234
+ cost: {
235
+ promptCost,
236
+ completionCost,
237
+ totalCost: promptCost + completionCost,
238
+ currency: 'USD',
239
+ },
240
+ };
241
+ }
242
+ } catch {
243
+ // Ignore parse errors
244
+ }
245
+ }
246
+ }
247
+ }
248
+ } catch (error) {
249
+ clearTimeout(timeout);
250
+ throw this.transformError(error);
251
+ } finally {
252
+ clearTimeout(timeout);
253
+ }
254
+ }
255
+
256
+ async listModels(): Promise<LLMModel[]> {
257
+ return this.capabilities.supportedModels;
258
+ }
259
+
260
+ async getModelInfo(model: LLMModel): Promise<ModelInfo> {
261
+ const descriptions: Record<string, string> = {
262
+ 'claude-3-5-sonnet-20241022': 'Latest Claude 3.5 Sonnet - Best balance of intelligence and speed',
263
+ 'claude-3-5-sonnet-latest': 'Claude 3.5 Sonnet latest version',
264
+ 'claude-3-opus-20240229': 'Most capable Claude model for complex tasks',
265
+ 'claude-3-sonnet-20240229': 'Balanced Claude 3 model',
266
+ 'claude-3-haiku-20240307': 'Fastest Claude 3 model for simple tasks',
267
+ };
268
+
269
+ return {
270
+ model,
271
+ name: model,
272
+ description: descriptions[model] || 'Anthropic Claude model',
273
+ contextLength: this.capabilities.maxContextLength[model] || 200000,
274
+ maxOutputTokens: this.capabilities.maxOutputTokens[model] || 4096,
275
+ supportedFeatures: ['chat', 'completion', 'vision', 'tool_calling'],
276
+ pricing: this.capabilities.pricing[model],
277
+ };
278
+ }
279
+
280
+ protected async doHealthCheck(): Promise<HealthCheckResult> {
281
+ try {
282
+ // Use a minimal request to check API availability
283
+ const response = await fetch(`${this.baseUrl}/messages`, {
284
+ method: 'POST',
285
+ headers: this.headers,
286
+ body: JSON.stringify({
287
+ model: this.config.model,
288
+ max_tokens: 1,
289
+ messages: [{ role: 'user', content: 'Hi' }],
290
+ }),
291
+ });
292
+
293
+ return {
294
+ healthy: response.ok,
295
+ timestamp: new Date(),
296
+ ...(response.ok ? {} : { error: `HTTP ${response.status}` }),
297
+ };
298
+ } catch (error) {
299
+ return {
300
+ healthy: false,
301
+ error: error instanceof Error ? error.message : 'Unknown error',
302
+ timestamp: new Date(),
303
+ };
304
+ }
305
+ }
306
+
307
+ private buildRequest(request: LLMRequest, stream = false): AnthropicRequest {
308
+ // Extract system message
309
+ const systemMessage = request.messages.find((m) => m.role === 'system');
310
+ const otherMessages = request.messages.filter((m) => m.role !== 'system');
311
+
312
+ // Transform messages
313
+ const messages = otherMessages.map((msg) => ({
314
+ role: msg.role as 'user' | 'assistant',
315
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
316
+ }));
317
+
318
+ const anthropicRequest: AnthropicRequest = {
319
+ model: request.model || this.config.model,
320
+ messages,
321
+ max_tokens: request.maxTokens || this.config.maxTokens || 4096,
322
+ stream,
323
+ };
324
+
325
+ if (systemMessage) {
326
+ anthropicRequest.system = typeof systemMessage.content === 'string'
327
+ ? systemMessage.content
328
+ : JSON.stringify(systemMessage.content);
329
+ }
330
+
331
+ if (request.temperature !== undefined) {
332
+ anthropicRequest.temperature = request.temperature;
333
+ } else if (this.config.temperature !== undefined) {
334
+ anthropicRequest.temperature = this.config.temperature;
335
+ }
336
+
337
+ if (request.topP !== undefined || this.config.topP !== undefined) {
338
+ anthropicRequest.top_p = request.topP ?? this.config.topP;
339
+ }
340
+
341
+ if (request.topK !== undefined || this.config.topK !== undefined) {
342
+ anthropicRequest.top_k = request.topK ?? this.config.topK;
343
+ }
344
+
345
+ if (request.stopSequences || this.config.stopSequences) {
346
+ anthropicRequest.stop_sequences = request.stopSequences || this.config.stopSequences;
347
+ }
348
+
349
+ // Add tools if present
350
+ if (request.tools) {
351
+ anthropicRequest.tools = request.tools.map((tool) => ({
352
+ name: tool.function.name,
353
+ description: tool.function.description,
354
+ input_schema: tool.function.parameters,
355
+ }));
356
+ }
357
+
358
+ return anthropicRequest;
359
+ }
360
+
361
+ private transformResponse(data: AnthropicResponse, request: LLMRequest): LLMResponse {
362
+ const model = request.model || this.config.model;
363
+ const pricing = this.capabilities.pricing[model];
364
+
365
+ const promptCost = (data.usage.input_tokens / 1000) * pricing.promptCostPer1k;
366
+ const completionCost = (data.usage.output_tokens / 1000) * pricing.completionCostPer1k;
367
+
368
+ // Extract text content
369
+ const textContent = data.content
370
+ .filter((c) => c.type === 'text')
371
+ .map((c) => c.text)
372
+ .join('');
373
+
374
+ // Extract tool calls
375
+ const toolCalls = data.content
376
+ .filter((c) => c.type === 'tool_use')
377
+ .map((c) => ({
378
+ id: `tool_${Date.now()}`,
379
+ type: 'function' as const,
380
+ function: {
381
+ name: c.name || '',
382
+ arguments: JSON.stringify(c.input || {}),
383
+ },
384
+ }));
385
+
386
+ return {
387
+ id: data.id,
388
+ model: model as LLMModel,
389
+ provider: 'anthropic',
390
+ content: textContent,
391
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
392
+ usage: {
393
+ promptTokens: data.usage.input_tokens,
394
+ completionTokens: data.usage.output_tokens,
395
+ totalTokens: data.usage.input_tokens + data.usage.output_tokens,
396
+ },
397
+ cost: {
398
+ promptCost,
399
+ completionCost,
400
+ totalCost: promptCost + completionCost,
401
+ currency: 'USD',
402
+ },
403
+ finishReason: data.stop_reason === 'end_turn' ? 'stop' : 'length',
404
+ };
405
+ }
406
+
407
+ private async handleErrorResponse(response: Response): Promise<never> {
408
+ const errorText = await response.text();
409
+ let errorData: { error?: { message?: string } };
410
+
411
+ try {
412
+ errorData = JSON.parse(errorText);
413
+ } catch {
414
+ errorData = { error: { message: errorText } };
415
+ }
416
+
417
+ const message = errorData.error?.message || 'Unknown error';
418
+
419
+ switch (response.status) {
420
+ case 401:
421
+ throw new AuthenticationError(message, 'anthropic', errorData);
422
+ case 429:
423
+ throw new RateLimitError(message, 'anthropic', undefined, errorData);
424
+ default:
425
+ throw new LLMProviderError(
426
+ message,
427
+ `ANTHROPIC_${response.status}`,
428
+ 'anthropic',
429
+ response.status,
430
+ response.status >= 500,
431
+ errorData
432
+ );
433
+ }
434
+ }
435
+ }