@tanstack/ai-anthropic 0.0.3 → 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.
@@ -1,32 +1,20 @@
1
- import Anthropic_SDK from '@anthropic-ai/sdk'
2
- import { BaseAdapter } from '@tanstack/ai'
3
- import { ANTHROPIC_MODELS } from './model-meta'
4
- import { convertToolsToProviderFormat } from './tools/tool-converter'
5
- import { validateTextProviderOptions } from './text/text-provider-options'
6
- import type {
7
- AnthropicDocumentMetadata,
8
- AnthropicImageMetadata,
9
- AnthropicMessageMetadataByModality,
10
- AnthropicTextMetadata,
11
- } from './message-types'
12
- import type {
13
- ChatOptions,
14
- ContentPart,
15
- EmbeddingOptions,
16
- EmbeddingResult,
17
- ModelMessage,
18
- StreamChunk,
19
- SummarizationOptions,
20
- SummarizationResult,
21
- } from '@tanstack/ai'
1
+ import { BaseTextAdapter } from '@tanstack/ai/adapters'
2
+ import { convertToolsToProviderFormat } from '../tools/tool-converter'
3
+ import { validateTextProviderOptions } from '../text/text-provider-options'
4
+ import {
5
+ createAnthropicClient,
6
+ generateId,
7
+ getAnthropicApiKeyFromEnv,
8
+ } from '../utils'
22
9
  import type {
10
+ ANTHROPIC_MODELS,
23
11
  AnthropicChatModelProviderOptionsByName,
24
12
  AnthropicModelInputModalitiesByName,
25
- } from './model-meta'
13
+ } from '../model-meta'
26
14
  import type {
27
- ExternalTextProviderOptions,
28
- InternalTextProviderOptions,
29
- } from './text/text-provider-options'
15
+ StructuredOutputOptions,
16
+ StructuredOutputResult,
17
+ } from '@tanstack/ai/adapters'
30
18
  import type {
31
19
  Base64ImageSource,
32
20
  Base64PDFSource,
@@ -37,16 +25,35 @@ import type {
37
25
  URLImageSource,
38
26
  URLPDFSource,
39
27
  } from '@anthropic-ai/sdk/resources/messages'
28
+ import type Anthropic_SDK from '@anthropic-ai/sdk'
29
+ import type {
30
+ ContentPart,
31
+ Modality,
32
+ ModelMessage,
33
+ StreamChunk,
34
+ TextOptions,
35
+ } from '@tanstack/ai'
36
+ import type {
37
+ ExternalTextProviderOptions,
38
+ InternalTextProviderOptions,
39
+ } from '../text/text-provider-options'
40
+ import type {
41
+ AnthropicDocumentMetadata,
42
+ AnthropicImageMetadata,
43
+ AnthropicMessageMetadataByModality,
44
+ AnthropicTextMetadata,
45
+ } from '../message-types'
46
+ import type { AnthropicClientConfig } from '../utils'
40
47
 
41
- export interface AnthropicConfig {
42
- apiKey: string
43
- }
48
+ /**
49
+ * Configuration for Anthropic text adapter
50
+ */
51
+ export interface AnthropicTextConfig extends AnthropicClientConfig {}
44
52
 
45
53
  /**
46
- * Anthropic-specific provider options
47
- * @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic
54
+ * Anthropic-specific provider options for text/chat
48
55
  */
49
- export type AnthropicProviderOptions = ExternalTextProviderOptions
56
+ export type AnthropicTextProviderOptions = ExternalTextProviderOptions
50
57
 
51
58
  type AnthropicContentBlocks =
52
59
  Extract<MessageParam['content'], Array<unknown>> extends Array<infer Block>
@@ -55,36 +62,63 @@ type AnthropicContentBlocks =
55
62
  type AnthropicContentBlock =
56
63
  AnthropicContentBlocks extends Array<infer Block> ? Block : never
57
64
 
58
- export class Anthropic extends BaseAdapter<
59
- typeof ANTHROPIC_MODELS,
60
- [],
61
- AnthropicProviderOptions,
62
- Record<string, any>,
63
- AnthropicChatModelProviderOptionsByName,
64
- AnthropicModelInputModalitiesByName,
65
+ // ===========================
66
+ // Type Resolution Helpers
67
+ // ===========================
68
+
69
+ /**
70
+ * Resolve provider options for a specific model.
71
+ * If the model has explicit options in the map, use those; otherwise use base options.
72
+ */
73
+ type ResolveProviderOptions<TModel extends string> =
74
+ TModel extends keyof AnthropicChatModelProviderOptionsByName
75
+ ? AnthropicChatModelProviderOptionsByName[TModel]
76
+ : AnthropicTextProviderOptions
77
+
78
+ /**
79
+ * Resolve input modalities for a specific model.
80
+ * If the model has explicit modalities in the map, use those; otherwise use default.
81
+ */
82
+ type ResolveInputModalities<TModel extends string> =
83
+ TModel extends keyof AnthropicModelInputModalitiesByName
84
+ ? AnthropicModelInputModalitiesByName[TModel]
85
+ : readonly ['text', 'image', 'document']
86
+
87
+ // ===========================
88
+ // Adapter Implementation
89
+ // ===========================
90
+
91
+ /**
92
+ * Anthropic Text (Chat) Adapter
93
+ *
94
+ * Tree-shakeable adapter for Anthropic chat/text completion functionality.
95
+ * Import only what you need for smaller bundle sizes.
96
+ */
97
+ export class AnthropicTextAdapter<
98
+ TModel extends (typeof ANTHROPIC_MODELS)[number],
99
+ TProviderOptions extends object = ResolveProviderOptions<TModel>,
100
+ TInputModalities extends ReadonlyArray<Modality> =
101
+ ResolveInputModalities<TModel>,
102
+ > extends BaseTextAdapter<
103
+ TModel,
104
+ TProviderOptions,
105
+ TInputModalities,
65
106
  AnthropicMessageMetadataByModality
66
107
  > {
67
- name = 'anthropic' as const
68
- models = ANTHROPIC_MODELS
69
-
70
- declare _modelProviderOptionsByName: AnthropicChatModelProviderOptionsByName
71
- declare _modelInputModalitiesByName: AnthropicModelInputModalitiesByName
72
- declare _messageMetadataByModality: AnthropicMessageMetadataByModality
108
+ readonly kind = 'text' as const
109
+ readonly name = 'anthropic' as const
73
110
 
74
111
  private client: Anthropic_SDK
75
112
 
76
- constructor(config: AnthropicConfig) {
77
- super({})
78
- this.client = new Anthropic_SDK({
79
- apiKey: config.apiKey,
80
- })
113
+ constructor(config: AnthropicTextConfig, model: TModel) {
114
+ super({}, model)
115
+ this.client = createAnthropicClient(config)
81
116
  }
82
117
 
83
118
  async *chatStream(
84
- options: ChatOptions<string, AnthropicProviderOptions>,
119
+ options: TextOptions<AnthropicTextProviderOptions>,
85
120
  ): AsyncIterable<StreamChunk> {
86
121
  try {
87
- // Map common options to Anthropic format using the centralized mapping function
88
122
  const requestParams = this.mapCommonOptionsToAnthropic(options)
89
123
 
90
124
  const stream = await this.client.beta.messages.create(
@@ -96,105 +130,111 @@ export class Anthropic extends BaseAdapter<
96
130
  )
97
131
 
98
132
  yield* this.processAnthropicStream(stream, options.model, () =>
99
- this.generateId(),
133
+ generateId(this.name),
100
134
  )
101
- } catch (error: any) {
102
- console.error('[Anthropic Adapter] Error in chatStream:', {
103
- message: error?.message,
104
- status: error?.status,
105
- statusText: error?.statusText,
106
- code: error?.code,
107
- type: error?.type,
108
- error: error,
109
- stack: error?.stack,
110
- })
111
-
112
- // Emit an error chunk
135
+ } catch (error: unknown) {
136
+ const err = error as Error & { status?: number; code?: string }
113
137
  yield {
114
138
  type: 'error',
115
- id: this.generateId(),
139
+ id: generateId(this.name),
116
140
  model: options.model,
117
141
  timestamp: Date.now(),
118
142
  error: {
119
- message: error?.message || 'Unknown error occurred',
120
- code: error?.code || error?.status,
143
+ message: err.message || 'Unknown error occurred',
144
+ code: err.code || String(err.status),
121
145
  },
122
146
  }
123
147
  }
124
148
  }
125
149
 
126
- async summarize(options: SummarizationOptions): Promise<SummarizationResult> {
127
- const systemPrompt = this.buildSummarizationPrompt(options)
128
-
129
- const response = await this.client.messages.create({
130
- model: options.model,
131
- messages: [{ role: 'user', content: options.text }],
132
- system: systemPrompt,
133
- max_tokens: options.maxLength || 500,
134
- temperature: 0.3,
135
- stream: false,
136
- })
137
-
138
- const content = response.content
139
- .map((c) => (c.type === 'text' ? c.text : ''))
140
- .join('')
141
-
142
- return {
143
- id: response.id,
144
- model: response.model,
145
- summary: content,
146
- usage: {
147
- promptTokens: response.usage.input_tokens,
148
- completionTokens: response.usage.output_tokens,
149
- totalTokens: response.usage.input_tokens + response.usage.output_tokens,
150
+ /**
151
+ * Generate structured output using Anthropic's tool-based approach.
152
+ * Anthropic doesn't have native structured output, so we use a tool with the schema
153
+ * and force the model to call it.
154
+ * The outputSchema is already JSON Schema (converted in the ai layer).
155
+ */
156
+ async structuredOutput(
157
+ options: StructuredOutputOptions<AnthropicTextProviderOptions>,
158
+ ): Promise<StructuredOutputResult<unknown>> {
159
+ const { chatOptions, outputSchema } = options
160
+
161
+ const requestParams = this.mapCommonOptionsToAnthropic(chatOptions)
162
+
163
+ // Create a tool that will capture the structured output
164
+ // Anthropic's SDK requires input_schema with type: 'object' literal
165
+ const structuredOutputTool = {
166
+ name: 'structured_output',
167
+ description:
168
+ 'Use this tool to provide your response in the required structured format.',
169
+ input_schema: {
170
+ type: 'object' as const,
171
+ properties: outputSchema.properties ?? {},
172
+ required: outputSchema.required ?? [],
150
173
  },
151
174
  }
152
- }
153
175
 
154
- createEmbeddings(_options: EmbeddingOptions): Promise<EmbeddingResult> {
155
- // Note: Anthropic doesn't have a native embeddings API
156
- // You would need to use a different service or implement a workaround
157
- throw new Error(
158
- 'Embeddings are not natively supported by Anthropic. Consider using OpenAI or another provider for embeddings.',
159
- )
160
- }
176
+ try {
177
+ // Make non-streaming request with tool_choice forced to our structured output tool
178
+ const response = await this.client.messages.create(
179
+ {
180
+ ...requestParams,
181
+ stream: false,
182
+ tools: [structuredOutputTool],
183
+ tool_choice: { type: 'tool', name: 'structured_output' },
184
+ },
185
+ {
186
+ signal: chatOptions.request?.signal,
187
+ headers: chatOptions.request?.headers,
188
+ },
189
+ )
161
190
 
162
- private buildSummarizationPrompt(options: SummarizationOptions): string {
163
- let prompt = 'You are a professional summarizer. '
164
-
165
- switch (options.style) {
166
- case 'bullet-points':
167
- prompt += 'Provide a summary in bullet point format. '
168
- break
169
- case 'paragraph':
170
- prompt += 'Provide a summary in paragraph format. '
171
- break
172
- case 'concise':
173
- prompt += 'Provide a very concise summary in 1-2 sentences. '
174
- break
175
- default:
176
- prompt += 'Provide a clear and concise summary. '
177
- }
191
+ // Extract the tool use content from the response
192
+ let parsed: unknown = null
193
+ let rawText = ''
178
194
 
179
- if (options.focus && options.focus.length > 0) {
180
- prompt += `Focus on the following aspects: ${options.focus.join(', ')}. `
181
- }
195
+ for (const block of response.content) {
196
+ if (block.type === 'tool_use' && block.name === 'structured_output') {
197
+ parsed = block.input
198
+ rawText = JSON.stringify(block.input)
199
+ break
200
+ }
201
+ }
182
202
 
183
- if (options.maxLength) {
184
- prompt += `Keep the summary under ${options.maxLength} tokens. `
185
- }
203
+ if (parsed === null) {
204
+ // Fallback: try to extract text content and parse as JSON
205
+ rawText = response.content
206
+ .map((b) => {
207
+ if (b.type === 'text') {
208
+ return b.text
209
+ }
210
+ return ''
211
+ })
212
+ .join('')
213
+ try {
214
+ parsed = JSON.parse(rawText)
215
+ } catch {
216
+ throw new Error(
217
+ `Failed to extract structured output from response. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,
218
+ )
219
+ }
220
+ }
186
221
 
187
- return prompt
222
+ return {
223
+ data: parsed,
224
+ rawText,
225
+ }
226
+ } catch (error: unknown) {
227
+ const err = error as Error
228
+ throw new Error(
229
+ `Structured output generation failed: ${err.message || 'Unknown error occurred'}`,
230
+ )
231
+ }
188
232
  }
189
233
 
190
- /**
191
- * Maps common options to Anthropic-specific format
192
- * Handles translation of normalized options to Anthropic's API format
193
- */
194
234
  private mapCommonOptionsToAnthropic(
195
- options: ChatOptions<string, AnthropicProviderOptions>,
235
+ options: TextOptions<AnthropicTextProviderOptions>,
196
236
  ) {
197
- const providerOptions = options.providerOptions as
237
+ const modelOptions = options.modelOptions as
198
238
  | InternalTextProviderOptions
199
239
  | undefined
200
240
 
@@ -203,9 +243,8 @@ export class Anthropic extends BaseAdapter<
203
243
  ? convertToolsToProviderFormat(options.tools)
204
244
  : undefined
205
245
 
206
- // Filter out invalid fields from providerOptions (like 'store' which is OpenAI-specific)
207
246
  const validProviderOptions: Partial<InternalTextProviderOptions> = {}
208
- if (providerOptions) {
247
+ if (modelOptions) {
209
248
  const validKeys: Array<keyof InternalTextProviderOptions> = [
210
249
  'container',
211
250
  'context_management',
@@ -218,34 +257,34 @@ export class Anthropic extends BaseAdapter<
218
257
  'top_k',
219
258
  ]
220
259
  for (const key of validKeys) {
221
- if (key in providerOptions) {
222
- const value = providerOptions[key]
223
- // Anthropic expects tool_choice to be an object, not a string
260
+ if (key in modelOptions) {
261
+ const value = modelOptions[key]
224
262
  if (key === 'tool_choice' && typeof value === 'string') {
225
- ;(validProviderOptions as any)[key] = { type: value }
263
+ ;(validProviderOptions as Record<string, unknown>)[key] = {
264
+ type: value,
265
+ }
226
266
  } else {
227
- ;(validProviderOptions as any)[key] = value
267
+ ;(validProviderOptions as Record<string, unknown>)[key] = value
228
268
  }
229
269
  }
230
270
  }
231
271
  }
232
272
 
233
- // Ensure max_tokens is greater than thinking.budget_tokens if thinking is enabled
234
273
  const thinkingBudget =
235
274
  validProviderOptions.thinking?.type === 'enabled'
236
275
  ? validProviderOptions.thinking.budget_tokens
237
276
  : undefined
238
- const defaultMaxTokens = options.options?.maxTokens || 1024
277
+ const defaultMaxTokens = options.maxTokens || 1024
239
278
  const maxTokens =
240
279
  thinkingBudget && thinkingBudget >= defaultMaxTokens
241
- ? thinkingBudget + 1 // Ensure max_tokens is greater than budget_tokens
280
+ ? thinkingBudget + 1
242
281
  : defaultMaxTokens
243
282
 
244
283
  const requestParams: InternalTextProviderOptions = {
245
284
  model: options.model,
246
285
  max_tokens: maxTokens,
247
- temperature: options.options?.temperature,
248
- top_p: options.options?.topP,
286
+ temperature: options.temperature,
287
+ top_p: options.topP,
249
288
  messages: formattedMessages,
250
289
  system: options.systemPrompts?.join('\n'),
251
290
  tools: tools,
@@ -260,9 +299,7 @@ export class Anthropic extends BaseAdapter<
260
299
  ): TextBlockParam | ImageBlockParam | DocumentBlockParam {
261
300
  switch (part.type) {
262
301
  case 'text': {
263
- const metadata = part.metadata as any as
264
- | AnthropicTextMetadata
265
- | undefined
302
+ const metadata = part.metadata as AnthropicTextMetadata | undefined
266
303
  return {
267
304
  type: 'text',
268
305
  text: part.content,
@@ -271,9 +308,7 @@ export class Anthropic extends BaseAdapter<
271
308
  }
272
309
 
273
310
  case 'image': {
274
- const metadata = part.metadata as any as
275
- | AnthropicImageMetadata
276
- | undefined
311
+ const metadata = part.metadata as AnthropicImageMetadata | undefined
277
312
  const imageSource: Base64ImageSource | URLImageSource =
278
313
  part.source.type === 'data'
279
314
  ? {
@@ -285,8 +320,7 @@ export class Anthropic extends BaseAdapter<
285
320
  type: 'url',
286
321
  url: part.source.value,
287
322
  }
288
- // exclude the media type
289
- const { mediaType, ...meta } = metadata || {}
323
+ const { mediaType: _mediaType, ...meta } = metadata || {}
290
324
  return {
291
325
  type: 'image',
292
326
  source: imageSource,
@@ -294,9 +328,7 @@ export class Anthropic extends BaseAdapter<
294
328
  }
295
329
  }
296
330
  case 'document': {
297
- const metadata = part.metadata as any as
298
- | AnthropicDocumentMetadata
299
- | undefined
331
+ const metadata = part.metadata as AnthropicDocumentMetadata | undefined
300
332
  const docSource: Base64PDFSource | URLPDFSource =
301
333
  part.source.type === 'data'
302
334
  ? {
@@ -316,12 +348,10 @@ export class Anthropic extends BaseAdapter<
316
348
  }
317
349
  case 'audio':
318
350
  case 'video':
319
- // Anthropic doesn't support audio/video directly, treat as text with a note
320
351
  throw new Error(
321
352
  `Anthropic does not support ${part.type} content directly`,
322
353
  )
323
354
  default: {
324
- // Exhaustive check - this should never happen with known types
325
355
  const _exhaustiveCheck: never = part
326
356
  throw new Error(
327
357
  `Unsupported content part type: ${(_exhaustiveCheck as ContentPart).type}`,
@@ -393,7 +423,6 @@ export class Anthropic extends BaseAdapter<
393
423
  continue
394
424
  }
395
425
 
396
- // Handle user messages with multimodal content
397
426
  if (role === 'user' && Array.isArray(message.content)) {
398
427
  const contentBlocks = message.content.map((part) =>
399
428
  this.convertContentPartToAnthropic(part),
@@ -424,7 +453,7 @@ export class Anthropic extends BaseAdapter<
424
453
  private async *processAnthropicStream(
425
454
  stream: AsyncIterable<Anthropic_SDK.Beta.BetaRawMessageStreamEvent>,
426
455
  model: string,
427
- generateId: () => string,
456
+ genId: () => string,
428
457
  ): AsyncIterable<StreamChunk> {
429
458
  let accumulatedContent = ''
430
459
  let accumulatedThinking = ''
@@ -446,7 +475,6 @@ export class Anthropic extends BaseAdapter<
446
475
  input: '',
447
476
  })
448
477
  } else if (event.content_block.type === 'thinking') {
449
- // Reset thinking content when a new thinking block starts
450
478
  accumulatedThinking = ''
451
479
  }
452
480
  } else if (event.type === 'content_block_delta') {
@@ -455,7 +483,7 @@ export class Anthropic extends BaseAdapter<
455
483
  accumulatedContent += delta
456
484
  yield {
457
485
  type: 'content',
458
- id: generateId(),
486
+ id: genId(),
459
487
  model: model,
460
488
  timestamp,
461
489
  delta,
@@ -463,29 +491,24 @@ export class Anthropic extends BaseAdapter<
463
491
  role: 'assistant',
464
492
  }
465
493
  } else if (event.delta.type === 'thinking_delta') {
466
- // Handle thinking content
467
494
  const delta = event.delta.thinking
468
495
  accumulatedThinking += delta
469
496
  yield {
470
497
  type: 'thinking',
471
- id: generateId(),
498
+ id: genId(),
472
499
  model: model,
473
500
  timestamp,
474
501
  delta,
475
502
  content: accumulatedThinking,
476
503
  }
477
504
  } else if (event.delta.type === 'input_json_delta') {
478
- // Tool input is being streamed
479
505
  const existing = toolCallsMap.get(currentToolIndex)
480
506
  if (existing) {
481
- // Accumulate the input for final processing
482
507
  existing.input += event.delta.partial_json
483
508
 
484
- // Yield the DELTA (partial_json), not the full accumulated input
485
- // The stream processor will concatenate these deltas
486
509
  yield {
487
510
  type: 'tool_call',
488
- id: generateId(),
511
+ id: genId(),
489
512
  model: model,
490
513
  timestamp,
491
514
  toolCall: {
@@ -501,14 +524,11 @@ export class Anthropic extends BaseAdapter<
501
524
  }
502
525
  }
503
526
  } else if (event.type === 'content_block_stop') {
504
- // If this is a tool call and we haven't received any input deltas,
505
- // emit a tool_call chunk with empty arguments
506
527
  const existing = toolCallsMap.get(currentToolIndex)
507
528
  if (existing && existing.input === '') {
508
- // No input_json_delta events received, emit empty arguments
509
529
  yield {
510
530
  type: 'tool_call',
511
- id: generateId(),
531
+ id: genId(),
512
532
  model: model,
513
533
  timestamp,
514
534
  toolCall: {
@@ -525,7 +545,7 @@ export class Anthropic extends BaseAdapter<
525
545
  } else if (event.type === 'message_stop') {
526
546
  yield {
527
547
  type: 'done',
528
- id: generateId(),
548
+ id: genId(),
529
549
  model: model,
530
550
  timestamp,
531
551
  finishReason: 'stop',
@@ -536,11 +556,10 @@ export class Anthropic extends BaseAdapter<
536
556
  case 'tool_use': {
537
557
  yield {
538
558
  type: 'done',
539
- id: generateId(),
559
+ id: genId(),
540
560
  model: model,
541
561
  timestamp,
542
562
  finishReason: 'tool_calls',
543
-
544
563
  usage: {
545
564
  promptTokens: event.usage.input_tokens || 0,
546
565
  completionTokens: event.usage.output_tokens || 0,
@@ -554,7 +573,7 @@ export class Anthropic extends BaseAdapter<
554
573
  case 'max_tokens': {
555
574
  yield {
556
575
  type: 'error',
557
- id: generateId(),
576
+ id: genId(),
558
577
  model: model,
559
578
  timestamp,
560
579
  error: {
@@ -565,37 +584,10 @@ export class Anthropic extends BaseAdapter<
565
584
  }
566
585
  break
567
586
  }
568
- case 'model_context_window_exceeded': {
569
- yield {
570
- type: 'error',
571
- id: generateId(),
572
- model: model,
573
- timestamp,
574
- error: {
575
- message:
576
- "The response was cut off because the model's context window was exceeded.",
577
- code: 'context_window_exceeded',
578
- },
579
- }
580
- break
581
- }
582
- case 'refusal': {
583
- yield {
584
- type: 'error',
585
- id: generateId(),
586
- model: model,
587
- timestamp,
588
- error: {
589
- message: 'The model refused to complete the request.',
590
- code: 'refusal',
591
- },
592
- }
593
- break
594
- }
595
587
  default: {
596
588
  yield {
597
589
  type: 'done',
598
- id: generateId(),
590
+ id: genId(),
599
591
  model: model,
600
592
  timestamp,
601
593
  finishReason: 'stop',
@@ -612,84 +604,53 @@ export class Anthropic extends BaseAdapter<
612
604
  }
613
605
  }
614
606
  }
615
- } catch (error: any) {
616
- console.error('[Anthropic Adapter] Error in processAnthropicStream:', {
617
- message: error?.message,
618
- status: error?.status,
619
- statusText: error?.statusText,
620
- code: error?.code,
621
- type: error?.type,
622
- error: error,
623
- stack: error?.stack,
624
- })
607
+ } catch (error: unknown) {
608
+ const err = error as Error & { status?: number; code?: string }
625
609
 
626
610
  yield {
627
611
  type: 'error',
628
- id: generateId(),
612
+ id: genId(),
629
613
  model: model,
630
614
  timestamp,
631
615
  error: {
632
- message: error?.message || 'Unknown error occurred',
633
- code: error?.code || error?.status,
616
+ message: err.message || 'Unknown error occurred',
617
+ code: err.code || String(err.status),
634
618
  },
635
619
  }
636
620
  }
637
621
  }
638
622
  }
623
+
639
624
  /**
640
- * Creates an Anthropic adapter with simplified configuration
641
- * @param apiKey - Your Anthropic API key
642
- * @returns A fully configured Anthropic adapter instance
643
- *
644
- * @example
645
- * ```typescript
646
- * const anthropic = createAnthropic("sk-ant-...");
647
- *
648
- * const ai = new AI({
649
- * adapters: {
650
- * anthropic,
651
- * }
652
- * });
653
- * ```
625
+ * Creates an Anthropic chat adapter with explicit API key.
626
+ * Type resolution happens here at the call site.
654
627
  */
655
- export function createAnthropic(
628
+ export function createAnthropicChat<
629
+ TModel extends (typeof ANTHROPIC_MODELS)[number],
630
+ >(
631
+ model: TModel,
656
632
  apiKey: string,
657
- config?: Omit<AnthropicConfig, 'apiKey'>,
658
- ): Anthropic {
659
- return new Anthropic({ apiKey, ...config })
633
+ config?: Omit<AnthropicTextConfig, 'apiKey'>,
634
+ ): AnthropicTextAdapter<
635
+ TModel,
636
+ ResolveProviderOptions<TModel>,
637
+ ResolveInputModalities<TModel>
638
+ > {
639
+ return new AnthropicTextAdapter({ apiKey, ...config }, model)
660
640
  }
661
641
 
662
642
  /**
663
- * Create an Anthropic adapter with automatic API key detection from environment variables.
664
- *
665
- * Looks for `ANTHROPIC_API_KEY` in:
666
- * - `process.env` (Node.js)
667
- * - `window.env` (Browser with injected env)
668
- *
669
- * @param config - Optional configuration (excluding apiKey which is auto-detected)
670
- * @returns Configured Anthropic adapter instance
671
- * @throws Error if ANTHROPIC_API_KEY is not found in environment
672
- *
673
- * @example
674
- * ```typescript
675
- * // Automatically uses ANTHROPIC_API_KEY from environment
676
- * const aiInstance = ai(anthropic());
677
- * ```
643
+ * Creates an Anthropic text adapter with automatic API key detection.
644
+ * Type resolution happens here at the call site.
678
645
  */
679
- export function anthropic(config?: Omit<AnthropicConfig, 'apiKey'>): Anthropic {
680
- const env =
681
- typeof globalThis !== 'undefined' && (globalThis as any).window?.env
682
- ? (globalThis as any).window.env
683
- : typeof process !== 'undefined'
684
- ? process.env
685
- : undefined
686
- const key = env?.ANTHROPIC_API_KEY
687
-
688
- if (!key) {
689
- throw new Error(
690
- 'ANTHROPIC_API_KEY is required. Please set it in your environment variables or use createAnthropic(apiKey, config) instead.',
691
- )
692
- }
693
-
694
- return createAnthropic(key, config)
646
+ export function anthropicText<TModel extends (typeof ANTHROPIC_MODELS)[number]>(
647
+ model: TModel,
648
+ config?: Omit<AnthropicTextConfig, 'apiKey'>,
649
+ ): AnthropicTextAdapter<
650
+ TModel,
651
+ ResolveProviderOptions<TModel>,
652
+ ResolveInputModalities<TModel>
653
+ > {
654
+ const apiKey = getAnthropicApiKeyFromEnv()
655
+ return createAnthropicChat(model, apiKey, config)
695
656
  }