From 19bb9b92014154d010b7638c26eef570e3794389 Mon Sep 17 00:00:00 2001 From: Muralidhar Reddy Challa Date: Mon, 12 Jan 2026 12:41:44 +0530 Subject: [PATCH 1/3] feat(ai-bedrock): add AWS Bedrock adapter with ConverseStream and tool-calling support - Implemented BedrockChatAdapter using the ConverseStream API. - Added support for text, image, video, and document modalities. - Implemented thinking tag parsing for Claude and Nova models. - Added comprehensive model metadata and type-safe modality mapping. - Added unit tests for model metadata and Converse API integration. --- .changeset/bedrock-adapter.md | 5 + knip.json | 33 +- .../ai-bedrock/live-tests/README.md | 36 + .../ai-bedrock/live-tests/package.json | 15 + .../ai-bedrock/live-tests/tool-test-haiku.ts | 86 ++ .../ai-bedrock/live-tests/tool-test-nova.ts | 98 ++ .../ai-bedrock/live-tests/tool-test.ts | 98 ++ packages/typescript/ai-bedrock/package.json | 57 + .../ai-bedrock/src/adapters/index.ts | 2 + .../ai-bedrock/src/adapters/text.ts | 419 +++++++ .../typescript/ai-bedrock/src/bedrock-chat.ts | 25 + packages/typescript/ai-bedrock/src/index.ts | 31 + .../ai-bedrock/src/message-types.ts | 70 ++ .../typescript/ai-bedrock/src/model-meta.ts | 99 ++ .../src/text/text-provider-options.ts | 64 + packages/typescript/ai-bedrock/src/utils.ts | 38 + .../ai-bedrock/tests/bedrock-adapter.test.ts | 84 ++ .../ai-bedrock/tests/model-meta.test.ts | 62 + packages/typescript/ai-bedrock/tsconfig.json | 10 + packages/typescript/ai-bedrock/vite.config.ts | 36 + pnpm-lock.yaml | 1117 ++++++++++++++++- 21 files changed, 2467 insertions(+), 18 deletions(-) create mode 100644 .changeset/bedrock-adapter.md create mode 100644 packages/typescript/ai-bedrock/live-tests/README.md create mode 100644 packages/typescript/ai-bedrock/live-tests/package.json create mode 100644 packages/typescript/ai-bedrock/live-tests/tool-test-haiku.ts create mode 100644 packages/typescript/ai-bedrock/live-tests/tool-test-nova.ts create mode 100644 packages/typescript/ai-bedrock/live-tests/tool-test.ts create mode 100644 packages/typescript/ai-bedrock/package.json create mode 100644 packages/typescript/ai-bedrock/src/adapters/index.ts create mode 100644 packages/typescript/ai-bedrock/src/adapters/text.ts create mode 100644 packages/typescript/ai-bedrock/src/bedrock-chat.ts create mode 100644 packages/typescript/ai-bedrock/src/index.ts create mode 100644 packages/typescript/ai-bedrock/src/message-types.ts create mode 100644 packages/typescript/ai-bedrock/src/model-meta.ts create mode 100644 packages/typescript/ai-bedrock/src/text/text-provider-options.ts create mode 100644 packages/typescript/ai-bedrock/src/utils.ts create mode 100644 packages/typescript/ai-bedrock/tests/bedrock-adapter.test.ts create mode 100644 packages/typescript/ai-bedrock/tests/model-meta.test.ts create mode 100644 packages/typescript/ai-bedrock/tsconfig.json create mode 100644 packages/typescript/ai-bedrock/vite.config.ts diff --git a/.changeset/bedrock-adapter.md b/.changeset/bedrock-adapter.md new file mode 100644 index 000000000..7603b95b8 --- /dev/null +++ b/.changeset/bedrock-adapter.md @@ -0,0 +1,5 @@ +--- +"@tanstack/ai-bedrock": minor +--- + +Add Amazon Bedrock adapter. diff --git a/knip.json b/knip.json index 26f314b66..9750b4b52 100644 --- a/knip.json +++ b/knip.json @@ -1,8 +1,15 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "ignoreDependencies": ["@faker-js/faker"], - "ignoreWorkspaces": ["examples/**", "testing/**", "**/smoke-tests/**"], + "ignoreDependencies": [ + "@faker-js/faker" + ], + "ignoreWorkspaces": [ + "examples/**", + "testing/**", + "**/smoke-tests/**" + ], "ignore": [ + "packages/typescript/ai-bedrock/live-tests/**", "packages/typescript/ai-openai/live-tests/**", "packages/typescript/ai-openai/src/**/*.test.ts", "packages/typescript/ai-openai/src/audio/audio-provider-options.ts", @@ -19,19 +26,29 @@ "ignore": [] }, "packages/typescript/ai-anthropic": { - "ignore": ["src/tools/**"] + "ignore": [ + "src/tools/**" + ] }, "packages/typescript/ai-gemini": { - "ignore": ["src/tools/**"] + "ignore": [ + "src/tools/**" + ] }, "packages/typescript/ai-openai": { - "ignore": ["src/tools/**"] + "ignore": [ + "src/tools/**" + ] }, "packages/typescript/ai-react-ui": { - "ignoreDependencies": ["react-dom"] + "ignoreDependencies": [ + "react-dom" + ] }, "packages/typescript/ai-vue-ui": { - "ignore": ["src/use-chat-context.ts"] + "ignore": [ + "src/use-chat-context.ts" + ] } } -} +} \ No newline at end of file diff --git a/packages/typescript/ai-bedrock/live-tests/README.md b/packages/typescript/ai-bedrock/live-tests/README.md new file mode 100644 index 000000000..5550ee064 --- /dev/null +++ b/packages/typescript/ai-bedrock/live-tests/README.md @@ -0,0 +1,36 @@ +# Bedrock Live Tests + +These tests verify that the Bedrock adapter correctly handles tool calling and multimodal inputs with various models (Nova, Claude). + +## Setup + +1. Create a `.env.local` file in this directory with your AWS credentials: + + ``` + AWS_ACCESS_KEY_ID=... + AWS_SECRET_ACCESS_KEY=... + AWS_REGION=us-east-1 + ``` + +2. Install dependencies: + ```bash + pnpm install + ``` + +## Tests + +### `tool-test.ts` +Tests basic tool calling with Claude 3.5 Sonnet. + +### `tool-test-nova.ts` +Tests Amazon Nova Pro with multimodal inputs (if applicable) and tool calling. + +## Running Tests + +```bash +# Run Claude tool test +pnpm test + +# Run Nova tool test +pnpm test:nova +``` diff --git a/packages/typescript/ai-bedrock/live-tests/package.json b/packages/typescript/ai-bedrock/live-tests/package.json new file mode 100644 index 000000000..d7a5b0973 --- /dev/null +++ b/packages/typescript/ai-bedrock/live-tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "@tanstack/ai-bedrock-live-tests", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "test": "tsx tool-test.ts", + "test:nova": "tsx tool-test-nova.ts" + }, + "devDependencies": { + "tsx": "^4.7.1", + "typescript": "^5.4.2", + "zod": "^4.2.1" + } +} \ No newline at end of file diff --git a/packages/typescript/ai-bedrock/live-tests/tool-test-haiku.ts b/packages/typescript/ai-bedrock/live-tests/tool-test-haiku.ts new file mode 100644 index 000000000..537a57d34 --- /dev/null +++ b/packages/typescript/ai-bedrock/live-tests/tool-test-haiku.ts @@ -0,0 +1,86 @@ +// import 'dotenv/config' +import { bedrockText } from '../src/bedrock-chat' +import { z } from 'zod' +import { chat } from '@tanstack/ai' + +function throwMissingEnv(name: string): never { + throw new Error(`Missing required environment variable: ${name}`) +} + +async function main() { + const accessKeyId = process.env.AWS_ACCESS_KEY_ID ?? throwMissingEnv('AWS_ACCESS_KEY_ID') + const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY ?? throwMissingEnv('AWS_SECRET_ACCESS_KEY') + + const modelId = 'anthropic.claude-4-5-haiku-20251001-v1:0' + console.log(`Running tool test for: ${modelId}`) + + const stream = await chat({ + adapter: bedrockText(modelId, { + region: process.env.AWS_REGION || 'us-east-1', + credentials: { + accessKeyId, + secretAccessKey, + }, + }), + modelOptions: { + thinking: { + type: 'enabled', + budget_tokens: 1024 + } + }, + messages: [ + { + role: 'user', + content: 'Use the `get_weather` tool to find the weather in New York and explain it.', + }, + ], + tools: [ + { + name: 'get_weather', + description: 'Get the current weather in a location', + inputSchema: z.object({ + location: z.string().describe('The city and state, e.g. New York, NY'), + }), + execute: async ({ location }) => { + console.log(`\n[TOOL Weather] Fetching weather for ${location}...`) + return { + temperature: 45, + unit: 'F', + condition: 'Cloudy', + } + }, + }, + ], + stream: true + }) + + let finalContent = '' + let hasThinking = false + let toolCallCount = 0 + + console.log('--- Stream Output ---') + for await (const chunk of stream) { + if (chunk.type === 'thinking') { + hasThinking = true + } else if (chunk.type === 'content') { + process.stdout.write(chunk.delta) + finalContent += chunk.delta + } else if (chunk.type === 'tool_call') { + toolCallCount++ + } + } + + console.log('--- Results ---') + console.log('Thinking:', hasThinking) + console.log('Tool calls:', toolCallCount) + console.log('Content length:', finalContent.length) + + if (!finalContent || finalContent.trim().length === 0) { + console.error('Test failed: No final content') + process.exit(1) + } + + console.log('Test passed') +} + +main().catch(console.error) diff --git a/packages/typescript/ai-bedrock/live-tests/tool-test-nova.ts b/packages/typescript/ai-bedrock/live-tests/tool-test-nova.ts new file mode 100644 index 000000000..b335991ad --- /dev/null +++ b/packages/typescript/ai-bedrock/live-tests/tool-test-nova.ts @@ -0,0 +1,98 @@ +import { bedrockText } from '../src/index' +import { z } from 'zod' +import { readFileSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' +import { chat } from '@tanstack/ai' + +// Load environment variables from .env.local manually +const __dirname = dirname(fileURLToPath(import.meta.url)) +try { + const envContent = readFileSync(join(__dirname, '.env.local'), 'utf-8') + envContent.split('\n').forEach((line) => { + const match = line.match(/^([^=]+)=(.*)$/) + if (match) { + process.env[match[1].trim()] = match[2].trim() + } + }) +} catch (e) { + // .env.local not found +} + +async function testBedrockNovaToolCalling() { + console.log('Testing Bedrock tool calling (Amazon Nova Pro)\n') + + const stream = await chat({ + adapter: bedrockText('us.amazon.nova-pro-v1:0', { + region: process.env.AWS_REGION || 'us-west-2', + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '', + } + }), + messages: [ + { + role: 'user', + content: 'Use the `get_temperature` tool to find the temperature in New York and explain why it is the way it is.', + }, + ], + tools: [ + { + name: 'get_temperature', + description: 'Get the current temperature for a specific location', + inputSchema: z.object({ + location: z.string().describe('The city or location'), + unit: z.enum(['celsius', 'fahrenheit']).describe('The temperature unit'), + }), + execute: async ({ location, unit }: { location: string; unit: string }) => { + console.log(`\n[TOOL Temperature] Fetching for ${location}...`) + return { + temperature: 45, + unit: unit, + condition: 'Cloudy', + } + }, + }, + ], + stream: true, + }) + + let finalContent = '' + let hasThinking = false + let toolCallCount = 0 + + console.log('--- Stream Output ---') + for await (const chunk of stream) { + if (chunk.type === 'thinking') { + hasThinking = true + } else if (chunk.type === 'content') { + process.stdout.write(chunk.delta) + finalContent += chunk.delta + } else if (chunk.type === 'tool_call') { + toolCallCount++ + } + } + + console.log('--- Test Results ---') + console.log('Thinking detected:', hasThinking) + console.log('Tool calls:', toolCallCount) + console.log('Final content length:', finalContent.length) + + if (!hasThinking) { + console.warn('Warning: No thinking blocks detected') + } + + if (finalContent.includes('')) { + console.error('Test failed: Thinking tags found in final content') + process.exit(1) + } + + if (!finalContent || finalContent.trim().length === 0) { + console.error('Test failed: No final content - model should explain the temperature') + process.exit(1) + } + + console.log('Test passed') +} + +testBedrockNovaToolCalling().catch(console.error) diff --git a/packages/typescript/ai-bedrock/live-tests/tool-test.ts b/packages/typescript/ai-bedrock/live-tests/tool-test.ts new file mode 100644 index 000000000..292f2154c --- /dev/null +++ b/packages/typescript/ai-bedrock/live-tests/tool-test.ts @@ -0,0 +1,98 @@ +// import 'dotenv/config' +import { bedrockText } from '../src/bedrock-chat' +import { z } from 'zod' +import { chat } from '@tanstack/ai' + +async function main() { + const modelId = 'us.anthropic.claude-sonnet-4-5-20250929-v1:0' + console.log(`Running tool test for: ${modelId}`) + + const stream = await chat({ + adapter: bedrockText(modelId, { + region: process.env.AWS_REGION || 'us-east-1', + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, + }, + }), + modelOptions: { + thinking: { + type: 'enabled', + budget_tokens: 2048 + } + }, + messages: [ + { + role: 'user', + content: 'Use the `get_weather` tool to find the weather in San Francisco and then explain why it is the way it is.', + }, + ], + tools: [ + { + name: 'get_weather', + description: 'Get the current weather in a location', + inputSchema: z.object({ + location: z.string().describe('The city and state, e.g. San Francisco, CA'), + }), + execute: async ({ location }) => { + console.log(`\n[TOOL Weather] Fetching weather for ${location}...`) + return { + temperature: 72, + unit: 'F', + condition: 'Sunny', + } + }, + }, + ], + stream: true + }) + + let finalContent = '' + let hasThinking = false + let toolCallCount = 0 + let doneCount = 0 + + console.log('--- Stream Output ---') + for await (const chunk of stream) { + if (chunk.type === 'thinking') { + hasThinking = true + } else if (chunk.type === 'content') { + process.stdout.write(chunk.delta) + finalContent += chunk.delta + } else if (chunk.type === 'tool_call') { + toolCallCount++ + console.log('\nTool call:', chunk.toolCall.function.name) + } else if (chunk.type === 'done') { + doneCount++ + } + } + + console.log('--- Test Results ---') + console.log('Thinking detected:', hasThinking) + console.log('Tool calls:', toolCallCount) + console.log('Done events:', doneCount) + console.log('Final content length:', finalContent.length) + + if (!hasThinking) { + console.error('Test failed: No thinking blocks detected for Claude 4.5') + process.exit(1) + } + + if (toolCallCount === 0) { + console.error('Test failed: No tool calls detected') + process.exit(1) + } + + if (!finalContent || finalContent.trim().length === 0) { + console.error('Test failed: Final content is empty - model should explain weather after getting tool results') + process.exit(1) + } + + if (!finalContent.toLowerCase().includes('72') && !finalContent.toLowerCase().includes('sunny')) { + console.warn('Warning: Final content does not mention the weather data') + } + + console.log('Test passed') +} + +main().catch(console.error) diff --git a/packages/typescript/ai-bedrock/package.json b/packages/typescript/ai-bedrock/package.json new file mode 100644 index 000000000..650640358 --- /dev/null +++ b/packages/typescript/ai-bedrock/package.json @@ -0,0 +1,57 @@ +{ + "name": "@tanstack/ai-bedrock", + "version": "0.0.1", + "description": "AWS Bedrock adapter for TanStack AI", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/ai.git", + "directory": "packages/typescript/ai-bedrock" + }, + "type": "module", + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", + "exports": { + ".": { + "types": "./dist/esm/index.d.ts", + "import": "./dist/esm/index.js" + }, + "./adapters": { + "types": "./dist/esm/adapters/index.d.ts", + "import": "./dist/esm/adapters/index.js" + } + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "vite build", + "clean": "premove ./build ./dist", + "lint:fix": "eslint ./src --fix", + "test:build": "publint --strict", + "test:eslint": "eslint ./src", + "test:lib": "vitest run", + "test:lib:dev": "pnpm test:lib --watch", + "test:types": "tsc" + }, + "keywords": [ + "ai", + "bedrock", + "aws", + "tanstack", + "adapter" + ], + "dependencies": { + "@aws-sdk/client-bedrock-runtime": "^3.723.0" + }, + "peerDependencies": { + "@tanstack/ai": "workspace:^", + "zod": "^4.0.0" + }, + "devDependencies": { + "@tanstack/ai": "workspace:*", + "@vitest/coverage-v8": "4.0.14", + "vite": "^7.2.7" + } +} \ No newline at end of file diff --git a/packages/typescript/ai-bedrock/src/adapters/index.ts b/packages/typescript/ai-bedrock/src/adapters/index.ts new file mode 100644 index 000000000..8675f95eb --- /dev/null +++ b/packages/typescript/ai-bedrock/src/adapters/index.ts @@ -0,0 +1,2 @@ +export { BedrockTextAdapter } from './text' +export type { BedrockTextConfig } from './text' diff --git a/packages/typescript/ai-bedrock/src/adapters/text.ts b/packages/typescript/ai-bedrock/src/adapters/text.ts new file mode 100644 index 000000000..01d7df4ae --- /dev/null +++ b/packages/typescript/ai-bedrock/src/adapters/text.ts @@ -0,0 +1,419 @@ +import { + BedrockRuntimeClient, + + ConverseStreamCommand + + + +} from '@aws-sdk/client-bedrock-runtime' +import { + BaseTextAdapter +} from '@tanstack/ai/adapters' +import { + + isClaude, + isNova +} from '../model-meta' +import type { BedrockModelId } from '../model-meta'; +import type { ContentBlock, Message, ToolResultBlock, ToolUseBlock } from '@aws-sdk/client-bedrock-runtime'; +import type { + StructuredOutputOptions, + StructuredOutputResult +} from '@tanstack/ai/adapters'; +import type { + DefaultMessageMetadataByModality, + ModelMessage, + StreamChunk, + TextOptions, +} from '@tanstack/ai' +import type { BedrockTextProviderOptions } from '../text/text-provider-options' + +export interface BedrockTextConfig { + region: string + credentials: { + accessKeyId: string + secretAccessKey: string + } +} + +export type BedrockInputModalities = readonly ['text', 'image', 'video', 'document'] + +export class BedrockTextAdapter< + TModel extends BedrockModelId = BedrockModelId, +> extends BaseTextAdapter< + TModel, + BedrockTextProviderOptions, + BedrockInputModalities, + DefaultMessageMetadataByModality +> { + readonly kind = 'text' as const + readonly name = 'bedrock' as const + + private client: BedrockRuntimeClient + + constructor(config: BedrockTextConfig, model: TModel) { + super({}, model) + this.client = new BedrockRuntimeClient({ + region: config.region, + credentials: config.credentials, + }) + } + + async *chatStream( + options: TextOptions, + ): AsyncIterable { + const id = this.generateId() + const timestamp = Date.now() + + try { + // Convert messages to Converse format (unified across all models) + const messages = options.messages.map(m => this.convertToConverseMessage(m)) + + const command = new ConverseStreamCommand({ + modelId: this.model, + messages, + system: options.systemPrompts?.map(text => ({ text })), + inferenceConfig: { + maxTokens: options.maxTokens, + temperature: options.temperature, + topP: options.topP, + ...options.modelOptions?.inferenceConfig, + }, + toolConfig: options.tools?.length ? { + tools: options.tools.map(t => ({ + toolSpec: { + name: t.name, + description: t.description, + inputSchema: { json: t.inputSchema }, + } + })) + } : undefined, + // Model-specific extended features via additionalModelRequestFields + additionalModelRequestFields: (() => { + if (isClaude(this.model) && options.modelOptions?.thinking && options.messages.length === 1) { + // Claude: native thinking support (only first turn) + return { thinking: options.modelOptions.thinking } + } + if (isNova(this.model) && options.modelOptions?.thinking) { + // Nova: extended thinking via reasoningConfig + // Note: produces tags in text (parsed universally below) + return { + reasoningConfig: { + enabled: true, + maxReasoningEffort: "medium" + } + } + } + return undefined + })() as any, // Type assertion for AWS SDK DocumentType + }) + + const response = await this.client.send(command) + + if (!response.stream) { + throw new Error('No stream received from Bedrock') + } + + yield* this.processConverseStream(response.stream, id, timestamp) + + } catch (error: unknown) { + const err = error as Error & { name?: string } + yield { + type: 'error', + id, + model: this.model, + timestamp, + error: { + message: err.message || 'Unknown Bedrock error', + code: err.name || 'INTERNAL_ERROR', + }, + } + } + } + + structuredOutput( + _options: StructuredOutputOptions, + ): Promise> { + // TODO: Migrate to Converse API for structured output + return Promise.reject(new Error('Structured output not yet migrated to ConverseStream API')) + } + + /** + * Convert ModelMessage to Converse API message format (unified across all models) + */ + private convertToConverseMessage(message: ModelMessage): Message { + // Handle tool result messages + if (message.role === 'tool' && message.toolCallId) { + const contentText = typeof message.content === 'string' ? message.content : JSON.stringify(message.content) + let contentBlock: any = { text: contentText } + + // Try to parse as JSON for better structure + try { + const parsed = JSON.parse(contentText) + contentBlock = { json: parsed } + } catch { + // Keep as text + } + + return { + role: 'user', + content: [{ + toolResult: { + toolUseId: message.toolCallId, + content: [contentBlock], + status: ((message as any).status === 'error' || (message as any).error) ? 'failure' : 'success', + } as ToolResultBlock + }] + } + } + + // Handle assistant messages with tool calls + if (message.role === 'assistant' && message.toolCalls?.length) { + const content: Array = [] + + // Add text content if present + if (typeof message.content === 'string' && message.content) { + content.push({ text: message.content }) + } else if (Array.isArray(message.content)) { + for (const part of message.content) { + const block = this.convertPartToConverseBlock(part) + if (block) content.push(block) + } + } + + // Add tool use blocks + for (const tc of message.toolCalls) { + let input = tc.function.arguments + if (typeof input === 'string') { + try { + input = JSON.parse(input) + } catch { + // Keep as string if parsing fails + } + } + + content.push({ + toolUse: { + toolUseId: tc.id, + name: tc.function.name, + input + } as ToolUseBlock + }) + } + + return { role: 'assistant', content } + } + + // Handle regular messages (user or assistant) + const content: Array = [] + + if (typeof message.content === 'string') { + content.push({ text: message.content }) + } else if (Array.isArray(message.content)) { + for (const part of message.content) { + const block = this.convertPartToConverseBlock(part) + if (block) content.push(block) + } + } + + return { + role: message.role === 'user' ? 'user' : 'assistant', + content + } + } + + /** + * Convert message part to Converse content block + */ + private convertPartToConverseBlock(part: any): ContentBlock | null { + if (part.type === 'text') { + return { text: part.content } + } + if (part.type === 'image') { + return { + image: { + format: (part.metadata)?.mediaType?.split('/')[1] || 'jpeg', + source: { bytes: part.source.value } + } + } + } + if (part.type === 'video') { + return { + video: { + format: (part.metadata)?.mediaType?.split('/')[1] || 'mp4', + source: { bytes: part.source.value } + } + } + } + if (part.type === 'document') { + return { + document: { + format: (part.metadata)?.mediaType?.split('/')[1] || 'pdf', + source: { bytes: part.source.value }, + name: (part.metadata)?.name || 'document' + } + } + } + // Skip thinking parts - they're not sent back to the model in Converse API + return null + } + + /** + * Process ConverseStream events and yield StreamChunks + */ + private async *processConverseStream(stream: AsyncIterable, id: string, timestamp: number): AsyncIterable { + let accumulatedContent = '' + let toolCallIndex = -1 + let currentToolUseId = '' + let lastStopReason: string | undefined + let lastUsage: any | undefined + let doneEmitted = false + + // Universal tag parsing for any model that emits them + // (Claude with showThinking, Nova with reasoningConfig or when using tools, etc.) + let isInsideThinking = false + let pendingTagBuffer = '' + let accumulatedThinking = '' + + for await (const event of stream) { + // Content block delta (text generation) + if (event.contentBlockDelta) { + const delta = event.contentBlockDelta.delta + + // Text content + if (delta?.text) { + + // Nova: Parse tags from text + let text = pendingTagBuffer + delta.text + pendingTagBuffer = '' + + while (text.length > 0) { + if (!isInsideThinking) { + const startIdx = text.indexOf('') + if (startIdx !== -1) { + if (startIdx > 0) { + const before = text.substring(0, startIdx) + accumulatedContent += before + yield { type: 'content', id, model: this.model, timestamp, delta: before, content: accumulatedContent, role: 'assistant' } + } + isInsideThinking = true + text = text.substring(startIdx + 10) + } else if (text.includes('<')) { + const idx = text.lastIndexOf('<') + const before = text.substring(0, idx) + if (before) { + accumulatedContent += before + yield { type: 'content', id, model: this.model, timestamp, delta: before, content: accumulatedContent, role: 'assistant' } + } + pendingTagBuffer = text.substring(idx) + break + } else { + accumulatedContent += text + yield { type: 'content', id, model: this.model, timestamp, delta: text, content: accumulatedContent, role: 'assistant' } + break + } + } else { + const endIdx = text.indexOf('') + if (endIdx !== -1) { + if (endIdx > 0) { + const thinking = text.substring(0, endIdx) + accumulatedThinking += thinking + yield { type: 'thinking', id, model: this.model, timestamp, delta: thinking, content: accumulatedThinking } + } + isInsideThinking = false + text = text.substring(endIdx + 11) + } else if (text.includes('<')) { + const idx = text.lastIndexOf('<') + const thinking = text.substring(0, idx) + if (thinking) { + accumulatedThinking += thinking + yield { type: 'thinking', id, model: this.model, timestamp, delta: thinking, content: accumulatedThinking } + } + pendingTagBuffer = text.substring(idx) + break + } else { + accumulatedThinking += text + yield { type: 'thinking', id, model: this.model, timestamp, delta: text, content: accumulatedThinking } + break + } + } + } + + } + + // Tool input (arguments) - comes as partial JSON string chunks + if (delta?.toolUse?.input) { + // Input is already a string, don't JSON.stringify it! + const inputDelta = delta.toolUse.input + yield { + type: 'tool_call', + id, + model: this.model, + timestamp, + index: toolCallIndex, + toolCall: { + id: currentToolUseId, + type: 'function', + function: { + name: '', + arguments: inputDelta, + } + } + } + } + } + + // Content block start (for tool use) + if (event.contentBlockStart?.start?.toolUse) { + toolCallIndex++ + const toolUse = event.contentBlockStart.start.toolUse + currentToolUseId = toolUse.toolUseId + + yield { + type: 'tool_call', + id, + model: this.model, + timestamp, + index: toolCallIndex, + toolCall: { + id: toolUse.toolUseId, + type: 'function', + function: { + name: toolUse.name, + arguments: '', + } + } + } + } + + + // Message stop (completion) + if (event.messageStop) { + lastStopReason = event.messageStop.stopReason + } + + // Metadata (token usage) + if (event.metadata?.usage) { + lastUsage = event.metadata.usage + } + } + + // Emit final consolidated done event + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!doneEmitted) { + yield { + type: 'done', + id, + model: this.model, + timestamp, + finishReason: lastStopReason === 'tool_use' ? 'tool_calls' : 'stop', + usage: lastUsage ? { + promptTokens: lastUsage.inputTokens || 0, + completionTokens: lastUsage.outputTokens || 0, + totalTokens: lastUsage.totalTokens || 0, + } : undefined + } + doneEmitted = true + } + } +} diff --git a/packages/typescript/ai-bedrock/src/bedrock-chat.ts b/packages/typescript/ai-bedrock/src/bedrock-chat.ts new file mode 100644 index 000000000..179786389 --- /dev/null +++ b/packages/typescript/ai-bedrock/src/bedrock-chat.ts @@ -0,0 +1,25 @@ +import { BedrockTextAdapter } from './adapters/text' +import { getBedrockConfigFromEnv } from './utils' +import type { BedrockTextConfig } from './adapters/text'; +import type { BedrockModelId } from './model-meta' + +export function createBedrockChat< + TModel extends BedrockModelId, +>(model: TModel, config: BedrockTextConfig): BedrockTextAdapter { + return new BedrockTextAdapter(config, model) +} + +export function bedrockText( + model: TModel, + config?: Partial, +): BedrockTextAdapter { + const envConfig = getBedrockConfigFromEnv() + const fullConfig: BedrockTextConfig = { + region: config?.region || envConfig.region || 'us-east-1', + credentials: { + accessKeyId: config?.credentials?.accessKeyId || envConfig.credentials?.accessKeyId || '', + secretAccessKey: config?.credentials?.secretAccessKey || envConfig.credentials?.secretAccessKey || '', + }, + } + return createBedrockChat(model, fullConfig) +} diff --git a/packages/typescript/ai-bedrock/src/index.ts b/packages/typescript/ai-bedrock/src/index.ts new file mode 100644 index 000000000..3f20f3511 --- /dev/null +++ b/packages/typescript/ai-bedrock/src/index.ts @@ -0,0 +1,31 @@ +export { + BedrockTextAdapter, + type BedrockTextConfig, +} from './adapters/text' + +export { + createBedrockChat, + bedrockText, +} from './bedrock-chat' + +export { + BEDROCK_AMAZON_NOVA_PRO_V1, + BEDROCK_AMAZON_NOVA_LITE_V1, + BEDROCK_AMAZON_NOVA_MICRO_V1, + BEDROCK_ANTHROPIC_CLAUDE_SONNET_4_5, + BEDROCK_ANTHROPIC_CLAUDE_HAIKU_4_5, + BEDROCK_CHAT_MODELS, + type BedrockModelMeta, + type BedrockModelId, + type BedrockModelInputModalitiesByName, +} from './model-meta' + +export type { + BedrockMessageMetadataByModality, + BedrockTextMetadata, + BedrockImageMetadata, + BedrockDocumentMetadata, + BedrockAudioMetadata, + BedrockVideoMetadata, + BedrockImageMediaType, +} from './message-types' diff --git a/packages/typescript/ai-bedrock/src/message-types.ts b/packages/typescript/ai-bedrock/src/message-types.ts new file mode 100644 index 000000000..17520c2fc --- /dev/null +++ b/packages/typescript/ai-bedrock/src/message-types.ts @@ -0,0 +1,70 @@ + +/** + * Bedrock-specific metadata types for multimodal content parts. + * These types extend the base ContentPart metadata with Bedrock-specific options. + */ + +/** + * Supported image media types for Bedrock. + */ +export type BedrockImageMediaType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' + +/** + * Metadata for Bedrock image content parts. + */ +export interface BedrockImageMetadata { + /** + * The MIME type of the image. + */ + mediaType?: BedrockImageMediaType +} + +/** + * Metadata for Bedrock text content parts. + */ +export interface BedrockTextMetadata { } + +/** + * Metadata for Bedrock document content parts. + */ +export interface BedrockDocumentMetadata { + /** + * The MIME type of the document. + */ + mediaType?: string + /** + * The name of the document. + */ + name?: string +} + +/** + * Metadata for Bedrock audio content parts. + */ +export interface BedrockAudioMetadata { + /** + * The MIME type of the audio. + */ + mediaType?: string +} + +/** + * Metadata for Bedrock video content parts. + */ +export interface BedrockVideoMetadata { + /** + * The MIME type of the video. + */ + mediaType?: string +} + +/** + * Map of modality types to their Bedrock-specific metadata types. + */ +export interface BedrockMessageMetadataByModality { + text: BedrockTextMetadata + image: BedrockImageMetadata + audio: BedrockAudioMetadata + video: BedrockVideoMetadata + document: BedrockDocumentMetadata +} diff --git a/packages/typescript/ai-bedrock/src/model-meta.ts b/packages/typescript/ai-bedrock/src/model-meta.ts new file mode 100644 index 000000000..0e82776a9 --- /dev/null +++ b/packages/typescript/ai-bedrock/src/model-meta.ts @@ -0,0 +1,99 @@ + +export interface BedrockModelMeta { + name: string + id: string + supports: { + input: Array<'text' | 'image' | 'audio' | 'video' | 'document'> + thinking?: boolean + } + context_window?: number + max_output_tokens?: number +} + +// =========================== +// Amazon Nova Models (Latest) +// =========================== + +export const BEDROCK_AMAZON_NOVA_PRO_V1 = { + name: 'nova-pro-v1', + id: 'amazon.nova-pro-v1:0', + context_window: 300_000, + max_output_tokens: 5120, + supports: { + input: ['text', 'image', 'video', 'document'], + }, +} as const satisfies BedrockModelMeta + +export const BEDROCK_AMAZON_NOVA_LITE_V1 = { + name: 'nova-lite-v1', + id: 'amazon.nova-lite-v1:0', + context_window: 300_000, + max_output_tokens: 5120, + supports: { + input: ['text', 'image', 'video', 'document'], + }, +} as const satisfies BedrockModelMeta + +export const BEDROCK_AMAZON_NOVA_MICRO_V1 = { + name: 'nova-micro-v1', + id: 'amazon.nova-micro-v1:0', + context_window: 128_000, + max_output_tokens: 5120, + supports: { + input: ['text'], + }, +} as const satisfies BedrockModelMeta + +// =========================== +// Flagship Anthropic Models (V4) +// =========================== + +export const BEDROCK_ANTHROPIC_CLAUDE_SONNET_4_5 = { + name: 'claude-4-5-sonnet', + id: 'anthropic.claude-4-5-sonnet-20250929-v1:0', + context_window: 1_000_000, + max_output_tokens: 64_000, + supports: { + input: ['text', 'image', 'document'], + thinking: true, + }, +} as const satisfies BedrockModelMeta + +export const BEDROCK_ANTHROPIC_CLAUDE_HAIKU_4_5 = { + name: 'claude-4-5-haiku', + id: 'anthropic.claude-4-5-haiku-20251001-v1:0', + context_window: 200_000, + max_output_tokens: 64_000, + supports: { + input: ['text', 'image', 'document'], + thinking: true, + }, +} as const satisfies BedrockModelMeta + +export const BEDROCK_CHAT_MODELS = [ + BEDROCK_AMAZON_NOVA_PRO_V1.id, + BEDROCK_AMAZON_NOVA_LITE_V1.id, + BEDROCK_AMAZON_NOVA_MICRO_V1.id, + BEDROCK_ANTHROPIC_CLAUDE_SONNET_4_5.id, + BEDROCK_ANTHROPIC_CLAUDE_HAIKU_4_5.id, +] as const + +export type BedrockModelId = (typeof BEDROCK_CHAT_MODELS)[number] | (string & {}) + +/** + * Type-only map from chat model name to its supported input modalities. + */ +export type BedrockModelInputModalitiesByName = { + [BEDROCK_AMAZON_NOVA_PRO_V1.id]: typeof BEDROCK_AMAZON_NOVA_PRO_V1.supports.input + [BEDROCK_AMAZON_NOVA_LITE_V1.id]: typeof BEDROCK_AMAZON_NOVA_LITE_V1.supports.input + [BEDROCK_AMAZON_NOVA_MICRO_V1.id]: typeof BEDROCK_AMAZON_NOVA_MICRO_V1.supports.input + [BEDROCK_ANTHROPIC_CLAUDE_SONNET_4_5.id]: typeof BEDROCK_ANTHROPIC_CLAUDE_SONNET_4_5.supports.input + [BEDROCK_ANTHROPIC_CLAUDE_HAIKU_4_5.id]: typeof BEDROCK_ANTHROPIC_CLAUDE_HAIKU_4_5.supports.input +} + +// =========================== +// Model Detection Helpers +// =========================== + +export const isClaude = (model: string) => model.includes('anthropic.claude') +export const isNova = (model: string) => model.includes('amazon.nova') diff --git a/packages/typescript/ai-bedrock/src/text/text-provider-options.ts b/packages/typescript/ai-bedrock/src/text/text-provider-options.ts new file mode 100644 index 000000000..4afafa45c --- /dev/null +++ b/packages/typescript/ai-bedrock/src/text/text-provider-options.ts @@ -0,0 +1,64 @@ +export interface BedrockMCPOptions { + /** + * MCP servers to be utilized in this request + * Maximum of 20 servers + */ + mcp_servers?: Array +} + +export interface BedrockStopSequencesOptions { + /** + * Custom text sequences that will cause the model to stop generating. + */ + stop_sequences?: Array +} + +export interface BedrockThinkingOptions { + /** + * Configuration for enabling Claude's extended thinking. + */ + thinking?: + | { + /** + * Determines how many tokens the model can use for its internal reasoning process. + */ + budget_tokens: number + type: 'enabled' + } + | { + type: 'disabled' + } +} + +export interface BedrockSamplingOptions { + /** + * Only sample from the top K options for each subsequent token. + */ + top_k?: number +} + +export interface BedrockInferenceConfig { + maxTokens?: number + temperature?: number + topP?: number + stopSequences?: Array +} + +export interface MCPServer { + name: string + url: string + type: 'url' + authorization_token?: string | null + tool_configuration: { + allowed_tools?: Array | null + enabled?: boolean | null + } | null +} + +export type BedrockTextProviderOptions = BedrockMCPOptions & + BedrockStopSequencesOptions & + BedrockThinkingOptions & + BedrockSamplingOptions & { + /** Additional inference configuration for Bedrock */ + inferenceConfig?: BedrockInferenceConfig + } diff --git a/packages/typescript/ai-bedrock/src/utils.ts b/packages/typescript/ai-bedrock/src/utils.ts new file mode 100644 index 000000000..b013a850c --- /dev/null +++ b/packages/typescript/ai-bedrock/src/utils.ts @@ -0,0 +1,38 @@ + + +export interface BedrockClientConfig { + region?: string + credentials?: { + accessKeyId: string + secretAccessKey: string + sessionToken?: string + } +} + +/** + * Gets Bedrock config from environment variables + */ +export function getBedrockConfigFromEnv(): BedrockClientConfig { + const env = + typeof globalThis !== 'undefined' && (globalThis as any).window?.env + ? (globalThis as any).window.env + : typeof process !== 'undefined' + ? process.env + : undefined + + const config: BedrockClientConfig = {} + + if (env?.AWS_REGION || env?.AWS_DEFAULT_REGION) { + config.region = env.AWS_REGION || env.AWS_DEFAULT_REGION + } + + if (env?.AWS_ACCESS_KEY_ID && env?.AWS_SECRET_ACCESS_KEY) { + config.credentials = { + accessKeyId: env.AWS_ACCESS_KEY_ID, + secretAccessKey: env.AWS_SECRET_ACCESS_KEY, + sessionToken: env.AWS_SESSION_TOKEN + } + } + + return config +} diff --git a/packages/typescript/ai-bedrock/tests/bedrock-adapter.test.ts b/packages/typescript/ai-bedrock/tests/bedrock-adapter.test.ts new file mode 100644 index 000000000..1703c49aa --- /dev/null +++ b/packages/typescript/ai-bedrock/tests/bedrock-adapter.test.ts @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + ConverseStreamCommand, +} from '@aws-sdk/client-bedrock-runtime' +import { BedrockTextAdapter } from '../src/adapters/text' + +// Mock the AWS SDK +const { sendMock } = vi.hoisted(() => { + return { sendMock: vi.fn() } +}) + +vi.mock('@aws-sdk/client-bedrock-runtime', () => { + return { + BedrockRuntimeClient: class { + send = sendMock + }, + ConverseStreamCommand: vi.fn(), + } +}) + +describe('BedrockTextAdapter', () => { + let adapter: BedrockTextAdapter + + beforeEach(() => { + vi.clearAllMocks() + adapter = new BedrockTextAdapter({ + region: 'us-east-1', + credentials: { accessKeyId: 'test', secretAccessKey: 'test' }, + }, 'anthropic.claude-3-sonnet-20240229-v1:0') + }) + + describe('chatStream', () => { + it('should handle streaming response', async () => { + // Mock ConverseStream API response format + const mockStream = [ + { + contentBlockDelta: { + delta: { text: 'Hello ' } + } + }, + { + contentBlockDelta: { + delta: { text: 'world' } + } + }, + { + messageStop: { + stopReason: 'end_turn' + } + }, + ] + + sendMock.mockResolvedValue({ + stream: (async function* () { + for (const event of mockStream) { + await Promise.resolve() + yield event + } + })(), + }) + + const stream = adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + }) + + const result = [] + for await (const chunk of stream) { + result.push(chunk) + } + + expect(ConverseStreamCommand).toHaveBeenCalled() + // Filter for content chunks only + const contentChunks = result.filter(c => c.type === 'content') + expect(contentChunks).toEqual([ + expect.objectContaining({ type: 'content', delta: 'Hello ' }), + expect.objectContaining({ type: 'content', delta: 'world' }), + ]) + + const doneChunk = result.find(c => c.type === 'done') + expect(doneChunk).toBeDefined() + }) + }) +}) diff --git a/packages/typescript/ai-bedrock/tests/model-meta.test.ts b/packages/typescript/ai-bedrock/tests/model-meta.test.ts new file mode 100644 index 000000000..18cc8559a --- /dev/null +++ b/packages/typescript/ai-bedrock/tests/model-meta.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expectTypeOf } from 'vitest' +import type { + BedrockModelInputModalitiesByName, +} from '../src/model-meta' +import type { + AudioPart, + ConstrainedModelMessage, + DocumentPart, + ImagePart, + TextPart, + VideoPart, + InputModalitiesTypes, + DefaultMessageMetadataByModality, +} from '@tanstack/ai' + +/** + * Helper to convert raw modality array to InputModalitiesTypes + */ +type ResolveModalities> = { + inputModalities: T + messageMetadataByModality: DefaultMessageMetadataByModality +} + +/** + * Type assertion tests for Bedrock model input modalities. + */ +describe('Bedrock Model Input Modality Type Assertions', () => { + // Helper type for creating a user message with specific content + type MessageWithContent = { role: 'user'; content: Array } + + describe('Amazon Nova Pro (text + image + video + document)', () => { + type Modalities = ResolveModalities + type Message = ConstrainedModelMessage + + it('should allow TextPart, ImagePart, VideoPart, and DocumentPart', () => { + expectTypeOf>().toExtend() + expectTypeOf>().toExtend() + expectTypeOf>().toExtend() + expectTypeOf>().toExtend() + }) + + it('should NOT allow AudioPart', () => { + expectTypeOf>().not.toExtend() + }) + }) + + describe('Claude 4.5 Sonnet (text + image + document)', () => { + type Modalities = ResolveModalities + type Message = ConstrainedModelMessage + + it('should allow TextPart, ImagePart, and DocumentPart', () => { + expectTypeOf>().toExtend() + expectTypeOf>().toExtend() + expectTypeOf>().toExtend() + }) + + it('should NOT allow AudioPart or VideoPart', () => { + expectTypeOf>().not.toExtend() + expectTypeOf>().not.toExtend() + }) + }) +}) diff --git a/packages/typescript/ai-bedrock/tsconfig.json b/packages/typescript/ai-bedrock/tsconfig.json new file mode 100644 index 000000000..b8bb330eb --- /dev/null +++ b/packages/typescript/ai-bedrock/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist/esm", + "declarationDir": "./dist/esm" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/typescript/ai-bedrock/vite.config.ts b/packages/typescript/ai-bedrock/vite.config.ts new file mode 100644 index 000000000..c605fd207 --- /dev/null +++ b/packages/typescript/ai-bedrock/vite.config.ts @@ -0,0 +1,36 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/vite-config' +import packageJson from './package.json' + +const config = defineConfig({ + test: { + name: packageJson.name, + dir: './', + watch: false, + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html', 'lcov'], + exclude: [ + 'node_modules/', + 'dist/', + 'tests/', + '**/*.test.ts', + '**/*.config.ts', + '**/types.ts', + ], + include: ['src/**/*.ts'], + }, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: ['./src/index.ts', './src/adapters/index.ts'], + srcDir: './src', + cjs: false, + }), +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21eb7cb9f..f515602e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -632,6 +632,25 @@ importers: specifier: ^4.2.0 version: 4.2.1 + packages/typescript/ai-bedrock: + dependencies: + '@aws-sdk/client-bedrock-runtime': + specifier: ^3.723.0 + version: 3.965.0 + zod: + specifier: ^4.0.0 + version: 4.2.1 + devDependencies: + '@tanstack/ai': + specifier: workspace:* + version: link:../ai + '@vitest/coverage-v8': + specifier: 4.0.14 + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + vite: + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + packages/typescript/ai-client: dependencies: '@tanstack/ai': @@ -658,7 +677,7 @@ importers: version: 0.4.4(csstype@3.2.3)(solid-js@1.9.10) '@tanstack/devtools-utils': specifier: ^0.2.3 - version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) + version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) goober: specifier: ^2.1.18 version: 2.1.18(csstype@3.2.3) @@ -1025,7 +1044,7 @@ importers: version: link:../ai-devtools '@tanstack/devtools-utils': specifier: ^0.2.3 - version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) + version: 0.2.3(@types/react@19.2.7)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) preact: specifier: ^10.0.0 version: 10.28.1 @@ -1044,7 +1063,7 @@ importers: version: link:../ai-devtools '@tanstack/devtools-utils': specifier: ^0.2.3 - version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) + version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) devDependencies: '@types/react': specifier: ^19.2.7 @@ -1175,7 +1194,7 @@ importers: version: link:../ai-devtools '@tanstack/devtools-utils': specifier: ^0.2.3 - version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) + version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 @@ -1318,6 +1337,143 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.965.0': + resolution: {integrity: sha512-ccx3IJcSYNrkj3lAojip2Esjd6YSbrfEvJmvunNkcciexJsEaykDQExN+RSxIcaSvqVXkfqoSbxapI62fOUOfg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.965.0': + resolution: {integrity: sha512-iv2tr+n4aZ+nPUFFvG00hISPuEd4DU+1/Q8rPAYKXsM+vEPJ2nAnP5duUOa2fbOLIUCRxX3dcQaQaghVHDHzQw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.965.0': + resolution: {integrity: sha512-aq9BhQxdHit8UUJ9C0im9TtuKeK0pT6NXmNJxMTCFeStI7GG7ImIsSislg3BZTIifVg1P6VLdzMyz9de85iutQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-env@3.965.0': + resolution: {integrity: sha512-mdGnaIjMxTIjsb70dEj3VsWPWpoq1V5MWzBSfJq2H8zgMBXjn6d5/qHP8HMf53l9PrsgqzMpXGv3Av549A2x1g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.965.0': + resolution: {integrity: sha512-YuGQel9EgA/z25oeLM+GYYQS750+8AESvr7ZEmVnRPL0sg+K3DmGqdv+9gFjFd0UkLjTlC/jtbP2cuY6UcPiHQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.965.0': + resolution: {integrity: sha512-xRo72Prer5s0xYVSCxCymVIRSqrVlevK5cmU0GWq9yJtaBNpnx02jwdJg80t/Ni7pgbkQyFWRMcq38c1tc6M/w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-login@3.965.0': + resolution: {integrity: sha512-43/H8Qku8LHyugbhLo8kjD+eauhybCeVkmrnvWl8bXNHJP7xi1jCdtBQJKKJqiIHZws4MOEwkji8kFdAVRCe6g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.965.0': + resolution: {integrity: sha512-cRxmMHF+Zh2lkkkEVduKl+8OQdtg/DhYA69+/7SPSQURlgyjFQGlRQ58B7q8abuNlrGT3sV+UzeOylZpJbV61Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.965.0': + resolution: {integrity: sha512-gmkPmdiR0yxnTzLPDb7rwrDhGuCUjtgnj8qWP+m0gSz/W43rR4jRPVEf6DUX2iC+ImQhxo3NFhuB3V42Kzo3TQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.965.0': + resolution: {integrity: sha512-N01AYvtCqG3Wo/s/LvYt19ity18/FqggiXT+elAs3X9Om/Wfx+hw9G+i7jaDmy+/xewmv8AdQ2SK5Q30dXw/Fw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.965.0': + resolution: {integrity: sha512-T4gMZ2JzXnfxe1oTD+EDGLSxFfk1+WkLZdiHXEMZp8bFI1swP/3YyDFXI+Ib9Uq1JhnAmrCXtOnkicKEhDkdhQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/eventstream-handler-node@3.965.0': + resolution: {integrity: sha512-QriACiXP+/x2xXw8u849BxID+zSUbh/7Gt0Zfaxeye0mIKVeSTid5776rXfrM8wcYhbVXWWZhKd1Du7oPuFwsg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-eventstream@3.965.0': + resolution: {integrity: sha512-YVNOPbc3r+gETUY6ufnJYsgIRMaBfoGRM9GzPb+gwtidCPd0BEpLjmZNIVGYawMrGc2kAdlV1kjBzAvmYaMINw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.965.0': + resolution: {integrity: sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.965.0': + resolution: {integrity: sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.965.0': + resolution: {integrity: sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.965.0': + resolution: {integrity: sha512-RBEYVGgu/WeAt+H/qLrGc+t8LqAUkbyvh3wBfTiuAD+uBcWsKnvnB1iSBX75FearC0fmoxzXRUc0PMxMdqpjJQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-websocket@3.965.0': + resolution: {integrity: sha512-BGU92StrWF0EJj8jX5EFvRkX9z4/CVIZfON0nWow8gb5ouKwz47o1rO9CP/k2b3F6g134/0XqwXvrUgIWfjJeA==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.965.0': + resolution: {integrity: sha512-muNVUjUEU+/KLFrLzQ8PMXyw4+a/MP6t4GIvwLtyx/kH0rpSy5s0YmqacMXheuIe6F/5QT8uksXGNAQenitkGQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/region-config-resolver@3.965.0': + resolution: {integrity: sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.965.0': + resolution: {integrity: sha512-aR0qxg0b8flkXJVE+CM1gzo7uJ57md50z2eyCwofC0QIz5Y0P7/7vvb9/dmUQt6eT9XRN5iRcUqq2IVxVDvJOw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.965.0': + resolution: {integrity: sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-endpoints@3.965.0': + resolution: {integrity: sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-format-url@3.965.0': + resolution: {integrity: sha512-KiplV4xYGXdNCcz5eRP8WfAejT5EkE2gQxC4IY6WsuxYprzQKsnGaAzEQ+giR5GgQLIRBkPaWT0xHEYkMiCQ1Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-locate-window@3.965.0': + resolution: {integrity: sha512-9LJFand4bIoOjOF4x3wx0UZYiFZRo4oUauxQSiEX2dVg+5qeBOJSjp2SeWykIE6+6frCZ5wvWm2fGLK8D32aJw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-user-agent-browser@3.965.0': + resolution: {integrity: sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ==} + + '@aws-sdk/util-user-agent-node@3.965.0': + resolution: {integrity: sha512-kokIHUfNT3/P55E4fUJJrFHuuA9BbjFKUIxoLrd3UaRfdafT0ScRfg2eaZie6arf60EuhlUIZH0yALxttMEjxQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.965.0': + resolution: {integrity: sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g==} + engines: {node: '>=18.0.0'} + + '@aws/lambda-invoke-store@0.2.3': + resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -2845,6 +3001,198 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@smithy/abort-controller@4.2.7': + resolution: {integrity: sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.5': + resolution: {integrity: sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.20.1': + resolution: {integrity: sha512-wOboSEdQ85dbKAJ0zL+wQ6b0HTSBRhtGa0PYKysQXkRg+vK0tdCRRVruiFM2QMprkOQwSYOnwF4og96PAaEGag==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.7': + resolution: {integrity: sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.7': + resolution: {integrity: sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.7': + resolution: {integrity: sha512-ujzPk8seYoDBmABDE5YqlhQZAXLOrtxtJLrbhHMKjBoG5b4dK4i6/mEU+6/7yXIAkqOO8sJ6YxZl+h0QQ1IJ7g==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.7': + resolution: {integrity: sha512-x7BtAiIPSaNaWuzm24Q/mtSkv+BrISO/fmheiJ39PKRNH3RmH2Hph/bUKSOBOBC9unqfIYDhKTHwpyZycLGPVQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.7': + resolution: {integrity: sha512-roySCtHC5+pQq5lK4be1fZ/WR6s/AxnPaLfCODIPArtN2du8s5Ot4mKVK3pPtijL/L654ws592JHJ1PbZFF6+A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.7': + resolution: {integrity: sha512-QVD+g3+icFkThoy4r8wVFZMsIP08taHVKjE6Jpmz8h5CgX/kk6pTODq5cht0OMtcapUx+xrPzUTQdA+TmO0m1g==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.8': + resolution: {integrity: sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.7': + resolution: {integrity: sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.7': + resolution: {integrity: sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.7': + resolution: {integrity: sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.2': + resolution: {integrity: sha512-mqpAdux0BNmZu/SqkFhQEnod4fX23xxTvU2LUpmKp0JpSI+kPYCiHJMmzREr8yxbNxKL2/DU1UZm9i++ayU+2g==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.18': + resolution: {integrity: sha512-E5hulijA59nBk/zvcwVMaS7FG7Y4l6hWA9vrW018r+8kiZef4/ETQaPI4oY+3zsy9f6KqDv3c4VKtO4DwwgpCg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.8': + resolution: {integrity: sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.7': + resolution: {integrity: sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.7': + resolution: {integrity: sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.4.7': + resolution: {integrity: sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.7': + resolution: {integrity: sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.7': + resolution: {integrity: sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.7': + resolution: {integrity: sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.7': + resolution: {integrity: sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.7': + resolution: {integrity: sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.2': + resolution: {integrity: sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.7': + resolution: {integrity: sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.10.3': + resolution: {integrity: sha512-EfECiO/0fAfb590LBnUe7rI5ux7XfquQ8LBzTe7gxw0j9QW/q8UT/EHWHlxV/+jhQ3+Ssga9uUYXCQgImGMbNg==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.11.0': + resolution: {integrity: sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.7': + resolution: {integrity: sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.0': + resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.1': + resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.17': + resolution: {integrity: sha512-dwN4GmivYF1QphnP3xJESXKtHvkkvKHSZI8GrSKMVoENVSKW2cFPRYC4ZgstYjUHdR3zwaDkIaTDIp26JuY7Cw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.20': + resolution: {integrity: sha512-VD/I4AEhF1lpB3B//pmOIMBNLMrtdMXwy9yCOfa2QkJGDr63vH3RqPbSAKzoGMov3iryCxTXCxSsyGmEB8PDpg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.2.7': + resolution: {integrity: sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.7': + resolution: {integrity: sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.7': + resolution: {integrity: sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.8': + resolution: {integrity: sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.0': + resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} + engines: {node: '>=18.0.0'} + '@solid-devtools/debugger@0.28.1': resolution: {integrity: sha512-6qIUI6VYkXoRnL8oF5bvh2KgH71qlJ18hNw/mwSyY6v48eb80ZR48/5PDXufUa3q+MBSuYa1uqTMwLewpay9eg==} peerDependencies: @@ -4180,6 +4528,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.13.1: + resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + boxen@7.1.1: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} @@ -4975,6 +5326,10 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -6558,12 +6913,12 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - preact@10.28.2: - resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==} - preact@10.28.1: resolution: {integrity: sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==} + preact@10.28.2: + resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -7183,6 +7538,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@2.1.2: + resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -8056,6 +8414,421 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.965.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-locate-window': 3.965.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.965.0 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.965.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/credential-provider-node': 3.965.0 + '@aws-sdk/eventstream-handler-node': 3.965.0 + '@aws-sdk/middleware-eventstream': 3.965.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.965.0 + '@aws-sdk/middleware-websocket': 3.965.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/token-providers': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.965.0 + '@smithy/config-resolver': 4.4.5 + '@smithy/core': 3.20.1 + '@smithy/eventstream-serde-browser': 4.2.7 + '@smithy/eventstream-serde-config-resolver': 4.3.7 + '@smithy/eventstream-serde-node': 4.2.7 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/hash-node': 4.2.7 + '@smithy/invalid-dependency': 4.2.7 + '@smithy/middleware-content-length': 4.2.7 + '@smithy/middleware-endpoint': 4.4.2 + '@smithy/middleware-retry': 4.4.18 + '@smithy/middleware-serde': 4.2.8 + '@smithy/middleware-stack': 4.2.7 + '@smithy/node-config-provider': 4.3.7 + '@smithy/node-http-handler': 4.4.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/smithy-client': 4.10.3 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.17 + '@smithy/util-defaults-mode-node': 4.2.20 + '@smithy/util-endpoints': 3.2.7 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-retry': 4.2.7 + '@smithy/util-stream': 4.5.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.965.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.965.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.965.0 + '@smithy/config-resolver': 4.4.5 + '@smithy/core': 3.20.1 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/hash-node': 4.2.7 + '@smithy/invalid-dependency': 4.2.7 + '@smithy/middleware-content-length': 4.2.7 + '@smithy/middleware-endpoint': 4.4.2 + '@smithy/middleware-retry': 4.4.18 + '@smithy/middleware-serde': 4.2.8 + '@smithy/middleware-stack': 4.2.7 + '@smithy/node-config-provider': 4.3.7 + '@smithy/node-http-handler': 4.4.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/smithy-client': 4.10.3 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.17 + '@smithy/util-defaults-mode-node': 4.2.20 + '@smithy/util-endpoints': 3.2.7 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-retry': 4.2.7 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@aws-sdk/xml-builder': 3.965.0 + '@smithy/core': 3.20.1 + '@smithy/node-config-provider': 4.3.7 + '@smithy/property-provider': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/signature-v4': 5.3.7 + '@smithy/smithy-client': 4.10.3 + '@smithy/types': 4.11.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.965.0': + dependencies: + '@aws-sdk/core': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.965.0': + dependencies: + '@aws-sdk/core': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/node-http-handler': 4.4.7 + '@smithy/property-provider': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/smithy-client': 4.10.3 + '@smithy/types': 4.11.0 + '@smithy/util-stream': 4.5.8 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.965.0': + dependencies: + '@aws-sdk/core': 3.965.0 + '@aws-sdk/credential-provider-env': 3.965.0 + '@aws-sdk/credential-provider-http': 3.965.0 + '@aws-sdk/credential-provider-login': 3.965.0 + '@aws-sdk/credential-provider-process': 3.965.0 + '@aws-sdk/credential-provider-sso': 3.965.0 + '@aws-sdk/credential-provider-web-identity': 3.965.0 + '@aws-sdk/nested-clients': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@smithy/credential-provider-imds': 4.2.7 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.965.0': + dependencies: + '@aws-sdk/core': 3.965.0 + '@aws-sdk/nested-clients': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.965.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.965.0 + '@aws-sdk/credential-provider-http': 3.965.0 + '@aws-sdk/credential-provider-ini': 3.965.0 + '@aws-sdk/credential-provider-process': 3.965.0 + '@aws-sdk/credential-provider-sso': 3.965.0 + '@aws-sdk/credential-provider-web-identity': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@smithy/credential-provider-imds': 4.2.7 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.965.0': + dependencies: + '@aws-sdk/core': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.965.0': + dependencies: + '@aws-sdk/client-sso': 3.965.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/token-providers': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.965.0': + dependencies: + '@aws-sdk/core': 3.965.0 + '@aws-sdk/nested-clients': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/eventstream-handler-node@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/eventstream-codec': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@aws/lambda-invoke-store': 0.2.3 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.965.0': + dependencies: + '@aws-sdk/core': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@smithy/core': 3.20.1 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-format-url': 3.965.0 + '@smithy/eventstream-codec': 4.2.7 + '@smithy/eventstream-serde-browser': 4.2.7 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/protocol-http': 5.3.7 + '@smithy/signature-v4': 5.3.7 + '@smithy/types': 4.11.0 + '@smithy/util-hex-encoding': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.965.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.965.0 + '@aws-sdk/middleware-host-header': 3.965.0 + '@aws-sdk/middleware-logger': 3.965.0 + '@aws-sdk/middleware-recursion-detection': 3.965.0 + '@aws-sdk/middleware-user-agent': 3.965.0 + '@aws-sdk/region-config-resolver': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@aws-sdk/util-endpoints': 3.965.0 + '@aws-sdk/util-user-agent-browser': 3.965.0 + '@aws-sdk/util-user-agent-node': 3.965.0 + '@smithy/config-resolver': 4.4.5 + '@smithy/core': 3.20.1 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/hash-node': 4.2.7 + '@smithy/invalid-dependency': 4.2.7 + '@smithy/middleware-content-length': 4.2.7 + '@smithy/middleware-endpoint': 4.4.2 + '@smithy/middleware-retry': 4.4.18 + '@smithy/middleware-serde': 4.2.8 + '@smithy/middleware-stack': 4.2.7 + '@smithy/node-config-provider': 4.3.7 + '@smithy/node-http-handler': 4.4.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/smithy-client': 4.10.3 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.17 + '@smithy/util-defaults-mode-node': 4.2.20 + '@smithy/util-endpoints': 3.2.7 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-retry': 4.2.7 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/config-resolver': 4.4.5 + '@smithy/node-config-provider': 4.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.965.0': + dependencies: + '@aws-sdk/core': 3.965.0 + '@aws-sdk/nested-clients': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.965.0': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-endpoints': 3.2.7 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/querystring-builder': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.965.0': + dependencies: + '@aws-sdk/types': 3.965.0 + '@smithy/types': 4.11.0 + bowser: 2.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.965.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.965.0 + '@aws-sdk/types': 3.965.0 + '@smithy/node-config-provider': 4.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.965.0': + dependencies: + '@smithy/types': 4.11.0 + fast-xml-parser: 5.2.5 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.3': {} + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -9399,6 +10172,310 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@smithy/abort-controller@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.5': + dependencies: + '@smithy/node-config-provider': 4.3.7 + '@smithy/types': 4.11.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-endpoints': 3.2.7 + '@smithy/util-middleware': 4.2.7 + tslib: 2.8.1 + + '@smithy/core@3.20.1': + dependencies: + '@smithy/middleware-serde': 4.2.8 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-stream': 4.5.8 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.7': + dependencies: + '@smithy/node-config-provider': 4.3.7 + '@smithy/property-provider': 4.2.7 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.7': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.11.0 + '@smithy/util-hex-encoding': 4.2.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.7': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.7': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.7': + dependencies: + '@smithy/eventstream-codec': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.8': + dependencies: + '@smithy/protocol-http': 5.3.7 + '@smithy/querystring-builder': 4.2.7 + '@smithy/types': 4.11.0 + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.7': + dependencies: + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.2': + dependencies: + '@smithy/core': 3.20.1 + '@smithy/middleware-serde': 4.2.8 + '@smithy/node-config-provider': 4.3.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-middleware': 4.2.7 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.18': + dependencies: + '@smithy/node-config-provider': 4.3.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/service-error-classification': 4.2.7 + '@smithy/smithy-client': 4.10.3 + '@smithy/types': 4.11.0 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-retry': 4.2.7 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.8': + dependencies: + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.7': + dependencies: + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.4.7': + dependencies: + '@smithy/abort-controller': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/querystring-builder': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + + '@smithy/shared-ini-file-loader@4.4.2': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.7': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.10.3': + dependencies: + '@smithy/core': 3.20.1 + '@smithy/middleware-endpoint': 4.4.2 + '@smithy/middleware-stack': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + '@smithy/util-stream': 4.5.8 + tslib: 2.8.1 + + '@smithy/types@4.11.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.7': + dependencies: + '@smithy/querystring-parser': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.17': + dependencies: + '@smithy/property-provider': 4.2.7 + '@smithy/smithy-client': 4.10.3 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.20': + dependencies: + '@smithy/config-resolver': 4.4.5 + '@smithy/credential-provider-imds': 4.2.7 + '@smithy/node-config-provider': 4.3.7 + '@smithy/property-provider': 4.2.7 + '@smithy/smithy-client': 4.10.3 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.2.7': + dependencies: + '@smithy/node-config-provider': 4.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.7': + dependencies: + '@smithy/service-error-classification': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.8': + dependencies: + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/node-http-handler': 4.4.7 + '@smithy/types': 4.11.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.0': + dependencies: + tslib: 2.8.1 + '@solid-devtools/debugger@0.28.1(solid-js@1.9.10)': dependencies: '@nothing-but/utils': 0.17.0 @@ -9680,7 +10757,19 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-utils@0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))': + '@tanstack/devtools-utils@0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10) + optionalDependencies: + '@types/react': 19.2.7 + preact: 10.28.2 + react: 19.2.3 + solid-js: 1.9.10 + vue: 3.5.25(typescript@5.9.3) + transitivePeerDependencies: + - csstype + + '@tanstack/devtools-utils@0.2.3(@types/react@19.2.7)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))': dependencies: '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10) optionalDependencies: @@ -11482,6 +12571,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.13.1: {} + boxen@7.1.1: dependencies: ansi-align: 3.0.1 @@ -12365,6 +13456,10 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.1.2 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -14333,10 +15428,10 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - preact@10.28.2: {} - preact@10.28.1: {} + preact@10.28.2: {} + prelude-ls@1.2.1: {} premove@4.0.0: {} @@ -15045,6 +16140,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + strnum@2.1.2: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 From 72e5969ee272280399b829e3173f005832b9fcfd Mon Sep 17 00:00:00 2001 From: Muralidhar Reddy Challa Date: Mon, 12 Jan 2026 13:29:46 +0530 Subject: [PATCH 2/3] Update packages/typescript/ai-bedrock/package.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../ai-bedrock/live-tests/tool-test-haiku.ts | 2 +- .../ai-bedrock/live-tests/tool-test-nova.ts | 13 +++++++++--- .../typescript/ai-bedrock/src/model-meta.ts | 4 ++-- .../src/text/text-provider-options.ts | 21 +------------------ .../ai-bedrock/tests/model-meta.test.ts | 2 +- 5 files changed, 15 insertions(+), 27 deletions(-) diff --git a/packages/typescript/ai-bedrock/live-tests/tool-test-haiku.ts b/packages/typescript/ai-bedrock/live-tests/tool-test-haiku.ts index 537a57d34..4f87ab6bd 100644 --- a/packages/typescript/ai-bedrock/live-tests/tool-test-haiku.ts +++ b/packages/typescript/ai-bedrock/live-tests/tool-test-haiku.ts @@ -11,7 +11,7 @@ async function main() { const accessKeyId = process.env.AWS_ACCESS_KEY_ID ?? throwMissingEnv('AWS_ACCESS_KEY_ID') const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY ?? throwMissingEnv('AWS_SECRET_ACCESS_KEY') - const modelId = 'anthropic.claude-4-5-haiku-20251001-v1:0' + const modelId = 'anthropic.claude-haiku-4-5-20251001-v1:0' console.log(`Running tool test for: ${modelId}`) const stream = await chat({ diff --git a/packages/typescript/ai-bedrock/live-tests/tool-test-nova.ts b/packages/typescript/ai-bedrock/live-tests/tool-test-nova.ts index b335991ad..210f24bce 100644 --- a/packages/typescript/ai-bedrock/live-tests/tool-test-nova.ts +++ b/packages/typescript/ai-bedrock/live-tests/tool-test-nova.ts @@ -19,15 +19,22 @@ try { // .env.local not found } +function throwMissingEnv(name: string): never { + throw new Error(`Missing required environment variable: ${name}`) +} + async function testBedrockNovaToolCalling() { console.log('Testing Bedrock tool calling (Amazon Nova Pro)\n') + const accessKeyId = process.env.AWS_ACCESS_KEY_ID ?? throwMissingEnv('AWS_ACCESS_KEY_ID') + const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY ?? throwMissingEnv('AWS_SECRET_ACCESS_KEY') + const stream = await chat({ adapter: bedrockText('us.amazon.nova-pro-v1:0', { - region: process.env.AWS_REGION || 'us-west-2', + region: process.env.AWS_REGION || 'us-east-1', credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '', + accessKeyId, + secretAccessKey, } }), messages: [ diff --git a/packages/typescript/ai-bedrock/src/model-meta.ts b/packages/typescript/ai-bedrock/src/model-meta.ts index 0e82776a9..b41b2dc1b 100644 --- a/packages/typescript/ai-bedrock/src/model-meta.ts +++ b/packages/typescript/ai-bedrock/src/model-meta.ts @@ -50,7 +50,7 @@ export const BEDROCK_AMAZON_NOVA_MICRO_V1 = { export const BEDROCK_ANTHROPIC_CLAUDE_SONNET_4_5 = { name: 'claude-4-5-sonnet', - id: 'anthropic.claude-4-5-sonnet-20250929-v1:0', + id: 'anthropic.claude-sonnet-4-5-20250929-v1:0', context_window: 1_000_000, max_output_tokens: 64_000, supports: { @@ -61,7 +61,7 @@ export const BEDROCK_ANTHROPIC_CLAUDE_SONNET_4_5 = { export const BEDROCK_ANTHROPIC_CLAUDE_HAIKU_4_5 = { name: 'claude-4-5-haiku', - id: 'anthropic.claude-4-5-haiku-20251001-v1:0', + id: 'anthropic.claude-haiku-4-5-20251001-v1:0', context_window: 200_000, max_output_tokens: 64_000, supports: { diff --git a/packages/typescript/ai-bedrock/src/text/text-provider-options.ts b/packages/typescript/ai-bedrock/src/text/text-provider-options.ts index 4afafa45c..15152bb89 100644 --- a/packages/typescript/ai-bedrock/src/text/text-provider-options.ts +++ b/packages/typescript/ai-bedrock/src/text/text-provider-options.ts @@ -1,11 +1,3 @@ -export interface BedrockMCPOptions { - /** - * MCP servers to be utilized in this request - * Maximum of 20 servers - */ - mcp_servers?: Array -} - export interface BedrockStopSequencesOptions { /** * Custom text sequences that will cause the model to stop generating. @@ -44,18 +36,7 @@ export interface BedrockInferenceConfig { stopSequences?: Array } -export interface MCPServer { - name: string - url: string - type: 'url' - authorization_token?: string | null - tool_configuration: { - allowed_tools?: Array | null - enabled?: boolean | null - } | null -} - -export type BedrockTextProviderOptions = BedrockMCPOptions & +export type BedrockTextProviderOptions = BedrockStopSequencesOptions & BedrockThinkingOptions & BedrockSamplingOptions & { diff --git a/packages/typescript/ai-bedrock/tests/model-meta.test.ts b/packages/typescript/ai-bedrock/tests/model-meta.test.ts index 18cc8559a..aeefe9f32 100644 --- a/packages/typescript/ai-bedrock/tests/model-meta.test.ts +++ b/packages/typescript/ai-bedrock/tests/model-meta.test.ts @@ -45,7 +45,7 @@ describe('Bedrock Model Input Modality Type Assertions', () => { }) describe('Claude 4.5 Sonnet (text + image + document)', () => { - type Modalities = ResolveModalities + type Modalities = ResolveModalities type Message = ConstrainedModelMessage it('should allow TextPart, ImagePart, and DocumentPart', () => { From 2d0a6ac4ad12bba64480667dd61fedfb1aae8966 Mon Sep 17 00:00:00 2001 From: srmurali002 Date: Thu, 19 Mar 2026 10:25:24 +0530 Subject: [PATCH 3/3] fix(ai-bedrock): address PR review issues and sync with upstream main - Fix Claude native reasoning: Bedrock Converse API streams Claude thinking as delta.reasoningContent.text blocks, not XML tags (which are Nova-only). Claude reasoning was silently dropped before this fix. - Fix totalTokens: AWS SDK TokenUsage does have a totalTokens field; use it directly with a computed fallback instead of always recomputing. - Fix BedrockTextMetadata empty interface -> type alias (Biome lint) - Remove redundant doneEmitted dead-code flag - Add JSDoc to all public symbols (BedrockTextAdapter, bedrockText, createBedrockChat, model constants, provider option types, utils) - Expand test suite from 1 to 21 tests covering: Claude reasoningContent streaming, Nova tag parsing (split-chunk edge cases), tool call streaming, error handling, and message conversion - Add docs/adapters/bedrock.md with full adapter documentation - Add AWS Bedrock to docs/config.json sidebar - Add ai-bedrock to copy:readme script - Merge upstream TanStack/ai main (resolve knip.json conflict) --- docs/adapters/bedrock.md | 239 ++++++++ docs/config.json | 4 + package.json | 2 +- .../ai-bedrock/src/adapters/text.ts | 81 ++- .../typescript/ai-bedrock/src/bedrock-chat.ts | 27 + .../ai-bedrock/src/message-types.ts | 2 +- .../typescript/ai-bedrock/src/model-meta.ts | 22 + .../src/text/text-provider-options.ts | 15 + packages/typescript/ai-bedrock/src/utils.ts | 8 + .../ai-bedrock/tests/bedrock-adapter.test.ts | 515 ++++++++++++++++-- 10 files changed, 860 insertions(+), 55 deletions(-) create mode 100644 docs/adapters/bedrock.md diff --git a/docs/adapters/bedrock.md b/docs/adapters/bedrock.md new file mode 100644 index 000000000..78f6e9c75 --- /dev/null +++ b/docs/adapters/bedrock.md @@ -0,0 +1,239 @@ +--- +title: AWS Bedrock +id: bedrock-adapter +order: 9 +--- + +The AWS Bedrock adapter provides access to Amazon Bedrock's managed AI models, including Amazon Nova and Anthropic Claude models, via the unified Converse API. + +## Installation + +```bash +npm install @tanstack/ai-bedrock +``` + +## Basic Usage + +```typescript +import { chat } from "@tanstack/ai"; +import { bedrockText } from "@tanstack/ai-bedrock"; + +const stream = chat({ + adapter: bedrockText("amazon.nova-pro-v1:0"), + messages: [{ role: "user", content: "Hello!" }], +}); +``` + +## Basic Usage - Custom Credentials + +```typescript +import { chat } from "@tanstack/ai"; +import { createBedrockChat } from "@tanstack/ai-bedrock"; + +const adapter = createBedrockChat("amazon.nova-pro-v1:0", { + region: "us-east-1", + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, + }, +}); + +const stream = chat({ + adapter, + messages: [{ role: "user", content: "Hello!" }], +}); +``` + +## Environment Variables + +The `bedrockText()` factory reads AWS credentials automatically from environment variables: + +```bash +AWS_REGION=us-east-1 +AWS_ACCESS_KEY_ID=AKIA... +AWS_SECRET_ACCESS_KEY=... +AWS_SESSION_TOKEN=... # Optional, for temporary credentials +``` + +## Example: Chat Completion + +```typescript +import { chat, toServerSentEventsResponse } from "@tanstack/ai"; +import { bedrockText } from "@tanstack/ai-bedrock"; + +export async function POST(request: Request) { + const { messages } = await request.json(); + + const stream = chat({ + adapter: bedrockText("amazon.nova-pro-v1:0"), + messages, + }); + + return toServerSentEventsResponse(stream); +} +``` + +## Example: With Tools + +```typescript +import { chat, toolDefinition } from "@tanstack/ai"; +import { bedrockText } from "@tanstack/ai-bedrock"; +import { z } from "zod"; + +const getWeatherDef = toolDefinition({ + name: "get_weather", + description: "Get current weather for a city", + inputSchema: z.object({ + city: z.string(), + }), +}); + +const getWeather = getWeatherDef.server(async ({ city }) => { + return { temperature: 72, conditions: "sunny", city }; +}); + +const stream = chat({ + adapter: bedrockText("anthropic.claude-sonnet-4-5-20250929-v1:0"), + messages: [{ role: "user", content: "What's the weather in Paris?" }], + tools: [getWeather], +}); +``` + +## Thinking / Extended Reasoning + +Models that support thinking (Claude Sonnet 4.5, Claude Haiku 4.5, and Nova models) can be configured to show their reasoning process, streamed as `thinking` chunks: + +```typescript +import { chat } from "@tanstack/ai"; +import { bedrockText } from "@tanstack/ai-bedrock"; + +const stream = chat({ + adapter: bedrockText("anthropic.claude-sonnet-4-5-20250929-v1:0"), + messages: [{ role: "user", content: "Solve this step by step: 17 * 24" }], + modelOptions: { + thinking: { + type: "enabled", + budget_tokens: 2000, + }, + }, +}); + +for await (const chunk of stream) { + if (chunk.type === "thinking") { + process.stdout.write(`[thinking] ${chunk.delta}`); + } else if (chunk.type === "content") { + process.stdout.write(chunk.delta); + } +} +``` + +Nova models use a `reasoningConfig` approach but produce the same `thinking` stream chunks — the adapter normalises both automatically. + +## Multimodal Content + +Nova Pro, Nova Lite, and Claude models support image and document inputs: + +```typescript +import { chat } from "@tanstack/ai"; +import { bedrockText } from "@tanstack/ai-bedrock"; +import { readFileSync } from "fs"; + +const imageBytes = readFileSync("./photo.jpg"); + +const stream = chat({ + adapter: bedrockText("amazon.nova-pro-v1:0"), + messages: [ + { + role: "user", + content: [ + { + type: "image", + source: { type: "base64", value: imageBytes }, + metadata: { mediaType: "image/jpeg" }, + }, + { type: "text", content: "What do you see in this image?" }, + ], + }, + ], +}); +``` + +## Model Options + +```typescript +const stream = chat({ + adapter: bedrockText("amazon.nova-pro-v1:0"), + messages: [{ role: "user", content: "Hello!" }], + modelOptions: { + inferenceConfig: { + maxTokens: 1024, + temperature: 0.7, + topP: 0.9, + }, + stop_sequences: ["END"], + top_k: 50, + }, +}); +``` + +## Supported Models + +### Amazon Nova + +| Model | ID | Context | Inputs | +|-------|----|---------|--------| +| Nova Pro | `amazon.nova-pro-v1:0` | 300K | text, image, video, document | +| Nova Lite | `amazon.nova-lite-v1:0` | 300K | text, image, video, document | +| Nova Micro | `amazon.nova-micro-v1:0` | 128K | text only | + +### Anthropic Claude (via Bedrock) + +| Model | ID | Context | Inputs | +|-------|----|---------|--------| +| Claude Sonnet 4.5 | `anthropic.claude-sonnet-4-5-20250929-v1:0` | 1M | text, image, document | +| Claude Haiku 4.5 | `anthropic.claude-haiku-4-5-20251001-v1:0` | 200K | text, image, document | + +Both Claude models support extended thinking. + +## API Reference + +### `bedrockText(model, config?)` + +Creates a Bedrock text adapter using environment variable credentials. + +**Parameters:** + +- `model` - The Bedrock model ID (e.g., `amazon.nova-pro-v1:0`) +- `config` (optional) - Partial configuration object: + - `region` - AWS region (falls back to `AWS_REGION` / `AWS_DEFAULT_REGION`) + - `credentials.accessKeyId` - AWS access key (falls back to `AWS_ACCESS_KEY_ID`) + - `credentials.secretAccessKey` - AWS secret key (falls back to `AWS_SECRET_ACCESS_KEY`) + +**Returns:** A `BedrockTextAdapter` instance. + +### `createBedrockChat(model, config)` + +Creates a Bedrock text adapter with explicit credentials. + +**Parameters:** + +- `model` - The Bedrock model ID +- `config` - Full configuration object: + - `region` - AWS region (required) + - `credentials.accessKeyId` - AWS access key (required) + - `credentials.secretAccessKey` - AWS secret key (required) + +**Returns:** A `BedrockTextAdapter` instance. + +## Limitations + +- **Structured output**: Not yet supported via the Converse API (planned). +- **Nova Micro**: Text-only; does not support image, video, or document inputs. +- **Thinking for Claude**: Only supported on the first turn of a conversation. + +## Next Steps + +- [Getting Started](../getting-started/quick-start) - Learn the basics +- [Tools Guide](../guides/tools) - Learn about tools +- [Multimodal Content](../guides/multimodal-content) - Using images and documents +- [Other Adapters](./anthropic) - Explore other providers diff --git a/docs/config.json b/docs/config.json index 0ed12fee1..fda9a5a88 100644 --- a/docs/config.json +++ b/docs/config.json @@ -175,6 +175,10 @@ { "label": "Fal", "to": "adapters/fal" + }, + { + "label": "AWS Bedrock", + "to": "adapters/bedrock" } ] }, diff --git a/package.json b/package.json index 640744b04..fb01121ef 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "generate-docs": "node scripts/generate-docs.ts && pnpm run copy:readme", "generate:models": "tsx scripts/convert-openrouter-models.ts", "sync-docs-config": "node scripts/sync-docs-config.ts", - "copy:readme": "cp README.md packages/typescript/ai/README.md && cp README.md packages/typescript/ai-devtools/README.md && cp README.md packages/typescript/preact-ai-devtools/README.md && cp README.md packages/typescript/ai-client/README.md && cp README.md packages/typescript/ai-gemini/README.md && cp README.md packages/typescript/ai-ollama/README.md && cp README.md packages/typescript/ai-openai/README.md && cp README.md packages/typescript/ai-react/README.md && cp README.md packages/typescript/ai-react-ui/README.md && cp README.md packages/typescript/react-ai-devtools/README.md && cp README.md packages/typescript/solid-ai-devtools/README.md", + "copy:readme": "cp README.md packages/typescript/ai/README.md && cp README.md packages/typescript/ai-devtools/README.md && cp README.md packages/typescript/preact-ai-devtools/README.md && cp README.md packages/typescript/ai-client/README.md && cp README.md packages/typescript/ai-gemini/README.md && cp README.md packages/typescript/ai-ollama/README.md && cp README.md packages/typescript/ai-openai/README.md && cp README.md packages/typescript/ai-react/README.md && cp README.md packages/typescript/ai-react-ui/README.md && cp README.md packages/typescript/react-ai-devtools/README.md && cp README.md packages/typescript/solid-ai-devtools/README.md && cp README.md packages/typescript/ai-bedrock/README.md", "changeset": "changeset", "changeset:publish": "changeset publish", "changeset:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm format" diff --git a/packages/typescript/ai-bedrock/src/adapters/text.ts b/packages/typescript/ai-bedrock/src/adapters/text.ts index 01d7df4ae..9b164b4a3 100644 --- a/packages/typescript/ai-bedrock/src/adapters/text.ts +++ b/packages/typescript/ai-bedrock/src/adapters/text.ts @@ -28,16 +28,44 @@ import type { } from '@tanstack/ai' import type { BedrockTextProviderOptions } from '../text/text-provider-options' +/** + * Configuration for the AWS Bedrock client. + */ export interface BedrockTextConfig { + /** AWS region where Bedrock is accessed (e.g. `'us-east-1'`). */ region: string + /** AWS credentials used to authenticate requests. */ credentials: { + /** AWS access key ID. */ accessKeyId: string + /** AWS secret access key. */ secretAccessKey: string } } +/** + * Supported input modalities for Bedrock text adapters. + * Nova Micro only supports `'text'`; other models support the full set. + */ export type BedrockInputModalities = readonly ['text', 'image', 'video', 'document'] +/** + * Text adapter for Amazon Bedrock using the unified ConverseStream API. + * + * Supports Amazon Nova and Anthropic Claude models with streaming, tool calling, + * multimodal inputs (text, image, video, document), and extended thinking. + * + * @example + * ```typescript + * import { bedrockText } from '@tanstack/ai-bedrock' + * import { chat } from '@tanstack/ai' + * + * const stream = chat({ + * adapter: bedrockText('amazon.nova-pro-v1:0'), + * messages: [{ role: 'user', content: 'Hello!' }], + * }) + * ``` + */ export class BedrockTextAdapter< TModel extends BedrockModelId = BedrockModelId, > extends BaseTextAdapter< @@ -51,6 +79,10 @@ export class BedrockTextAdapter< private client: BedrockRuntimeClient + /** + * @param config - AWS region and credentials for the Bedrock client. + * @param model - The Bedrock model ID to use for requests. + */ constructor(config: BedrockTextConfig, model: TModel) { super({}, model) this.client = new BedrockRuntimeClient({ @@ -59,6 +91,12 @@ export class BedrockTextAdapter< }) } + /** + * Streams a chat completion from the Bedrock ConverseStream API. + * Yields {@link StreamChunk} events including `content`, `thinking`, `tool_call`, `done`, and `error`. + * + * @param options - Text generation options (messages, tools, modelOptions, etc.) + */ async *chatStream( options: TextOptions, ): AsyncIterable { @@ -131,6 +169,10 @@ export class BedrockTextAdapter< } } + /** + * Structured output is not yet supported for the Bedrock ConverseStream API. + * @throws Always rejects with a not-implemented error. + */ structuredOutput( _options: StructuredOutputOptions, ): Promise> { @@ -267,7 +309,6 @@ export class BedrockTextAdapter< let currentToolUseId = '' let lastStopReason: string | undefined let lastUsage: any | undefined - let doneEmitted = false // Universal tag parsing for any model that emits them // (Claude with showThinking, Nova with reasoningConfig or when using tools, etc.) @@ -341,6 +382,17 @@ export class BedrockTextAdapter< } + // Claude native reasoning via Bedrock Converse API. + // Streamed as delta.reasoningContent.text (NOT tags). + // The final chunk carries a delta.reasoningContent.signature — skip it, + // it's a tamper-proof token needed only when passing reasoning back in + // multi-turn conversations. + if (delta?.reasoningContent?.text !== undefined) { + const reasoningDelta = delta.reasoningContent.text + accumulatedThinking += reasoningDelta + yield { type: 'thinking', id, model: this.model, timestamp, delta: reasoningDelta, content: accumulatedThinking } + } + // Tool input (arguments) - comes as partial JSON string chunks if (delta?.toolUse?.input) { // Input is already a string, don't JSON.stringify it! @@ -399,21 +451,18 @@ export class BedrockTextAdapter< } // Emit final consolidated done event - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!doneEmitted) { - yield { - type: 'done', - id, - model: this.model, - timestamp, - finishReason: lastStopReason === 'tool_use' ? 'tool_calls' : 'stop', - usage: lastUsage ? { - promptTokens: lastUsage.inputTokens || 0, - completionTokens: lastUsage.outputTokens || 0, - totalTokens: lastUsage.totalTokens || 0, - } : undefined - } - doneEmitted = true + yield { + type: 'done', + id, + model: this.model, + timestamp, + finishReason: lastStopReason === 'tool_use' ? 'tool_calls' : 'stop', + usage: lastUsage ? { + promptTokens: lastUsage.inputTokens ?? 0, + completionTokens: lastUsage.outputTokens ?? 0, + // Use SDK-provided totalTokens when available; fall back to sum of parts + totalTokens: lastUsage.totalTokens ?? ((lastUsage.inputTokens ?? 0) + (lastUsage.outputTokens ?? 0)), + } : undefined } } } diff --git a/packages/typescript/ai-bedrock/src/bedrock-chat.ts b/packages/typescript/ai-bedrock/src/bedrock-chat.ts index 179786389..eeca61c23 100644 --- a/packages/typescript/ai-bedrock/src/bedrock-chat.ts +++ b/packages/typescript/ai-bedrock/src/bedrock-chat.ts @@ -3,12 +3,39 @@ import { getBedrockConfigFromEnv } from './utils' import type { BedrockTextConfig } from './adapters/text'; import type { BedrockModelId } from './model-meta' +/** + * Creates a Bedrock text adapter with an explicit full configuration. + * + * @param model - The Bedrock model ID (e.g. `'amazon.nova-pro-v1:0'`). + * @param config - Full AWS region and credentials configuration. + * @returns A configured {@link BedrockTextAdapter} instance. + */ export function createBedrockChat< TModel extends BedrockModelId, >(model: TModel, config: BedrockTextConfig): BedrockTextAdapter { return new BedrockTextAdapter(config, model) } +/** + * Creates a Bedrock text adapter, reading AWS credentials from environment variables + * (`AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`). + * Any values provided in `config` take precedence over environment variables. + * + * @param model - The Bedrock model ID (e.g. `'amazon.nova-pro-v1:0'`). + * @param config - Optional partial configuration to override environment variable defaults. + * @returns A configured {@link BedrockTextAdapter} instance. + * + * @example + * ```typescript + * import { bedrockText } from '@tanstack/ai-bedrock' + * import { chat } from '@tanstack/ai' + * + * const stream = chat({ + * adapter: bedrockText('amazon.nova-pro-v1:0'), + * messages: [{ role: 'user', content: 'Hello!' }], + * }) + * ``` + */ export function bedrockText( model: TModel, config?: Partial, diff --git a/packages/typescript/ai-bedrock/src/message-types.ts b/packages/typescript/ai-bedrock/src/message-types.ts index 17520c2fc..20ea41b3c 100644 --- a/packages/typescript/ai-bedrock/src/message-types.ts +++ b/packages/typescript/ai-bedrock/src/message-types.ts @@ -22,7 +22,7 @@ export interface BedrockImageMetadata { /** * Metadata for Bedrock text content parts. */ -export interface BedrockTextMetadata { } +export type BedrockTextMetadata = Record /** * Metadata for Bedrock document content parts. diff --git a/packages/typescript/ai-bedrock/src/model-meta.ts b/packages/typescript/ai-bedrock/src/model-meta.ts index b41b2dc1b..40b225770 100644 --- a/packages/typescript/ai-bedrock/src/model-meta.ts +++ b/packages/typescript/ai-bedrock/src/model-meta.ts @@ -1,12 +1,22 @@ +/** + * Metadata describing a Bedrock chat model's capabilities. + */ export interface BedrockModelMeta { + /** Human-readable short name for the model. */ name: string + /** Full Bedrock model ID used in API requests. */ id: string + /** Supported capabilities for this model. */ supports: { + /** Input modalities the model accepts. */ input: Array<'text' | 'image' | 'audio' | 'video' | 'document'> + /** Whether the model supports extended thinking / reasoning. */ thinking?: boolean } + /** Maximum number of tokens in the context window. */ context_window?: number + /** Maximum number of output tokens the model can generate. */ max_output_tokens?: number } @@ -14,6 +24,7 @@ export interface BedrockModelMeta { // Amazon Nova Models (Latest) // =========================== +/** Amazon Nova Pro v1 — multimodal model with 300K context. */ export const BEDROCK_AMAZON_NOVA_PRO_V1 = { name: 'nova-pro-v1', id: 'amazon.nova-pro-v1:0', @@ -24,6 +35,7 @@ export const BEDROCK_AMAZON_NOVA_PRO_V1 = { }, } as const satisfies BedrockModelMeta +/** Amazon Nova Lite v1 — cost-effective multimodal model with 300K context. */ export const BEDROCK_AMAZON_NOVA_LITE_V1 = { name: 'nova-lite-v1', id: 'amazon.nova-lite-v1:0', @@ -34,6 +46,7 @@ export const BEDROCK_AMAZON_NOVA_LITE_V1 = { }, } as const satisfies BedrockModelMeta +/** Amazon Nova Micro v1 — text-only model with 128K context, lowest latency. */ export const BEDROCK_AMAZON_NOVA_MICRO_V1 = { name: 'nova-micro-v1', id: 'amazon.nova-micro-v1:0', @@ -48,6 +61,7 @@ export const BEDROCK_AMAZON_NOVA_MICRO_V1 = { // Flagship Anthropic Models (V4) // =========================== +/** Anthropic Claude Sonnet 4.5 via Bedrock — 1M context, thinking support. */ export const BEDROCK_ANTHROPIC_CLAUDE_SONNET_4_5 = { name: 'claude-4-5-sonnet', id: 'anthropic.claude-sonnet-4-5-20250929-v1:0', @@ -59,6 +73,7 @@ export const BEDROCK_ANTHROPIC_CLAUDE_SONNET_4_5 = { }, } as const satisfies BedrockModelMeta +/** Anthropic Claude Haiku 4.5 via Bedrock — 200K context, fast and cost-effective, thinking support. */ export const BEDROCK_ANTHROPIC_CLAUDE_HAIKU_4_5 = { name: 'claude-4-5-haiku', id: 'anthropic.claude-haiku-4-5-20251001-v1:0', @@ -70,6 +85,7 @@ export const BEDROCK_ANTHROPIC_CLAUDE_HAIKU_4_5 = { }, } as const satisfies BedrockModelMeta +/** All supported Bedrock chat model IDs. */ export const BEDROCK_CHAT_MODELS = [ BEDROCK_AMAZON_NOVA_PRO_V1.id, BEDROCK_AMAZON_NOVA_LITE_V1.id, @@ -78,6 +94,10 @@ export const BEDROCK_CHAT_MODELS = [ BEDROCK_ANTHROPIC_CLAUDE_HAIKU_4_5.id, ] as const +/** + * Union of known Bedrock model IDs plus an open `string` escape hatch for + * models not yet listed in this package. + */ export type BedrockModelId = (typeof BEDROCK_CHAT_MODELS)[number] | (string & {}) /** @@ -95,5 +115,7 @@ export type BedrockModelInputModalitiesByName = { // Model Detection Helpers // =========================== +/** Returns `true` if the model ID refers to an Anthropic Claude model on Bedrock. */ export const isClaude = (model: string) => model.includes('anthropic.claude') +/** Returns `true` if the model ID refers to an Amazon Nova model on Bedrock. */ export const isNova = (model: string) => model.includes('amazon.nova') diff --git a/packages/typescript/ai-bedrock/src/text/text-provider-options.ts b/packages/typescript/ai-bedrock/src/text/text-provider-options.ts index 15152bb89..1b0a5845b 100644 --- a/packages/typescript/ai-bedrock/src/text/text-provider-options.ts +++ b/packages/typescript/ai-bedrock/src/text/text-provider-options.ts @@ -1,3 +1,4 @@ +/** Provider options controlling stop sequences for Bedrock models. */ export interface BedrockStopSequencesOptions { /** * Custom text sequences that will cause the model to stop generating. @@ -5,6 +6,7 @@ export interface BedrockStopSequencesOptions { stop_sequences?: Array } +/** Provider options for enabling or disabling extended thinking on Claude models. */ export interface BedrockThinkingOptions { /** * Configuration for enabling Claude's extended thinking. @@ -22,6 +24,7 @@ export interface BedrockThinkingOptions { } } +/** Provider options for token sampling on Bedrock models. */ export interface BedrockSamplingOptions { /** * Only sample from the top K options for each subsequent token. @@ -29,13 +32,25 @@ export interface BedrockSamplingOptions { top_k?: number } +/** + * Inference configuration passed directly to the Bedrock ConverseStream API. + * These override the top-level `maxTokens`, `temperature`, and `topP` options. + */ export interface BedrockInferenceConfig { + /** Maximum number of tokens to generate. */ maxTokens?: number + /** Sampling temperature (0–1). Higher values produce more varied output. */ temperature?: number + /** Nucleus sampling probability (0–1). */ topP?: number + /** Text sequences that cause the model to stop generating. */ stopSequences?: Array } +/** + * All provider-specific options for Bedrock text generation requests. + * Combines stop sequences, thinking, sampling, and low-level inference config. + */ export type BedrockTextProviderOptions = BedrockStopSequencesOptions & BedrockThinkingOptions & diff --git a/packages/typescript/ai-bedrock/src/utils.ts b/packages/typescript/ai-bedrock/src/utils.ts index b013a850c..42533c5eb 100644 --- a/packages/typescript/ai-bedrock/src/utils.ts +++ b/packages/typescript/ai-bedrock/src/utils.ts @@ -1,10 +1,18 @@ +/** + * Resolved AWS configuration used to initialise the Bedrock client. + */ export interface BedrockClientConfig { + /** AWS region (e.g. `'us-east-1'`). */ region?: string + /** AWS credentials. */ credentials?: { + /** AWS access key ID. */ accessKeyId: string + /** AWS secret access key. */ secretAccessKey: string + /** Temporary session token (for STS / assumed roles). */ sessionToken?: string } } diff --git a/packages/typescript/ai-bedrock/tests/bedrock-adapter.test.ts b/packages/typescript/ai-bedrock/tests/bedrock-adapter.test.ts index 1703c49aa..3c2328613 100644 --- a/packages/typescript/ai-bedrock/tests/bedrock-adapter.test.ts +++ b/packages/typescript/ai-bedrock/tests/bedrock-adapter.test.ts @@ -18,6 +18,33 @@ vi.mock('@aws-sdk/client-bedrock-runtime', () => { } }) +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeStream(events: Array) { + sendMock.mockResolvedValue({ + stream: (async function* () { + for (const event of events) { + await Promise.resolve() + yield event + } + })(), + }) +} + +async function collectChunks(stream: AsyncIterable): Promise { + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + return chunks +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + describe('BedrockTextAdapter', () => { let adapter: BedrockTextAdapter @@ -31,54 +58,468 @@ describe('BedrockTextAdapter', () => { describe('chatStream', () => { it('should handle streaming response', async () => { - // Mock ConverseStream API response format - const mockStream = [ - { - contentBlockDelta: { - delta: { text: 'Hello ' } - } - }, - { - contentBlockDelta: { - delta: { text: 'world' } - } - }, - { - messageStop: { - stopReason: 'end_turn' - } - }, - ] - - sendMock.mockResolvedValue({ - stream: (async function* () { - for (const event of mockStream) { - await Promise.resolve() - yield event - } - })(), - }) + makeStream([ + { contentBlockDelta: { delta: { text: 'Hello ' } } }, + { contentBlockDelta: { delta: { text: 'world' } } }, + { messageStop: { stopReason: 'end_turn' } }, + ]) - const stream = adapter.chatStream({ + const chunks = await collectChunks(adapter.chatStream({ model: 'anthropic.claude-3-sonnet-20240229-v1:0', messages: [{ role: 'user', content: 'Hi' }], - }) - - const result = [] - for await (const chunk of stream) { - result.push(chunk) - } + })) expect(ConverseStreamCommand).toHaveBeenCalled() - // Filter for content chunks only - const contentChunks = result.filter(c => c.type === 'content') + + const contentChunks = chunks.filter(c => c.type === 'content') expect(contentChunks).toEqual([ expect.objectContaining({ type: 'content', delta: 'Hello ' }), expect.objectContaining({ type: 'content', delta: 'world' }), ]) - const doneChunk = result.find(c => c.type === 'done') + expect(chunks.find(c => c.type === 'done')).toBeDefined() + }) + + it('should use SDK totalTokens when provided', async () => { + makeStream([ + { contentBlockDelta: { delta: { text: 'Hello' } } }, + { messageStop: { stopReason: 'end_turn' } }, + { metadata: { usage: { inputTokens: 10, outputTokens: 25, totalTokens: 35 } } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + const doneChunk = chunks.find(c => c.type === 'done') as any + expect(doneChunk.usage).toEqual({ + promptTokens: 10, + completionTokens: 25, + totalTokens: 35, + }) + }) + + it('should fall back to computing totalTokens when SDK omits it', async () => { + makeStream([ + { contentBlockDelta: { delta: { text: 'Hello' } } }, + { messageStop: { stopReason: 'end_turn' } }, + { metadata: { usage: { inputTokens: 10, outputTokens: 25 } } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + const doneChunk = chunks.find(c => c.type === 'done') expect(doneChunk).toBeDefined() + expect(doneChunk.usage).toEqual({ + promptTokens: 10, + completionTokens: 25, + totalTokens: 35, + }) + }) + + it('should emit done with tool_calls finishReason on tool_use stop', async () => { + makeStream([ + { contentBlockStart: { start: { toolUse: { toolUseId: 'tu-1', name: 'myTool' } } } }, + { contentBlockDelta: { delta: { toolUse: { input: '{"x":1}' } } } }, + { messageStop: { stopReason: 'tool_use' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + const doneChunk = chunks.find(c => c.type === 'done') + expect(doneChunk?.finishReason).toBe('tool_calls') + }) + }) + + // ----------------------------------------------------------------------- + // Claude native reasoning (delta.reasoningContent — Bedrock Converse API) + // ----------------------------------------------------------------------- + + describe('Claude native reasoning (reasoningContent blocks)', () => { + it('emits thinking chunks from delta.reasoningContent.text', async () => { + makeStream([ + { contentBlockDelta: { delta: { reasoningContent: { text: 'step one ' } } } }, + { contentBlockDelta: { delta: { reasoningContent: { text: 'step two' } } } }, + // signature delta — should be silently ignored + { contentBlockDelta: { delta: { reasoningContent: { signature: 'sig-abc' } } } }, + { contentBlockDelta: { delta: { text: 'answer' } } }, + { messageStop: { stopReason: 'end_turn' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Reason through this' }], + })) + + const thinkingChunks = chunks.filter(c => c.type === 'thinking') + expect(thinkingChunks).toHaveLength(2) + expect(thinkingChunks[0]).toMatchObject({ delta: 'step one ', content: 'step one ' }) + expect(thinkingChunks[1]).toMatchObject({ delta: 'step two', content: 'step one step two' }) + + // signature chunk must NOT produce any output + expect(chunks.filter(c => c.type === 'thinking')).toHaveLength(2) + + const contentChunks = chunks.filter(c => c.type === 'content') + expect(contentChunks).toHaveLength(1) + expect(contentChunks[0]).toMatchObject({ delta: 'answer' }) + }) + + it('does not emit thinking chunks for signature-only deltas', async () => { + makeStream([ + { contentBlockDelta: { delta: { reasoningContent: { signature: 'sig-xyz' } } } }, + { messageStop: { stopReason: 'end_turn' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + expect(chunks.filter(c => c.type === 'thinking')).toHaveLength(0) + }) + }) + + // ----------------------------------------------------------------------- + // Nova thinking tag parsing (text-based tags in delta.text) + // ----------------------------------------------------------------------- + + describe('Nova thinking tag parsing (text-based)', () => { + it('emits thinking and content chunks from a single chunk containing full tags', async () => { + makeStream([ + { contentBlockDelta: { delta: { text: 'some thoughtanswer' } } }, + { messageStop: { stopReason: 'end_turn' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + const thinkingChunks = chunks.filter(c => c.type === 'thinking') + const contentChunks = chunks.filter(c => c.type === 'content') + + expect(thinkingChunks).toHaveLength(1) + expect(thinkingChunks[0]).toMatchObject({ + type: 'thinking', + delta: 'some thought', + content: 'some thought', + }) + + expect(contentChunks).toHaveLength(1) + expect(contentChunks[0]).toMatchObject({ + type: 'content', + delta: 'answer', + content: 'answer', + }) + }) + + it('emits content before thinking tag then thinking content', async () => { + makeStream([ + { contentBlockDelta: { delta: { text: 'prefixinside' } } }, + { messageStop: { stopReason: 'end_turn' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + const thinkingChunks = chunks.filter(c => c.type === 'thinking') + const contentChunks = chunks.filter(c => c.type === 'content') + + expect(contentChunks[0]).toMatchObject({ delta: 'prefix' }) + expect(thinkingChunks[0]).toMatchObject({ delta: 'inside' }) + }) + + it('handles opening thinking tag split across two chunks', async () => { + makeStream([ + { contentBlockDelta: { delta: { text: 'thoughttext' } } }, + { messageStop: { stopReason: 'end_turn' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + const thinkingChunks = chunks.filter(c => c.type === 'thinking') + const contentChunks = chunks.filter(c => c.type === 'content') + + expect(thinkingChunks.length).toBeGreaterThan(0) + expect(thinkingChunks.at(-1)?.content).toBe('thought') + + expect(contentChunks.length).toBeGreaterThan(0) + expect(contentChunks.at(-1)?.content).toBe('text') + }) + + it('handles closing thinking tag split across two chunks', async () => { + makeStream([ + { contentBlockDelta: { delta: { text: 'thoughtafter' } } }, + { messageStop: { stopReason: 'end_turn' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + const thinkingChunks = chunks.filter(c => c.type === 'thinking') + const contentChunks = chunks.filter(c => c.type === 'content') + + expect(thinkingChunks.at(-1)?.content).toBe('thought') + expect(contentChunks.length).toBeGreaterThan(0) + expect(contentChunks.at(-1)?.content).toBe('after') + }) + + it('accumulates thinking content across multiple chunks within tags', async () => { + makeStream([ + { contentBlockDelta: { delta: { text: 'part one ' } } }, + { contentBlockDelta: { delta: { text: 'part two' } } }, + { messageStop: { stopReason: 'end_turn' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + const thinkingChunks = chunks.filter(c => c.type === 'thinking') + expect(thinkingChunks.length).toBeGreaterThan(0) + // Final accumulated content should contain both parts + expect(thinkingChunks.at(-1)?.content).toBe('part one part two') + }) + + it('does not emit thinking chunks when no thinking tags are present', async () => { + makeStream([ + { contentBlockDelta: { delta: { text: 'plain text response' } } }, + { messageStop: { stopReason: 'end_turn' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + expect(chunks.filter(c => c.type === 'thinking')).toHaveLength(0) + expect(chunks.filter(c => c.type === 'content')).toHaveLength(1) + }) + }) + + // ----------------------------------------------------------------------- + // Tool call streaming + // ----------------------------------------------------------------------- + + describe('tool call streaming', () => { + it('emits tool_call chunk with name and id when contentBlockStart fires', async () => { + makeStream([ + { contentBlockStart: { start: { toolUse: { toolUseId: 'tool-abc', name: 'getWeather' } } } }, + { messageStop: { stopReason: 'tool_use' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'weather?' }], + })) + + const toolCallChunks = chunks.filter(c => c.type === 'tool_call') + expect(toolCallChunks.length).toBeGreaterThan(0) + expect(toolCallChunks[0]).toMatchObject({ + type: 'tool_call', + index: 0, + toolCall: { + id: 'tool-abc', + type: 'function', + function: { name: 'getWeather', arguments: '' }, + }, + }) + }) + + it('emits argument delta chunks for tool input', async () => { + makeStream([ + { contentBlockStart: { start: { toolUse: { toolUseId: 'tool-abc', name: 'getWeather' } } } }, + { contentBlockDelta: { delta: { toolUse: { input: '{"city"' } } } }, + { contentBlockDelta: { delta: { toolUse: { input: ':"Paris"}' } } } }, + { messageStop: { stopReason: 'tool_use' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'weather?' }], + })) + + // Argument delta chunks have an empty name (only the start chunk has the name) + const argChunks = chunks.filter( + c => c.type === 'tool_call' && c.toolCall.function.name === '' + ) + expect(argChunks).toHaveLength(2) + expect(argChunks[0].toolCall.function.arguments).toBe('{"city"') + expect(argChunks[1].toolCall.function.arguments).toBe(':"Paris"}') + }) + + it('increments index for each new tool call', async () => { + makeStream([ + { contentBlockStart: { start: { toolUse: { toolUseId: 'tool-1', name: 'toolA' } } } }, + { contentBlockStart: { start: { toolUse: { toolUseId: 'tool-2', name: 'toolB' } } } }, + { messageStop: { stopReason: 'tool_use' } }, + ]) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + // Start chunks have non-empty names + const startChunks = chunks.filter( + c => c.type === 'tool_call' && c.toolCall.function.name !== '' + ) + expect(startChunks[0]).toMatchObject({ index: 0, toolCall: { id: 'tool-1' } }) + expect(startChunks[1]).toMatchObject({ index: 1, toolCall: { id: 'tool-2' } }) + }) + }) + + // ----------------------------------------------------------------------- + // Error handling + // ----------------------------------------------------------------------- + + describe('error handling', () => { + it('yields an error chunk when no stream is returned', async () => { + sendMock.mockResolvedValue({ stream: undefined }) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toMatchObject({ + type: 'error', + error: { message: 'No stream received from Bedrock' }, + }) + }) + + it('yields an error chunk when client.send throws', async () => { + sendMock.mockRejectedValue(new Error('Network failure')) + + const chunks = await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + })) + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toMatchObject({ + type: 'error', + error: { message: 'Network failure' }, + }) + }) + }) + + // ----------------------------------------------------------------------- + // Message conversion (verified via ConverseStreamCommand call args) + // ----------------------------------------------------------------------- + + describe('message conversion', () => { + it('converts tool result message to Converse toolResult format', async () => { + makeStream([{ messageStop: { stopReason: 'end_turn' } }]) + + await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [ + { role: 'user', content: 'Use tool' }, + { + role: 'assistant', + content: '', + toolCalls: [{ id: 'tu-1', type: 'function', function: { name: 'myTool', arguments: '{}' } }], + }, + { role: 'tool', toolCallId: 'tu-1', content: '{"result":"ok"}' } as any, + ], + })) + + const [command] = (ConverseStreamCommand as any).mock.calls[0] + const toolResultMsg = command.messages.find( + (m: any) => m.content?.[0]?.toolResult !== undefined + ) + expect(toolResultMsg).toBeDefined() + expect(toolResultMsg.role).toBe('user') + expect(toolResultMsg.content[0].toolResult).toMatchObject({ + toolUseId: 'tu-1', + status: 'success', + }) + }) + + it('marks tool result as failure when message has error status', async () => { + makeStream([{ messageStop: { stopReason: 'end_turn' } }]) + + await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [ + { role: 'user', content: 'Use tool' }, + { + role: 'assistant', + content: '', + toolCalls: [{ id: 'tu-1', type: 'function', function: { name: 'myTool', arguments: '{}' } }], + }, + { role: 'tool', toolCallId: 'tu-1', content: 'failed', status: 'error' } as any, + ], + })) + + const [command] = (ConverseStreamCommand as any).mock.calls[0] + const toolResultMsg = command.messages.find( + (m: any) => m.content?.[0]?.toolResult !== undefined + ) + expect(toolResultMsg.content[0].toolResult.status).toBe('failure') + }) + + it('converts assistant message with tool calls to Converse content blocks', async () => { + makeStream([{ messageStop: { stopReason: 'end_turn' } }]) + + await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [ + { role: 'user', content: 'Hi' }, + { + role: 'assistant', + content: 'Let me check that', + toolCalls: [{ id: 'tc-1', type: 'function', function: { name: 'lookup', arguments: '{"q":"foo"}' } }], + }, + { role: 'tool', toolCallId: 'tc-1', content: 'result' } as any, + ], + })) + + const [command] = (ConverseStreamCommand as any).mock.calls[0] + const assistantMsg = command.messages.find( + (m: any) => m.role === 'assistant' && m.content?.some((b: any) => b.toolUse) + ) + expect(assistantMsg).toBeDefined() + expect(assistantMsg.content).toEqual( + expect.arrayContaining([ + expect.objectContaining({ text: 'Let me check that' }), + expect.objectContaining({ + toolUse: expect.objectContaining({ name: 'lookup', toolUseId: 'tc-1' }), + }), + ]) + ) + }) + + it('passes system prompts to ConverseStreamCommand', async () => { + makeStream([{ messageStop: { stopReason: 'end_turn' } }]) + + await collectChunks(adapter.chatStream({ + model: 'anthropic.claude-3-sonnet-20240229-v1:0', + messages: [{ role: 'user', content: 'Hi' }], + systemPrompts: ['You are a helpful assistant.'], + })) + + const [command] = (ConverseStreamCommand as any).mock.calls[0] + expect(command.system).toEqual([{ text: 'You are a helpful assistant.' }]) }) }) })