-
-
Notifications
You must be signed in to change notification settings - Fork 119
feat(ai-bedrock): add AWS Bedrock adapter with ConverseStream and tool-calling support #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@tanstack/ai-bedrock": minor | ||
| --- | ||
|
|
||
| Add Amazon Bedrock adapter. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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-haiku-4-5-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) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,105 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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-east-1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| credentials: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accessKeyId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| secretAccessKey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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('<thinking>')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+83
to
+100
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider asserting that the tool was actually invoked. This test specifically validates "Bedrock tool calling" but doesn't assert that 🐛 Suggested assertion console.log('--- Test Results ---')
console.log('Thinking detected:', hasThinking)
console.log('Tool calls:', toolCallCount)
console.log('Final content length:', finalContent.length)
+ if (toolCallCount === 0) {
+ console.error('Test failed: No tool calls detected - model should have called get_temperature')
+ process.exit(1)
+ }
+
if (!hasThinking) {
console.warn('Warning: No thinking blocks detected')
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Test passed') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| testBedrockNovaToolCalling().catch(console.error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.