@tanstack/ai-bedrock 0.0.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.
Files changed (64) hide show
  1. package/README.md +95 -0
  2. package/dist/esm/adapters/converse-text.d.ts +93 -0
  3. package/dist/esm/adapters/converse-text.js +430 -0
  4. package/dist/esm/adapters/converse-text.js.map +1 -0
  5. package/dist/esm/adapters/responses-text.d.ts +23 -0
  6. package/dist/esm/adapters/responses-text.js +22 -0
  7. package/dist/esm/adapters/responses-text.js.map +1 -0
  8. package/dist/esm/adapters/text.d.ts +30 -0
  9. package/dist/esm/adapters/text.js +38 -0
  10. package/dist/esm/adapters/text.js.map +1 -0
  11. package/dist/esm/converse/message-converter.d.ts +14 -0
  12. package/dist/esm/converse/message-converter.js +181 -0
  13. package/dist/esm/converse/message-converter.js.map +1 -0
  14. package/dist/esm/converse/provider-options.d.ts +19 -0
  15. package/dist/esm/converse/stream-processor.d.ts +46 -0
  16. package/dist/esm/converse/stream-processor.js +203 -0
  17. package/dist/esm/converse/stream-processor.js.map +1 -0
  18. package/dist/esm/converse/structured-output.d.ts +8 -0
  19. package/dist/esm/converse/structured-output.js +20 -0
  20. package/dist/esm/converse/structured-output.js.map +1 -0
  21. package/dist/esm/converse/tool-converter.d.ts +11 -0
  22. package/dist/esm/converse/tool-converter.js +21 -0
  23. package/dist/esm/converse/tool-converter.js.map +1 -0
  24. package/dist/esm/index.d.ts +29 -0
  25. package/dist/esm/index.js +62 -0
  26. package/dist/esm/index.js.map +1 -0
  27. package/dist/esm/message-types.d.ts +25 -0
  28. package/dist/esm/model-catalog.generated.d.ts +127 -0
  29. package/dist/esm/model-catalog.generated.js +90 -0
  30. package/dist/esm/model-catalog.generated.js.map +1 -0
  31. package/dist/esm/model-meta.d.ts +38 -0
  32. package/dist/esm/model-meta.js +14 -0
  33. package/dist/esm/model-meta.js.map +1 -0
  34. package/dist/esm/text/responses-provider-options.d.ts +29 -0
  35. package/dist/esm/text/text-provider-options.d.ts +36 -0
  36. package/dist/esm/utils/auth.d.ts +20 -0
  37. package/dist/esm/utils/auth.js +49 -0
  38. package/dist/esm/utils/auth.js.map +1 -0
  39. package/dist/esm/utils/client.d.ts +19 -0
  40. package/dist/esm/utils/client.js +35 -0
  41. package/dist/esm/utils/client.js.map +1 -0
  42. package/dist/esm/utils/index.d.ts +1 -0
  43. package/dist/esm/utils/openai-sigv4-fetch.d.ts +10 -0
  44. package/dist/esm/utils/openai-sigv4-fetch.js +35 -0
  45. package/dist/esm/utils/openai-sigv4-fetch.js.map +1 -0
  46. package/package.json +67 -0
  47. package/src/adapters/converse-text.ts +603 -0
  48. package/src/adapters/responses-text.ts +73 -0
  49. package/src/adapters/text.ts +97 -0
  50. package/src/converse/message-converter.ts +268 -0
  51. package/src/converse/provider-options.ts +19 -0
  52. package/src/converse/stream-processor.ts +309 -0
  53. package/src/converse/structured-output.ts +24 -0
  54. package/src/converse/tool-converter.ts +49 -0
  55. package/src/index.ts +190 -0
  56. package/src/message-types.ts +24 -0
  57. package/src/model-catalog.generated.ts +88 -0
  58. package/src/model-meta.ts +69 -0
  59. package/src/text/responses-provider-options.ts +28 -0
  60. package/src/text/text-provider-options.ts +33 -0
  61. package/src/utils/auth.ts +80 -0
  62. package/src/utils/client.ts +62 -0
  63. package/src/utils/index.ts +7 -0
  64. package/src/utils/openai-sigv4-fetch.ts +55 -0
