Skip to content

Commit b7f9e35

Browse files
committed
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.
1 parent 0e37d8b commit b7f9e35

File tree

20 files changed

+2444
-18
lines changed

20 files changed

+2444
-18
lines changed

.changeset/bedrock-adapter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/ai-bedrock": minor
3+
---
4+
5+
Add Amazon Bedrock adapter.

knip.json

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
{
22
"$schema": "https://unpkg.com/knip@5/schema.json",
3-
"ignoreDependencies": ["@faker-js/faker"],
4-
"ignoreWorkspaces": ["examples/**", "testing/**", "**/smoke-tests/**"],
3+
"ignoreDependencies": [
4+
"@faker-js/faker"
5+
],
6+
"ignoreWorkspaces": [
7+
"examples/**",
8+
"testing/**",
9+
"**/smoke-tests/**"
10+
],
511
"ignore": [
12+
"packages/typescript/ai-bedrock/live-tests/**",
613
"packages/typescript/ai-openai/live-tests/**",
714
"packages/typescript/ai-openai/src/**/*.test.ts",
815
"packages/typescript/ai-openai/src/audio/audio-provider-options.ts",
@@ -19,19 +26,29 @@
1926
"ignore": []
2027
},
2128
"packages/typescript/ai-anthropic": {
22-
"ignore": ["src/tools/**"]
29+
"ignore": [
30+
"src/tools/**"
31+
]
2332
},
2433
"packages/typescript/ai-gemini": {
25-
"ignore": ["src/tools/**"]
34+
"ignore": [
35+
"src/tools/**"
36+
]
2637
},
2738
"packages/typescript/ai-openai": {
28-
"ignore": ["src/tools/**"]
39+
"ignore": [
40+
"src/tools/**"
41+
]
2942
},
3043
"packages/typescript/ai-react-ui": {
31-
"ignoreDependencies": ["react-dom"]
44+
"ignoreDependencies": [
45+
"react-dom"
46+
]
3247
},
3348
"packages/typescript/ai-vue-ui": {
34-
"ignore": ["src/use-chat-context.ts"]
49+
"ignore": [
50+
"src/use-chat-context.ts"
51+
]
3552
}
3653
}
37-
}
54+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Bedrock Live Tests
2+
3+
These tests verify that the Bedrock adapter correctly handles tool calling and multimodal inputs with various models (Nova, Claude).
4+
5+
## Setup
6+
7+
1. Create a `.env.local` file in this directory with your AWS credentials:
8+
9+
```
10+
AWS_ACCESS_KEY_ID=...
11+
AWS_SECRET_ACCESS_KEY=...
12+
AWS_REGION=us-east-1
13+
```
14+
15+
2. Install dependencies:
16+
```bash
17+
pnpm install
18+
```
19+
20+
## Tests
21+
22+
### `tool-test.ts`
23+
Tests basic tool calling with Claude 3.5 Sonnet.
24+
25+
### `tool-test-nova.ts`
26+
Tests Amazon Nova Pro with multimodal inputs (if applicable) and tool calling.
27+
28+
## Running Tests
29+
30+
```bash
31+
# Run Claude tool test
32+
pnpm test
33+
34+
# Run Nova tool test
35+
pnpm test:nova
36+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "@tanstack/ai-bedrock-live-tests",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"test": "tsx tool-test.ts",
8+
"test:nova": "tsx tool-test-nova.ts"
9+
},
10+
"devDependencies": {
11+
"tsx": "^4.7.1",
12+
"typescript": "^5.4.2",
13+
"zod": "^4.2.1"
14+
}
15+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// import 'dotenv/config'
2+
import { bedrockText } from '../src/bedrock-chat'
3+
import { z } from 'zod'
4+
import { chat } from '@tanstack/ai'
5+
6+
async function main() {
7+
const modelId = 'us.anthropic.claude-haiku-4-5-20251001-v1:0'
8+
console.log(`Running tool test for: ${modelId}`)
9+
10+
const stream = await chat({
11+
adapter: bedrockText(modelId, {
12+
region: process.env.AWS_REGION || 'us-east-1',
13+
credentials: {
14+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
15+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
16+
},
17+
}),
18+
modelOptions: {
19+
thinking: {
20+
type: 'enabled',
21+
budget_tokens: 1024
22+
}
23+
},
24+
messages: [
25+
{
26+
role: 'user',
27+
content: 'Use the `get_weather` tool to find the weather in New York and explain it.',
28+
},
29+
],
30+
tools: [
31+
{
32+
name: 'get_weather',
33+
description: 'Get the current weather in a location',
34+
inputSchema: z.object({
35+
location: z.string().describe('The city and state, e.g. New York, NY'),
36+
}),
37+
execute: async ({ location }) => {
38+
console.log(`\n[TOOL Weather] Fetching weather for ${location}...`)
39+
return {
40+
temperature: 45,
41+
unit: 'F',
42+
condition: 'Cloudy',
43+
}
44+
},
45+
},
46+
],
47+
stream: true
48+
})
49+
50+
let finalContent = ''
51+
let hasThinking = false
52+
let toolCallCount = 0
53+
54+
console.log('--- Stream Output ---')
55+
for await (const chunk of stream) {
56+
if (chunk.type === 'thinking') {
57+
hasThinking = true
58+
} else if (chunk.type === 'content') {
59+
process.stdout.write(chunk.delta)
60+
finalContent += chunk.delta
61+
} else if (chunk.type === 'tool_call') {
62+
toolCallCount++
63+
}
64+
}
65+
66+
console.log('--- Results ---')
67+
console.log('Thinking:', hasThinking)
68+
console.log('Tool calls:', toolCallCount)
69+
console.log('Content length:', finalContent.length)
70+
71+
if (!finalContent || finalContent.trim().length === 0) {
72+
console.error('Test failed: No final content')
73+
process.exit(1)
74+
}
75+
76+
console.log('Test passed')
77+
}
78+
79+
main().catch(console.error)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { bedrockText } from '../src/index'
2+
import { z } from 'zod'
3+
import { readFileSync } from 'fs'
4+
import { join, dirname } from 'path'
5+
import { fileURLToPath } from 'url'
6+
import { chat } from '@tanstack/ai'
7+
8+
// Load environment variables from .env.local manually
9+
const __dirname = dirname(fileURLToPath(import.meta.url))
10+
try {
11+
const envContent = readFileSync(join(__dirname, '.env.local'), 'utf-8')
12+
envContent.split('\n').forEach((line) => {
13+
const match = line.match(/^([^=]+)=(.*)$/)
14+
if (match) {
15+
process.env[match[1].trim()] = match[2].trim()
16+
}
17+
})
18+
} catch (e) {
19+
// .env.local not found
20+
}
21+
22+
async function testBedrockNovaToolCalling() {
23+
console.log('Testing Bedrock tool calling (Amazon Nova Pro)\n')
24+
25+
const stream = await chat({
26+
adapter: bedrockText('us.amazon.nova-pro-v1:0', {
27+
region: process.env.AWS_REGION || 'us-west-2',
28+
credentials: {
29+
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
30+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
31+
}
32+
}),
33+
messages: [
34+
{
35+
role: 'user',
36+
content: 'Use the `get_temperature` tool to find the temperature in New York and explain why it is the way it is.',
37+
},
38+
],
39+
tools: [
40+
{
41+
name: 'get_temperature',
42+
description: 'Get the current temperature for a specific location',
43+
inputSchema: z.object({
44+
location: z.string().describe('The city or location'),
45+
unit: z.enum(['celsius', 'fahrenheit']).describe('The temperature unit'),
46+
}),
47+
execute: async ({ location, unit }: { location: string; unit: string }) => {
48+
console.log(`\n[TOOL Temperature] Fetching for ${location}...`)
49+
return {
50+
temperature: 45,
51+
unit: unit,
52+
condition: 'Cloudy',
53+
}
54+
},
55+
},
56+
],
57+
stream: true,
58+
})
59+
60+
let finalContent = ''
61+
let hasThinking = false
62+
let toolCallCount = 0
63+
64+
console.log('--- Stream Output ---')
65+
for await (const chunk of stream) {
66+
if (chunk.type === 'thinking') {
67+
hasThinking = true
68+
} else if (chunk.type === 'content') {
69+
process.stdout.write(chunk.delta)
70+
finalContent += chunk.delta
71+
} else if (chunk.type === 'tool_call') {
72+
toolCallCount++
73+
}
74+
}
75+
76+
console.log('--- Test Results ---')
77+
console.log('Thinking detected:', hasThinking)
78+
console.log('Tool calls:', toolCallCount)
79+
console.log('Final content length:', finalContent.length)
80+
81+
if (!hasThinking) {
82+
console.warn('Warning: No thinking blocks detected')
83+
}
84+
85+
if (finalContent.includes('<thinking>')) {
86+
console.error('Test failed: Thinking tags found in final content')
87+
process.exit(1)
88+
}
89+
90+
if (!finalContent || finalContent.trim().length === 0) {
91+
console.error('Test failed: No final content - model should explain the temperature')
92+
process.exit(1)
93+
}
94+
95+
console.log('Test passed')
96+
}
97+
98+
testBedrockNovaToolCalling()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// import 'dotenv/config'
2+
import { bedrockText } from '../src/bedrock-chat'
3+
import { z } from 'zod'
4+
import { chat } from '@tanstack/ai'
5+
6+
async function main() {
7+
const modelId = 'us.anthropic.claude-sonnet-4-5-20250929-v1:0'
8+
console.log(`Running tool test for: ${modelId}`)
9+
10+
const stream = await chat({
11+
adapter: bedrockText(modelId, {
12+
region: process.env.AWS_REGION || 'us-east-1',
13+
credentials: {
14+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
15+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
16+
},
17+
}),
18+
modelOptions: {
19+
thinking: {
20+
type: 'enabled',
21+
budget_tokens: 2048
22+
}
23+
},
24+
messages: [
25+
{
26+
role: 'user',
27+
content: 'Use the `get_weather` tool to find the weather in San Francisco and then explain why it is the way it is.',
28+
},
29+
],
30+
tools: [
31+
{
32+
name: 'get_weather',
33+
description: 'Get the current weather in a location',
34+
inputSchema: z.object({
35+
location: z.string().describe('The city and state, e.g. San Francisco, CA'),
36+
}),
37+
execute: async ({ location }) => {
38+
console.log(`\n[TOOL Weather] Fetching weather for ${location}...`)
39+
return {
40+
temperature: 72,
41+
unit: 'F',
42+
condition: 'Sunny',
43+
}
44+
},
45+
},
46+
],
47+
stream: true
48+
})
49+
50+
let finalContent = ''
51+
let hasThinking = false
52+
let toolCallCount = 0
53+
let doneCount = 0
54+
55+
console.log('--- Stream Output ---')
56+
for await (const chunk of stream) {
57+
if (chunk.type === 'thinking') {
58+
hasThinking = true
59+
} else if (chunk.type === 'content') {
60+
process.stdout.write(chunk.delta)
61+
finalContent += chunk.delta
62+
} else if (chunk.type === 'tool_call') {
63+
toolCallCount++
64+
console.log('\nTool call:', chunk.toolCall.function.name)
65+
} else if (chunk.type === 'done') {
66+
doneCount++
67+
}
68+
}
69+
70+
console.log('--- Test Results ---')
71+
console.log('Thinking detected:', hasThinking)
72+
console.log('Tool calls:', toolCallCount)
73+
console.log('Done events:', doneCount)
74+
console.log('Final content length:', finalContent.length)
75+
76+
if (!hasThinking) {
77+
console.error('Test failed: No thinking blocks detected for Claude 4.5')
78+
process.exit(1)
79+
}
80+
81+
if (toolCallCount === 0) {
82+
console.error('Test failed: No tool calls detected')
83+
process.exit(1)
84+
}
85+
86+
if (!finalContent || finalContent.trim().length === 0) {
87+
console.error('Test failed: Final content is empty - model should explain weather after getting tool results')
88+
process.exit(1)
89+
}
90+
91+
if (!finalContent.toLowerCase().includes('72') && !finalContent.toLowerCase().includes('sunny')) {
92+
console.warn('Warning: Final content does not mention the weather data')
93+
}
94+
95+
console.log('Test passed')
96+
}
97+
98+
main().catch(console.error)

0 commit comments

Comments
 (0)