ai.matey.backend 0.2.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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/index.js +60 -0
  3. package/dist/cjs/index.js.map +1 -0
  4. package/dist/cjs/providers/ai21.js +331 -0
  5. package/dist/cjs/providers/ai21.js.map +1 -0
  6. package/dist/cjs/providers/anthropic.js +664 -0
  7. package/dist/cjs/providers/anthropic.js.map +1 -0
  8. package/dist/cjs/providers/anyscale.js +338 -0
  9. package/dist/cjs/providers/anyscale.js.map +1 -0
  10. package/dist/cjs/providers/aws-bedrock.js +374 -0
  11. package/dist/cjs/providers/aws-bedrock.js.map +1 -0
  12. package/dist/cjs/providers/azure-openai.js +406 -0
  13. package/dist/cjs/providers/azure-openai.js.map +1 -0
  14. package/dist/cjs/providers/cerebras.js +356 -0
  15. package/dist/cjs/providers/cerebras.js.map +1 -0
  16. package/dist/cjs/providers/cloudflare.js +359 -0
  17. package/dist/cjs/providers/cloudflare.js.map +1 -0
  18. package/dist/cjs/providers/cohere.js +368 -0
  19. package/dist/cjs/providers/cohere.js.map +1 -0
  20. package/dist/cjs/providers/deepinfra.js +343 -0
  21. package/dist/cjs/providers/deepinfra.js.map +1 -0
  22. package/dist/cjs/providers/deepseek.js +104 -0
  23. package/dist/cjs/providers/deepseek.js.map +1 -0
  24. package/dist/cjs/providers/fireworks.js +363 -0
  25. package/dist/cjs/providers/fireworks.js.map +1 -0
  26. package/dist/cjs/providers/gemini.js +292 -0
  27. package/dist/cjs/providers/gemini.js.map +1 -0
  28. package/dist/cjs/providers/groq.js +143 -0
  29. package/dist/cjs/providers/groq.js.map +1 -0
  30. package/dist/cjs/providers/huggingface.js +392 -0
  31. package/dist/cjs/providers/huggingface.js.map +1 -0
  32. package/dist/cjs/providers/lmstudio.js +144 -0
  33. package/dist/cjs/providers/lmstudio.js.map +1 -0
  34. package/dist/cjs/providers/mistral.js +288 -0
  35. package/dist/cjs/providers/mistral.js.map +1 -0
  36. package/dist/cjs/providers/nvidia.js +167 -0
  37. package/dist/cjs/providers/nvidia.js.map +1 -0
  38. package/dist/cjs/providers/ollama.js +257 -0
  39. package/dist/cjs/providers/ollama.js.map +1 -0
  40. package/dist/cjs/providers/openai.js +640 -0
  41. package/dist/cjs/providers/openai.js.map +1 -0
  42. package/dist/cjs/providers/openrouter.js +379 -0
  43. package/dist/cjs/providers/openrouter.js.map +1 -0
  44. package/dist/cjs/providers/perplexity.js +372 -0
  45. package/dist/cjs/providers/perplexity.js.map +1 -0
  46. package/dist/cjs/providers/replicate.js +340 -0
  47. package/dist/cjs/providers/replicate.js.map +1 -0
  48. package/dist/cjs/providers/together-ai.js +341 -0
  49. package/dist/cjs/providers/together-ai.js.map +1 -0
  50. package/dist/cjs/providers/xai.js +339 -0
  51. package/dist/cjs/providers/xai.js.map +1 -0
  52. package/dist/cjs/shared.js +279 -0
  53. package/dist/cjs/shared.js.map +1 -0
  54. package/dist/esm/index.js +44 -0
  55. package/dist/esm/index.js.map +1 -0
  56. package/dist/esm/providers/ai21.js +327 -0
  57. package/dist/esm/providers/ai21.js.map +1 -0
  58. package/dist/esm/providers/anthropic.js +660 -0
  59. package/dist/esm/providers/anthropic.js.map +1 -0
  60. package/dist/esm/providers/anyscale.js +334 -0
  61. package/dist/esm/providers/anyscale.js.map +1 -0
  62. package/dist/esm/providers/aws-bedrock.js +370 -0
  63. package/dist/esm/providers/aws-bedrock.js.map +1 -0
  64. package/dist/esm/providers/azure-openai.js +402 -0
  65. package/dist/esm/providers/azure-openai.js.map +1 -0
  66. package/dist/esm/providers/cerebras.js +352 -0
  67. package/dist/esm/providers/cerebras.js.map +1 -0
  68. package/dist/esm/providers/cloudflare.js +355 -0
  69. package/dist/esm/providers/cloudflare.js.map +1 -0
  70. package/dist/esm/providers/cohere.js +364 -0
  71. package/dist/esm/providers/cohere.js.map +1 -0
  72. package/dist/esm/providers/deepinfra.js +339 -0
  73. package/dist/esm/providers/deepinfra.js.map +1 -0
  74. package/dist/esm/providers/deepseek.js +99 -0
  75. package/dist/esm/providers/deepseek.js.map +1 -0
  76. package/dist/esm/providers/fireworks.js +359 -0
  77. package/dist/esm/providers/fireworks.js.map +1 -0
  78. package/dist/esm/providers/gemini.js +288 -0
  79. package/dist/esm/providers/gemini.js.map +1 -0
  80. package/dist/esm/providers/groq.js +138 -0
  81. package/dist/esm/providers/groq.js.map +1 -0
  82. package/dist/esm/providers/huggingface.js +387 -0
  83. package/dist/esm/providers/huggingface.js.map +1 -0
  84. package/dist/esm/providers/lmstudio.js +139 -0
  85. package/dist/esm/providers/lmstudio.js.map +1 -0
  86. package/dist/esm/providers/mistral.js +284 -0
  87. package/dist/esm/providers/mistral.js.map +1 -0
  88. package/dist/esm/providers/nvidia.js +162 -0
  89. package/dist/esm/providers/nvidia.js.map +1 -0
  90. package/dist/esm/providers/ollama.js +253 -0
  91. package/dist/esm/providers/ollama.js.map +1 -0
  92. package/dist/esm/providers/openai.js +636 -0
  93. package/dist/esm/providers/openai.js.map +1 -0
  94. package/dist/esm/providers/openrouter.js +375 -0
  95. package/dist/esm/providers/openrouter.js.map +1 -0
  96. package/dist/esm/providers/perplexity.js +368 -0
  97. package/dist/esm/providers/perplexity.js.map +1 -0
  98. package/dist/esm/providers/replicate.js +336 -0
  99. package/dist/esm/providers/replicate.js.map +1 -0
  100. package/dist/esm/providers/together-ai.js +337 -0
  101. package/dist/esm/providers/together-ai.js.map +1 -0
  102. package/dist/esm/providers/xai.js +335 -0
  103. package/dist/esm/providers/xai.js.map +1 -0
  104. package/dist/esm/shared.js +272 -0
  105. package/dist/esm/shared.js.map +1 -0
  106. package/dist/types/index.d.ts +38 -0
  107. package/dist/types/index.d.ts.map +1 -0
  108. package/dist/types/providers/ai21.d.ts +106 -0
  109. package/dist/types/providers/ai21.d.ts.map +1 -0
  110. package/dist/types/providers/anthropic.d.ts +194 -0
  111. package/dist/types/providers/anthropic.d.ts.map +1 -0
  112. package/dist/types/providers/anyscale.d.ts +109 -0
  113. package/dist/types/providers/anyscale.d.ts.map +1 -0
  114. package/dist/types/providers/aws-bedrock.d.ts +152 -0
  115. package/dist/types/providers/aws-bedrock.d.ts.map +1 -0
  116. package/dist/types/providers/azure-openai.d.ts +142 -0
  117. package/dist/types/providers/azure-openai.d.ts.map +1 -0
  118. package/dist/types/providers/cerebras.d.ts +130 -0
  119. package/dist/types/providers/cerebras.d.ts.map +1 -0
  120. package/dist/types/providers/cloudflare.d.ts +125 -0
  121. package/dist/types/providers/cloudflare.d.ts.map +1 -0
  122. package/dist/types/providers/cohere.d.ts +114 -0
  123. package/dist/types/providers/cohere.d.ts.map +1 -0
  124. package/dist/types/providers/deepinfra.d.ts +118 -0
  125. package/dist/types/providers/deepinfra.d.ts.map +1 -0
  126. package/dist/types/providers/deepseek.d.ts +68 -0
  127. package/dist/types/providers/deepseek.d.ts.map +1 -0
  128. package/dist/types/providers/fireworks.d.ts +127 -0
  129. package/dist/types/providers/fireworks.d.ts.map +1 -0
  130. package/dist/types/providers/gemini.d.ts +71 -0
  131. package/dist/types/providers/gemini.d.ts.map +1 -0
  132. package/dist/types/providers/groq.d.ts +83 -0
  133. package/dist/types/providers/groq.d.ts.map +1 -0
  134. package/dist/types/providers/huggingface.d.ts +154 -0
  135. package/dist/types/providers/huggingface.d.ts.map +1 -0
  136. package/dist/types/providers/lmstudio.d.ts +88 -0
  137. package/dist/types/providers/lmstudio.d.ts.map +1 -0
  138. package/dist/types/providers/mistral.d.ts +65 -0
  139. package/dist/types/providers/mistral.d.ts.map +1 -0
  140. package/dist/types/providers/nvidia.d.ts +100 -0
  141. package/dist/types/providers/nvidia.d.ts.map +1 -0
  142. package/dist/types/providers/ollama.d.ts +59 -0
  143. package/dist/types/providers/ollama.d.ts.map +1 -0
  144. package/dist/types/providers/openai.d.ts +205 -0
  145. package/dist/types/providers/openai.d.ts.map +1 -0
  146. package/dist/types/providers/openrouter.d.ts +135 -0
  147. package/dist/types/providers/openrouter.d.ts.map +1 -0
  148. package/dist/types/providers/perplexity.d.ts +116 -0
  149. package/dist/types/providers/perplexity.d.ts.map +1 -0
  150. package/dist/types/providers/replicate.d.ts +91 -0
  151. package/dist/types/providers/replicate.d.ts.map +1 -0
  152. package/dist/types/providers/together-ai.d.ts +118 -0
  153. package/dist/types/providers/together-ai.d.ts.map +1 -0
  154. package/dist/types/providers/xai.d.ts +119 -0
  155. package/dist/types/providers/xai.d.ts.map +1 -0
  156. package/dist/types/shared.d.ts +116 -0
  157. package/dist/types/shared.d.ts.map +1 -0
  158. package/package.json +327 -0
  159. package/readme.md +86 -0