@@ -0,0 +1,603 @@
1
+ import { EventType, convertSchemaToJsonSchema } from '@tanstack/ai'
2
+ import { BaseTextAdapter } from '@tanstack/ai/adapters'
3
+ import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
4
+ import { resolveBedrockAuth } from '../utils/auth'
5
+ import { toConverseMessages } from '../converse/message-converter'
6
+ import { toToolConfig } from '../converse/tool-converter'
7
+ import {
8
+ processConverseStream,
9
+ throwIfConverseStreamError,
10
+ } from '../converse/stream-processor'
11
+ import {
12
+ STRUCTURED_TOOL_NAME,
13
+ buildStructuredToolConfig,
14
+ } from '../converse/structured-output'
15
+ import type { ResolvedBedrockAuth } from '../utils/auth'
16
+ import type { ConverseToolInput } from '../converse/tool-converter'
17
+ import type * as BedrockRuntime from '@aws-sdk/client-bedrock-runtime'
18
+ import type {
19
+ BedrockRuntimeClient,
20
+ BedrockRuntimeClientConfig,
21
+ ContentBlock,
22
+ ConverseCommandInput,
23
+ ConverseCommandOutput,
24
+ ConverseStreamCommandInput,
25
+ ConverseStreamOutput,
26
+ } from '@aws-sdk/client-bedrock-runtime'
27
+ import type {
28
+ JSONSchema,
29
+ Modality,
30
+ StreamChunk,
31
+ TextOptions,
32
+ Tool,
33
+ } from '@tanstack/ai'
34
+ import type {
35
+ StructuredOutputOptions,
36
+ StructuredOutputResult,
37
+ } from '@tanstack/ai/adapters'
38
+ import type { BedrockClientConfig } from '../utils/client'
39
+ import type { BedrockMessageMetadataByModality } from '../message-types'
40
+ import type {
41
+ BedrockConverseModels,
42
+ ResolveConverseProviderOptions,
43
+ ResolveInputModalities,
44
+ } from '../model-meta'
45
+
46
+ /** Config for the Converse adapter — same client config as the chat adapter. */
47
+ export interface BedrockConverseConfig extends BedrockClientConfig {}
48
+
49
+ /**
50
+ * Bedrock Converse text adapter. Wires the Converse translation modules (message
51
+ * converter, tool converter, stream processor, structured-output forced-tool
52
+ * builder) onto `@tanstack/ai`'s `BaseTextAdapter` and the
53
+ * `@aws-sdk/client-bedrock-runtime` `BedrockRuntimeClient`.
54
+ *
55
+ * The success-path AG-UI lifecycle (`RUN_STARTED`..`RUN_FINISHED`) is owned by
56
+ * `processConverseStream`; this adapter only owns the catch/`RUN_ERROR` path,
57
+ * mirroring openai-base's `chatStream`.
58
+ *
59
+ * The actual SDK calls live behind two protected seams (`sendStream` / `send`)
60
+ * so tests can subclass and inject canned Converse SDK shapes without a real
61
+ * AWS request.
62
+ */
63
+ export class BedrockConverseTextAdapter<
64
+ TModel extends BedrockConverseModels,
65
+ // Constraint mirrors the chat adapter (text.ts): the base parameterises
66
+ // `TProviderOptions extends Record<string, any>`, and our default
67
+ // `ResolveConverseProviderOptions<TModel>` resolves to an interface lacking an
68
+ // implicit index signature — which `Record<string, unknown>` would reject but
69
+ // `Record<string, any>` accepts. Confined to the generic constraint (the
70
+ // established adapter pattern) — no value `as` cast is introduced.
71
+ TProviderOptions extends Record<string, any> =
72
+ ResolveConverseProviderOptions<TModel>,
73
+ TInputModalities extends ReadonlyArray<Modality> =
74
+ ResolveInputModalities<TModel>,
75
+ > extends BaseTextAdapter<
76
+ TModel,
77
+ TProviderOptions,
78
+ TInputModalities,
79
+ BedrockMessageMetadataByModality
80
+ > {
81
+ override readonly kind = 'text' as const
82
+ override readonly name = 'bedrock-converse' as const
83
+ private clientPromise?: Promise<BedrockRuntimeClient>
84
+ private readonly clientConfig: BedrockConverseConfig
85
+
86
+ constructor(config: BedrockConverseConfig, model: TModel) {
87
+ super({}, model)
88
+ // Defer client construction and auth resolution: the AWS SDK is Node/
89
+ // server-only, so we must not pull it into the static graph here. The
90
+ // client (and its dynamic import) is built lazily on first SDK call.
91
+ this.clientConfig = config
92
+ }
93
+
94
+ /**
95
+ * Dynamically import `@aws-sdk/client-bedrock-runtime`. The specifier is held
96
+ * in a variable (not a string literal) so bundler dep scanners (e.g. Vite/
97
+ * esbuild optimizeDeps) cannot statically discover the AWS SDK and try to
98
+ * pre-bundle it for the browser — it would fail on the SDK's Node-only
99
+ * `fromTokenFile` export chain. The SDK is Node/server-only and is only
100
+ * reached on a real request. `typeof import(...)` is a type-only reference
101
+ * (erased at emit) so the imported members keep full typing.
102
+ */
103
+ protected importBedrockRuntime(): Promise<typeof BedrockRuntime> {
104
+ const mod = '@aws-sdk/client-bedrock-runtime'
105
+ return import(/* @vite-ignore */ mod) as Promise<typeof BedrockRuntime>
106
+ }
107
+
108
+ /**
109
+ * Lazily construct the `BedrockRuntimeClient`. The dynamic import keeps
110
+ * `@aws-sdk/client-bedrock-runtime` out of the static/browser graph and
111
+ * defers `resolveBedrockAuth` until a real request is made.
112
+ */
113
+ protected async getClient(): Promise<BedrockRuntimeClient> {
114
+ if (!this.clientPromise) {
115
+ this.clientPromise = (async () => {
116
+ const { BedrockRuntimeClient } = await this.importBedrockRuntime()
117
+ const region = this.clientConfig.region ?? 'us-east-1'
118
+ const resolved = resolveBedrockAuth(
119
+ {
120
+ apiKey: this.clientConfig.apiKey,
121
+ region,
122
+ auth: this.clientConfig.auth,
123
+ },
124
+ 'runtime',
125
+ )
126
+ return new BedrockRuntimeClient(
127
+ this.buildClientConfig(resolved, region, this.clientConfig.baseURL),
128
+ )
129
+ })().catch((error: unknown) => {
130
+ // Don't cache a rejected promise — clear it so a later call can retry
131
+ // (e.g. after a transient import failure or fixed auth config).
132
+ this.clientPromise = undefined
133
+ throw error
134
+ })
135
+ }
136
+ return this.clientPromise
137
+ }
138
+
139
+ /**
140
+ * Map resolved auth + endpoint to a `BedrockRuntimeClientConfig`.
141
+ *
142
+ * Recent `@aws-sdk/client-bedrock-runtime` exposes a first-class `token`
143
+ * config field for Bedrock API-key bearer auth. But the client's default
144
+ * auth-scheme order is SigV4 first, then bearer — so passing `token` alone is
145
+ * not enough: the SDK still resolves SigV4 and throws "Could not load
146
+ * credentials from any providers". Pinning `authSchemePreference` to the
147
+ * bearer scheme makes the API key actually get used. SigV4 uses the AWS
148
+ * credential provider chain and the default scheme order.
149
+ */
150
+ protected buildClientConfig(
151
+ resolved: ResolvedBedrockAuth,
152
+ region: string,
153
+ endpoint: string | undefined,
154
+ ): BedrockRuntimeClientConfig {
155
+ if (resolved.kind === 'bearer') {
156
+ return {
157
+ region,
158
+ token: { token: resolved.token },
159
+ authSchemePreference: ['httpBearerAuth'],
160
+ ...(endpoint ? { endpoint } : {}),
161
+ }
162
+ }
163
+ return {
164
+ region: resolved.region,
165
+ credentials: resolved.credentials,
166
+ ...(endpoint ? { endpoint } : {}),
167
+ }
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // SDK seams (overridden in tests so no real AWS call happens)
172
+ // ---------------------------------------------------------------------------
173
+
174
+ protected async sendStream(
175
+ input: ConverseStreamCommandInput,
176
+ ): Promise<AsyncIterable<ConverseStreamOutput>> {
177
+ const { ConverseStreamCommand } = await this.importBedrockRuntime()
178
+ const client = await this.getClient()
179
+ const res = await client.send(new ConverseStreamCommand(input))
180
+ if (!res.stream) {
181
+ throw new Error('Bedrock Converse: empty stream response')
182
+ }
183
+ return res.stream
184
+ }
185
+
186
+ protected async send(
187
+ input: ConverseCommandInput,
188
+ ): Promise<ConverseCommandOutput> {
189
+ const { ConverseCommand } = await this.importBedrockRuntime()
190
+ const client = await this.getClient()
191
+ return client.send(new ConverseCommand(input))
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Public adapter surface
196
+ // ---------------------------------------------------------------------------
197
+
198
+ async *chatStream(
199
+ options: TextOptions<TProviderOptions>,
200
+ ): AsyncIterable<StreamChunk> {
201
+ try {
202
+ options.logger.request(
203
+ `activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,
204
+ { provider: this.name, model: this.model },
205
+ )
206
+ const input = this.buildInput(options)
207
+ const stream = await this.sendStream(input)
208
+ yield* processConverseStream(stream, () => this.generateId(), {
209
+ threadId: options.threadId,
210
+ parentRunId: options.parentRunId,
211
+ model: options.model,
212
+ })
213
+ } catch (error: unknown) {
214
+ const errorPayload = toRunErrorPayload(
215
+ error,
216
+ `${this.name}.chatStream failed`,
217
+ )
218
+ options.logger.errors(`${this.name}.chatStream fatal`, {
219
+ error: errorPayload,
220
+ source: `${this.name}.chatStream`,
221
+ })
222
+ // Conditional `code` spread keeps the wire shape spec-compliant under
223
+ // `exactOptionalPropertyTypes` (AG-UI's `RunErrorEvent.code` is optional).
224
+ yield {
225
+ type: EventType.RUN_ERROR,
226
+ model: options.model,
227
+ timestamp: Date.now(),
228
+ message: errorPayload.message,
229
+ ...(errorPayload.code !== undefined && { code: errorPayload.code }),
230
+ error: {
231
+ message: errorPayload.message,
232
+ ...(errorPayload.code !== undefined && { code: errorPayload.code }),
233
+ },
234
+ }
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Structured output via the forced-tool strategy. Converse has no native
240
+ * json_schema response_format, so we force a single tool whose input schema
241
+ * is the requested output schema and read the model's `toolUse.input` back as
242
+ * the structured result.
243
+ */
244
+ async structuredOutput(
245
+ options: StructuredOutputOptions<TProviderOptions>,
246
+ ): Promise<StructuredOutputResult<unknown>> {
247
+ const { chatOptions, outputSchema } = options
248
+ try {
249
+ chatOptions.logger.request(
250
+ `activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,
251
+ { provider: this.name, model: this.model },
252
+ )
253
+ const input: ConverseCommandInput = {
254
+ ...this.buildInput(chatOptions),
255
+ toolConfig: buildStructuredToolConfig(outputSchema),
256
+ }
257
+ const res = await this.send(input)
258
+ const structured = extractStructuredToolInput(res)
259
+ if (structured === undefined) {
260
+ throw new Error(
261
+ `${this.name}.structuredOutput: response contained no forced-tool output`,
262
+ )
263
+ }
264
+ return {
265
+ data: structured,
266
+ rawText: JSON.stringify(structured),
267
+ }
268
+ } catch (error: unknown) {
269
+ chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, {
270
+ error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`),
271
+ source: `${this.name}.structuredOutput`,
272
+ })
273
+ throw error
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Streaming structured output. Same forced-tool strategy as
279
+ * `structuredOutput`, but streamed: the forced tool's `toolUse.input` JSON
280
+ * fragments are accumulated from the Converse stream and a terminal
281
+ * `CUSTOM 'structured-output.complete'` event carries `{ object, raw }`,
282
+ * mirroring openai-base's `structuredOutputStream` contract exactly.
283
+ */
284
+ async *structuredOutputStream(
285
+ options: StructuredOutputOptions<TProviderOptions>,
286
+ ): AsyncIterable<StreamChunk> {
287
+ const { chatOptions, outputSchema } = options
288
+ const timestamp = Date.now()
289
+ const runId = this.generateId()
290
+ const threadId = chatOptions.threadId ?? this.generateId()
291
+ const messageId = this.generateId()
292
+
293
+ let hasEmittedRunStarted = false
294
+ let hasEmittedTextMessageStart = false
295
+ let accumulatedRaw = ''
296
+ let finishReason: 'stop' | 'length' | 'content_filter' = 'stop'
297
+
298
+ try {
299
+ chatOptions.logger.request(
300
+ `activity=structuredOutputStream provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,
301
+ { provider: this.name, model: this.model },
302
+ )
303
+ const input: ConverseStreamCommandInput = {
304
+ ...this.buildInput(chatOptions),
305
+ toolConfig: buildStructuredToolConfig(outputSchema),
306
+ }
307
+ const stream = await this.sendStream(input)
308
+
309
+ // The forced tool streams its `input` as partial-JSON fragments inside
310
+ // `contentBlockDelta.delta.toolUse.input`. We surface them as
311
+ // TEXT_MESSAGE_CONTENT deltas (raw JSON text), matching openai-base which
312
+ // carries the structured JSON as text deltas.
313
+ for await (const ev of stream) {
314
+ if (!hasEmittedRunStarted) {
315
+ hasEmittedRunStarted = true
316
+ yield {
317
+ type: EventType.RUN_STARTED,
318
+ runId,
319
+ threadId,
320
+ model: chatOptions.model,
321
+ timestamp,
322
+ parentRunId: chatOptions.parentRunId,
323
+ }
324
+ }
325
+
326
+ // Surface in-band server/throttle/validation errors instead of
327
+ // letting them fall through and masquerade as an empty response.
328
+ throwIfConverseStreamError(ev)
329
+
330
+ if ('contentBlockDelta' in ev) {
331
+ const delta = ev.contentBlockDelta?.delta
332
+ const fragment =
333
+ delta && 'toolUse' in delta ? delta.toolUse?.input : undefined
334
+ if (fragment !== undefined) {
335
+ if (!hasEmittedTextMessageStart) {
336
+ hasEmittedTextMessageStart = true
337
+ yield {
338
+ type: EventType.TEXT_MESSAGE_START,
339
+ messageId,
340
+ role: 'assistant',
341
+ model: chatOptions.model,
342
+ timestamp,
343
+ }
344
+ }
345
+ accumulatedRaw += fragment
346
+ yield {
347
+ type: EventType.TEXT_MESSAGE_CONTENT,
348
+ messageId,
349
+ delta: fragment,
350
+ content: accumulatedRaw,
351
+ model: chatOptions.model,
352
+ timestamp,
353
+ }
354
+ }
355
+ continue
356
+ }
357
+
358
+ if ('messageStop' in ev) {
359
+ const stopReason = ev.messageStop?.stopReason
360
+ // The forced structured-output tool produces stopReason 'tool_use' on
361
+ // success, but that's an implementation detail — a cleanly-completed
362
+ // structured run reports 'stop', matching openai-base's contract.
363
+ finishReason =
364
+ stopReason === 'max_tokens'
365
+ ? 'length'
366
+ : stopReason === 'content_filtered'
367
+ ? 'content_filter'
368
+ : 'stop'
369
+ continue
370
+ }
371
+ }
372
+
373
+ if (!hasEmittedRunStarted) {
374
+ hasEmittedRunStarted = true
375
+ yield {
376
+ type: EventType.RUN_STARTED,
377
+ runId,
378
+ threadId,
379
+ model: chatOptions.model,
380
+ timestamp,
381
+ parentRunId: chatOptions.parentRunId,
382
+ }
383
+ }
384
+
385
+ if (hasEmittedTextMessageStart) {
386
+ yield {
387
+ type: EventType.TEXT_MESSAGE_END,
388
+ messageId,
389
+ model: chatOptions.model,
390
+ timestamp,
391
+ }
392
+ }
393
+
394
+ if (accumulatedRaw.length === 0) {
395
+ yield {
396
+ type: EventType.RUN_ERROR,
397
+ runId,
398
+ model: chatOptions.model,
399
+ timestamp,
400
+ message: `${this.name}.structuredOutputStream: response contained no content`,
401
+ code: 'empty-response',
402
+ error: {
403
+ message: `${this.name}.structuredOutputStream: response contained no content`,
404
+ code: 'empty-response',
405
+ },
406
+ }
407
+ return
408
+ }
409
+
410
+ let parsed: unknown
411
+ try {
412
+ parsed = JSON.parse(accumulatedRaw)
413
+ } catch {
414
+ yield {
415
+ type: EventType.RUN_ERROR,
416
+ runId,
417
+ model: chatOptions.model,
418
+ timestamp,
419
+ message: `Failed to parse structured output as JSON. Content: ${accumulatedRaw.slice(0, 200)}${accumulatedRaw.length > 200 ? '...' : ''}`,
420
+ code: 'parse-error',
421
+ error: {
422
+ message: 'Failed to parse structured output as JSON',
423
+ code: 'parse-error',
424
+ },
425
+ }
426
+ return
427
+ }
428
+
429
+ yield {
430
+ type: EventType.CUSTOM,
431
+ name: 'structured-output.complete',
432
+ value: {
433
+ object: parsed,
434
+ raw: accumulatedRaw,
435
+ },
436
+ model: chatOptions.model,
437
+ timestamp,
438
+ }
439
+
440
+ yield {
441
+ type: EventType.RUN_FINISHED,
442
+ runId,
443
+ threadId,
444
+ model: chatOptions.model,
445
+ timestamp,
446
+ finishReason,
447
+ }
448
+ } catch (error: unknown) {
449
+ if (!hasEmittedRunStarted) {
450
+ hasEmittedRunStarted = true
451
+ yield {
452
+ type: EventType.RUN_STARTED,
453
+ runId,
454
+ threadId,
455
+ model: chatOptions.model,
456
+ timestamp,
457
+ parentRunId: chatOptions.parentRunId,
458
+ }
459
+ }
460
+ const errorPayload = toRunErrorPayload(
461
+ error,
462
+ `${this.name}.structuredOutputStream failed`,
463
+ )
464
+ chatOptions.logger.errors(`${this.name}.structuredOutputStream fatal`, {
465
+ error: errorPayload,
466
+ source: `${this.name}.structuredOutputStream`,
467
+ })
468
+ yield {
469
+ type: EventType.RUN_ERROR,
470
+ runId,
471
+ model: chatOptions.model,
472
+ timestamp,
473
+ message: errorPayload.message,
474
+ ...(errorPayload.code !== undefined && { code: errorPayload.code }),
475
+ error: {
476
+ message: errorPayload.message,
477
+ ...(errorPayload.code !== undefined && { code: errorPayload.code }),
478
+ },
479
+ }
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Converse sends `tools` and a forced structured-output tool via two separate
485
+ * mechanisms, never together. Declaring `false` makes the engine run the
486
+ * agent loop without `outputSchema` and finalize via `structuredOutput` /
487
+ * `structuredOutputStream`.
488
+ */
489
+ supportsCombinedToolsAndSchema(): boolean {
490
+ return false
491
+ }
492
+
493
+ // ---------------------------------------------------------------------------
494
+ // Request construction
495
+ // ---------------------------------------------------------------------------
496
+
497
+ /**
498
+ * Translate `TextOptions` into a `ConverseCommandInput`. Shared by chatStream,
499
+ * structuredOutput, and structuredOutputStream (the latter two override
500
+ * `toolConfig` with the forced structured tool afterwards).
501
+ */
502
+ protected buildInput(
503
+ options: TextOptions<TProviderOptions>,
504
+ ): ConverseCommandInput {
505
+ const { system, messages } = toConverseMessages(
506
+ options.messages,
507
+ options.systemPrompts,
508
+ )
509
+
510
+ const toolConfig = options.tools
511
+ ? toToolConfig(convertTools(options.tools), 'auto')
512
+ : undefined
513
+
514
+ // Sampling options live on `modelOptions` (typed as the narrowed
515
+ // `BedrockConverseProviderOptions`, which surfaces the OpenAI Chat
516
+ // Completions field names); translate them into Converse's `inferenceConfig`,
517
+ // which uses AWS-native camelCase keys.
518
+ const modelOptions = options.modelOptions
519
+ const temperature = modelOptions?.temperature
520
+ const topP = modelOptions?.top_p
521
+ const maxTokens = modelOptions?.max_completion_tokens
522
+ const stop = modelOptions?.stop
523
+ const stopSequences =
524
+ stop == null ? undefined : Array.isArray(stop) ? stop : [stop]
525
+
526
+ const inferenceConfig =
527
+ temperature != null ||
528
+ topP != null ||
529
+ maxTokens != null ||
530
+ stopSequences != null
531
+ ? {
532
+ ...(temperature != null && { temperature }),
533
+ ...(topP != null && { topP }),
534
+ ...(maxTokens != null && { maxTokens }),
535
+ ...(stopSequences != null && { stopSequences }),
536
+ }
537
+ : undefined
538
+
539
+ return {
540
+ modelId: this.model,
541
+ messages,
542
+ ...(system.length > 0 && { system }),
543
+ ...(toolConfig && { toolConfig }),
544
+ ...(inferenceConfig && { inferenceConfig }),
545
+ }
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Convert TanStack `Tool[]` to the Converse tool-converter input shape. Reuses
551
+ * the SAME `convertSchemaToJsonSchema` the other adapters use so the Converse
552
+ * tool input schemas match what every other provider sends.
553
+ */
554
+ function convertTools(tools: Array<Tool>): Array<ConverseToolInput> {
555
+ return tools.map((tool) => {
556
+ const inputSchema: JSONSchema = convertSchemaToJsonSchema(
557
+ tool.inputSchema,
558
+ ) ?? { type: 'object', properties: {}, required: [] }
559
+ return {
560
+ name: tool.name,
561
+ description: tool.description,
562
+ inputSchema,
563
+ }
564
+ })
565
+ }
566
+
567
+ /**
568
+ * Find the forced structured-output tool's `input` in a non-streaming Converse
569
+ * response. SDK-boundary narrowing only — `ConverseOutput` is a tagged union
570
+ * (`{ message }`) and a tool-use block is `{ toolUse: { input } }`.
571
+ */
572
+ function extractStructuredToolInput(
573
+ res: ConverseCommandOutput,
574
+ ): unknown | undefined {
575
+ const message =
576
+ res.output && 'message' in res.output ? res.output.message : undefined
577
+ const content: Array<ContentBlock> = message?.content ?? []
578
+ for (const block of content) {
579
+ if ('toolUse' in block && block.toolUse) {
580
+ // Only accept the forced structured tool (an unnamed block is allowed,
581
+ // since the forced tool is the only one configured). A differently-named
582
+ // tool-use block is a hallucinated/leftover call whose arbitrary input
583
+ // must not be returned as the validated result — leave it to the caller's
584
+ // `throw` so the failure is accurate instead of silently wrong.
585
+ if (
586
+ block.toolUse.name === STRUCTURED_TOOL_NAME ||
587
+ block.toolUse.name === undefined
588
+ ) {
589
+ return block.toolUse.input
590
+ }
591
+ }
592
+ }
593
+ return undefined
594
+ }
595
+
596
+ /** Converse adapter with an explicit API key (low-level; mirrors createBedrockChat). */
597
+ export function createBedrockConverse<TModel extends BedrockConverseModels>(
598
+ model: TModel,
599
+ apiKey: string,
600
+ config?: Omit<BedrockConverseConfig, 'apiKey'>,
601
+ ): BedrockConverseTextAdapter<TModel> {
602
+ return new BedrockConverseTextAdapter({ ...config, apiKey }, model)
603
+ }
@@ -0,0 +1,73 @@
1
+ import OpenAI from 'openai'
2
+ import { OpenAIBaseResponsesTextAdapter } from '@tanstack/openai-base'
3
+ import { withBedrockDefaults } from '../utils/client'
4
+ import type { Modality } from '@tanstack/ai'
5
+ import type { BedrockClientConfig } from '../utils/client'
6
+ import type { BedrockMessageMetadataByModality } from '../message-types'
7
+ import type {
8
+ BedrockChatModelToolCapabilitiesByName,
9
+ BedrockResponsesModels,
10
+ ResolveInputModalities,
11
+ } from '../model-meta'
12
+ import type { ExternalResponsesProviderOptions } from '../text/responses-provider-options'
13
+
14
+ export interface BedrockResponsesConfig extends BedrockClientConfig {}
15
+
16
+ export type { ExternalResponsesProviderOptions as BedrockResponsesProviderOptions } from '../text/responses-provider-options'
17
+
18
+ type ResolveToolCapabilities<TModel extends string> =
19
+ TModel extends keyof BedrockChatModelToolCapabilitiesByName
20
+ ? NonNullable<BedrockChatModelToolCapabilitiesByName[TModel]>
21
+ : readonly []
22
+
23
+ /**
24
+ * Bedrock Responses adapter. Drives mantle's OpenAI-compatible `/responses`
25
+ * endpoint via the OpenAI SDK (`client.responses.create`) — the same base
26
+ * class ai-openai's `openaiText` uses. Responses is mantle-only, so the
27
+ * constructor forces the mantle baseURL.
28
+ */
29
+ export class BedrockResponsesTextAdapter<
30
+ TModel extends BedrockResponsesModels,
31
+ // Constraint mirrors the chat adapter (and ai-groq / ai-openai) and the base,
32
+ // which parameterises `TProviderOptions extends Record<string, any>`. Our
33
+ // default `ExternalResponsesProviderOptions` is an interface that (lacking an
34
+ // implicit index signature) `Record<string, unknown>` would reject but
35
+ // `Record<string, any>` accepts. This `any` is confined to the generic
36
+ // constraint — no value/shape `as` cast is introduced.
37
+ TProviderOptions extends Record<string, any> =
38
+ ExternalResponsesProviderOptions,
39
+ TInputModalities extends ReadonlyArray<Modality> =
40
+ ResolveInputModalities<TModel>,
41
+ TToolCapabilities extends ReadonlyArray<string> =
42
+ ResolveToolCapabilities<TModel>,
43
+ > extends OpenAIBaseResponsesTextAdapter<
44
+ TModel,
45
+ TProviderOptions,
46
+ TInputModalities,
47
+ BedrockMessageMetadataByModality,
48
+ TToolCapabilities
49
+ > {
50
+ override readonly kind = 'text' as const
51
+ override readonly name = 'bedrock-responses' as const
52
+
53
+ constructor(config: BedrockResponsesConfig, model: TModel) {
54
+ // Responses is mantle-only — force the mantle base URL (an explicit
55
+ // config.baseURL still wins, e.g. E2E pointing at aimock).
56
+ super(
57
+ model,
58
+ 'bedrock-responses',
59
+ new OpenAI(withBedrockDefaults(config, 'mantle')),
60
+ )
61
+ }
62
+ }
63
+
64
+ /** Responses adapter with an explicit API key (low-level; the public branching factory delegates here). */
65
+ export function createBedrockResponsesText<
66
+ TModel extends BedrockResponsesModels,
67
+ >(
68
+ model: TModel,
69
+ apiKey: string,
70
+ config?: Omit<BedrockResponsesConfig, 'apiKey'>,
71
+ ): BedrockResponsesTextAdapter<TModel> {
72
+ return new BedrockResponsesTextAdapter({ ...config, apiKey }, model)
73
+ }