@@ -0,0 +1,660 @@
1
+ /**
2
+ * Anthropic Backend Adapter
3
+ *
4
+ * Adapts Universal IR to Anthropic Messages API.
5
+ * Handles Anthropic's separate system parameter and SSE streaming format.
6
+ *
7
+ * @module
8
+ */
9
+ import { AdapterConversionError, NetworkError, ProviderError, StreamError, ErrorCode, createErrorFromHttpResponse, } from 'ai.matey.errors';
10
+ import { normalizeSystemMessages } from 'ai.matey.utils';
11
+ import { getEffectiveStreamMode, mergeStreamingConfig } from 'ai.matey.utils';
12
+ import { estimateTokens, buildStaticResult, applyModelFilter, } from '../shared.js';
13
+ // ============================================================================
14
+ // Default Anthropic Models
15
+ // ============================================================================
16
+ /**
17
+ * Default list of Anthropic Claude models.
18
+ * Used when no custom model list is provided in config.
19
+ */
20
+ const DEFAULT_ANTHROPIC_MODELS = [
21
+ {
22
+ id: 'claude-3-5-sonnet-20241022',
23
+ name: 'Claude 3.5 Sonnet (Oct 2024)',
24
+ description: 'Most intelligent model with excellent reasoning and analysis',
25
+ ownedBy: 'anthropic',
26
+ capabilities: {
27
+ maxTokens: 8192,
28
+ contextWindow: 200000,
29
+ supportsStreaming: true,
30
+ supportsVision: true,
31
+ supportsTools: true,
32
+ supportsJSON: false,
33
+ },
34
+ },
35
+ {
36
+ id: 'claude-3-5-haiku-20241022',
37
+ name: 'Claude 3.5 Haiku (Oct 2024)',
38
+ description: 'Fastest and most compact model for high-throughput tasks',
39
+ ownedBy: 'anthropic',
40
+ capabilities: {
41
+ maxTokens: 8192,
42
+ contextWindow: 200000,
43
+ supportsStreaming: true,
44
+ supportsVision: false,
45
+ supportsTools: true,
46
+ supportsJSON: false,
47
+ },
48
+ },
49
+ {
50
+ id: 'claude-3-opus-20240229',
51
+ name: 'Claude 3 Opus (Feb 2024)',
52
+ description: 'Previous top-tier model with strong performance',
53
+ ownedBy: 'anthropic',
54
+ capabilities: {
55
+ maxTokens: 4096,
56
+ contextWindow: 200000,
57
+ supportsStreaming: true,
58
+ supportsVision: true,
59
+ supportsTools: true,
60
+ supportsJSON: false,
61
+ },
62
+ },
63
+ {
64
+ id: 'claude-3-sonnet-20240229',
65
+ name: 'Claude 3 Sonnet (Feb 2024)',
66
+ description: 'Balanced intelligence and speed',
67
+ ownedBy: 'anthropic',
68
+ capabilities: {
69
+ maxTokens: 4096,
70
+ contextWindow: 200000,
71
+ supportsStreaming: true,
72
+ supportsVision: true,
73
+ supportsTools: true,
74
+ supportsJSON: false,
75
+ },
76
+ },
77
+ {
78
+ id: 'claude-3-haiku-20240307',
79
+ name: 'Claude 3 Haiku (Mar 2024)',
80
+ description: 'Fast and compact model',
81
+ ownedBy: 'anthropic',
82
+ capabilities: {
83
+ maxTokens: 4096,
84
+ contextWindow: 200000,
85
+ supportsStreaming: true,
86
+ supportsVision: false,
87
+ supportsTools: false,
88
+ supportsJSON: false,
89
+ },
90
+ },
91
+ ];
92
+ // ============================================================================
93
+ // Anthropic Backend Adapter
94
+ // ============================================================================
95
+ /**
96
+ * Backend adapter for Anthropic Messages API.
97
+ */
98
+ export class AnthropicBackendAdapter {
99
+ metadata;
100
+ config;
101
+ baseURL;
102
+ constructor(config) {
103
+ this.config = config;
104
+ this.baseURL = config.baseURL || 'https://api.anthropic.com/v1';
105
+ this.metadata = {
106
+ name: 'anthropic-backend',
107
+ version: '1.0.0',
108
+ provider: 'Anthropic',
109
+ capabilities: {
110
+ streaming: true,
111
+ multiModal: true,
112
+ tools: true,
113
+ maxContextTokens: 200000,
114
+ systemMessageStrategy: 'separate-parameter',
115
+ supportsMultipleSystemMessages: false, // Anthropic merges multiple system messages
116
+ supportsTemperature: true,
117
+ supportsTopP: true,
118
+ supportsTopK: true,
119
+ supportsSeed: false,
120
+ supportsFrequencyPenalty: false,
121
+ supportsPresencePenalty: false,
122
+ maxStopSequences: 4,
123
+ },
124
+ config: {
125
+ baseURL: this.baseURL,
126
+ },
127
+ };
128
+ }
129
+ /**
130
+ * Execute non-streaming chat completion request.
131
+ */
132
+ async execute(request, signal) {
133
+ try {
134
+ // Convert IR to Anthropic format
135
+ const anthropicRequest = this.fromIR(request);
136
+ // Make HTTP request
137
+ const startTime = Date.now();
138
+ const response = await this.makeRequest(anthropicRequest, signal);
139
+ // Convert response to IR
140
+ const irResponse = this.toIR(response, request, Date.now() - startTime);
141
+ return irResponse;
142
+ }
143
+ catch (error) {
144
+ // Re-throw adapter errors
145
+ if (error instanceof AdapterConversionError ||
146
+ error instanceof NetworkError ||
147
+ error instanceof ProviderError) {
148
+ throw error;
149
+ }
150
+ // Wrap unknown errors
151
+ throw new ProviderError({
152
+ code: ErrorCode.PROVIDER_ERROR,
153
+ message: `Anthropic request failed: ${error instanceof Error ? error.message : String(error)}`,
154
+ isRetryable: true,
155
+ provenance: {
156
+ backend: this.metadata.name,
157
+ },
158
+ cause: error instanceof Error ? error : undefined,
159
+ });
160
+ }
161
+ }
162
+ /**
163
+ * Execute streaming chat completion request.
164
+ */
165
+ async *executeStream(request, signal) {
166
+ try {
167
+ // Convert IR to Anthropic format
168
+ const anthropicRequest = this.fromIR(request);
169
+ anthropicRequest.stream = true;
170
+ // Get effective streaming configuration
171
+ const streamingConfig = mergeStreamingConfig(this.config.streaming);
172
+ const effectiveMode = getEffectiveStreamMode(request.streamMode, undefined, streamingConfig);
173
+ const includeBoth = streamingConfig.includeBoth || effectiveMode === 'accumulated';
174
+ // Make streaming HTTP request
175
+ const response = await fetch(`${this.baseURL}/messages`, {
176
+ method: 'POST',
177
+ headers: this.getHeaders(),
178
+ body: JSON.stringify(anthropicRequest),
179
+ signal,
180
+ });
181
+ if (!response.ok) {
182
+ const errorBody = await response.text();
183
+ throw createErrorFromHttpResponse(response.status, response.statusText, errorBody, {
184
+ backend: this.metadata.name,
185
+ });
186
+ }
187
+ if (!response.body) {
188
+ throw new StreamError({
189
+ code: ErrorCode.STREAM_INTERRUPTED,
190
+ message: 'Response body is null',
191
+ provenance: {
192
+ backend: this.metadata.name,
193
+ },
194
+ });
195
+ }
196
+ // Parse SSE stream
197
+ let sequence = 0;
198
+ let contentBuffer = '';
199
+ let messageId = '';
200
+ let model = '';
201
+ let usage;
202
+ // Read stream
203
+ const reader = response.body.getReader();
204
+ const decoder = new TextDecoder();
205
+ let buffer = '';
206
+ try {
207
+ while (true) {
208
+ const { done, value } = await reader.read();
209
+ if (done) {
210
+ break;
211
+ }
212
+ buffer += decoder.decode(value, { stream: true });
213
+ const lines = buffer.split('\n');
214
+ buffer = lines.pop() || '';
215
+ for (const line of lines) {
216
+ // Parse SSE format: "event: <type>" or "data: <json>"
217
+ if (line.startsWith('event:')) {
218
+ // Event type line (Anthropic doesn't always use this)
219
+ continue;
220
+ }
221
+ if (line.startsWith('data:')) {
222
+ const data = line.slice(5).trim();
223
+ if (!data) {
224
+ continue;
225
+ }
226
+ try {
227
+ const event = JSON.parse(data);
228
+ // Handle different event types
229
+ switch (event.type) {
230
+ case 'message_start':
231
+ // Extract message ID and model
232
+ messageId = event.message.id;
233
+ model = event.message.model;
234
+ usage = {
235
+ promptTokens: event.message.usage.input_tokens,
236
+ completionTokens: 0,
237
+ totalTokens: event.message.usage.input_tokens,
238
+ };
239
+ // Yield start chunk
240
+ yield {
241
+ type: 'start',
242
+ sequence: sequence++,
243
+ metadata: {
244
+ ...request.metadata,
245
+ requestId: messageId,
246
+ provenance: {
247
+ ...request.metadata.provenance,
248
+ backend: this.metadata.name,
249
+ },
250
+ custom: {
251
+ ...request.metadata.custom,
252
+ anthropicMessageId: messageId,
253
+ model,
254
+ },
255
+ },
256
+ };
257
+ break;
258
+ case 'content_block_start':
259
+ // Content block started (we'll handle deltas)
260
+ break;
261
+ case 'content_block_delta':
262
+ // Content delta
263
+ if (event.delta.type === 'text_delta') {
264
+ contentBuffer += event.delta.text;
265
+ // Build content chunk with optional accumulated field
266
+ const contentChunk = {
267
+ type: 'content',
268
+ sequence: sequence++,
269
+ delta: event.delta.text,
270
+ role: 'assistant',
271
+ };
272
+ // Add accumulated field if configured
273
+ if (includeBoth) {
274
+ contentChunk.accumulated = contentBuffer;
275
+ }
276
+ yield contentChunk;
277
+ }
278
+ else if (event.delta.type === 'input_json_delta') {
279
+ // Tool use delta (not implemented yet)
280
+ // TODO: Handle tool use deltas in Phase 5
281
+ }
282
+ break;
283
+ case 'content_block_stop':
284
+ // Content block completed
285
+ break;
286
+ case 'message_delta':
287
+ // Message metadata delta (stop reason, usage)
288
+ if (event.delta.stop_reason && usage) {
289
+ usage.completionTokens = event.usage.output_tokens;
290
+ usage.totalTokens = usage.promptTokens + event.usage.output_tokens;
291
+ }
292
+ break;
293
+ case 'message_stop': {
294
+ // Stream complete
295
+ const finishReason = this.mapStopReason(contentBuffer ? 'end_turn' : 'stop');
296
+ // Build final message
297
+ const message = {
298
+ role: 'assistant',
299
+ content: contentBuffer,
300
+ };
301
+ yield {
302
+ type: 'done',
303
+ sequence: sequence++,
304
+ finishReason,
305
+ usage,
306
+ message,
307
+ };
308
+ break;
309
+ }
310
+ case 'ping':
311
+ // Keep-alive ping, ignore
312
+ break;
313
+ case 'error':
314
+ // Error event
315
+ yield {
316
+ type: 'error',
317
+ sequence: sequence++,
318
+ error: {
319
+ code: event.error.type,
320
+ message: event.error.message,
321
+ },
322
+ };
323
+ break;
324
+ }
325
+ }
326
+ catch (parseError) {
327
+ // Skip malformed chunks
328
+ console.warn('Failed to parse SSE event:', data, parseError);
329
+ continue;
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+ finally {
336
+ reader.releaseLock();
337
+ }
338
+ }
339
+ catch (error) {
340
+ // Yield error chunk
341
+ yield {
342
+ type: 'error',
343
+ sequence: 0,
344
+ error: {
345
+ code: error instanceof Error ? error.name : 'UNKNOWN_ERROR',
346
+ message: error instanceof Error ? error.message : String(error),
347
+ },
348
+ };
349
+ }
350
+ }
351
+ /**
352
+ * Health check to verify Anthropic API is accessible.
353
+ */
354
+ async healthCheck() {
355
+ try {
356
+ // Anthropic doesn't have a dedicated health endpoint
357
+ // We'll try a minimal request to verify auth
358
+ const response = await fetch(`${this.baseURL}/messages`, {
359
+ method: 'POST',
360
+ headers: this.getHeaders(),
361
+ body: JSON.stringify({
362
+ model: 'claude-3-haiku-20240307',
363
+ max_tokens: 1,
364
+ messages: [{ role: 'user', content: 'test' }],
365
+ }),
366
+ signal: AbortSignal.timeout(5000),
367
+ });
368
+ return response.ok || response.status === 400; // 400 is acceptable (validation error)
369
+ }
370
+ catch {
371
+ return false;
372
+ }
373
+ }
374
+ /**
375
+ * Estimate cost for a request (rough heuristic).
376
+ */
377
+ estimateCost(request) {
378
+ // Use shared token estimation utility
379
+ const estimatedInputTokens = estimateTokens(request);
380
+ // Rough cost: $0.015 per 1000 tokens for Claude 3.5 Sonnet
381
+ return Promise.resolve((estimatedInputTokens / 1000) * 0.015);
382
+ }
383
+ /**
384
+ * List available Anthropic models.
385
+ *
386
+ * Since Anthropic doesn't have a public models endpoint, this uses:
387
+ * 1. Static config (config.models) - if provided
388
+ * 2. Default model list - built-in list of Claude models
389
+ */
390
+ listModels(options) {
391
+ // 1. Check static config first
392
+ if (this.config.models) {
393
+ return Promise.resolve(buildStaticResult(this.config.models, 'anthropic'));
394
+ }
395
+ // 2. Use default Anthropic models
396
+ const result = {
397
+ models: [...DEFAULT_ANTHROPIC_MODELS],
398
+ source: 'static',
399
+ fetchedAt: Date.now(),
400
+ isComplete: true,
401
+ };
402
+ // 3. Apply filter if requested
403
+ return Promise.resolve(applyModelFilter(result, options?.filter));
404
+ }
405
+ // ==========================================================================
406
+ // Private Helper Methods
407
+ // ==========================================================================
408
+ /**
409
+ * Convert IR request to Anthropic format.
410
+ *
411
+ * Public method for testing and debugging - see what will be sent to Anthropic.
412
+ */
413
+ fromIR(request) {
414
+ try {
415
+ // Normalize system messages (Anthropic uses separate-parameter strategy)
416
+ const { systemParameter, messages } = normalizeSystemMessages(request.messages, this.metadata.capabilities.systemMessageStrategy, this.metadata.capabilities.supportsMultipleSystemMessages);
417
+ // Convert messages
418
+ const anthropicMessages = messages.map((msg) => this.convertMessageToAnthropic(msg));
419
+ // Validate max_tokens is present (required by Anthropic)
420
+ const maxTokens = request.parameters?.maxTokens || 4096;
421
+ // Build Anthropic request
422
+ const anthropicRequest = {
423
+ model: request.parameters?.model || this.config.defaultModel || 'claude-3-5-sonnet-20241022',
424
+ messages: anthropicMessages,
425
+ system: systemParameter || undefined,
426
+ max_tokens: maxTokens,
427
+ temperature: request.parameters?.temperature,
428
+ top_p: request.parameters?.topP,
429
+ top_k: request.parameters?.topK,
430
+ stop_sequences: request.parameters?.stopSequences
431
+ ? [...request.parameters.stopSequences].slice(0, 4) // Anthropic max is 4
432
+ : undefined,
433
+ stream: request.stream,
434
+ metadata: request.metadata.custom?.userId !== undefined &&
435
+ (typeof request.metadata.custom.userId === 'string' ||
436
+ typeof request.metadata.custom.userId === 'number')
437
+ ? { user_id: String(request.metadata.custom.userId) }
438
+ : undefined,
439
+ };
440
+ return anthropicRequest;
441
+ }
442
+ catch (error) {
443
+ throw new AdapterConversionError({
444
+ code: ErrorCode.ADAPTER_CONVERSION_ERROR,
445
+ message: `Failed to convert IR to Anthropic format: ${error instanceof Error ? error.message : String(error)}`,
446
+ provenance: {
447
+ backend: this.metadata.name,
448
+ },
449
+ cause: error instanceof Error ? error : undefined,
450
+ });
451
+ }
452
+ }
453
+ /**
454
+ * Convert Anthropic response to IR format.
455
+ *
456
+ * Public method for testing and debugging - parse Anthropic responses manually.
457
+ */
458
+ toIR(response, originalRequest, latencyMs) {
459
+ try {
460
+ // Extract text content from content blocks
461
+ let textContent = '';
462
+ const contentBlocks = [];
463
+ for (const block of response.content) {
464
+ if (block.type === 'text') {
465
+ textContent += block.text;
466
+ contentBlocks.push({ type: 'text', text: block.text });
467
+ }
468
+ else if (block.type === 'tool_use') {
469
+ contentBlocks.push({
470
+ type: 'tool_use',
471
+ id: block.id,
472
+ name: block.name,
473
+ input: block.input,
474
+ });
475
+ }
476
+ }
477
+ // Build message (use simple string if only text, otherwise structured content)
478
+ const firstBlock = contentBlocks[0];
479
+ const message = {
480
+ role: 'assistant',
481
+ content: contentBlocks.length === 1 && firstBlock?.type === 'text' ? textContent : contentBlocks,
482
+ };
483
+ // Map stop reason
484
+ const finishReason = this.mapStopReason(response.stop_reason || 'end_turn');
485
+ // Build IR response
486
+ const irResponse = {
487
+ message,
488
+ finishReason,
489
+ usage: {
490
+ promptTokens: response.usage.input_tokens,
491
+ completionTokens: response.usage.output_tokens,
492
+ totalTokens: response.usage.input_tokens + response.usage.output_tokens,
493
+ },
494
+ metadata: {
495
+ ...originalRequest.metadata,
496
+ providerResponseId: response.id, // Anthropic's msg_xxx ID
497
+ provenance: {
498
+ ...originalRequest.metadata.provenance,
499
+ backend: this.metadata.name,
500
+ },
501
+ custom: {
502
+ ...originalRequest.metadata.custom,
503
+ anthropicMessageId: response.id,
504
+ latencyMs,
505
+ },
506
+ },
507
+ raw: response,
508
+ };
509
+ return irResponse;
510
+ }
511
+ catch (error) {
512
+ throw new AdapterConversionError({
513
+ code: ErrorCode.ADAPTER_CONVERSION_ERROR,
514
+ message: `Failed to convert Anthropic response to IR: ${error instanceof Error ? error.message : String(error)}`,
515
+ provenance: {
516
+ backend: this.metadata.name,
517
+ },
518
+ cause: error instanceof Error ? error : undefined,
519
+ });
520
+ }
521
+ }
522
+ /**
523
+ * Convert IR message to Anthropic message.
524
+ */
525
+ convertMessageToAnthropic(message) {
526
+ // Anthropic only supports user/assistant roles (system handled separately)
527
+ if (message.role === 'system') {
528
+ throw new AdapterConversionError({
529
+ code: ErrorCode.ADAPTER_CONVERSION_ERROR,
530
+ message: 'System messages should be extracted before conversion to Anthropic format',
531
+ provenance: {
532
+ backend: this.metadata.name,
533
+ },
534
+ });
535
+ }
536
+ // Convert content
537
+ let content;
538
+ if (typeof message.content === 'string') {
539
+ content = message.content;
540
+ }
541
+ else {
542
+ // Convert content blocks
543
+ content = message.content.map((block) => {
544
+ switch (block.type) {
545
+ case 'text':
546
+ return { type: 'text', text: block.text };
547
+ case 'image':
548
+ if (block.source.type === 'url') {
549
+ return {
550
+ type: 'image',
551
+ source: { type: 'url', url: block.source.url },
552
+ };
553
+ }
554
+ else {
555
+ return {
556
+ type: 'image',
557
+ source: {
558
+ type: 'base64',
559
+ media_type: block.source.mediaType,
560
+ data: block.source.data,
561
+ },
562
+ };
563
+ }
564
+ case 'tool_use':
565
+ return {
566
+ type: 'tool_use',
567
+ id: block.id,
568
+ name: block.name,
569
+ input: block.input,
570
+ };
571
+ case 'tool_result':
572
+ return {
573
+ type: 'tool_result',
574
+ tool_use_id: block.toolUseId,
575
+ content: typeof block.content === 'string'
576
+ ? block.content
577
+ : block.content.map((c) => c.type === 'text'
578
+ ? { type: 'text', text: c.text }
579
+ : { type: 'text', text: '' }),
580
+ };
581
+ default:
582
+ // Fallback to text for unsupported types
583
+ return { type: 'text', text: JSON.stringify(block) };
584
+ }
585
+ });
586
+ }
587
+ return {
588
+ role: message.role,
589
+ content,
590
+ };
591
+ }
592
+ /**
593
+ * Map Anthropic stop reason to IR finish reason.
594
+ */
595
+ mapStopReason(stopReason) {
596
+ switch (stopReason) {
597
+ case 'end_turn':
598
+ return 'stop';
599
+ case 'max_tokens':
600
+ return 'length';
601
+ case 'stop_sequence':
602
+ return 'stop';
603
+ case 'tool_use':
604
+ return 'tool_calls';
605
+ default:
606
+ return 'stop';
607
+ }
608
+ }
609
+ /**
610
+ * Make HTTP request to Anthropic API.
611
+ */
612
+ async makeRequest(request, signal) {
613
+ try {
614
+ const response = await fetch(`${this.baseURL}/messages`, {
615
+ method: 'POST',
616
+ headers: this.getHeaders(),
617
+ body: JSON.stringify(request),
618
+ signal,
619
+ });
620
+ if (!response.ok) {
621
+ const errorBody = await response.text();
622
+ throw createErrorFromHttpResponse(response.status, response.statusText, errorBody, {
623
+ backend: this.metadata.name,
624
+ });
625
+ }
626
+ const data = (await response.json());
627
+ return data;
628
+ }
629
+ catch (error) {
630
+ if (error instanceof TypeError && error.message.includes('fetch')) {
631
+ throw new NetworkError({
632
+ code: ErrorCode.NETWORK_ERROR,
633
+ message: `Network request failed: ${error.message}`,
634
+ provenance: {
635
+ backend: this.metadata.name,
636
+ },
637
+ cause: error,
638
+ });
639
+ }
640
+ throw error;
641
+ }
642
+ }
643
+ /**
644
+ * Get HTTP headers for Anthropic API requests.
645
+ */
646
+ getHeaders() {
647
+ const headers = {
648
+ 'Content-Type': 'application/json',
649
+ 'x-api-key': this.config.apiKey,
650
+ 'anthropic-version': '2023-06-01',
651
+ };
652
+ // Add dangerous browser access header if browserMode is enabled
653
+ if (this.config.browserMode) {
654
+ headers['anthropic-dangerous-direct-browser-access'] = 'true';
655
+ }
656
+ // Merge with custom headers (custom headers can override)
657
+ return { ...headers, ...this.config.headers };
658
+ }
659
+ }
660
+ //# sourceMappingURL=anthropic.js.map