From 885b05b8cc6f200f5b7da9489b3c5de8b73ebc58 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 19:05:11 +0200 Subject: [PATCH 01/46] chore(ai-bedrock): scaffold package --- packages/ai-bedrock/README.md | 3 + packages/ai-bedrock/package.json | 53 +++ packages/ai-bedrock/src/index.ts | 1 + packages/ai-bedrock/tsconfig.json | 8 + packages/ai-bedrock/vite.config.ts | 29 ++ pnpm-lock.yaml | 566 ++++++++++++++++++++++++++++- pnpm-workspace.yaml | 2 + 7 files changed, 660 insertions(+), 2 deletions(-) create mode 100644 packages/ai-bedrock/README.md create mode 100644 packages/ai-bedrock/package.json create mode 100644 packages/ai-bedrock/src/index.ts create mode 100644 packages/ai-bedrock/tsconfig.json create mode 100644 packages/ai-bedrock/vite.config.ts diff --git a/packages/ai-bedrock/README.md b/packages/ai-bedrock/README.md new file mode 100644 index 000000000..7b9b5c2e1 --- /dev/null +++ b/packages/ai-bedrock/README.md @@ -0,0 +1,3 @@ +# @tanstack/ai-bedrock + +Amazon Bedrock adapter for TanStack AI via Bedrock's OpenAI-compatible APIs. See the docs for usage. diff --git a/packages/ai-bedrock/package.json b/packages/ai-bedrock/package.json new file mode 100644 index 000000000..769c8d968 --- /dev/null +++ b/packages/ai-bedrock/package.json @@ -0,0 +1,53 @@ +{ + "name": "@tanstack/ai-bedrock", + "version": "0.0.1", + "type": "module", + "description": "Amazon Bedrock adapter for TanStack AI — OpenAI-compatible chat, responses, tools, and reasoning.", + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/ai.git", + "directory": "packages/ai-bedrock" + }, + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", + "exports": { + ".": { + "types": "./dist/esm/index.d.ts", + "import": "./dist/esm/index.js" + }, + "./sigv4": { + "types": "./dist/esm/sigv4/index.d.ts", + "import": "./dist/esm/sigv4/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", "ai-sdk", "typescript", "tanstack", "bedrock", "aws", "adapter", "llm", "chat", "tool-calling"], + "devDependencies": { + "@vitest/coverage-v8": "4.0.14", + "vite": "^7.3.3" + }, + "peerDependencies": { + "@tanstack/ai": "workspace:^", + "zod": "^4.0.0" + }, + "dependencies": { + "@tanstack/ai-utils": "workspace:*", + "@tanstack/openai-base": "workspace:*", + "openai": "^6.9.1" + }, + "optionalDependencies": { + "aws-sigv4-fetch": "4.3.1" + } +} diff --git a/packages/ai-bedrock/src/index.ts b/packages/ai-bedrock/src/index.ts new file mode 100644 index 000000000..336ce12bb --- /dev/null +++ b/packages/ai-bedrock/src/index.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/ai-bedrock/tsconfig.json b/packages/ai-bedrock/tsconfig.json new file mode 100644 index 000000000..c38689f4e --- /dev/null +++ b/packages/ai-bedrock/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src", "tests"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ai-bedrock/vite.config.ts b/packages/ai-bedrock/vite.config.ts new file mode 100644 index 000000000..813bf1f10 --- /dev/null +++ b/packages/ai-bedrock/vite.config.ts @@ -0,0 +1,29 @@ +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/sigv4/index.ts'], + srcDir: './src', + cjs: false, + }), +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbdb8c90d..4c861ff1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1026,6 +1026,35 @@ importers: specifier: ^4.2.0 version: 4.2.1 + packages/ai-bedrock: + dependencies: + '@tanstack/ai': + specifier: workspace:^ + version: link:../ai + '@tanstack/ai-utils': + specifier: workspace:* + version: link:../ai-utils + '@tanstack/openai-base': + specifier: workspace:* + version: link:../openai-base + openai: + specifier: ^6.9.1 + version: 6.10.0(ws@8.19.0)(zod@4.3.6) + zod: + specifier: ^4.0.0 + version: 4.3.6 + devDependencies: + '@vitest/coverage-v8': + specifier: 4.0.14 + version: 4.0.14(vitest@4.0.14(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.15))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + vite: + specifier: ^7.3.3 + version: 7.3.3(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + optionalDependencies: + aws-sigv4-fetch: + specifier: 4.3.1 + version: 4.3.1 + packages/ai-client: dependencies: '@tanstack/ai': @@ -2089,6 +2118,87 @@ 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/core@3.974.15': + resolution: {integrity: sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.41': + resolution: {integrity: sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.43': + resolution: {integrity: sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.45': + resolution: {integrity: sha512-sJe5ZWibO4s7RWjFQ8Zol76KxoJcIYyEZH1/wxQSBMSIAAxzaJ8cS/ITAaIHWUQvDKQdt18+cJAHKWB7n1Jmrg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.45': + resolution: {integrity: sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.46': + resolution: {integrity: sha512-cS4w0jzDRb1jOlkiJS3y80OxddHzkky/MN9k3NYs5jganNKVLjF0lpvjlwS118oGMr3cdAfOlVdo8gLurTSE7w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.41': + resolution: {integrity: sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.45': + resolution: {integrity: sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.45': + resolution: {integrity: sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.13': + resolution: {integrity: sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.30': + resolution: {integrity: sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1056.0': + resolution: {integrity: sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.9': + resolution: {integrity: sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.26': + resolution: {integrity: sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -4195,6 +4305,9 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@nodable/entities@2.1.1': + resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -6201,6 +6314,78 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@smithy/core@3.24.5': + resolution: {integrity: sha512-Kt8phUg45M15EjhYAbZ+fFikYneijLu9Liugz8ZsYz2i8j0hzGv27LWKpEHYRfvj+LyCOSijpcR/2i8RouV+cA==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.3.6': + resolution: {integrity: sha512-tHhdiWZfG1ZIh2YcRfPJmY2gHcBmqbAzqm3ER4TIDFYsSEqTD5tICT7cgQ/kI8LRakxp12myOYyK68XPn7MnHw==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.4.5': + resolution: {integrity: sha512-SK3VMeH0fibgdTg2QeB+O4p7Yy/2E5HBOHJeC58FshkDdeuX8lOgO7PfjYfLyPLP1ch55j91cQqKBzDS0mRjSQ==} + 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@3.0.0': + resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} + engines: {node: '>=16.0.0'} + + '@smithy/node-http-handler@4.7.5': + resolution: {integrity: sha512-3dA9TQ+ybRSZ/m0wnbZhiBy4Dezjgq1Ib/ZZrYTpJDBgpoLLU/SDzZc/g0x0MNAdOJe1wPcM+x2PBRmoOur+Sw==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@4.1.8': + resolution: {integrity: sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==} + engines: {node: '>=16.0.0'} + + '@smithy/signature-v4@3.1.2': + resolution: {integrity: sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA==} + engines: {node: '>=16.0.0'} + + '@smithy/signature-v4@5.4.5': + resolution: {integrity: sha512-QBJKWGqIknH0dc9LWpfH1mkdokAx6iXYN3UcQ3eY6uIEyScuoQAhfl94ge7ozUy9WgFUdE8xsvwBjaYBbWmPNA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@3.7.2': + resolution: {integrity: sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==} + engines: {node: '>=16.0.0'} + + '@smithy/types@4.14.2': + resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} + 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@3.0.0': + resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==} + engines: {node: '>=16.0.0'} + + '@smithy/util-hex-encoding@3.0.0': + resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} + engines: {node: '>=16.0.0'} + + '@smithy/util-middleware@3.0.11': + resolution: {integrity: sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow==} + engines: {node: '>=16.0.0'} + + '@smithy/util-uri-escape@3.0.0': + resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} + engines: {node: '>=16.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@3.0.0': + resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} + engines: {node: '>=16.0.0'} + '@solid-devtools/debugger@0.28.1': resolution: {integrity: sha512-6qIUI6VYkXoRnL8oF5bvh2KgH71qlJ18hNw/mwSyY6v48eb80ZR48/5PDXufUa3q+MBSuYa1uqTMwLewpay9eg==} peerDependencies: @@ -7833,6 +8018,14 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-sigv4-fetch@4.3.1: + resolution: {integrity: sha512-TqwVFsch0PAOYOFjpmE54cSSeGCLlUn72oBhEZWCgj7i2vWUo7ahbuJ0UVf9cLippSiXR3TJoA5bGQl3NXvfbA==} + engines: {node: '>=18'} + + aws-sigv4-sign@1.1.0: + resolution: {integrity: sha512-yBCJu8LbcZTp6xq+pcdSKs91yoDdsr6xwv0hapw1u9RXJ2SJFhSP9iXLWMleh+snqXi0COl+DTbw6t9nUeyphQ==} + engines: {node: '>=18'} + axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} @@ -8007,6 +8200,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + boxen@7.1.1: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} @@ -9197,6 +9393,13 @@ packages: fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -11295,6 +11498,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -12374,6 +12581,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@2.3.0: + resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} @@ -13681,6 +13891,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xml2js@0.6.0: resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==} engines: {node: '>=4.0.0'} @@ -13799,6 +14013,200 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.9 + tslib: 2.8.1 + optional: true + + '@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.973.9 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + optional: true + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.9 + tslib: 2.8.1 + optional: true + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/core@3.974.15': + dependencies: + '@aws-sdk/types': 3.973.9 + '@aws-sdk/xml-builder': 3.972.26 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.5 + '@smithy/signature-v4': 5.4.5 + '@smithy/types': 4.14.2 + bowser: 2.14.1 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-env@3.972.41': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-http@3.972.43': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/fetch-http-handler': 5.4.5 + '@smithy/node-http-handler': 4.7.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-ini@3.972.45': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/credential-provider-env': 3.972.41 + '@aws-sdk/credential-provider-http': 3.972.43 + '@aws-sdk/credential-provider-login': 3.972.45 + '@aws-sdk/credential-provider-process': 3.972.41 + '@aws-sdk/credential-provider-sso': 3.972.45 + '@aws-sdk/credential-provider-web-identity': 3.972.45 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/credential-provider-imds': 4.3.6 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-login@3.972.45': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-node@3.972.46': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.41 + '@aws-sdk/credential-provider-http': 3.972.43 + '@aws-sdk/credential-provider-ini': 3.972.45 + '@aws-sdk/credential-provider-process': 3.972.41 + '@aws-sdk/credential-provider-sso': 3.972.45 + '@aws-sdk/credential-provider-web-identity': 3.972.45 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/credential-provider-imds': 4.3.6 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-process@3.972.41': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-sso@3.972.45': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/token-providers': 3.1056.0 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-web-identity@3.972.45': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/nested-clients@3.997.13': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.15 + '@aws-sdk/signature-v4-multi-region': 3.996.30 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/fetch-http-handler': 5.4.5 + '@smithy/node-http-handler': 4.7.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/signature-v4-multi-region@3.996.30': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/signature-v4': 5.4.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/token-providers@3.1056.0': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/types@3.973.9': + dependencies: + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + optional: true + + '@aws-sdk/xml-builder@3.972.26': + dependencies: + '@smithy/types': 4.14.2 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + optional: true + + '@aws/lambda-invoke-store@0.2.4': + optional: true + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -15970,6 +16378,9 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@nodable/entities@2.1.1': + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -17649,6 +18060,118 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@smithy/core@3.24.5': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@smithy/credential-provider-imds@4.3.6': + dependencies: + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@smithy/fetch-http-handler@5.4.5': + dependencies: + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/is-array-buffer@3.0.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/node-http-handler@4.7.5': + dependencies: + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@smithy/protocol-http@4.1.8': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + optional: true + + '@smithy/signature-v4@3.1.2': + dependencies: + '@smithy/is-array-buffer': 3.0.0 + '@smithy/types': 3.7.2 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-uri-escape': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + optional: true + + '@smithy/signature-v4@5.4.5': + dependencies: + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + optional: true + + '@smithy/types@3.7.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/types@4.14.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-buffer-from@3.0.0': + dependencies: + '@smithy/is-array-buffer': 3.0.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-hex-encoding@3.0.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/util-middleware@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + optional: true + + '@smithy/util-uri-escape@3.0.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-utf8@3.0.0': + dependencies: + '@smithy/util-buffer-from': 3.0.0 + tslib: 2.8.1 + optional: true + '@solid-devtools/debugger@0.28.1(solid-js@1.9.10)': dependencies: '@nothing-but/utils': 0.17.0 @@ -20208,6 +20731,19 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + aws-sigv4-fetch@4.3.1: + dependencies: + aws-sigv4-sign: 1.1.0 + optional: true + + aws-sigv4-sign@1.1.0: + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/credential-provider-node': 3.972.46 + '@smithy/protocol-http': 4.1.8 + '@smithy/signature-v4': 3.1.2 + optional: true + axios@1.13.2: dependencies: follow-redirects: 1.15.11 @@ -20440,6 +20976,9 @@ snapshots: boolbase@1.0.0: {} + bowser@2.14.1: + optional: true + boxen@7.1.1: dependencies: ansi-align: 3.0.1 @@ -21820,6 +22359,20 @@ snapshots: fast-sha256@1.3.0: {} + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 + optional: true + + fast-xml-parser@5.7.3: + dependencies: + '@nodable/entities': 2.1.1 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.3.0 + optional: true + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -23158,8 +23711,8 @@ snapshots: magicast@0.5.2: dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 source-map-js: 1.2.1 make-dir@2.1.0: @@ -24646,6 +25199,9 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.5.0: + optional: true + path-key@3.1.1: {} path-key@4.0.0: {} @@ -25991,6 +26547,9 @@ snapshots: dependencies: js-tokens: 9.0.1 + strnum@2.3.0: + optional: true + structured-headers@0.4.1: {} style-to-js@1.1.21: @@ -27221,6 +27780,9 @@ snapshots: xml-name-validator@5.0.0: {} + xml-naming@0.1.0: + optional: true + xml2js@0.6.0: dependencies: sax: 1.6.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dc784eb75..497c8b342 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,3 +39,5 @@ allowBuilds: sharp: false unrs-resolver: false workerd: false + +trustPolicyExclude: aws-sigv4-fetch From 19ff7b0502f0a2afbdaff51806c386a8b85bf719 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 19:18:28 +0200 Subject: [PATCH 02/46] chore(ai-bedrock): make aws-sigv4-fetch an optional peer dep; revert trust-policy change --- packages/ai-bedrock/package.json | 9 +++++---- pnpm-lock.yaml | 25 ------------------------- pnpm-workspace.yaml | 2 -- 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/packages/ai-bedrock/package.json b/packages/ai-bedrock/package.json index 769c8d968..01e3e04b1 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -40,14 +40,15 @@ }, "peerDependencies": { "@tanstack/ai": "workspace:^", - "zod": "^4.0.0" + "zod": "^4.0.0", + "aws-sigv4-fetch": "^4.3.1" + }, + "peerDependenciesMeta": { + "aws-sigv4-fetch": { "optional": true } }, "dependencies": { "@tanstack/ai-utils": "workspace:*", "@tanstack/openai-base": "workspace:*", "openai": "^6.9.1" - }, - "optionalDependencies": { - "aws-sigv4-fetch": "4.3.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c861ff1a..5be74a169 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1050,10 +1050,6 @@ importers: vite: specifier: ^7.3.3 version: 7.3.3(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - optionalDependencies: - aws-sigv4-fetch: - specifier: 4.3.1 - version: 4.3.1 packages/ai-client: dependencies: @@ -8018,14 +8014,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - aws-sigv4-fetch@4.3.1: - resolution: {integrity: sha512-TqwVFsch0PAOYOFjpmE54cSSeGCLlUn72oBhEZWCgj7i2vWUo7ahbuJ0UVf9cLippSiXR3TJoA5bGQl3NXvfbA==} - engines: {node: '>=18'} - - aws-sigv4-sign@1.1.0: - resolution: {integrity: sha512-yBCJu8LbcZTp6xq+pcdSKs91yoDdsr6xwv0hapw1u9RXJ2SJFhSP9iXLWMleh+snqXi0COl+DTbw6t9nUeyphQ==} - engines: {node: '>=18'} - axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} @@ -20731,19 +20719,6 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - aws-sigv4-fetch@4.3.1: - dependencies: - aws-sigv4-sign: 1.1.0 - optional: true - - aws-sigv4-sign@1.1.0: - dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/credential-provider-node': 3.972.46 - '@smithy/protocol-http': 4.1.8 - '@smithy/signature-v4': 3.1.2 - optional: true - axios@1.13.2: dependencies: follow-redirects: 1.15.11 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 497c8b342..dc784eb75 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,5 +39,3 @@ allowBuilds: sharp: false unrs-resolver: false workerd: false - -trustPolicyExclude: aws-sigv4-fetch From 12797e95aff940f456195d754a82677cb6824429 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 19:20:28 +0200 Subject: [PATCH 03/46] chore(ai-bedrock): drop aws-sigv4-fetch from manifest (user-installed optional integration) --- packages/ai-bedrock/package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/ai-bedrock/package.json b/packages/ai-bedrock/package.json index 01e3e04b1..195c3a9f3 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -40,11 +40,7 @@ }, "peerDependencies": { "@tanstack/ai": "workspace:^", - "zod": "^4.0.0", - "aws-sigv4-fetch": "^4.3.1" - }, - "peerDependenciesMeta": { - "aws-sigv4-fetch": { "optional": true } + "zod": "^4.0.0" }, "dependencies": { "@tanstack/ai-utils": "workspace:*", From 3805d5245ad23de0953de20c2913b2565ed64293 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 19:25:15 +0200 Subject: [PATCH 04/46] feat(ai-bedrock): client config + auth resolution (apikey + SigV4 cascade) --- packages/ai-bedrock/src/utils/client.ts | 129 +++++++++++++++++++++++ packages/ai-bedrock/src/utils/index.ts | 8 ++ packages/ai-bedrock/tests/client.test.ts | 81 ++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 packages/ai-bedrock/src/utils/client.ts create mode 100644 packages/ai-bedrock/src/utils/index.ts create mode 100644 packages/ai-bedrock/tests/client.test.ts diff --git a/packages/ai-bedrock/src/utils/client.ts b/packages/ai-bedrock/src/utils/client.ts new file mode 100644 index 000000000..6f9333488 --- /dev/null +++ b/packages/ai-bedrock/src/utils/client.ts @@ -0,0 +1,129 @@ +import { getApiKeyFromEnv } from '@tanstack/ai-utils' +import type { ClientOptions } from 'openai' + +export type BedrockEndpoint = 'runtime' | 'mantle' + +export interface BedrockClientConfig + extends Omit { + /** Bedrock API key (bearer). Optional — falls back to env, then SigV4. */ + apiKey?: string + /** Full AWS region (e.g. 'us-east-1'). Default 'us-east-1'. */ + region?: string + /** Chat adapter only; the responses adapter forces 'mantle'. Default 'runtime'. */ + endpoint?: BedrockEndpoint + /** Auth strategy. Default 'auto' (apiKey → env → SigV4). */ + auth?: 'apikey' | 'sigv4' | 'auto' + /** Explicit override; wins over the computed endpoint URL (used by E2E → aimock). */ + baseURL?: string +} + +const DEFAULT_REGION = 'us-east-1' +/** OpenAI SDK requires a non-empty apiKey even when a signed fetch overrides Authorization. */ +const SIGV4_PLACEHOLDER_KEY = 'bedrock-sigv4' + +function buildBaseURL(region: string, endpoint: BedrockEndpoint): string { + return endpoint === 'mantle' + ? `https://bedrock-mantle.${region}.api.aws/v1` + : `https://bedrock-runtime.${region}.amazonaws.com/openai/v1` +} + +/** Reads BEDROCK_API_KEY, then AWS_BEARER_TOKEN_BEDROCK. Returns undefined if neither is set. */ +function readApiKeyFromEnv(): string | undefined { + try { + return getApiKeyFromEnv('BEDROCK_API_KEY') + } catch { + try { + return getApiKeyFromEnv('AWS_BEARER_TOKEN_BEDROCK') + } catch { + return undefined + } + } +} + +/** Throws if no Bedrock API key is available via config or env. */ +export function getBedrockApiKeyFromEnv(): string { + const key = readApiKeyFromEnv() + if (!key) { + throw new Error( + 'No Bedrock API key found. Set BEDROCK_API_KEY (or AWS_BEARER_TOKEN_BEDROCK) in your ' + + 'environment, pass `apiKey` to the factory, or use SigV4 auth (set auth: "sigv4" with ' + + 'AWS credentials configured).', + ) + } + return key +} + +export interface ResolvedBedrockAuth { + apiKey: string + /** Present only for the SigV4 path — a signing fetch for the OpenAI SDK. */ + fetch?: ClientOptions['fetch'] +} + +/** + * Resolves auth per the cascade: explicit apiKey → BEDROCK_API_KEY → + * AWS_BEARER_TOKEN_BEDROCK → SigV4. `auth: 'apikey'` forces the bearer path + * (throws with no key); `auth: 'sigv4'` forces signing. + */ +export function resolveBedrockAuth( + config: BedrockClientConfig, + endpoint: BedrockEndpoint, +): ResolvedBedrockAuth { + const mode = config.auth ?? 'auto' + + if (mode !== 'sigv4') { + const key = config.apiKey ?? readApiKeyFromEnv() + if (key) return { apiKey: key } + if (mode === 'apikey') { + // Reuse the canonical error. + getBedrockApiKeyFromEnv() + } + } + + // SigV4 path — build a lazily-imported signing fetch. + const region = config.region ?? DEFAULT_REGION + return { + apiKey: SIGV4_PLACEHOLDER_KEY, + fetch: createLazySigV4Fetch(region, endpoint), + } +} + +/** + * Returns a fetch that, on first call, dynamically imports the SigV4 signer + * from the `./sigv4` subpath (which holds the optional `aws-sigv4-fetch` dep) + * and delegates to it. Keeps the AWS signing code out of the default bundle. + */ +function createLazySigV4Fetch( + region: string, + endpoint: BedrockEndpoint, +): NonNullable { + let signed: NonNullable | undefined + return async (url, init) => { + if (!signed) { + const { bedrockSigV4Fetch } = await import('../sigv4/index') + signed = bedrockSigV4Fetch({ region, endpoint }) + } + return signed(url, init) + } +} + +/** Builds OpenAI ClientOptions for the requested endpoint. `forced` pins the endpoint (responses → 'mantle'). */ +export function withBedrockDefaults( + config: BedrockClientConfig, + forced?: BedrockEndpoint, +): ClientOptions { + const { region, endpoint, auth, apiKey, baseURL, fetch, ...rest } = config + const resolvedRegion = region ?? DEFAULT_REGION + const resolvedEndpoint = forced ?? endpoint ?? 'runtime' + const resolvedAuth = resolveBedrockAuth(config, resolvedEndpoint) + return { + ...rest, + baseURL: baseURL ?? buildBaseURL(resolvedRegion, resolvedEndpoint), + apiKey: resolvedAuth.apiKey, + // A user-supplied fetch wins over the SigV4 signer. + ...(fetch + ? { fetch } + : resolvedAuth.fetch + ? { fetch: resolvedAuth.fetch } + : {}), + } +} diff --git a/packages/ai-bedrock/src/utils/index.ts b/packages/ai-bedrock/src/utils/index.ts new file mode 100644 index 000000000..b47d6a9b1 --- /dev/null +++ b/packages/ai-bedrock/src/utils/index.ts @@ -0,0 +1,8 @@ +export { + getBedrockApiKeyFromEnv, + resolveBedrockAuth, + withBedrockDefaults, + type BedrockClientConfig, + type BedrockEndpoint, + type ResolvedBedrockAuth, +} from './client' diff --git a/packages/ai-bedrock/tests/client.test.ts b/packages/ai-bedrock/tests/client.test.ts new file mode 100644 index 000000000..139b8bfc3 --- /dev/null +++ b/packages/ai-bedrock/tests/client.test.ts @@ -0,0 +1,81 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { resolveBedrockAuth, withBedrockDefaults } from '../src/utils/client' + +const ORIGINAL_ENV = { ...process.env } +afterEach(() => { + process.env = { ...ORIGINAL_ENV } +}) + +describe('withBedrockDefaults', () => { + it('builds the runtime URL by default', () => { + const out = withBedrockDefaults({ apiKey: 'k', region: 'us-east-1' }) + expect(out.baseURL).toBe('https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1') + }) + + it('defaults region to us-east-1', () => { + const out = withBedrockDefaults({ apiKey: 'k' }) + expect(out.baseURL).toBe('https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1') + }) + + it('builds the mantle URL when endpoint is mantle', () => { + const out = withBedrockDefaults({ apiKey: 'k', region: 'eu-west-1', endpoint: 'mantle' }) + expect(out.baseURL).toBe('https://bedrock-mantle.eu-west-1.api.aws/v1') + }) + + it('forces mantle when the `forced` arg is mantle, ignoring config.endpoint', () => { + const out = withBedrockDefaults({ apiKey: 'k', region: 'us-west-2', endpoint: 'runtime' }, 'mantle') + expect(out.baseURL).toBe('https://bedrock-mantle.us-west-2.api.aws/v1') + }) + + it('honors an explicit baseURL override', () => { + const out = withBedrockDefaults({ apiKey: 'k', baseURL: 'http://127.0.0.1:4010/v1' }) + expect(out.baseURL).toBe('http://127.0.0.1:4010/v1') + }) + + it('does not leak region/endpoint/auth into the OpenAI ClientOptions', () => { + const out = withBedrockDefaults({ apiKey: 'k', region: 'us-east-1', endpoint: 'runtime', auth: 'apikey' }) + expect('region' in out).toBe(false) + expect('endpoint' in out).toBe(false) + expect('auth' in out).toBe(false) + }) +}) + +describe('resolveBedrockAuth', () => { + it('uses an explicit apiKey', () => { + const r = resolveBedrockAuth({ apiKey: 'explicit' }, 'runtime') + expect(r).toEqual({ apiKey: 'explicit' }) + }) + + it('falls back to BEDROCK_API_KEY', () => { + delete process.env.AWS_BEARER_TOKEN_BEDROCK + process.env.BEDROCK_API_KEY = 'from-bedrock-env' + const r = resolveBedrockAuth({}, 'runtime') + expect(r).toEqual({ apiKey: 'from-bedrock-env' }) + }) + + it('falls back to AWS_BEARER_TOKEN_BEDROCK', () => { + delete process.env.BEDROCK_API_KEY + process.env.AWS_BEARER_TOKEN_BEDROCK = 'from-aws-env' + const r = resolveBedrockAuth({}, 'runtime') + expect(r).toEqual({ apiKey: 'from-aws-env' }) + }) + + it("auth: 'apikey' with no key throws an actionable error", () => { + delete process.env.BEDROCK_API_KEY + delete process.env.AWS_BEARER_TOKEN_BEDROCK + expect(() => resolveBedrockAuth({ auth: 'apikey' }, 'runtime')).toThrowError(/BEDROCK_API_KEY/) + }) + + it("auth: 'sigv4' returns a signing fetch and a placeholder apiKey", () => { + const r = resolveBedrockAuth({ auth: 'sigv4', region: 'us-east-1' }, 'runtime') + expect(typeof r.fetch).toBe('function') + expect(r.apiKey.length).toBeGreaterThan(0) + }) + + it("'auto' with no key falls through to SigV4", () => { + delete process.env.BEDROCK_API_KEY + delete process.env.AWS_BEARER_TOKEN_BEDROCK + const r = resolveBedrockAuth({ region: 'us-east-1' }, 'runtime') + expect(typeof r.fetch).toBe('function') + }) +}) From 3fa84ba63612825030998d568d2732856251fb54 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 19:30:50 +0200 Subject: [PATCH 05/46] fix(ai-bedrock): close apikey-mode fall-through; robust env test restore; cover baseURL/fetch precedence --- packages/ai-bedrock/src/utils/client.ts | 4 +-- packages/ai-bedrock/tests/client.test.ts | 31 +++++++++++++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/ai-bedrock/src/utils/client.ts b/packages/ai-bedrock/src/utils/client.ts index 6f9333488..d1b73bc6f 100644 --- a/packages/ai-bedrock/src/utils/client.ts +++ b/packages/ai-bedrock/src/utils/client.ts @@ -74,8 +74,8 @@ export function resolveBedrockAuth( const key = config.apiKey ?? readApiKeyFromEnv() if (key) return { apiKey: key } if (mode === 'apikey') { - // Reuse the canonical error. - getBedrockApiKeyFromEnv() + // No key and apikey mode forced — throw the canonical error (terminal). + return { apiKey: getBedrockApiKeyFromEnv() } } } diff --git a/packages/ai-bedrock/tests/client.test.ts b/packages/ai-bedrock/tests/client.test.ts index 139b8bfc3..9dae1b546 100644 --- a/packages/ai-bedrock/tests/client.test.ts +++ b/packages/ai-bedrock/tests/client.test.ts @@ -1,9 +1,8 @@ -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { resolveBedrockAuth, withBedrockDefaults } from '../src/utils/client' -const ORIGINAL_ENV = { ...process.env } afterEach(() => { - process.env = { ...ORIGINAL_ENV } + vi.unstubAllEnvs() }) describe('withBedrockDefaults', () => { @@ -38,6 +37,18 @@ describe('withBedrockDefaults', () => { expect('endpoint' in out).toBe(false) expect('auth' in out).toBe(false) }) + + it('explicit baseURL survives the SigV4 path and signer is attached', () => { + const out = withBedrockDefaults({ baseURL: 'http://127.0.0.1:4010/v1', auth: 'sigv4', region: 'us-east-1' }) + expect(out.baseURL).toBe('http://127.0.0.1:4010/v1') + expect(typeof out.fetch).toBe('function') + }) + + it('user-supplied fetch wins over the SigV4 signer', () => { + const userFetch: NonNullable = async () => new Response() + const out = withBedrockDefaults({ auth: 'sigv4', region: 'us-east-1', fetch: userFetch }) + expect(out.fetch).toBe(userFetch) + }) }) describe('resolveBedrockAuth', () => { @@ -47,22 +58,20 @@ describe('resolveBedrockAuth', () => { }) it('falls back to BEDROCK_API_KEY', () => { - delete process.env.AWS_BEARER_TOKEN_BEDROCK - process.env.BEDROCK_API_KEY = 'from-bedrock-env' + vi.stubEnv('BEDROCK_API_KEY', 'from-bedrock-env') const r = resolveBedrockAuth({}, 'runtime') expect(r).toEqual({ apiKey: 'from-bedrock-env' }) }) it('falls back to AWS_BEARER_TOKEN_BEDROCK', () => { - delete process.env.BEDROCK_API_KEY - process.env.AWS_BEARER_TOKEN_BEDROCK = 'from-aws-env' + vi.stubEnv('AWS_BEARER_TOKEN_BEDROCK', 'from-aws-env') const r = resolveBedrockAuth({}, 'runtime') expect(r).toEqual({ apiKey: 'from-aws-env' }) }) it("auth: 'apikey' with no key throws an actionable error", () => { - delete process.env.BEDROCK_API_KEY - delete process.env.AWS_BEARER_TOKEN_BEDROCK + vi.stubEnv('BEDROCK_API_KEY', '') + vi.stubEnv('AWS_BEARER_TOKEN_BEDROCK', '') expect(() => resolveBedrockAuth({ auth: 'apikey' }, 'runtime')).toThrowError(/BEDROCK_API_KEY/) }) @@ -73,8 +82,8 @@ describe('resolveBedrockAuth', () => { }) it("'auto' with no key falls through to SigV4", () => { - delete process.env.BEDROCK_API_KEY - delete process.env.AWS_BEARER_TOKEN_BEDROCK + vi.stubEnv('BEDROCK_API_KEY', '') + vi.stubEnv('AWS_BEARER_TOKEN_BEDROCK', '') const r = resolveBedrockAuth({ region: 'us-east-1' }, 'runtime') expect(typeof r.fetch).toBe('function') }) From 8498890a8b35c3bed7f4bd19bbf42e6d28896a14 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 19:39:57 +0200 Subject: [PATCH 06/46] feat(ai-bedrock): SigV4 signing fetch behind /sigv4 subpath --- knip.json | 3 + .../ai-bedrock/src/sigv4/aws-sigv4-fetch.d.ts | 10 +++ packages/ai-bedrock/src/sigv4/index.ts | 66 +++++++++++++++++++ packages/ai-bedrock/tests/sigv4.test.ts | 18 +++++ 4 files changed, 97 insertions(+) create mode 100644 packages/ai-bedrock/src/sigv4/aws-sigv4-fetch.d.ts create mode 100644 packages/ai-bedrock/src/sigv4/index.ts create mode 100644 packages/ai-bedrock/tests/sigv4.test.ts diff --git a/knip.json b/knip.json index 67ae81303..40927f9ed 100644 --- a/knip.json +++ b/knip.json @@ -43,6 +43,9 @@ }, "packages/ai-vue-ui": { "ignore": ["src/use-chat-context.ts"] + }, + "packages/ai-bedrock": { + "ignoreDependencies": ["aws-sigv4-fetch"] } } } diff --git a/packages/ai-bedrock/src/sigv4/aws-sigv4-fetch.d.ts b/packages/ai-bedrock/src/sigv4/aws-sigv4-fetch.d.ts new file mode 100644 index 000000000..3c0eb3f58 --- /dev/null +++ b/packages/ai-bedrock/src/sigv4/aws-sigv4-fetch.d.ts @@ -0,0 +1,10 @@ +// aws-sigv4-fetch is an optional, user-installed dependency (NOT in our +// manifest — see package docs). This ambient declaration lets `tsc` resolve +// the dynamic `import('aws-sigv4-fetch')` without the package being present. +// No `any`, no cast. +declare module 'aws-sigv4-fetch' { + export function createSignedFetcher(opts: { + service: string + region: string + }): (input: string | URL | Request, init?: RequestInit) => Promise +} diff --git a/packages/ai-bedrock/src/sigv4/index.ts b/packages/ai-bedrock/src/sigv4/index.ts new file mode 100644 index 000000000..1f27ae6cc --- /dev/null +++ b/packages/ai-bedrock/src/sigv4/index.ts @@ -0,0 +1,66 @@ +import type { ClientOptions } from 'openai' +import type { BedrockEndpoint } from '../utils/client' + +export interface BedrockSigV4Options { + region: string + endpoint: BedrockEndpoint + /** Override the SigV4 service name (default 'bedrock'). */ + service?: string +} + +interface SigV4Params { + service: string + region: string +} + +// Mirrors the createSignedFetcher signature from `aws-sigv4-fetch` (see +// aws-sigv4-fetch.d.ts). Defined here so we can type the variable without +// using `import()` in a type annotation (forbidden by consistent-type-imports). +type SignedFetcher = ( + input: string | URL | Request, + init?: RequestInit, +) => Promise + +type CreateSignedFetcher = (opts: { + service: string + region: string +}) => SignedFetcher + +/** Pure resolver — testable without network or credentials. */ +export function resolveSigV4Params(options: BedrockSigV4Options): SigV4Params { + return { service: options.service ?? 'bedrock', region: options.region } +} + +/** + * Builds a fetch that signs each request with AWS SigV4, suitable for the + * OpenAI SDK `fetch` option against Bedrock's OpenAI-compatible endpoints. + * + * Requires the optional `aws-sigv4-fetch` dependency (install it yourself: + * `pnpm add aws-sigv4-fetch`). AWS credentials are resolved from the standard + * provider chain. Throws an actionable error if the dep is absent. + */ +export function bedrockSigV4Fetch( + options: BedrockSigV4Options, +): NonNullable { + const { service, region } = resolveSigV4Params(options) + let signedFetch: SignedFetcher | undefined + + const fn: NonNullable = async (url, init) => { + if (!signedFetch) { + let createSignedFetcher: CreateSignedFetcher + try { + const mod = await import('aws-sigv4-fetch') + createSignedFetcher = mod.createSignedFetcher + } catch { + throw new Error( + 'SigV4 auth for @tanstack/ai-bedrock requires the optional "aws-sigv4-fetch" ' + + 'package. Install it (`pnpm add aws-sigv4-fetch`) or use API-key auth via BEDROCK_API_KEY.', + ) + } + signedFetch = createSignedFetcher({ service, region }) + } + const fetcher = signedFetch + return fetcher(url, init) + } + return fn +} diff --git a/packages/ai-bedrock/tests/sigv4.test.ts b/packages/ai-bedrock/tests/sigv4.test.ts new file mode 100644 index 000000000..a090a32e9 --- /dev/null +++ b/packages/ai-bedrock/tests/sigv4.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import { resolveSigV4Params } from '../src/sigv4/index' + +describe('resolveSigV4Params', () => { + it('uses service "bedrock" and the given region', () => { + expect(resolveSigV4Params({ region: 'us-east-1', endpoint: 'runtime' })).toEqual({ + service: 'bedrock', + region: 'us-east-1', + }) + }) + + it('keeps service "bedrock" for the mantle endpoint', () => { + expect(resolveSigV4Params({ region: 'eu-west-1', endpoint: 'mantle' })).toEqual({ + service: 'bedrock', + region: 'eu-west-1', + }) + }) +}) From 00b4ae6f7f02212f6e6ca26d5542d3555c999f54 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 19:46:16 +0200 Subject: [PATCH 07/46] feat(ai-bedrock): message metadata + chat/responses provider options --- packages/ai-bedrock/src/message-types.ts | 24 ++++++++++++++ .../src/text/responses-provider-options.ts | 23 +++++++++++++ .../src/text/text-provider-options.ts | 33 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 packages/ai-bedrock/src/message-types.ts create mode 100644 packages/ai-bedrock/src/text/responses-provider-options.ts create mode 100644 packages/ai-bedrock/src/text/text-provider-options.ts diff --git a/packages/ai-bedrock/src/message-types.ts b/packages/ai-bedrock/src/message-types.ts new file mode 100644 index 000000000..8ca1846ee --- /dev/null +++ b/packages/ai-bedrock/src/message-types.ts @@ -0,0 +1,24 @@ +/** + * Bedrock content-part metadata by modality, used for type inference when + * constructing multimodal messages. Bedrock's OpenAI-compatible Chat + * Completions accepts the standard OpenAI image-detail hint; other modalities + * carry no extra metadata today. + */ +export interface BedrockTextMetadata {} + +export interface BedrockImageMetadata { + /** Image processing detail: 'auto' (default), 'low', or 'high'. */ + detail?: 'auto' | 'low' | 'high' +} + +export interface BedrockAudioMetadata {} +export interface BedrockVideoMetadata {} +export interface BedrockDocumentMetadata {} + +export interface BedrockMessageMetadataByModality { + text: BedrockTextMetadata + image: BedrockImageMetadata + audio: BedrockAudioMetadata + video: BedrockVideoMetadata + document: BedrockDocumentMetadata +} diff --git a/packages/ai-bedrock/src/text/responses-provider-options.ts b/packages/ai-bedrock/src/text/responses-provider-options.ts new file mode 100644 index 000000000..164343472 --- /dev/null +++ b/packages/ai-bedrock/src/text/responses-provider-options.ts @@ -0,0 +1,23 @@ +/** + * Bedrock Responses API provider options. Mantle's Responses endpoint adds + * stateful conversation management on top of the OpenAI Responses fields. + * + * @see https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html + */ +export interface BedrockResponsesProviderOptions { + /** Continue a stored conversation from a prior response. */ + previous_response_id?: string | null + /** Whether Bedrock retains the response for 30 days (default true). Set false to opt out. */ + store?: boolean | null + metadata?: { [key: string]: string } | null + max_output_tokens?: number | null + temperature?: number | null + top_p?: number | null + parallel_tool_calls?: boolean | null + tool_choice?: 'none' | 'auto' | 'required' | { type: 'function'; name: string } | null + /** Reasoning controls for reasoning-capable models. */ + reasoning?: { effort?: 'low' | 'medium' | 'high' } | null + user?: string | null +} + +export type ExternalResponsesProviderOptions = BedrockResponsesProviderOptions diff --git a/packages/ai-bedrock/src/text/text-provider-options.ts b/packages/ai-bedrock/src/text/text-provider-options.ts new file mode 100644 index 000000000..ef7edcdf3 --- /dev/null +++ b/packages/ai-bedrock/src/text/text-provider-options.ts @@ -0,0 +1,33 @@ +/** + * Bedrock Chat Completions provider options. Bedrock accepts the standard + * OpenAI Chat Completions request fields; we surface the commonly-used ones + * plus `reasoning_effort` (supported by gpt-oss and reasoning models). + * + * @see https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-openai.html + */ +export interface BedrockTextProviderOptions { + frequency_penalty?: number | null + presence_penalty?: number | null + logit_bias?: { [token: string]: number } | null + logprobs?: boolean | null + top_logprobs?: number | null + max_completion_tokens?: number | null + metadata?: { [key: string]: string } | null + n?: number | null + parallel_tool_calls?: boolean | null + /** gpt-oss / reasoning models: 'low' | 'medium' (default) | 'high'. */ + reasoning_effort?: 'low' | 'medium' | 'high' | null + seed?: number | null + stop?: string | Array | null + temperature?: number | null + tool_choice?: + | 'none' + | 'auto' + | 'required' + | { type: 'function'; function: { name: string } } + | null + top_p?: number | null + user?: string | null +} + +export type ExternalTextProviderOptions = BedrockTextProviderOptions From 0f09fe93184291e1e31f18a973ab11996dbcffe8 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 19:50:08 +0200 Subject: [PATCH 08/46] feat(ai-bedrock): seed model catalog (broad chat + responses subset) --- packages/ai-bedrock/src/model-meta.ts | 150 +++++++++++++++++++ packages/ai-bedrock/tests/model-meta.test.ts | 27 ++++ 2 files changed, 177 insertions(+) create mode 100644 packages/ai-bedrock/src/model-meta.ts create mode 100644 packages/ai-bedrock/tests/model-meta.test.ts diff --git a/packages/ai-bedrock/src/model-meta.ts b/packages/ai-bedrock/src/model-meta.ts new file mode 100644 index 000000000..8879e13bb --- /dev/null +++ b/packages/ai-bedrock/src/model-meta.ts @@ -0,0 +1,150 @@ +import type { BedrockTextProviderOptions } from './text/text-provider-options' + +/** Bedrock model metadata. `pricing` is intentionally optional and unpopulated initially. */ +interface ModelMeta { + name: string + context_window?: number + max_completion_tokens?: number + pricing?: { + input?: { normal: number; cached?: number } + output?: { normal: number } + } + supports: { + input: Array<'text' | 'image' | 'document'> + output: Array<'text'> + endpoints: Array<'chat' | 'responses'> + features: Array<'streaming' | 'tools' | 'reasoning' | 'json_schema' | 'vision'> + tools: ReadonlyArray + } +} + +// --- OpenAI gpt-oss (text-only; chat + responses) --- +const GPT_OSS_120B = { + name: 'openai.gpt-oss-120b', + context_window: 128_000, + supports: { input: ['text'], output: ['text'], endpoints: ['chat', 'responses'], features: ['streaming', 'tools', 'reasoning'], tools: [] as const }, +} as const satisfies ModelMeta +const GPT_OSS_20B = { + name: 'openai.gpt-oss-20b-1:0', + context_window: 128_000, + supports: { input: ['text'], output: ['text'], endpoints: ['chat', 'responses'], features: ['streaming', 'tools', 'reasoning'], tools: [] as const }, +} as const satisfies ModelMeta + +// --- Anthropic Claude (US cross-region inference profiles; chat) --- +const CLAUDE_SONNET_4_5 = { + name: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', + context_window: 200_000, + supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision', 'reasoning'], tools: [] as const }, +} as const satisfies ModelMeta +const CLAUDE_HAIKU_4_5 = { + name: 'us.anthropic.claude-haiku-4-5-20251001-v1:0', + context_window: 200_000, + supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, +} as const satisfies ModelMeta +const CLAUDE_3_7_SONNET = { + name: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + context_window: 200_000, + supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision', 'reasoning'], tools: [] as const }, +} as const satisfies ModelMeta +const CLAUDE_3_5_SONNET_V2 = { + name: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0', + context_window: 200_000, + supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, +} as const satisfies ModelMeta +const CLAUDE_3_5_HAIKU = { + name: 'us.anthropic.claude-3-5-haiku-20241022-v1:0', + context_window: 200_000, + supports: { input: ['text'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools'], tools: [] as const }, +} as const satisfies ModelMeta + +// --- Amazon Nova (US profiles; chat) --- +const NOVA_PRO = { + name: 'us.amazon.nova-pro-v1:0', + context_window: 300_000, + supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, +} as const satisfies ModelMeta +const NOVA_LITE = { + name: 'us.amazon.nova-lite-v1:0', + context_window: 300_000, + supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, +} as const satisfies ModelMeta +const NOVA_MICRO = { + name: 'us.amazon.nova-micro-v1:0', + context_window: 128_000, + supports: { input: ['text'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools'], tools: [] as const }, +} as const satisfies ModelMeta + +// --- Meta Llama (US profiles; chat) --- +const LLAMA_3_3_70B = { + name: 'us.meta.llama3-3-70b-instruct-v1:0', + context_window: 128_000, + supports: { input: ['text'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools'], tools: [] as const }, +} as const satisfies ModelMeta +const LLAMA_4_MAVERICK = { + name: 'us.meta.llama4-maverick-17b-instruct-v1:0', + context_window: 128_000, + supports: { input: ['text', 'image'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, +} as const satisfies ModelMeta + +// --- Mistral / DeepSeek (US profiles; chat) --- +const MISTRAL_PIXTRAL_LARGE = { + name: 'us.mistral.pixtral-large-2502-v1:0', + context_window: 128_000, + supports: { input: ['text', 'image'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, +} as const satisfies ModelMeta +const DEEPSEEK_R1 = { + name: 'us.deepseek.r1-v1:0', + context_window: 128_000, + supports: { input: ['text'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'reasoning'], tools: [] as const }, +} as const satisfies ModelMeta + +const CHAT_MODELS = [ + GPT_OSS_20B, GPT_OSS_120B, + CLAUDE_SONNET_4_5, CLAUDE_HAIKU_4_5, CLAUDE_3_7_SONNET, CLAUDE_3_5_SONNET_V2, CLAUDE_3_5_HAIKU, + NOVA_PRO, NOVA_LITE, NOVA_MICRO, + LLAMA_3_3_70B, LLAMA_4_MAVERICK, + MISTRAL_PIXTRAL_LARGE, DEEPSEEK_R1, +] as const + +// Cast-free: explicit `.name` lists with `as const` (the ai-groq pattern). +export const BEDROCK_CHAT_MODELS = [ + GPT_OSS_20B.name, GPT_OSS_120B.name, + CLAUDE_SONNET_4_5.name, CLAUDE_HAIKU_4_5.name, CLAUDE_3_7_SONNET.name, + CLAUDE_3_5_SONNET_V2.name, CLAUDE_3_5_HAIKU.name, + NOVA_PRO.name, NOVA_LITE.name, NOVA_MICRO.name, + LLAMA_3_3_70B.name, LLAMA_4_MAVERICK.name, + MISTRAL_PIXTRAL_LARGE.name, DEEPSEEK_R1.name, +] as const +export const BEDROCK_RESPONSES_MODELS = [GPT_OSS_20B.name, GPT_OSS_120B.name] as const + +export type BedrockChatModels = (typeof BEDROCK_CHAT_MODELS)[number] +export type BedrockResponsesModels = (typeof BEDROCK_RESPONSES_MODELS)[number] + +// Mapped types keyed off the model-constant tuple union. The `as M['name']` +// is mapped-type KEY REMAPPING (legal syntax), NOT a value cast. +type ChatModelMeta = (typeof CHAT_MODELS)[number] + +/** Per-model input modalities (drives type-safe multimodal content). */ +export type BedrockModelInputModalitiesByName = { + [M in ChatModelMeta as M['name']]: M['supports']['input'] +} + +/** Provider options per model — mapped type (ai-grok pattern). */ +export type BedrockChatModelProviderOptionsByName = { + [K in BedrockChatModels]: BedrockTextProviderOptions +} + +/** No provider-specific tools — empty tuple makes cross-provider ProviderTool a compile error. */ +export type BedrockChatModelToolCapabilitiesByName = { + [M in ChatModelMeta as M['name']]: M['supports']['tools'] +} + +export type ResolveProviderOptions = + TModel extends keyof BedrockChatModelProviderOptionsByName + ? BedrockChatModelProviderOptionsByName[TModel] + : BedrockTextProviderOptions + +export type ResolveInputModalities = + TModel extends keyof BedrockModelInputModalitiesByName + ? BedrockModelInputModalitiesByName[TModel] + : readonly ['text'] diff --git a/packages/ai-bedrock/tests/model-meta.test.ts b/packages/ai-bedrock/tests/model-meta.test.ts new file mode 100644 index 000000000..42c3df90c --- /dev/null +++ b/packages/ai-bedrock/tests/model-meta.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { + BEDROCK_CHAT_MODELS, + BEDROCK_RESPONSES_MODELS, +} from '../src/model-meta' + +describe('bedrock model-meta', () => { + it('chat catalog is non-empty and unique', () => { + expect(BEDROCK_CHAT_MODELS.length).toBeGreaterThan(0) + expect(new Set(BEDROCK_CHAT_MODELS).size).toBe(BEDROCK_CHAT_MODELS.length) + }) + + it('responses catalog is non-empty and unique', () => { + expect(BEDROCK_RESPONSES_MODELS.length).toBeGreaterThan(0) + expect(new Set(BEDROCK_RESPONSES_MODELS).size).toBe(BEDROCK_RESPONSES_MODELS.length) + }) + + it('every responses model is also a chat model (Responses subset of Chat reach)', () => { + const chat = new Set(BEDROCK_CHAT_MODELS) + for (const m of BEDROCK_RESPONSES_MODELS) expect(chat.has(m)).toBe(true) + }) + + it('includes the confirmed gpt-oss ids', () => { + expect(BEDROCK_CHAT_MODELS).toContain('openai.gpt-oss-120b') + expect(BEDROCK_RESPONSES_MODELS).toContain('openai.gpt-oss-120b') + }) +}) From fbdf6efc2f349e07ee9c599a9317a4bce3db8d79 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 19:57:51 +0200 Subject: [PATCH 09/46] fix(ai-bedrock): compile-time parity guard for model catalog arrays --- packages/ai-bedrock/src/model-meta.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/ai-bedrock/src/model-meta.ts b/packages/ai-bedrock/src/model-meta.ts index 8879e13bb..aafa2f9b7 100644 --- a/packages/ai-bedrock/src/model-meta.ts +++ b/packages/ai-bedrock/src/model-meta.ts @@ -19,6 +19,8 @@ interface ModelMeta { } // --- OpenAI gpt-oss (text-only; chat + responses) --- +// Note: `openai.gpt-oss-120b` has no version suffix while `openai.gpt-oss-20b-1:0` does; +// this asymmetry is intentional (seed IDs as published) and will be reconciled by the refresh script. const GPT_OSS_120B = { name: 'openai.gpt-oss-120b', context_window: 128_000, @@ -124,6 +126,21 @@ export type BedrockResponsesModels = (typeof BEDROCK_RESPONSES_MODELS)[number] // is mapped-type KEY REMAPPING (legal syntax), NOT a value cast. type ChatModelMeta = (typeof CHAT_MODELS)[number] +// Compile-time guard: CHAT_MODELS (drives the per-model type maps) and +// BEDROCK_CHAT_MODELS (the public runtime catalog) must list the same models. +// If they diverge, the type argument to `_AssertTrue` stops satisfying +// `extends true` and tsc fails with a readable message. +// The `declare const` form has no runtime cost and avoids a `noUnusedLocals` +// error on a `const` whose value is never read. +type _AssertTrue = TResult +declare const _chatModelsInSync: _AssertTrue< + ChatModelMeta['name'] extends BedrockChatModels + ? BedrockChatModels extends ChatModelMeta['name'] + ? true + : ['BEDROCK_CHAT_MODELS has a name missing from CHAT_MODELS'] + : ['CHAT_MODELS has a name missing from BEDROCK_CHAT_MODELS'] +> + /** Per-model input modalities (drives type-safe multimodal content). */ export type BedrockModelInputModalitiesByName = { [M in ChatModelMeta as M['name']]: M['supports']['input'] From aa8ac3d90e4a4e5eb89de33621fb4a761e8eb546 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:02:43 +0200 Subject: [PATCH 10/46] feat(ai-bedrock): chat adapter with cast-free reasoning extraction --- packages/ai-bedrock/src/adapters/text.ts | 98 +++++++++++++++++++++++ packages/ai-bedrock/tests/adapter.test.ts | 42 ++++++++++ 2 files changed, 140 insertions(+) create mode 100644 packages/ai-bedrock/src/adapters/text.ts create mode 100644 packages/ai-bedrock/tests/adapter.test.ts diff --git a/packages/ai-bedrock/src/adapters/text.ts b/packages/ai-bedrock/src/adapters/text.ts new file mode 100644 index 000000000..4bc9679e7 --- /dev/null +++ b/packages/ai-bedrock/src/adapters/text.ts @@ -0,0 +1,98 @@ +import OpenAI from 'openai' +import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base' +import { withBedrockDefaults } from '../utils/client' +import type { Modality } from '@tanstack/ai' +import type { BedrockClientConfig } from '../utils/client' +import type { BedrockMessageMetadataByModality } from '../message-types' +import type { + BedrockChatModelToolCapabilitiesByName, + BedrockChatModels, + ResolveInputModalities, + ResolveProviderOptions, +} from '../model-meta' + +export interface BedrockTextConfig extends BedrockClientConfig {} + +export type { ExternalTextProviderOptions as BedrockTextProviderOptions } from '../text/text-provider-options' + +type ResolveToolCapabilities = + TModel extends keyof BedrockChatModelToolCapabilitiesByName + ? NonNullable + : readonly [] + +/** + * Bedrock Chat Completions adapter. Drives Bedrock's OpenAI-compatible + * `/chat/completions` endpoint via the OpenAI SDK with a baseURL override + * (same pattern as ai-groq). Tool conversion, streaming, structured output, + * and the agent loop come from the base. + */ +export class BedrockTextAdapter< + TModel extends BedrockChatModels, + // Constraint mirrors ai-groq: the base parameterises `TProviderOptions + // extends Record`, but our default + // `ResolveProviderOptions` resolves to the `BedrockTextProviderOptions` + // interface, which (lacking an implicit index signature) does not satisfy + // `Record`. `Record` is the only constraint that + // both accepts that interface default AND satisfies the base's constraint. + // This `any` is confined to the generic constraint (the established ai-groq + // pattern) — no value/shape `as` cast is introduced. + TProviderOptions extends Record = ResolveProviderOptions, + TInputModalities extends + ReadonlyArray = ResolveInputModalities, + TToolCapabilities extends + ReadonlyArray = ResolveToolCapabilities, +> extends OpenAIBaseChatCompletionsTextAdapter< + TModel, + TProviderOptions, + TInputModalities, + BedrockMessageMetadataByModality, + TToolCapabilities +> { + override readonly kind = 'text' as const + override readonly name = 'bedrock' as const + + constructor(config: BedrockTextConfig, model: TModel) { + // No `forced` -> honors config.endpoint ('runtime' default, 'mantle' allowed). + super(model, 'bedrock', new OpenAI(withBedrockDefaults(config))) + } + + /** + * Surface reasoning deltas (gpt-oss / Claude reasoning) the OpenAI-compatible + * way. Base types the chunk as `unknown`; narrow with runtime guards — no + * `as` casts, no `any`. + */ + protected override extractReasoning( + chunk: unknown, + ): { text: string } | undefined { + return readDeltaReasoning(chunk) + } +} + +/** Cast-free narrowing of a Chat Completions chunk's reasoning delta. */ +function readDeltaReasoning(chunk: unknown): { text: string } | undefined { + if (typeof chunk !== 'object' || chunk === null || !('choices' in chunk)) + return undefined + if (!Array.isArray(chunk.choices)) return undefined + const choice: unknown = chunk.choices[0] + if (typeof choice !== 'object' || choice === null || !('delta' in choice)) + return undefined + const delta = choice.delta + if (typeof delta !== 'object' || delta === null) return undefined + const raw = + 'reasoning' in delta && typeof delta.reasoning === 'string' + ? delta.reasoning + : 'reasoning_content' in delta && + typeof delta.reasoning_content === 'string' + ? delta.reasoning_content + : undefined + return raw && raw.length > 0 ? { text: raw } : undefined +} + +/** Chat adapter with an explicit API key (low-level; the public branching factory delegates here). */ +export function createBedrockChat( + model: TModel, + apiKey: string, + config?: Omit, +): BedrockTextAdapter { + return new BedrockTextAdapter({ apiKey, ...config }, model) +} diff --git a/packages/ai-bedrock/tests/adapter.test.ts b/packages/ai-bedrock/tests/adapter.test.ts new file mode 100644 index 000000000..05ca0599b --- /dev/null +++ b/packages/ai-bedrock/tests/adapter.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { BedrockTextAdapter, createBedrockChat } from '../src/adapters/text' + +describe('BedrockTextAdapter', () => { + it('constructs with name "bedrock" and kind "text"', () => { + const a = createBedrockChat('openai.gpt-oss-120b', 'test-key', { + region: 'us-east-1', + }) + expect(a).toBeInstanceOf(BedrockTextAdapter) + expect(a.name).toBe('bedrock') + expect(a.kind).toBe('text') + expect(a.model).toBe('openai.gpt-oss-120b') + }) + + describe('extractReasoning (cast-free)', () => { + // Access the protected hook through a tiny typed subclass — no `as` casts. + class Probe extends BedrockTextAdapter<'openai.gpt-oss-120b'> { + read(chunk: unknown) { + return this.extractReasoning(chunk) + } + } + const probe = new Probe({ apiKey: 'k' }, 'openai.gpt-oss-120b') + + it('reads delta.reasoning', () => { + expect( + probe.read({ choices: [{ delta: { reasoning: 'thinking' } }] }), + ).toEqual({ text: 'thinking' }) + }) + it('reads delta.reasoning_content', () => { + expect( + probe.read({ choices: [{ delta: { reasoning_content: 'rc' } }] }), + ).toEqual({ text: 'rc' }) + }) + it('returns undefined for unrelated chunks', () => { + expect( + probe.read({ choices: [{ delta: { content: 'hi' } }] }), + ).toBeUndefined() + expect(probe.read({})).toBeUndefined() + expect(probe.read(null)).toBeUndefined() + }) + }) +}) From 69442668955638283d1d4f6a2fad9b063097e840 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:05:42 +0200 Subject: [PATCH 11/46] test(ai-bedrock): cover empty-string + non-array-choices reasoning guards --- packages/ai-bedrock/tests/adapter.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ai-bedrock/tests/adapter.test.ts b/packages/ai-bedrock/tests/adapter.test.ts index 05ca0599b..f88e8e9da 100644 --- a/packages/ai-bedrock/tests/adapter.test.ts +++ b/packages/ai-bedrock/tests/adapter.test.ts @@ -38,5 +38,11 @@ describe('BedrockTextAdapter', () => { expect(probe.read({})).toBeUndefined() expect(probe.read(null)).toBeUndefined() }) + it('returns undefined for empty-string reasoning', () => { + expect(probe.read({ choices: [{ delta: { reasoning: '' } }] })).toBeUndefined() + }) + it('returns undefined for non-array choices', () => { + expect(probe.read({ choices: 'not-an-array' })).toBeUndefined() + }) }) }) From a42d7ee541f2fcd21fa5df7662ae981673e5670e Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:08:18 +0200 Subject: [PATCH 12/46] feat(ai-bedrock): responses adapter (mantle-only) on OpenAI Responses base --- .../ai-bedrock/src/adapters/responses-text.ts | 74 +++++++++++++++++++ packages/ai-bedrock/tests/adapter.test.ts | 19 ++++- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 packages/ai-bedrock/src/adapters/responses-text.ts diff --git a/packages/ai-bedrock/src/adapters/responses-text.ts b/packages/ai-bedrock/src/adapters/responses-text.ts new file mode 100644 index 000000000..28f9f2d37 --- /dev/null +++ b/packages/ai-bedrock/src/adapters/responses-text.ts @@ -0,0 +1,74 @@ +import OpenAI from 'openai' +import { OpenAIBaseResponsesTextAdapter } from '@tanstack/openai-base' +import { withBedrockDefaults } from '../utils/client' +import type { Modality } from '@tanstack/ai' +import type { BedrockClientConfig } from '../utils/client' +import type { BedrockMessageMetadataByModality } from '../message-types' +import type { + BedrockChatModelToolCapabilitiesByName, + BedrockResponsesModels, + ResolveInputModalities, +} from '../model-meta' +import type { ExternalResponsesProviderOptions } from '../text/responses-provider-options' + +export interface BedrockResponsesConfig extends BedrockClientConfig {} + +export type { ExternalResponsesProviderOptions as BedrockResponsesProviderOptions } from '../text/responses-provider-options' + +type ResolveToolCapabilities = + TModel extends keyof BedrockChatModelToolCapabilitiesByName + ? NonNullable + : readonly [] + +/** + * Bedrock Responses adapter. Drives mantle's OpenAI-compatible `/responses` + * endpoint via the OpenAI SDK (`client.responses.create`) — the same base + * class ai-openai's `openaiText` uses. Responses is mantle-only, so the + * constructor forces the mantle baseURL. + */ +export class BedrockResponsesTextAdapter< + TModel extends BedrockResponsesModels, + // Constraint mirrors the chat adapter (and ai-groq / ai-openai): the base + // parameterises `TProviderOptions extends Record`, but our + // default `ExternalResponsesProviderOptions` is an interface that (lacking + // an implicit index signature) does not satisfy `Record`. + // `Record` is the only constraint that both accepts that + // interface default AND satisfies the base's constraint. This `any` is + // confined to the generic constraint — no value/shape `as` cast is introduced. + TProviderOptions extends Record = + ExternalResponsesProviderOptions, + TInputModalities extends ReadonlyArray = + ResolveInputModalities, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, +> extends OpenAIBaseResponsesTextAdapter< + TModel, + TProviderOptions, + TInputModalities, + BedrockMessageMetadataByModality, + TToolCapabilities +> { + override readonly kind = 'text' as const + override readonly name = 'bedrock-responses' as const + + constructor(config: BedrockResponsesConfig, model: TModel) { + // Responses is mantle-only — force the mantle base URL (an explicit + // config.baseURL still wins, e.g. E2E pointing at aimock). + super( + model, + 'bedrock-responses', + new OpenAI(withBedrockDefaults(config, 'mantle')), + ) + } +} + +/** Responses adapter with an explicit API key (low-level; the public branching factory delegates here). */ +export function createBedrockResponsesText< + TModel extends BedrockResponsesModels, +>( + model: TModel, + apiKey: string, + config?: Omit, +): BedrockResponsesTextAdapter { + return new BedrockResponsesTextAdapter({ apiKey, ...config }, model) +} diff --git a/packages/ai-bedrock/tests/adapter.test.ts b/packages/ai-bedrock/tests/adapter.test.ts index f88e8e9da..d56d61acf 100644 --- a/packages/ai-bedrock/tests/adapter.test.ts +++ b/packages/ai-bedrock/tests/adapter.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' import { BedrockTextAdapter, createBedrockChat } from '../src/adapters/text' +import { + BedrockResponsesTextAdapter, + createBedrockResponsesText, +} from '../src/adapters/responses-text' describe('BedrockTextAdapter', () => { it('constructs with name "bedrock" and kind "text"', () => { @@ -39,10 +43,23 @@ describe('BedrockTextAdapter', () => { expect(probe.read(null)).toBeUndefined() }) it('returns undefined for empty-string reasoning', () => { - expect(probe.read({ choices: [{ delta: { reasoning: '' } }] })).toBeUndefined() + expect( + probe.read({ choices: [{ delta: { reasoning: '' } }] }), + ).toBeUndefined() }) it('returns undefined for non-array choices', () => { expect(probe.read({ choices: 'not-an-array' })).toBeUndefined() }) }) }) + +describe('BedrockResponsesTextAdapter', () => { + it('constructs with name "bedrock-responses", forces mantle baseURL', () => { + const a = createBedrockResponsesText('openai.gpt-oss-120b', 'test-key', { + region: 'us-east-1', + }) + expect(a).toBeInstanceOf(BedrockResponsesTextAdapter) + expect(a.name).toBe('bedrock-responses') + expect(a.kind).toBe('text') + }) +}) From d11eb3f29ec7f84621e4f31666f42f41fc6339b5 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:17:24 +0200 Subject: [PATCH 13/46] feat(ai-bedrock): branching bedrockText factory (api: chat | responses) --- packages/ai-bedrock/src/index.ts | 137 +++++++++++++++++++++- packages/ai-bedrock/tests/adapter.test.ts | 46 +++++++- packages/ai-bedrock/vite.config.ts | 7 ++ 3 files changed, 188 insertions(+), 2 deletions(-) diff --git a/packages/ai-bedrock/src/index.ts b/packages/ai-bedrock/src/index.ts index 336ce12bb..a762c9278 100644 --- a/packages/ai-bedrock/src/index.ts +++ b/packages/ai-bedrock/src/index.ts @@ -1 +1,136 @@ -export {} +/** + * @module @tanstack/ai-bedrock + * + * Amazon Bedrock adapter for TanStack AI via Bedrock's OpenAI-compatible APIs. + * The public `bedrockText` / `createBedrockText` factory branches between the + * Chat Completions adapter (default) and the Responses adapter via `api`. + */ +import { createBedrockChat } from './adapters/text' +import { createBedrockResponsesText } from './adapters/responses-text' +import { getBedrockApiKeyFromEnv } from './utils' +import { BEDROCK_RESPONSES_MODELS } from './model-meta' +import type { BedrockTextAdapter, BedrockTextConfig } from './adapters/text' +import type { + BedrockResponsesConfig, + BedrockResponsesTextAdapter, +} from './adapters/responses-text' +import type { BedrockChatModels, BedrockResponsesModels } from './model-meta' + +/** Config for the branching factory's chat mode (api omitted or 'chat'). */ +export type BedrockChatApiConfig = Omit & { + api?: 'chat' +} +/** Config for the branching factory's responses mode (api: 'responses' required). */ +export type BedrockResponsesApiConfig = Omit< + BedrockResponsesConfig, + 'apiKey' +> & { api: 'responses' } + +type AnyBedrockAdapter = + | BedrockTextAdapter + | BedrockResponsesTextAdapter + +/** Cast-free runtime guard: is this model in the Responses-capable subset? */ +function isResponsesModel(model: string): model is BedrockResponsesModels { + return BEDROCK_RESPONSES_MODELS.some((m) => m === model) +} + +/** Strip the `api` discriminator from a config without an unused-var lint error. */ +function stripApi(config: T): Omit { + const { api, ...rest } = config + void api + return rest +} + +/** Shared branching used by both public factories. */ +function build( + model: BedrockChatModels, + apiKey: string, + config?: BedrockChatApiConfig | BedrockResponsesApiConfig, +): AnyBedrockAdapter { + if (config?.api === 'responses') { + const rest = stripApi(config) + if (!isResponsesModel(model)) { + throw new Error( + `Model "${model}" is not available on the Bedrock Responses API. ` + + `Responses-capable models: ${BEDROCK_RESPONSES_MODELS.join(', ')}.`, + ) + } + return createBedrockResponsesText(model, apiKey, rest) + } + const rest = config ? stripApi(config) : undefined + return createBedrockChat(model, apiKey, rest) +} + +// --- createBedrockText: explicit key, overloaded on `api` --- +export function createBedrockText( + model: TModel, + apiKey: string, + config?: BedrockChatApiConfig, +): BedrockTextAdapter +export function createBedrockText( + model: TModel, + apiKey: string, + config: BedrockResponsesApiConfig, +): BedrockResponsesTextAdapter +export function createBedrockText( + model: BedrockChatModels, + apiKey: string, + config?: BedrockChatApiConfig | BedrockResponsesApiConfig, +): AnyBedrockAdapter { + return build(model, apiKey, config) +} + +// --- bedrockText: env-key counterpart, same overloads --- +export function bedrockText( + model: TModel, + config?: BedrockChatApiConfig, +): BedrockTextAdapter +export function bedrockText( + model: TModel, + config: BedrockResponsesApiConfig, +): BedrockResponsesTextAdapter +export function bedrockText( + model: BedrockChatModels, + config?: BedrockChatApiConfig | BedrockResponsesApiConfig, +): AnyBedrockAdapter { + return build(model, getBedrockApiKeyFromEnv(), config) +} + +// --- Re-exports --- +export { + BedrockTextAdapter, + createBedrockChat, + type BedrockTextConfig, + type BedrockTextProviderOptions, +} from './adapters/text' +export { + BedrockResponsesTextAdapter, + createBedrockResponsesText, + type BedrockResponsesConfig, + type BedrockResponsesProviderOptions, +} from './adapters/responses-text' +export { + getBedrockApiKeyFromEnv, + resolveBedrockAuth, + withBedrockDefaults, + type BedrockClientConfig, + type BedrockEndpoint, +} from './utils' +export { + BEDROCK_CHAT_MODELS, + BEDROCK_RESPONSES_MODELS, + type BedrockChatModels, + type BedrockResponsesModels, + type BedrockChatModelProviderOptionsByName, + type BedrockChatModelToolCapabilitiesByName, + type BedrockModelInputModalitiesByName, +} from './model-meta' +export type { + BedrockMessageMetadataByModality, + BedrockTextMetadata, + BedrockImageMetadata, + BedrockAudioMetadata, + BedrockVideoMetadata, + BedrockDocumentMetadata, +} from './message-types' diff --git a/packages/ai-bedrock/tests/adapter.test.ts b/packages/ai-bedrock/tests/adapter.test.ts index d56d61acf..d20a1ddf2 100644 --- a/packages/ai-bedrock/tests/adapter.test.ts +++ b/packages/ai-bedrock/tests/adapter.test.ts @@ -1,9 +1,14 @@ -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { BedrockTextAdapter, createBedrockChat } from '../src/adapters/text' import { BedrockResponsesTextAdapter, createBedrockResponsesText, } from '../src/adapters/responses-text' +import { bedrockText, createBedrockText } from '../src/index' +import { BedrockTextAdapter as ChatAdapter } from '../src/adapters/text' +import { BedrockResponsesTextAdapter as RespAdapter } from '../src/adapters/responses-text' + +afterEach(() => vi.unstubAllEnvs()) describe('BedrockTextAdapter', () => { it('constructs with name "bedrock" and kind "text"', () => { @@ -63,3 +68,42 @@ describe('BedrockResponsesTextAdapter', () => { expect(a.kind).toBe('text') }) }) + +describe('createBedrockText (branching factory)', () => { + it('defaults to the chat adapter', () => { + const a = createBedrockText('openai.gpt-oss-120b', 'k', { + region: 'us-east-1', + }) + expect(a).toBeInstanceOf(ChatAdapter) + expect(a.name).toBe('bedrock') + }) + + it("returns the responses adapter when api: 'responses'", () => { + const a = createBedrockText('openai.gpt-oss-120b', 'k', { + region: 'us-east-1', + api: 'responses', + }) + expect(a).toBeInstanceOf(RespAdapter) + expect(a.name).toBe('bedrock-responses') + }) + + it("explicit api: 'chat' returns the chat adapter", () => { + const a = createBedrockText('openai.gpt-oss-120b', 'k', { api: 'chat' }) + expect(a).toBeInstanceOf(ChatAdapter) + }) +}) + +describe('bedrockText (env-key branching factory)', () => { + it('reads the key from BEDROCK_API_KEY and branches on api', () => { + vi.stubEnv('BEDROCK_API_KEY', 'env-key') + expect( + bedrockText('openai.gpt-oss-120b', { region: 'us-east-1' }), + ).toBeInstanceOf(ChatAdapter) + expect( + bedrockText('openai.gpt-oss-120b', { + region: 'us-east-1', + api: 'responses', + }), + ).toBeInstanceOf(RespAdapter) + }) +}) diff --git a/packages/ai-bedrock/vite.config.ts b/packages/ai-bedrock/vite.config.ts index 813bf1f10..f6ab00f03 100644 --- a/packages/ai-bedrock/vite.config.ts +++ b/packages/ai-bedrock/vite.config.ts @@ -25,5 +25,12 @@ export default mergeConfig( entry: ['./src/index.ts', './src/sigv4/index.ts'], srcDir: './src', cjs: false, + // `aws-sigv4-fetch` is an optional, user-installed dependency that the + // `/sigv4` subpath dynamically imports. It is intentionally NOT declared in + // package.json (pnpm v11 autoInstallPeers + trust-policy interaction), so + // externalizeDeps (which reads the manifest) does not pick it up. Externalize + // it explicitly so Rollup leaves the dynamic import in place instead of + // trying — and failing — to bundle it. + externalDeps: ['aws-sigv4-fetch'], }), ) From cdc5f51f6f4fec1461989199eea2a756fe38534b Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:24:15 +0200 Subject: [PATCH 14/46] fix(ai-bedrock): export ResolvedBedrockAuth; lock api:responses type+runtime contract --- packages/ai-bedrock/src/index.ts | 1 + packages/ai-bedrock/tests/adapter.test.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/packages/ai-bedrock/src/index.ts b/packages/ai-bedrock/src/index.ts index a762c9278..ac911a195 100644 --- a/packages/ai-bedrock/src/index.ts +++ b/packages/ai-bedrock/src/index.ts @@ -116,6 +116,7 @@ export { withBedrockDefaults, type BedrockClientConfig, type BedrockEndpoint, + type ResolvedBedrockAuth, } from './utils' export { BEDROCK_CHAT_MODELS, diff --git a/packages/ai-bedrock/tests/adapter.test.ts b/packages/ai-bedrock/tests/adapter.test.ts index d20a1ddf2..e53d33ad4 100644 --- a/packages/ai-bedrock/tests/adapter.test.ts +++ b/packages/ai-bedrock/tests/adapter.test.ts @@ -91,6 +91,15 @@ describe('createBedrockText (branching factory)', () => { const a = createBedrockText('openai.gpt-oss-120b', 'k', { api: 'chat' }) expect(a).toBeInstanceOf(ChatAdapter) }) + + it('rejects a chat-only model with api:responses (compile-time) and throws at runtime', () => { + expect(() => { + // @ts-expect-error — a chat-only model is not assignable to the api:'responses' overload + // (BedrockResponsesModels). This line also locks the compile-time contract: if the + // overloads ever stop rejecting it, the @ts-expect-error becomes unused and tsc fails. + createBedrockText('us.anthropic.claude-3-5-haiku-20241022-v1:0', 'k', { api: 'responses' }) + }).toThrowError(/Responses-capable models:/) + }) }) describe('bedrockText (env-key branching factory)', () => { From 7f5459c67748f38c2a234a6879fbe58ac68cd021 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:27:52 +0200 Subject: [PATCH 15/46] chore(ai-bedrock): add maintainer model-catalog refresh script --- scripts/fetch-bedrock-models.ts | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 scripts/fetch-bedrock-models.ts diff --git a/scripts/fetch-bedrock-models.ts b/scripts/fetch-bedrock-models.ts new file mode 100644 index 000000000..5e8277788 --- /dev/null +++ b/scripts/fetch-bedrock-models.ts @@ -0,0 +1,55 @@ +/** + * Fetches the Bedrock foundation-model + inference-profile catalog and prints + * the chat-capable invocation IDs and cross-region inference-profile IDs so a + * maintainer can refresh packages/ai-bedrock/src/model-meta.ts. + * + * MAINTAINER-ONLY. Not run in CI. Requires AWS credentials (standard provider + * chain) with bedrock:List* permissions, and the AWS SDK: + * pnpm add -Dw @aws-sdk/client-bedrock # if not already installed + * AWS_REGION=us-east-1 pnpm tsx scripts/fetch-bedrock-models.ts + * + * Why manual: ListFoundationModels carries modalities + inference types but no + * pricing, and per-account/region availability varies. The committed model-meta + * is a hand-transcribed seed; this script is the long-term source of truth. + * Responses-capable models are those with Responses=Yes in + * https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html + */ +import { + BedrockClient, + ListFoundationModelsCommand, + ListInferenceProfilesCommand, +} from '@aws-sdk/client-bedrock' + +async function main() { + const region = process.env['AWS_REGION'] ?? 'us-east-1' + const client = new BedrockClient({ region }) + + const models = await client.send( + new ListFoundationModelsCommand({ byOutputModality: 'TEXT' }), + ) + const profiles = await client.send(new ListInferenceProfilesCommand({})) + + const textModels = (models.modelSummaries ?? []) + .filter((m) => (m.outputModalities ?? []).includes('TEXT')) + .map((m) => ({ + id: m.modelId ?? '', + input: (m.inputModalities ?? []).map((x) => x.toLowerCase()), + })) + .filter((m) => m.id.length > 0) + + const inferenceProfileIds = (profiles.inferenceProfileSummaries ?? []) + .map((p) => p.inferenceProfileId ?? '') + .filter((id) => id.length > 0) + + console.log('# Base foundation text models:') + for (const m of textModels) console.log(`${m.id}\tinput=${m.input.join(',')}`) + console.log( + '\n# Cross-region inference profile IDs (use as `model` for runtime chat):', + ) + for (const id of inferenceProfileIds.sort()) console.log(id) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) From 33778ca58d14860fdf682c9f58a89373b3a5201c Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:37:47 +0200 Subject: [PATCH 16/46] test(e2e): register bedrock + bedrock-responses providers --- pnpm-lock.yaml | 515 +------------------------ testing/e2e/package.json | 1 + testing/e2e/src/lib/feature-support.ts | 24 +- testing/e2e/src/lib/providers.ts | 18 + testing/e2e/src/lib/types.ts | 4 + testing/e2e/tests/test-matrix.ts | 2 + 6 files changed, 51 insertions(+), 513 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5be74a169..2a1293161 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1841,6 +1841,9 @@ importers: '@tanstack/ai-anthropic': specifier: workspace:* version: link:../../packages/ai-anthropic + '@tanstack/ai-bedrock': + specifier: workspace:* + version: link:../../packages/ai-bedrock '@tanstack/ai-client': specifier: workspace:* version: link:../../packages/ai-client @@ -2114,87 +2117,6 @@ 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/core@3.974.15': - resolution: {integrity: sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-env@3.972.41': - resolution: {integrity: sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-http@3.972.43': - resolution: {integrity: sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-ini@3.972.45': - resolution: {integrity: sha512-sJe5ZWibO4s7RWjFQ8Zol76KxoJcIYyEZH1/wxQSBMSIAAxzaJ8cS/ITAaIHWUQvDKQdt18+cJAHKWB7n1Jmrg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-login@3.972.45': - resolution: {integrity: sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-node@3.972.46': - resolution: {integrity: sha512-cS4w0jzDRb1jOlkiJS3y80OxddHzkky/MN9k3NYs5jganNKVLjF0lpvjlwS118oGMr3cdAfOlVdo8gLurTSE7w==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-process@3.972.41': - resolution: {integrity: sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-sso@3.972.45': - resolution: {integrity: sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-web-identity@3.972.45': - resolution: {integrity: sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/nested-clients@3.997.13': - resolution: {integrity: sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/signature-v4-multi-region@3.996.30': - resolution: {integrity: sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1056.0': - resolution: {integrity: sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/types@3.973.9': - resolution: {integrity: sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-locate-window@3.965.5': - resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/xml-builder@3.972.26': - resolution: {integrity: sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==} - engines: {node: '>=20.0.0'} - - '@aws/lambda-invoke-store@0.2.4': - resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} - engines: {node: '>=18.0.0'} - '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -4301,9 +4223,6 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@nodable/entities@2.1.1': - resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -6310,78 +6229,6 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@smithy/core@3.24.5': - resolution: {integrity: sha512-Kt8phUg45M15EjhYAbZ+fFikYneijLu9Liugz8ZsYz2i8j0hzGv27LWKpEHYRfvj+LyCOSijpcR/2i8RouV+cA==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.3.6': - resolution: {integrity: sha512-tHhdiWZfG1ZIh2YcRfPJmY2gHcBmqbAzqm3ER4TIDFYsSEqTD5tICT7cgQ/kI8LRakxp12myOYyK68XPn7MnHw==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.4.5': - resolution: {integrity: sha512-SK3VMeH0fibgdTg2QeB+O4p7Yy/2E5HBOHJeC58FshkDdeuX8lOgO7PfjYfLyPLP1ch55j91cQqKBzDS0mRjSQ==} - 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@3.0.0': - resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} - engines: {node: '>=16.0.0'} - - '@smithy/node-http-handler@4.7.5': - resolution: {integrity: sha512-3dA9TQ+ybRSZ/m0wnbZhiBy4Dezjgq1Ib/ZZrYTpJDBgpoLLU/SDzZc/g0x0MNAdOJe1wPcM+x2PBRmoOur+Sw==} - engines: {node: '>=18.0.0'} - - '@smithy/protocol-http@4.1.8': - resolution: {integrity: sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==} - engines: {node: '>=16.0.0'} - - '@smithy/signature-v4@3.1.2': - resolution: {integrity: sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA==} - engines: {node: '>=16.0.0'} - - '@smithy/signature-v4@5.4.5': - resolution: {integrity: sha512-QBJKWGqIknH0dc9LWpfH1mkdokAx6iXYN3UcQ3eY6uIEyScuoQAhfl94ge7ozUy9WgFUdE8xsvwBjaYBbWmPNA==} - engines: {node: '>=18.0.0'} - - '@smithy/types@3.7.2': - resolution: {integrity: sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==} - engines: {node: '>=16.0.0'} - - '@smithy/types@4.14.2': - resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} - 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@3.0.0': - resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==} - engines: {node: '>=16.0.0'} - - '@smithy/util-hex-encoding@3.0.0': - resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} - engines: {node: '>=16.0.0'} - - '@smithy/util-middleware@3.0.11': - resolution: {integrity: sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow==} - engines: {node: '>=16.0.0'} - - '@smithy/util-uri-escape@3.0.0': - resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} - engines: {node: '>=16.0.0'} - - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} - - '@smithy/util-utf8@3.0.0': - resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} - engines: {node: '>=16.0.0'} - '@solid-devtools/debugger@0.28.1': resolution: {integrity: sha512-6qIUI6VYkXoRnL8oF5bvh2KgH71qlJ18hNw/mwSyY6v48eb80ZR48/5PDXufUa3q+MBSuYa1uqTMwLewpay9eg==} peerDependencies: @@ -8188,9 +8035,6 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - bowser@2.14.1: - resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} - boxen@7.1.1: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} @@ -9381,13 +9225,6 @@ packages: fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - fast-xml-builder@1.2.0: - resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} - - fast-xml-parser@5.7.3: - resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} - hasBin: true - fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -11486,10 +11323,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-expression-matcher@1.5.0: - resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} - engines: {node: '>=14.0.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -12569,9 +12402,6 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strnum@2.3.0: - resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} - structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} @@ -13879,10 +13709,6 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} - xml-naming@0.1.0: - resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} - engines: {node: '>=16.0.0'} - xml2js@0.6.0: resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==} engines: {node: '>=4.0.0'} @@ -14001,200 +13827,6 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} - '@aws-crypto/crc32@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.9 - tslib: 2.8.1 - optional: true - - '@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.973.9 - '@aws-sdk/util-locate-window': 3.965.5 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - optional: true - - '@aws-crypto/sha256-js@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.9 - tslib: 2.8.1 - optional: true - - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@aws-crypto/util@5.2.0': - dependencies: - '@aws-sdk/types': 3.973.9 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - optional: true - - '@aws-sdk/core@3.974.15': - dependencies: - '@aws-sdk/types': 3.973.9 - '@aws-sdk/xml-builder': 3.972.26 - '@aws/lambda-invoke-store': 0.2.4 - '@smithy/core': 3.24.5 - '@smithy/signature-v4': 5.4.5 - '@smithy/types': 4.14.2 - bowser: 2.14.1 - tslib: 2.8.1 - optional: true - - '@aws-sdk/credential-provider-env@3.972.41': - dependencies: - '@aws-sdk/core': 3.974.15 - '@aws-sdk/types': 3.973.9 - '@smithy/core': 3.24.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/credential-provider-http@3.972.43': - dependencies: - '@aws-sdk/core': 3.974.15 - '@aws-sdk/types': 3.973.9 - '@smithy/core': 3.24.5 - '@smithy/fetch-http-handler': 5.4.5 - '@smithy/node-http-handler': 4.7.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/credential-provider-ini@3.972.45': - dependencies: - '@aws-sdk/core': 3.974.15 - '@aws-sdk/credential-provider-env': 3.972.41 - '@aws-sdk/credential-provider-http': 3.972.43 - '@aws-sdk/credential-provider-login': 3.972.45 - '@aws-sdk/credential-provider-process': 3.972.41 - '@aws-sdk/credential-provider-sso': 3.972.45 - '@aws-sdk/credential-provider-web-identity': 3.972.45 - '@aws-sdk/nested-clients': 3.997.13 - '@aws-sdk/types': 3.973.9 - '@smithy/core': 3.24.5 - '@smithy/credential-provider-imds': 4.3.6 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/credential-provider-login@3.972.45': - dependencies: - '@aws-sdk/core': 3.974.15 - '@aws-sdk/nested-clients': 3.997.13 - '@aws-sdk/types': 3.973.9 - '@smithy/core': 3.24.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/credential-provider-node@3.972.46': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.41 - '@aws-sdk/credential-provider-http': 3.972.43 - '@aws-sdk/credential-provider-ini': 3.972.45 - '@aws-sdk/credential-provider-process': 3.972.41 - '@aws-sdk/credential-provider-sso': 3.972.45 - '@aws-sdk/credential-provider-web-identity': 3.972.45 - '@aws-sdk/types': 3.973.9 - '@smithy/core': 3.24.5 - '@smithy/credential-provider-imds': 4.3.6 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/credential-provider-process@3.972.41': - dependencies: - '@aws-sdk/core': 3.974.15 - '@aws-sdk/types': 3.973.9 - '@smithy/core': 3.24.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/credential-provider-sso@3.972.45': - dependencies: - '@aws-sdk/core': 3.974.15 - '@aws-sdk/nested-clients': 3.997.13 - '@aws-sdk/token-providers': 3.1056.0 - '@aws-sdk/types': 3.973.9 - '@smithy/core': 3.24.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/credential-provider-web-identity@3.972.45': - dependencies: - '@aws-sdk/core': 3.974.15 - '@aws-sdk/nested-clients': 3.997.13 - '@aws-sdk/types': 3.973.9 - '@smithy/core': 3.24.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/nested-clients@3.997.13': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.15 - '@aws-sdk/signature-v4-multi-region': 3.996.30 - '@aws-sdk/types': 3.973.9 - '@smithy/core': 3.24.5 - '@smithy/fetch-http-handler': 5.4.5 - '@smithy/node-http-handler': 4.7.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/signature-v4-multi-region@3.996.30': - dependencies: - '@aws-sdk/types': 3.973.9 - '@smithy/signature-v4': 5.4.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/token-providers@3.1056.0': - dependencies: - '@aws-sdk/core': 3.974.15 - '@aws-sdk/nested-clients': 3.997.13 - '@aws-sdk/types': 3.973.9 - '@smithy/core': 3.24.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/types@3.973.9': - dependencies: - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@aws-sdk/util-locate-window@3.965.5': - dependencies: - tslib: 2.8.1 - optional: true - - '@aws-sdk/xml-builder@3.972.26': - dependencies: - '@smithy/types': 4.14.2 - fast-xml-parser: 5.7.3 - tslib: 2.8.1 - optional: true - - '@aws/lambda-invoke-store@0.2.4': - optional: true - '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -16366,9 +15998,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@nodable/entities@2.1.1': - optional: true - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -18048,118 +17677,6 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@smithy/core@3.24.5': - dependencies: - '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@smithy/credential-provider-imds@4.3.6': - dependencies: - '@smithy/core': 3.24.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@smithy/fetch-http-handler@5.4.5': - dependencies: - '@smithy/core': 3.24.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@smithy/is-array-buffer@3.0.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@smithy/node-http-handler@4.7.5': - dependencies: - '@smithy/core': 3.24.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@smithy/protocol-http@4.1.8': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - optional: true - - '@smithy/signature-v4@3.1.2': - dependencies: - '@smithy/is-array-buffer': 3.0.0 - '@smithy/types': 3.7.2 - '@smithy/util-hex-encoding': 3.0.0 - '@smithy/util-middleware': 3.0.11 - '@smithy/util-uri-escape': 3.0.0 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - optional: true - - '@smithy/signature-v4@5.4.5': - dependencies: - '@smithy/core': 3.24.5 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - optional: true - - '@smithy/types@3.7.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@smithy/types@4.14.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@smithy/util-buffer-from@2.2.0': - dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 - optional: true - - '@smithy/util-buffer-from@3.0.0': - dependencies: - '@smithy/is-array-buffer': 3.0.0 - tslib: 2.8.1 - optional: true - - '@smithy/util-hex-encoding@3.0.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@smithy/util-middleware@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - optional: true - - '@smithy/util-uri-escape@3.0.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@smithy/util-utf8@2.3.0': - dependencies: - '@smithy/util-buffer-from': 2.2.0 - tslib: 2.8.1 - optional: true - - '@smithy/util-utf8@3.0.0': - dependencies: - '@smithy/util-buffer-from': 3.0.0 - tslib: 2.8.1 - optional: true - '@solid-devtools/debugger@0.28.1(solid-js@1.9.10)': dependencies: '@nothing-but/utils': 0.17.0 @@ -20951,9 +20468,6 @@ snapshots: boolbase@1.0.0: {} - bowser@2.14.1: - optional: true - boxen@7.1.1: dependencies: ansi-align: 3.0.1 @@ -22334,20 +21848,6 @@ snapshots: fast-sha256@1.3.0: {} - fast-xml-builder@1.2.0: - dependencies: - path-expression-matcher: 1.5.0 - xml-naming: 0.1.0 - optional: true - - fast-xml-parser@5.7.3: - dependencies: - '@nodable/entities': 2.1.1 - fast-xml-builder: 1.2.0 - path-expression-matcher: 1.5.0 - strnum: 2.3.0 - optional: true - fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -25174,9 +24674,6 @@ snapshots: path-exists@4.0.0: {} - path-expression-matcher@1.5.0: - optional: true - path-key@3.1.1: {} path-key@4.0.0: {} @@ -26522,9 +26019,6 @@ snapshots: dependencies: js-tokens: 9.0.1 - strnum@2.3.0: - optional: true - structured-headers@0.4.1: {} style-to-js@1.1.21: @@ -27755,9 +27249,6 @@ snapshots: xml-name-validator@5.0.0: {} - xml-naming@0.1.0: - optional: true - xml2js@0.6.0: dependencies: sax: 1.6.0 diff --git a/testing/e2e/package.json b/testing/e2e/package.json index 54b5fcef5..b67aee849 100644 --- a/testing/e2e/package.json +++ b/testing/e2e/package.json @@ -17,6 +17,7 @@ "@tailwindcss/vite": "^4.1.18", "@tanstack/ai": "workspace:*", "@tanstack/ai-anthropic": "workspace:*", + "@tanstack/ai-bedrock": "workspace:*", "@tanstack/ai-client": "workspace:*", "@tanstack/ai-elevenlabs": "workspace:*", "@tanstack/ai-gemini": "workspace:*", diff --git a/testing/e2e/src/lib/feature-support.ts b/testing/e2e/src/lib/feature-support.ts index 49aa74708..a34ab06f0 100644 --- a/testing/e2e/src/lib/feature-support.ts +++ b/testing/e2e/src/lib/feature-support.ts @@ -15,6 +15,7 @@ export const matrix: Record> = { 'ollama', 'groq', 'grok', + 'bedrock', 'openrouter', ]), 'one-shot-text': new Set([ @@ -24,6 +25,7 @@ export const matrix: Record> = { 'ollama', 'groq', 'grok', + 'bedrock', 'openrouter', ]), reasoning: new Set(['openai', 'anthropic', 'gemini']), @@ -34,6 +36,7 @@ export const matrix: Record> = { 'ollama', 'groq', 'grok', + 'bedrock', 'openrouter', ]), 'tool-calling': new Set([ @@ -43,6 +46,7 @@ export const matrix: Record> = { 'ollama', 'groq', 'grok', + 'bedrock', 'openrouter', ]), 'parallel-tool-calls': new Set([ @@ -51,6 +55,7 @@ export const matrix: Record> = { 'gemini', 'groq', 'grok', + 'bedrock', 'openrouter', ]), // Gemini excluded: approval flow timing issues with Gemini's streaming format @@ -60,6 +65,7 @@ export const matrix: Record> = { 'ollama', 'groq', 'grok', + 'bedrock', 'openrouter', ]), // Ollama excluded: aimock doesn't support content+toolCalls for /api/chat format @@ -69,6 +75,7 @@ export const matrix: Record> = { 'gemini', 'groq', 'grok', + 'bedrock', 'openrouter', ]), 'structured-output': new Set([ @@ -78,13 +85,20 @@ export const matrix: Record> = { 'ollama', 'groq', 'grok', + 'bedrock', 'openrouter', ]), // Streaming structured output: only providers with native streaming JSON // schema support are listed here. Other providers fall back to the // activity-layer `fallbackStructuredOutputStream` (which wraps the // non-streaming `structuredOutput`) but aren't exercised by E2E yet. - 'structured-output-stream': new Set(['openai', 'groq', 'grok', 'openrouter']), + 'structured-output-stream': new Set([ + 'openai', + 'groq', + 'grok', + 'bedrock', + 'openrouter', + ]), // Multi-turn structured output: every turn produces its own typed // `structured-output` part on the assistant message, and historical // turns stay renderable. Works for every provider that supports both @@ -109,6 +123,7 @@ export const matrix: Record> = { 'ollama', 'groq', 'grok', + 'bedrock', 'openrouter', ]), 'agentic-structured': new Set([ @@ -118,6 +133,7 @@ export const matrix: Record> = { 'ollama', 'groq', 'grok', + 'bedrock', 'openrouter', ]), // Native-combined-mode adapters only. Each provider's default test model @@ -130,6 +146,9 @@ export const matrix: Record> = { 'gemini', 'grok', ]), + // Bedrock excluded: the default e2e model (openai.gpt-oss-120b) is text-only + // (input: ['text'], no vision) — image input isn't supported, so the + // multimodal request never carries the image and the description comes back empty. 'multimodal-image': new Set([ 'openai', 'anthropic', @@ -137,6 +156,7 @@ export const matrix: Record> = { 'grok', 'openrouter', ]), + // Bedrock excluded: same text-only default e2e model as multimodal-image above. 'multimodal-structured': new Set([ 'openai', 'anthropic', @@ -150,6 +170,7 @@ export const matrix: Record> = { 'gemini', 'ollama', 'grok', + 'bedrock', 'openrouter', ]), 'summarize-stream': new Set([ @@ -158,6 +179,7 @@ export const matrix: Record> = { 'gemini', 'ollama', 'grok', + 'bedrock', 'openrouter', ]), // Gemini excluded: aimock doesn't mock Gemini's Imagen predict endpoint format diff --git a/testing/e2e/src/lib/providers.ts b/testing/e2e/src/lib/providers.ts index fe80ed5e4..43f67607e 100644 --- a/testing/e2e/src/lib/providers.ts +++ b/testing/e2e/src/lib/providers.ts @@ -7,6 +7,7 @@ import { createGeminiTextInteractions } from '@tanstack/ai-gemini/experimental' import { createOllamaChat } from '@tanstack/ai-ollama' import { createGroqText } from '@tanstack/ai-groq' import { createGrokText } from '@tanstack/ai-grok' +import { createBedrockText } from '@tanstack/ai-bedrock' import { createOpenRouterResponsesText, createOpenRouterText, @@ -24,6 +25,8 @@ const defaultModels: Record = { ollama: 'mistral', groq: 'llama-3.3-70b-versatile', grok: 'grok-3', + bedrock: 'openai.gpt-oss-120b', + 'bedrock-responses': 'openai.gpt-oss-120b', openrouter: 'openai/gpt-4o', 'openrouter-responses': 'openai/gpt-4o', // ElevenLabs has no chat/text model — the support matrix already filters @@ -112,6 +115,21 @@ export function createTextAdapter( defaultHeaders: testHeaders, }), }), + bedrock: () => + createChatOptions({ + adapter: createBedrockText(model as 'openai.gpt-oss-120b', DUMMY_KEY, { + baseURL: openaiUrl, + defaultHeaders: testHeaders, + }), + }), + 'bedrock-responses': () => + createChatOptions({ + adapter: createBedrockText(model as 'openai.gpt-oss-120b', DUMMY_KEY, { + baseURL: openaiUrl, + defaultHeaders: testHeaders, + api: 'responses', + }), + }), openrouter: () => { // OpenRouter SDK exposes an HTTPClient with beforeRequest hooks. Use // that to inject X-Test-Id, since `defaultHeaders` isn't supported and diff --git a/testing/e2e/src/lib/types.ts b/testing/e2e/src/lib/types.ts index a8dbd0cf1..3a161acea 100644 --- a/testing/e2e/src/lib/types.ts +++ b/testing/e2e/src/lib/types.ts @@ -7,6 +7,8 @@ export type Provider = | 'ollama' | 'grok' | 'groq' + | 'bedrock' + | 'bedrock-responses' | 'openrouter' | 'openrouter-responses' | 'elevenlabs' @@ -44,6 +46,8 @@ export const ALL_PROVIDERS: Provider[] = [ 'ollama', 'grok', 'groq', + 'bedrock', + 'bedrock-responses', 'openrouter', 'openrouter-responses', 'elevenlabs', diff --git a/testing/e2e/tests/test-matrix.ts b/testing/e2e/tests/test-matrix.ts index f48dcebc0..a1473f5b0 100644 --- a/testing/e2e/tests/test-matrix.ts +++ b/testing/e2e/tests/test-matrix.ts @@ -20,6 +20,8 @@ export const providers: Provider[] = [ 'ollama', 'groq', 'grok', + 'bedrock', + 'bedrock-responses', 'openrouter', 'openrouter-responses', 'elevenlabs', From 0c35a85932fa25dc8b3bc86b18f681c6d6955558 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:43:11 +0200 Subject: [PATCH 17/46] test(e2e): add feature coverage for bedrock-responses --- testing/e2e/src/lib/feature-support.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/testing/e2e/src/lib/feature-support.ts b/testing/e2e/src/lib/feature-support.ts index a34ab06f0..3f8a45990 100644 --- a/testing/e2e/src/lib/feature-support.ts +++ b/testing/e2e/src/lib/feature-support.ts @@ -16,6 +16,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), 'one-shot-text': new Set([ @@ -26,6 +27,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), reasoning: new Set(['openai', 'anthropic', 'gemini']), @@ -37,6 +39,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), 'tool-calling': new Set([ @@ -47,6 +50,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), 'parallel-tool-calls': new Set([ @@ -56,6 +60,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), // Gemini excluded: approval flow timing issues with Gemini's streaming format @@ -66,6 +71,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), // Ollama excluded: aimock doesn't support content+toolCalls for /api/chat format @@ -76,6 +82,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), 'structured-output': new Set([ @@ -86,6 +93,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), // Streaming structured output: only providers with native streaming JSON @@ -97,6 +105,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), // Multi-turn structured output: every turn produces its own typed @@ -124,6 +133,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), 'agentic-structured': new Set([ @@ -134,6 +144,7 @@ export const matrix: Record> = { 'groq', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), // Native-combined-mode adapters only. Each provider's default test model @@ -171,6 +182,7 @@ export const matrix: Record> = { 'ollama', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), 'summarize-stream': new Set([ @@ -180,6 +192,7 @@ export const matrix: Record> = { 'ollama', 'grok', 'bedrock', + 'bedrock-responses', 'openrouter', ]), // Gemini excluded: aimock doesn't mock Gemini's Imagen predict endpoint format From 3ef2bbd47c5c17421e36557c1afd7e787bcd5b19 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:46:49 +0200 Subject: [PATCH 18/46] docs(ai-bedrock): adapter guide, nav, changeset --- .changeset/ai-bedrock-adapter.md | 5 + docs/community-adapters/bedrock.md | 173 +++++++++++++++++++++++++++++ docs/config.json | 112 ++++++++++--------- packages/ai-bedrock/README.md | 93 +++++++++++++++- 4 files changed, 330 insertions(+), 53 deletions(-) create mode 100644 .changeset/ai-bedrock-adapter.md create mode 100644 docs/community-adapters/bedrock.md diff --git a/.changeset/ai-bedrock-adapter.md b/.changeset/ai-bedrock-adapter.md new file mode 100644 index 000000000..0315785ec --- /dev/null +++ b/.changeset/ai-bedrock-adapter.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-bedrock': minor +--- + +Add `@tanstack/ai-bedrock`: an Amazon Bedrock adapter built on Bedrock's OpenAI-compatible Chat Completions and Responses APIs. A single branching `bedrockText` factory (`api: 'chat' | 'responses'`) supports streaming, tools, and reasoning, with API-key or SigV4 authentication and configurable `runtime`/`mantle` endpoints. diff --git a/docs/community-adapters/bedrock.md b/docs/community-adapters/bedrock.md new file mode 100644 index 000000000..86b1f3b5e --- /dev/null +++ b/docs/community-adapters/bedrock.md @@ -0,0 +1,173 @@ +--- +title: Amazon Bedrock +id: bedrock-adapter +order: 4 +description: "Use Amazon Bedrock's OpenAI-compatible Chat Completions and Responses APIs with TanStack AI — streaming, tools, reasoning, API-key or SigV4 auth, and configurable runtime/mantle endpoints." +keywords: + - tanstack ai + - amazon bedrock + - aws + - bedrock + - openai compatible + - chat completions + - responses api + - sigv4 + - claude + - nova + - llama + - community adapter +--- + +The Bedrock adapter connects TanStack AI to [Amazon Bedrock](https://aws.amazon.com/bedrock/) via Bedrock's OpenAI-compatible Chat Completions and Responses APIs. It is built on top of the `openai` SDK and supports streaming, client-side tool calling, and reasoning. + +## Installation + +```bash +pnpm add @tanstack/ai-bedrock +``` + +If you want to use **SigV4 authentication** (AWS credentials instead of an API key), also install the optional peer: + +```bash +pnpm add aws-sigv4-fetch +``` + +`aws-sigv4-fetch` is not bundled with `@tanstack/ai-bedrock` — it is an optional install you only need when `auth: 'sigv4'` (or `auth: 'auto'` with no API key in the environment). + +## Authentication + +Bedrock supports two authentication modes. + +### API Key + +Bedrock issues API keys from the AWS Console. See the [Bedrock API keys guide](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html) for instructions. + +Set one of the following environment variables and the adapter picks it up automatically: + +```bash +BEDROCK_API_KEY=your-bedrock-api-key +# or the legacy name: +AWS_BEARER_TOKEN_BEDROCK=your-bedrock-api-key +``` + +### SigV4 (AWS credential chain) + +For workloads that use IAM roles, instance profiles, or `~/.aws/credentials`, set `auth: 'sigv4'`. The adapter uses the standard AWS credential chain (environment variables, shared credential file, instance metadata, etc.) via `aws-sigv4-fetch`. + +```bash +AWS_ACCESS_KEY_ID=... +AWS_SECRET_ACCESS_KEY=... +AWS_SESSION_TOKEN=... # optional, for temporary credentials +``` + +### Auth resolution order (`auth: 'auto'`, the default) + +1. Explicit `apiKey` passed to the factory +2. `BEDROCK_API_KEY` environment variable +3. `AWS_BEARER_TOKEN_BEDROCK` environment variable +4. SigV4 via the AWS credential chain (requires `aws-sigv4-fetch`) + +## Configuration + +`BedrockClientConfig` accepts the following options: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `region` | `string` | `'us-east-1'` | Full AWS region string (e.g. `'us-west-2'`) | +| `endpoint` | `'runtime' \| 'mantle'` | `'runtime'` | Bedrock endpoint to target (Chat API only) | +| `auth` | `'apikey' \| 'sigv4' \| 'auto'` | `'auto'` | Authentication mode | +| `apiKey` | `string` | — | Explicit API key (overrides env vars) | +| `baseURL` | `string` | — | Override the computed base URL entirely | + +The `endpoint` option applies only when `api: 'chat'` (or omitted). The `runtime` endpoint (`bedrock-runtime`) hosts the broad model catalog; `mantle` is an optional alternative. The Responses API always targets mantle. + +## Chat Completions (default) + +Use `bedrockText` with no `api` option, or `api: 'chat'`, to call Bedrock's Chat Completions endpoint. This gives you access to the broadest model catalog: Claude, Amazon Nova, Meta Llama, Mistral, DeepSeek, and OpenAI gpt-oss models. + +```typescript +import { bedrockText } from '@tanstack/ai-bedrock' +import { chat } from '@tanstack/ai' + +const adapter = bedrockText('us.anthropic.claude-3-7-sonnet-20250219-v1:0', { + region: 'us-east-1', +}) + +for await (const chunk of chat({ + adapter, + messages: [{ role: 'user', content: 'What is the capital of France?' }], +})) { + if (chunk.type === 'content') process.stdout.write(chunk.delta) +} +``` + +### Explicit API key + +```typescript +import { createBedrockText } from '@tanstack/ai-bedrock' + +const adapter = createBedrockText( + 'us.amazon.nova-pro-v1:0', + 'your-bedrock-api-key', + { region: 'us-west-2' }, +) +``` + +## Responses API + +Set `api: 'responses'` to use Bedrock's Responses API. This API is mantle-only, supports a narrower model set (currently the OpenAI gpt-oss family), and is stateful — you can pass `previous_response_id` and `store` through `modelOptions` to continue a conversation server-side. + +```typescript +import { bedrockText } from '@tanstack/ai-bedrock' +import { chat } from '@tanstack/ai' + +const adapter = bedrockText('openai.gpt-oss-120b', { + region: 'us-east-1', + api: 'responses', +}) + +for await (const chunk of chat({ + adapter, + messages: [{ role: 'user', content: 'Summarize the Bedrock pricing page.' }], +})) { + if (chunk.type === 'content') process.stdout.write(chunk.delta) +} +``` + +## Model Availability + +The model catalog (`BEDROCK_CHAT_MODELS`, `BEDROCK_RESPONSES_MODELS`) is a hand-seeded snapshot of cross-region inference profile IDs. **Actual model availability depends on your AWS account's model access configuration and the region you are targeting.** Enable model access in the [Amazon Bedrock console](https://console.aws.amazon.com/bedrock/home#/modelaccess) before use. A maintainer refresh script (`scripts/fetch-bedrock-models.ts`) can regenerate the catalog. + +## Supported Capabilities + +- Streaming chat completions +- Client-side tool calling +- Reasoning (extended thinking) +- Multimodal input (text, images, documents — model-dependent) +- JSON schema / structured output + +## API Reference + +### `bedrockText(model, config?)` + +Creates a Bedrock adapter using environment-variable auth. + +- `model` — Model ID (e.g. `'us.anthropic.claude-3-7-sonnet-20250219-v1:0'`) +- `config.api` — `'chat'` (default) or `'responses'` +- `config.region` — AWS region string (default `'us-east-1'`) +- `config.endpoint` — `'runtime'` (default) or `'mantle'` (Chat API only) +- `config.auth` — `'auto'` (default), `'apikey'`, or `'sigv4'` +- `config.baseURL` — Override base URL + +Returns a chat adapter for use with `chat()` or `generate()`. + +### `createBedrockText(model, apiKey, config?)` + +Creates a Bedrock adapter with an explicit API key, bypassing the environment-variable lookup. + +## Next Steps + +- [Amazon Bedrock API keys](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html) — Create and manage API keys +- [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) — Enable models in your account +- [Streaming Guide](../chat/streaming) — Learn about streaming responses +- [Tools Guide](../tools/tools) — Learn about tool calling diff --git a/docs/config.json b/docs/config.json index eb9f7a97c..bc2b22277 100644 --- a/docs/config.json +++ b/docs/config.json @@ -17,14 +17,14 @@ "label": "Quick Start: React", "to": "getting-started/quick-start" }, - { - "label": "Quick Start: React Native", - "to": "getting-started/quick-start-react-native" - }, { "label": "Devtools", "to": "getting-started/devtools" }, + { + "label": "Quick Start: React Native", + "to": "getting-started/quick-start-react-native" + }, { "label": "Quick Start: Vue", "to": "getting-started/quick-start-vue" @@ -43,15 +43,6 @@ } ] }, - { - "label": "Comparison", - "children": [ - { - "label": "TanStack AI vs Vercel AI SDK", - "to": "comparison/vercel-ai-sdk" - } - ] - }, { "label": "Tools", "children": [ @@ -101,33 +92,12 @@ "to": "chat/connection-adapters" }, { - "label": "Thinking & Reasoning", - "to": "chat/thinking-content" - } - ] - }, - { - "label": "Structured Outputs", - "children": [ - { - "label": "Overview", - "to": "structured-outputs/overview" - }, - { - "label": "One-Shot Extraction", - "to": "structured-outputs/one-shot" - }, - { - "label": "Streaming UIs", - "to": "structured-outputs/streaming" - }, - { - "label": "Multi-Turn Chat", - "to": "structured-outputs/multi-turn" + "label": "Structured Outputs (Moved)", + "to": "chat/structured-outputs" }, { - "label": "With Tools", - "to": "structured-outputs/with-tools" + "label": "Thinking & Reasoning", + "to": "chat/thinking-content" } ] }, @@ -171,10 +141,6 @@ "label": "Transcription", "to": "media/transcription" }, - { - "label": "Audio Generation", - "to": "media/audio-generation" - }, { "label": "Image Generation", "to": "media/image-generation" @@ -186,6 +152,10 @@ { "label": "Generation Hooks", "to": "media/generation-hooks" + }, + { + "label": "Audio Generation", + "to": "media/audio-generation" } ] }, @@ -196,22 +166,22 @@ "label": "Middleware", "to": "advanced/middleware" }, - { - "label": "Debug Logging", - "to": "advanced/debug-logging" - }, - { - "label": "OpenTelemetry", - "to": "advanced/otel" - }, { "label": "Observability", "to": "advanced/observability" }, + { + "label": "Debug Logging", + "to": "advanced/debug-logging" + }, { "label": "Multimodal Content", "to": "advanced/multimodal-content" }, + { + "label": "OpenTelemetry", + "to": "advanced/otel" + }, { "label": "Per-Model Type Safety", "to": "advanced/per-model-type-safety" @@ -242,11 +212,11 @@ "to": "migration/migration" }, { - "label": "From Vercel AI SDK", + "label": "Migration from Vercel AI SDK", "to": "migration/migration-from-vercel-ai" }, { - "label": "AG-UI Client Compliance", + "label": "Migrating to AG-UI Client-to-Server Compliance", "to": "migration/ag-ui-compliance" } ] @@ -348,12 +318,50 @@ "label": "Soniox", "to": "community-adapters/soniox" }, + { + "label": "Amazon Bedrock", + "to": "community-adapters/bedrock" + }, { "label": "Mynth", "to": "community-adapters/mynth" } ] }, + { + "label": "Comparison", + "children": [ + { + "label": "TanStack AI vs Vercel AI SDK", + "to": "comparison/vercel-ai-sdk" + } + ] + }, + { + "label": "Structured Outputs", + "children": [ + { + "label": "Structured Outputs Overview", + "to": "structured-outputs/overview" + }, + { + "label": "One-Shot Extraction", + "to": "structured-outputs/one-shot" + }, + { + "label": "Streaming Structured Output UIs", + "to": "structured-outputs/streaming" + }, + { + "label": "Multi-Turn Structured Chat", + "to": "structured-outputs/multi-turn" + }, + { + "label": "Structured Outputs With Tools", + "to": "structured-outputs/with-tools" + } + ] + }, { "label": "Class References", "collapsible": true, diff --git a/packages/ai-bedrock/README.md b/packages/ai-bedrock/README.md index 7b9b5c2e1..ae64ff990 100644 --- a/packages/ai-bedrock/README.md +++ b/packages/ai-bedrock/README.md @@ -1,3 +1,94 @@ # @tanstack/ai-bedrock -Amazon Bedrock adapter for TanStack AI via Bedrock's OpenAI-compatible APIs. See the docs for usage. +Amazon Bedrock adapter for TanStack AI — OpenAI-compatible Chat Completions and Responses APIs with streaming, tool calling, and reasoning. + +## Installation + +```bash +pnpm add @tanstack/ai-bedrock +# or +npm install @tanstack/ai-bedrock +# or +yarn add @tanstack/ai-bedrock +``` + +## Setup + +Get a Bedrock API key from the [Amazon Bedrock console](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html) and set it as an environment variable: + +```bash +BEDROCK_API_KEY=your-bedrock-api-key +``` + +Alternatively, configure AWS credentials for SigV4 auth (see below). + +## Usage + +```typescript +import { bedrockText } from '@tanstack/ai-bedrock' +import { chat } from '@tanstack/ai' + +const adapter = bedrockText('us.anthropic.claude-3-7-sonnet-20250219-v1:0', { + region: 'us-east-1', +}) + +for await (const chunk of chat({ + adapter, + messages: [{ role: 'user', content: 'Hello from Bedrock!' }], +})) { + if (chunk.type === 'content') process.stdout.write(chunk.delta) +} +``` + +### Responses API + +```typescript +import { bedrockText } from '@tanstack/ai-bedrock' + +const adapter = bedrockText('openai.gpt-oss-120b', { + region: 'us-east-1', + api: 'responses', +}) +``` + +### With Explicit API Key + +```typescript +import { createBedrockText } from '@tanstack/ai-bedrock' + +const adapter = createBedrockText( + 'us.amazon.nova-pro-v1:0', + 'your-bedrock-api-key', + { region: 'us-west-2' }, +) +``` + +## Authentication + +Auth is resolved in this order: + +1. Explicit `apiKey` passed to the factory +2. `BEDROCK_API_KEY` environment variable +3. `AWS_BEARER_TOKEN_BEDROCK` environment variable +4. SigV4 via the AWS credential chain (requires `pnpm add aws-sigv4-fetch`) + +To use SigV4, install the optional peer dependency and set `auth: 'sigv4'`: + +```bash +pnpm add aws-sigv4-fetch +``` + +```typescript +const adapter = bedrockText('us.anthropic.claude-3-7-sonnet-20250219-v1:0', { + auth: 'sigv4', + region: 'us-east-1', +}) +``` + +## Documentation + +Full documentation: [TanStack AI — Amazon Bedrock adapter](https://tanstack.com/ai/latest/docs/community-adapters/bedrock) + +## License + +MIT From a0050c5868014079e5e2b41242f986b6cba9e120 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:51:01 +0200 Subject: [PATCH 19/46] docs(ai-bedrock): register Bedrock in gap-analysis skill and provider listings --- .claude/skills/gap-analysis/SKILL.md | 9 ++++++--- .../references/provider-doc-urls.md | 9 +++++++++ .../ai-core/adapter-configuration/SKILL.md | 18 +++++++++++++++--- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/.claude/skills/gap-analysis/SKILL.md b/.claude/skills/gap-analysis/SKILL.md index 0178a00c3..0cc605785 100644 --- a/.claude/skills/gap-analysis/SKILL.md +++ b/.claude/skills/gap-analysis/SKILL.md @@ -73,9 +73,12 @@ markdown report under `.agent/gap-analysis/`. **Do not edit source files.** ## Known providers -`openai`, `anthropic`, `gemini`, `ollama`, `grok`, `groq`, `openrouter`, `fal` -(media-only), `elevenlabs` (TTS-only). The feature matrix tracks the first -seven; `fal` and `elevenlabs` only appear in model/media audits. +`openai`, `anthropic`, `gemini`, `ollama`, `grok`, `groq`, `openrouter`, +`bedrock` (`@tanstack/ai-bedrock`; adapter names `bedrock` / +`bedrock-responses`), `fal` (media-only), `elevenlabs` (TTS-only). The +feature matrix tracks `openai`, `anthropic`, `gemini`, `ollama`, `grok`, +`groq`, `openrouter`, `bedrock`, and `bedrock-responses`; `fal` and +`elevenlabs` only appear in model/media audits. ## Known features (19) diff --git a/.claude/skills/gap-analysis/references/provider-doc-urls.md b/.claude/skills/gap-analysis/references/provider-doc-urls.md index 2dc1818d4..3f733a0c3 100644 --- a/.claude/skills/gap-analysis/references/provider-doc-urls.md +++ b/.claude/skills/gap-analysis/references/provider-doc-urls.md @@ -70,6 +70,15 @@ WebFetch — call `resolve-library-id` with the SDK npm name, then `query-docs`. - Provider routing: https://openrouter.ai/docs/features/provider-routing - (Proxies many providers; uses OpenAI-compatible API.) +## bedrock (Amazon Bedrock) + +- Models / API compatibility: https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html +- OpenAI-compatible Chat Completions: https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions-mantle.html +- Responses API (mantle): https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html +- Cross-region inference profiles: https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html +- API keys: https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html +- (Uses OpenAI-compatible API; SDK is the openai package. Adapters: `bedrock` (chat) / `bedrock-responses`.) + ## fal (media-only) - Models catalog: https://fal.ai/models diff --git a/packages/ai/skills/ai-core/adapter-configuration/SKILL.md b/packages/ai/skills/ai-core/adapter-configuration/SKILL.md index 621bd1c2c..9d0b7f0f8 100644 --- a/packages/ai/skills/ai-core/adapter-configuration/SKILL.md +++ b/packages/ai/skills/ai-core/adapter-configuration/SKILL.md @@ -2,11 +2,11 @@ name: ai-core/adapter-configuration description: > Provider adapter selection and configuration: openaiText, anthropicText, - geminiText, ollamaText, grokText, groqText, openRouterText. Per-model - type safety with modelOptions, reasoning/thinking configuration, + geminiText, ollamaText, grokText, groqText, openRouterText, bedrockText. + Per-model type safety with modelOptions, reasoning/thinking configuration, runtime adapter switching, extendAdapter() for custom models, createModel(). API key env vars: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY/GEMINI_API_KEY, - XAI_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY, OLLAMA_HOST. + XAI_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY, OLLAMA_HOST, BEDROCK_API_KEY. type: sub-skill library: tanstack-ai library_version: '0.10.0' @@ -69,6 +69,7 @@ The text adapter is the primary one for chat/completions: | Groq | `@tanstack/ai-groq` | `groqText` | `GROQ_API_KEY` | | OpenRouter | `@tanstack/ai-openrouter` | `openRouterText` | `OPENROUTER_API_KEY` | | Ollama | `@tanstack/ai-ollama` | `ollamaText` | `OLLAMA_HOST` (default: `http://localhost:11434`) | +| Bedrock | `@tanstack/ai-bedrock` | `bedrockText` | `BEDROCK_API_KEY` or `AWS_BEARER_TOKEN_BEDROCK` | ```typescript // Each factory takes model as first arg, optional config as second @@ -79,6 +80,7 @@ import { grokText } from '@tanstack/ai-grok' import { groqText } from '@tanstack/ai-groq' import { openRouterText } from '@tanstack/ai-openrouter' import { ollamaText } from '@tanstack/ai-ollama' +import { bedrockText } from '@tanstack/ai-bedrock' // Model string is passed to the factory, NOT to chat() const adapter = openaiText('gpt-5.2') @@ -88,6 +90,7 @@ const adapter4 = grokText('grok-4') const adapter5 = groqText('llama-3.3-70b-versatile') const adapter6 = openRouterText('anthropic/claude-sonnet-4') const adapter7 = ollamaText('llama3.3') +const adapter8 = bedrockText('us.anthropic.claude-3-7-sonnet-20250219-v1:0') // Optional: pass explicit API key const adapterWithKey = openaiText('gpt-5.2', { @@ -95,6 +98,14 @@ const adapterWithKey = openaiText('gpt-5.2', { }) ``` +`@tanstack/ai-bedrock` (Amazon Bedrock, via Bedrock's OpenAI-compatible +APIs) branches on `config.api`: `bedrockText(model, { api: 'chat' })` (the +default) targets the Chat Completions endpoint (adapter name `bedrock`), +while `bedrockText(model, { api: 'responses' })` targets the Responses API +(adapter name `bedrock-responses`). Use `createBedrockText(model, apiKey, +config?)` to pass the key explicitly. Auth resolves from `BEDROCK_API_KEY` +/ `AWS_BEARER_TOKEN_BEDROCK`, or SigV4 credentials. + ### 2. Runtime Adapter Switching Use an adapter factory map to switch providers dynamically based on user @@ -285,6 +296,7 @@ runtime error: | Groq | `GROQ_API_KEY` | | | OpenRouter | `OPENROUTER_API_KEY` | | | Ollama | `OLLAMA_HOST` | No API key needed, just the host URL (default: `http://localhost:11434`) | +| Bedrock | `BEDROCK_API_KEY` / `AWS_BEARER_TOKEN_BEDROCK` | Falls back to SigV4 credentials when no API key is set | Source: adapter source code (`utils/client.ts` in each adapter package). From 13b5bb6d4bdd4d04a59235494b71e629735f2533 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 20:56:01 +0200 Subject: [PATCH 20/46] docs(ai-bedrock): move Bedrock to first-party adapters section --- docs/{community-adapters => adapters}/bedrock.md | 4 ++-- docs/config.json | 8 ++++---- docs/getting-started/overview.md | 1 + packages/ai-bedrock/README.md | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) rename docs/{community-adapters => adapters}/bedrock.md (99%) diff --git a/docs/community-adapters/bedrock.md b/docs/adapters/bedrock.md similarity index 99% rename from docs/community-adapters/bedrock.md rename to docs/adapters/bedrock.md index 86b1f3b5e..6700352fc 100644 --- a/docs/community-adapters/bedrock.md +++ b/docs/adapters/bedrock.md @@ -1,7 +1,7 @@ --- title: Amazon Bedrock id: bedrock-adapter -order: 4 +order: 7 description: "Use Amazon Bedrock's OpenAI-compatible Chat Completions and Responses APIs with TanStack AI — streaming, tools, reasoning, API-key or SigV4 auth, and configurable runtime/mantle endpoints." keywords: - tanstack ai @@ -15,7 +15,7 @@ keywords: - claude - nova - llama - - community adapter + - adapter --- The Bedrock adapter connects TanStack AI to [Amazon Bedrock](https://aws.amazon.com/bedrock/) via Bedrock's OpenAI-compatible Chat Completions and Responses APIs. It is built on top of the `openai` SDK and supports streaming, client-side tool calling, and reasoning. diff --git a/docs/config.json b/docs/config.json index bc2b22277..2af4f4006 100644 --- a/docs/config.json +++ b/docs/config.json @@ -281,6 +281,10 @@ "label": "Groq", "to": "adapters/groq" }, + { + "label": "Amazon Bedrock", + "to": "adapters/bedrock" + }, { "label": "ElevenLabs", "to": "adapters/elevenlabs" @@ -318,10 +322,6 @@ "label": "Soniox", "to": "community-adapters/soniox" }, - { - "label": "Amazon Bedrock", - "to": "community-adapters/bedrock" - }, { "label": "Mynth", "to": "community-adapters/mynth" diff --git a/docs/getting-started/overview.md b/docs/getting-started/overview.md index bba915f5f..24b67d4eb 100644 --- a/docs/getting-started/overview.md +++ b/docs/getting-started/overview.md @@ -110,6 +110,7 @@ With the help of adapters, TanStack AI can connect to various LLM providers. Ava - **@tanstack/ai-ollama** - Ollama (local models) - **@tanstack/ai-groq** - Groq - **@tanstack/ai-grok** - xAI Grok +- **@tanstack/ai-bedrock** - Amazon Bedrock (Claude, Nova, Llama, and more via AWS) - **@tanstack/ai-fal** - fal (image & video generation) ## Next Steps diff --git a/packages/ai-bedrock/README.md b/packages/ai-bedrock/README.md index ae64ff990..58238d81b 100644 --- a/packages/ai-bedrock/README.md +++ b/packages/ai-bedrock/README.md @@ -87,7 +87,7 @@ const adapter = bedrockText('us.anthropic.claude-3-7-sonnet-20250219-v1:0', { ## Documentation -Full documentation: [TanStack AI — Amazon Bedrock adapter](https://tanstack.com/ai/latest/docs/community-adapters/bedrock) +Full documentation: [TanStack AI — Amazon Bedrock adapter](https://tanstack.com/ai/latest/docs/adapters/bedrock) ## License From 89567f334ff2c9bcd2aa004dad23adfd3e8d370c Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 21:15:06 +0200 Subject: [PATCH 21/46] fix(ai-bedrock): SigV4 service for mantle; canonical versioned gpt-oss-120b id --- docs/adapters/bedrock.md | 2 +- packages/ai-bedrock/README.md | 2 +- packages/ai-bedrock/src/model-meta.ts | 6 +++--- packages/ai-bedrock/src/sigv4/index.ts | 4 +++- packages/ai-bedrock/tests/adapter.test.ts | 20 ++++++++++---------- packages/ai-bedrock/tests/model-meta.test.ts | 4 ++-- packages/ai-bedrock/tests/sigv4.test.ts | 4 ++-- testing/e2e/src/lib/providers.ts | 8 ++++---- 8 files changed, 26 insertions(+), 24 deletions(-) diff --git a/docs/adapters/bedrock.md b/docs/adapters/bedrock.md index 6700352fc..0111867e4 100644 --- a/docs/adapters/bedrock.md +++ b/docs/adapters/bedrock.md @@ -121,7 +121,7 @@ Set `api: 'responses'` to use Bedrock's Responses API. This API is mantle-only, import { bedrockText } from '@tanstack/ai-bedrock' import { chat } from '@tanstack/ai' -const adapter = bedrockText('openai.gpt-oss-120b', { +const adapter = bedrockText('openai.gpt-oss-120b-1:0', { region: 'us-east-1', api: 'responses', }) diff --git a/packages/ai-bedrock/README.md b/packages/ai-bedrock/README.md index 58238d81b..c4e2a269d 100644 --- a/packages/ai-bedrock/README.md +++ b/packages/ai-bedrock/README.md @@ -45,7 +45,7 @@ for await (const chunk of chat({ ```typescript import { bedrockText } from '@tanstack/ai-bedrock' -const adapter = bedrockText('openai.gpt-oss-120b', { +const adapter = bedrockText('openai.gpt-oss-120b-1:0', { region: 'us-east-1', api: 'responses', }) diff --git a/packages/ai-bedrock/src/model-meta.ts b/packages/ai-bedrock/src/model-meta.ts index aafa2f9b7..5e6030270 100644 --- a/packages/ai-bedrock/src/model-meta.ts +++ b/packages/ai-bedrock/src/model-meta.ts @@ -19,10 +19,10 @@ interface ModelMeta { } // --- OpenAI gpt-oss (text-only; chat + responses) --- -// Note: `openai.gpt-oss-120b` has no version suffix while `openai.gpt-oss-20b-1:0` does; -// this asymmetry is intentional (seed IDs as published) and will be reconciled by the refresh script. +// Both IDs use AWS's canonical versioned Model IDs (`-1:0`). The mantle/Responses +// endpoint may also accept an unversioned alias; that is reconciled by the refresh script. const GPT_OSS_120B = { - name: 'openai.gpt-oss-120b', + name: 'openai.gpt-oss-120b-1:0', context_window: 128_000, supports: { input: ['text'], output: ['text'], endpoints: ['chat', 'responses'], features: ['streaming', 'tools', 'reasoning'], tools: [] as const }, } as const satisfies ModelMeta diff --git a/packages/ai-bedrock/src/sigv4/index.ts b/packages/ai-bedrock/src/sigv4/index.ts index 1f27ae6cc..d212d6618 100644 --- a/packages/ai-bedrock/src/sigv4/index.ts +++ b/packages/ai-bedrock/src/sigv4/index.ts @@ -28,7 +28,9 @@ type CreateSignedFetcher = (opts: { /** Pure resolver — testable without network or credentials. */ export function resolveSigV4Params(options: BedrockSigV4Options): SigV4Params { - return { service: options.service ?? 'bedrock', region: options.region } + const defaultService = + options.endpoint === 'mantle' ? 'bedrock-mantle' : 'bedrock' + return { service: options.service ?? defaultService, region: options.region } } /** diff --git a/packages/ai-bedrock/tests/adapter.test.ts b/packages/ai-bedrock/tests/adapter.test.ts index e53d33ad4..7edda47c3 100644 --- a/packages/ai-bedrock/tests/adapter.test.ts +++ b/packages/ai-bedrock/tests/adapter.test.ts @@ -12,23 +12,23 @@ afterEach(() => vi.unstubAllEnvs()) describe('BedrockTextAdapter', () => { it('constructs with name "bedrock" and kind "text"', () => { - const a = createBedrockChat('openai.gpt-oss-120b', 'test-key', { + const a = createBedrockChat('openai.gpt-oss-120b-1:0', 'test-key', { region: 'us-east-1', }) expect(a).toBeInstanceOf(BedrockTextAdapter) expect(a.name).toBe('bedrock') expect(a.kind).toBe('text') - expect(a.model).toBe('openai.gpt-oss-120b') + expect(a.model).toBe('openai.gpt-oss-120b-1:0') }) describe('extractReasoning (cast-free)', () => { // Access the protected hook through a tiny typed subclass — no `as` casts. - class Probe extends BedrockTextAdapter<'openai.gpt-oss-120b'> { + class Probe extends BedrockTextAdapter<'openai.gpt-oss-120b-1:0'> { read(chunk: unknown) { return this.extractReasoning(chunk) } } - const probe = new Probe({ apiKey: 'k' }, 'openai.gpt-oss-120b') + const probe = new Probe({ apiKey: 'k' }, 'openai.gpt-oss-120b-1:0') it('reads delta.reasoning', () => { expect( @@ -60,7 +60,7 @@ describe('BedrockTextAdapter', () => { describe('BedrockResponsesTextAdapter', () => { it('constructs with name "bedrock-responses", forces mantle baseURL', () => { - const a = createBedrockResponsesText('openai.gpt-oss-120b', 'test-key', { + const a = createBedrockResponsesText('openai.gpt-oss-120b-1:0', 'test-key', { region: 'us-east-1', }) expect(a).toBeInstanceOf(BedrockResponsesTextAdapter) @@ -71,7 +71,7 @@ describe('BedrockResponsesTextAdapter', () => { describe('createBedrockText (branching factory)', () => { it('defaults to the chat adapter', () => { - const a = createBedrockText('openai.gpt-oss-120b', 'k', { + const a = createBedrockText('openai.gpt-oss-120b-1:0', 'k', { region: 'us-east-1', }) expect(a).toBeInstanceOf(ChatAdapter) @@ -79,7 +79,7 @@ describe('createBedrockText (branching factory)', () => { }) it("returns the responses adapter when api: 'responses'", () => { - const a = createBedrockText('openai.gpt-oss-120b', 'k', { + const a = createBedrockText('openai.gpt-oss-120b-1:0', 'k', { region: 'us-east-1', api: 'responses', }) @@ -88,7 +88,7 @@ describe('createBedrockText (branching factory)', () => { }) it("explicit api: 'chat' returns the chat adapter", () => { - const a = createBedrockText('openai.gpt-oss-120b', 'k', { api: 'chat' }) + const a = createBedrockText('openai.gpt-oss-120b-1:0', 'k', { api: 'chat' }) expect(a).toBeInstanceOf(ChatAdapter) }) @@ -106,10 +106,10 @@ describe('bedrockText (env-key branching factory)', () => { it('reads the key from BEDROCK_API_KEY and branches on api', () => { vi.stubEnv('BEDROCK_API_KEY', 'env-key') expect( - bedrockText('openai.gpt-oss-120b', { region: 'us-east-1' }), + bedrockText('openai.gpt-oss-120b-1:0', { region: 'us-east-1' }), ).toBeInstanceOf(ChatAdapter) expect( - bedrockText('openai.gpt-oss-120b', { + bedrockText('openai.gpt-oss-120b-1:0', { region: 'us-east-1', api: 'responses', }), diff --git a/packages/ai-bedrock/tests/model-meta.test.ts b/packages/ai-bedrock/tests/model-meta.test.ts index 42c3df90c..9b0adfa38 100644 --- a/packages/ai-bedrock/tests/model-meta.test.ts +++ b/packages/ai-bedrock/tests/model-meta.test.ts @@ -21,7 +21,7 @@ describe('bedrock model-meta', () => { }) it('includes the confirmed gpt-oss ids', () => { - expect(BEDROCK_CHAT_MODELS).toContain('openai.gpt-oss-120b') - expect(BEDROCK_RESPONSES_MODELS).toContain('openai.gpt-oss-120b') + expect(BEDROCK_CHAT_MODELS).toContain('openai.gpt-oss-120b-1:0') + expect(BEDROCK_RESPONSES_MODELS).toContain('openai.gpt-oss-120b-1:0') }) }) diff --git a/packages/ai-bedrock/tests/sigv4.test.ts b/packages/ai-bedrock/tests/sigv4.test.ts index a090a32e9..8339f7679 100644 --- a/packages/ai-bedrock/tests/sigv4.test.ts +++ b/packages/ai-bedrock/tests/sigv4.test.ts @@ -9,9 +9,9 @@ describe('resolveSigV4Params', () => { }) }) - it('keeps service "bedrock" for the mantle endpoint', () => { + it('uses service "bedrock-mantle" for the mantle endpoint', () => { expect(resolveSigV4Params({ region: 'eu-west-1', endpoint: 'mantle' })).toEqual({ - service: 'bedrock', + service: 'bedrock-mantle', region: 'eu-west-1', }) }) diff --git a/testing/e2e/src/lib/providers.ts b/testing/e2e/src/lib/providers.ts index 43f67607e..3352f7f9e 100644 --- a/testing/e2e/src/lib/providers.ts +++ b/testing/e2e/src/lib/providers.ts @@ -25,8 +25,8 @@ const defaultModels: Record = { ollama: 'mistral', groq: 'llama-3.3-70b-versatile', grok: 'grok-3', - bedrock: 'openai.gpt-oss-120b', - 'bedrock-responses': 'openai.gpt-oss-120b', + bedrock: 'openai.gpt-oss-120b-1:0', + 'bedrock-responses': 'openai.gpt-oss-120b-1:0', openrouter: 'openai/gpt-4o', 'openrouter-responses': 'openai/gpt-4o', // ElevenLabs has no chat/text model — the support matrix already filters @@ -117,14 +117,14 @@ export function createTextAdapter( }), bedrock: () => createChatOptions({ - adapter: createBedrockText(model as 'openai.gpt-oss-120b', DUMMY_KEY, { + adapter: createBedrockText(model as 'openai.gpt-oss-120b-1:0', DUMMY_KEY, { baseURL: openaiUrl, defaultHeaders: testHeaders, }), }), 'bedrock-responses': () => createChatOptions({ - adapter: createBedrockText(model as 'openai.gpt-oss-120b', DUMMY_KEY, { + adapter: createBedrockText(model as 'openai.gpt-oss-120b-1:0', DUMMY_KEY, { baseURL: openaiUrl, defaultHeaders: testHeaders, api: 'responses', From f8460d2fbef4f395531d9f88dbc4be02ec4efaff Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 30 May 2026 19:18:40 +0000 Subject: [PATCH 22/46] ci: apply automated fixes --- packages/ai-bedrock/package.json | 18 +- packages/ai-bedrock/src/adapters/text.ts | 8 +- packages/ai-bedrock/src/model-meta.ts | 160 +++++++++++++++--- .../src/text/responses-provider-options.ts | 7 +- packages/ai-bedrock/src/utils/client.ts | 6 +- packages/ai-bedrock/tests/adapter.test.ts | 14 +- packages/ai-bedrock/tests/client.test.ts | 56 ++++-- packages/ai-bedrock/tests/model-meta.test.ts | 4 +- packages/ai-bedrock/tests/sigv4.test.ts | 8 +- packages/ai-bedrock/vite.config.ts | 9 +- .../ai-core/adapter-configuration/SKILL.md | 20 +-- testing/e2e/src/lib/providers.ts | 26 ++- 12 files changed, 262 insertions(+), 74 deletions(-) diff --git a/packages/ai-bedrock/package.json b/packages/ai-bedrock/package.json index 195c3a9f3..60a425854 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -22,7 +22,10 @@ "import": "./dist/esm/sigv4/index.js" } }, - "files": ["dist", "src"], + "files": [ + "dist", + "src" + ], "scripts": { "build": "vite build", "clean": "premove ./build ./dist", @@ -33,7 +36,18 @@ "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" }, - "keywords": ["ai", "ai-sdk", "typescript", "tanstack", "bedrock", "aws", "adapter", "llm", "chat", "tool-calling"], + "keywords": [ + "ai", + "ai-sdk", + "typescript", + "tanstack", + "bedrock", + "aws", + "adapter", + "llm", + "chat", + "tool-calling" + ], "devDependencies": { "@vitest/coverage-v8": "4.0.14", "vite": "^7.3.3" diff --git a/packages/ai-bedrock/src/adapters/text.ts b/packages/ai-bedrock/src/adapters/text.ts index 4bc9679e7..4c7326db5 100644 --- a/packages/ai-bedrock/src/adapters/text.ts +++ b/packages/ai-bedrock/src/adapters/text.ts @@ -37,10 +37,10 @@ export class BedrockTextAdapter< // This `any` is confined to the generic constraint (the established ai-groq // pattern) — no value/shape `as` cast is introduced. TProviderOptions extends Record = ResolveProviderOptions, - TInputModalities extends - ReadonlyArray = ResolveInputModalities, - TToolCapabilities extends - ReadonlyArray = ResolveToolCapabilities, + TInputModalities extends ReadonlyArray = + ResolveInputModalities, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, > extends OpenAIBaseChatCompletionsTextAdapter< TModel, TProviderOptions, diff --git a/packages/ai-bedrock/src/model-meta.ts b/packages/ai-bedrock/src/model-meta.ts index 5e6030270..59fb70e12 100644 --- a/packages/ai-bedrock/src/model-meta.ts +++ b/packages/ai-bedrock/src/model-meta.ts @@ -13,7 +13,9 @@ interface ModelMeta { input: Array<'text' | 'image' | 'document'> output: Array<'text'> endpoints: Array<'chat' | 'responses'> - features: Array<'streaming' | 'tools' | 'reasoning' | 'json_schema' | 'vision'> + features: Array< + 'streaming' | 'tools' | 'reasoning' | 'json_schema' | 'vision' + > tools: ReadonlyArray } } @@ -24,100 +26,204 @@ interface ModelMeta { const GPT_OSS_120B = { name: 'openai.gpt-oss-120b-1:0', context_window: 128_000, - supports: { input: ['text'], output: ['text'], endpoints: ['chat', 'responses'], features: ['streaming', 'tools', 'reasoning'], tools: [] as const }, + supports: { + input: ['text'], + output: ['text'], + endpoints: ['chat', 'responses'], + features: ['streaming', 'tools', 'reasoning'], + tools: [] as const, + }, } as const satisfies ModelMeta const GPT_OSS_20B = { name: 'openai.gpt-oss-20b-1:0', context_window: 128_000, - supports: { input: ['text'], output: ['text'], endpoints: ['chat', 'responses'], features: ['streaming', 'tools', 'reasoning'], tools: [] as const }, + supports: { + input: ['text'], + output: ['text'], + endpoints: ['chat', 'responses'], + features: ['streaming', 'tools', 'reasoning'], + tools: [] as const, + }, } as const satisfies ModelMeta // --- Anthropic Claude (US cross-region inference profiles; chat) --- const CLAUDE_SONNET_4_5 = { name: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', context_window: 200_000, - supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision', 'reasoning'], tools: [] as const }, + supports: { + input: ['text', 'image', 'document'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools', 'vision', 'reasoning'], + tools: [] as const, + }, } as const satisfies ModelMeta const CLAUDE_HAIKU_4_5 = { name: 'us.anthropic.claude-haiku-4-5-20251001-v1:0', context_window: 200_000, - supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, + supports: { + input: ['text', 'image', 'document'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools', 'vision'], + tools: [] as const, + }, } as const satisfies ModelMeta const CLAUDE_3_7_SONNET = { name: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', context_window: 200_000, - supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision', 'reasoning'], tools: [] as const }, + supports: { + input: ['text', 'image', 'document'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools', 'vision', 'reasoning'], + tools: [] as const, + }, } as const satisfies ModelMeta const CLAUDE_3_5_SONNET_V2 = { name: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0', context_window: 200_000, - supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, + supports: { + input: ['text', 'image', 'document'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools', 'vision'], + tools: [] as const, + }, } as const satisfies ModelMeta const CLAUDE_3_5_HAIKU = { name: 'us.anthropic.claude-3-5-haiku-20241022-v1:0', context_window: 200_000, - supports: { input: ['text'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools'], tools: [] as const }, + supports: { + input: ['text'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools'], + tools: [] as const, + }, } as const satisfies ModelMeta // --- Amazon Nova (US profiles; chat) --- const NOVA_PRO = { name: 'us.amazon.nova-pro-v1:0', context_window: 300_000, - supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, + supports: { + input: ['text', 'image', 'document'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools', 'vision'], + tools: [] as const, + }, } as const satisfies ModelMeta const NOVA_LITE = { name: 'us.amazon.nova-lite-v1:0', context_window: 300_000, - supports: { input: ['text', 'image', 'document'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, + supports: { + input: ['text', 'image', 'document'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools', 'vision'], + tools: [] as const, + }, } as const satisfies ModelMeta const NOVA_MICRO = { name: 'us.amazon.nova-micro-v1:0', context_window: 128_000, - supports: { input: ['text'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools'], tools: [] as const }, + supports: { + input: ['text'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools'], + tools: [] as const, + }, } as const satisfies ModelMeta // --- Meta Llama (US profiles; chat) --- const LLAMA_3_3_70B = { name: 'us.meta.llama3-3-70b-instruct-v1:0', context_window: 128_000, - supports: { input: ['text'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools'], tools: [] as const }, + supports: { + input: ['text'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools'], + tools: [] as const, + }, } as const satisfies ModelMeta const LLAMA_4_MAVERICK = { name: 'us.meta.llama4-maverick-17b-instruct-v1:0', context_window: 128_000, - supports: { input: ['text', 'image'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, + supports: { + input: ['text', 'image'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools', 'vision'], + tools: [] as const, + }, } as const satisfies ModelMeta // --- Mistral / DeepSeek (US profiles; chat) --- const MISTRAL_PIXTRAL_LARGE = { name: 'us.mistral.pixtral-large-2502-v1:0', context_window: 128_000, - supports: { input: ['text', 'image'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'vision'], tools: [] as const }, + supports: { + input: ['text', 'image'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'tools', 'vision'], + tools: [] as const, + }, } as const satisfies ModelMeta const DEEPSEEK_R1 = { name: 'us.deepseek.r1-v1:0', context_window: 128_000, - supports: { input: ['text'], output: ['text'], endpoints: ['chat'], features: ['streaming', 'reasoning'], tools: [] as const }, + supports: { + input: ['text'], + output: ['text'], + endpoints: ['chat'], + features: ['streaming', 'reasoning'], + tools: [] as const, + }, } as const satisfies ModelMeta const CHAT_MODELS = [ - GPT_OSS_20B, GPT_OSS_120B, - CLAUDE_SONNET_4_5, CLAUDE_HAIKU_4_5, CLAUDE_3_7_SONNET, CLAUDE_3_5_SONNET_V2, CLAUDE_3_5_HAIKU, - NOVA_PRO, NOVA_LITE, NOVA_MICRO, - LLAMA_3_3_70B, LLAMA_4_MAVERICK, - MISTRAL_PIXTRAL_LARGE, DEEPSEEK_R1, + GPT_OSS_20B, + GPT_OSS_120B, + CLAUDE_SONNET_4_5, + CLAUDE_HAIKU_4_5, + CLAUDE_3_7_SONNET, + CLAUDE_3_5_SONNET_V2, + CLAUDE_3_5_HAIKU, + NOVA_PRO, + NOVA_LITE, + NOVA_MICRO, + LLAMA_3_3_70B, + LLAMA_4_MAVERICK, + MISTRAL_PIXTRAL_LARGE, + DEEPSEEK_R1, ] as const // Cast-free: explicit `.name` lists with `as const` (the ai-groq pattern). export const BEDROCK_CHAT_MODELS = [ - GPT_OSS_20B.name, GPT_OSS_120B.name, - CLAUDE_SONNET_4_5.name, CLAUDE_HAIKU_4_5.name, CLAUDE_3_7_SONNET.name, - CLAUDE_3_5_SONNET_V2.name, CLAUDE_3_5_HAIKU.name, - NOVA_PRO.name, NOVA_LITE.name, NOVA_MICRO.name, - LLAMA_3_3_70B.name, LLAMA_4_MAVERICK.name, - MISTRAL_PIXTRAL_LARGE.name, DEEPSEEK_R1.name, + GPT_OSS_20B.name, + GPT_OSS_120B.name, + CLAUDE_SONNET_4_5.name, + CLAUDE_HAIKU_4_5.name, + CLAUDE_3_7_SONNET.name, + CLAUDE_3_5_SONNET_V2.name, + CLAUDE_3_5_HAIKU.name, + NOVA_PRO.name, + NOVA_LITE.name, + NOVA_MICRO.name, + LLAMA_3_3_70B.name, + LLAMA_4_MAVERICK.name, + MISTRAL_PIXTRAL_LARGE.name, + DEEPSEEK_R1.name, +] as const +export const BEDROCK_RESPONSES_MODELS = [ + GPT_OSS_20B.name, + GPT_OSS_120B.name, ] as const -export const BEDROCK_RESPONSES_MODELS = [GPT_OSS_20B.name, GPT_OSS_120B.name] as const export type BedrockChatModels = (typeof BEDROCK_CHAT_MODELS)[number] export type BedrockResponsesModels = (typeof BEDROCK_RESPONSES_MODELS)[number] diff --git a/packages/ai-bedrock/src/text/responses-provider-options.ts b/packages/ai-bedrock/src/text/responses-provider-options.ts index 164343472..a2f0c28a7 100644 --- a/packages/ai-bedrock/src/text/responses-provider-options.ts +++ b/packages/ai-bedrock/src/text/responses-provider-options.ts @@ -14,7 +14,12 @@ export interface BedrockResponsesProviderOptions { temperature?: number | null top_p?: number | null parallel_tool_calls?: boolean | null - tool_choice?: 'none' | 'auto' | 'required' | { type: 'function'; name: string } | null + tool_choice?: + | 'none' + | 'auto' + | 'required' + | { type: 'function'; name: string } + | null /** Reasoning controls for reasoning-capable models. */ reasoning?: { effort?: 'low' | 'medium' | 'high' } | null user?: string | null diff --git a/packages/ai-bedrock/src/utils/client.ts b/packages/ai-bedrock/src/utils/client.ts index d1b73bc6f..d236e2223 100644 --- a/packages/ai-bedrock/src/utils/client.ts +++ b/packages/ai-bedrock/src/utils/client.ts @@ -3,8 +3,10 @@ import type { ClientOptions } from 'openai' export type BedrockEndpoint = 'runtime' | 'mantle' -export interface BedrockClientConfig - extends Omit { +export interface BedrockClientConfig extends Omit< + ClientOptions, + 'apiKey' | 'baseURL' +> { /** Bedrock API key (bearer). Optional — falls back to env, then SigV4. */ apiKey?: string /** Full AWS region (e.g. 'us-east-1'). Default 'us-east-1'. */ diff --git a/packages/ai-bedrock/tests/adapter.test.ts b/packages/ai-bedrock/tests/adapter.test.ts index 7edda47c3..ee257a2d0 100644 --- a/packages/ai-bedrock/tests/adapter.test.ts +++ b/packages/ai-bedrock/tests/adapter.test.ts @@ -60,9 +60,13 @@ describe('BedrockTextAdapter', () => { describe('BedrockResponsesTextAdapter', () => { it('constructs with name "bedrock-responses", forces mantle baseURL', () => { - const a = createBedrockResponsesText('openai.gpt-oss-120b-1:0', 'test-key', { - region: 'us-east-1', - }) + const a = createBedrockResponsesText( + 'openai.gpt-oss-120b-1:0', + 'test-key', + { + region: 'us-east-1', + }, + ) expect(a).toBeInstanceOf(BedrockResponsesTextAdapter) expect(a.name).toBe('bedrock-responses') expect(a.kind).toBe('text') @@ -97,7 +101,9 @@ describe('createBedrockText (branching factory)', () => { // @ts-expect-error — a chat-only model is not assignable to the api:'responses' overload // (BedrockResponsesModels). This line also locks the compile-time contract: if the // overloads ever stop rejecting it, the @ts-expect-error becomes unused and tsc fails. - createBedrockText('us.anthropic.claude-3-5-haiku-20241022-v1:0', 'k', { api: 'responses' }) + createBedrockText('us.anthropic.claude-3-5-haiku-20241022-v1:0', 'k', { + api: 'responses', + }) }).toThrowError(/Responses-capable models:/) }) }) diff --git a/packages/ai-bedrock/tests/client.test.ts b/packages/ai-bedrock/tests/client.test.ts index 9dae1b546..373d0c943 100644 --- a/packages/ai-bedrock/tests/client.test.ts +++ b/packages/ai-bedrock/tests/client.test.ts @@ -8,45 +8,74 @@ afterEach(() => { describe('withBedrockDefaults', () => { it('builds the runtime URL by default', () => { const out = withBedrockDefaults({ apiKey: 'k', region: 'us-east-1' }) - expect(out.baseURL).toBe('https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1') + expect(out.baseURL).toBe( + 'https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1', + ) }) it('defaults region to us-east-1', () => { const out = withBedrockDefaults({ apiKey: 'k' }) - expect(out.baseURL).toBe('https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1') + expect(out.baseURL).toBe( + 'https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1', + ) }) it('builds the mantle URL when endpoint is mantle', () => { - const out = withBedrockDefaults({ apiKey: 'k', region: 'eu-west-1', endpoint: 'mantle' }) + const out = withBedrockDefaults({ + apiKey: 'k', + region: 'eu-west-1', + endpoint: 'mantle', + }) expect(out.baseURL).toBe('https://bedrock-mantle.eu-west-1.api.aws/v1') }) it('forces mantle when the `forced` arg is mantle, ignoring config.endpoint', () => { - const out = withBedrockDefaults({ apiKey: 'k', region: 'us-west-2', endpoint: 'runtime' }, 'mantle') + const out = withBedrockDefaults( + { apiKey: 'k', region: 'us-west-2', endpoint: 'runtime' }, + 'mantle', + ) expect(out.baseURL).toBe('https://bedrock-mantle.us-west-2.api.aws/v1') }) it('honors an explicit baseURL override', () => { - const out = withBedrockDefaults({ apiKey: 'k', baseURL: 'http://127.0.0.1:4010/v1' }) + const out = withBedrockDefaults({ + apiKey: 'k', + baseURL: 'http://127.0.0.1:4010/v1', + }) expect(out.baseURL).toBe('http://127.0.0.1:4010/v1') }) it('does not leak region/endpoint/auth into the OpenAI ClientOptions', () => { - const out = withBedrockDefaults({ apiKey: 'k', region: 'us-east-1', endpoint: 'runtime', auth: 'apikey' }) + const out = withBedrockDefaults({ + apiKey: 'k', + region: 'us-east-1', + endpoint: 'runtime', + auth: 'apikey', + }) expect('region' in out).toBe(false) expect('endpoint' in out).toBe(false) expect('auth' in out).toBe(false) }) it('explicit baseURL survives the SigV4 path and signer is attached', () => { - const out = withBedrockDefaults({ baseURL: 'http://127.0.0.1:4010/v1', auth: 'sigv4', region: 'us-east-1' }) + const out = withBedrockDefaults({ + baseURL: 'http://127.0.0.1:4010/v1', + auth: 'sigv4', + region: 'us-east-1', + }) expect(out.baseURL).toBe('http://127.0.0.1:4010/v1') expect(typeof out.fetch).toBe('function') }) it('user-supplied fetch wins over the SigV4 signer', () => { - const userFetch: NonNullable = async () => new Response() - const out = withBedrockDefaults({ auth: 'sigv4', region: 'us-east-1', fetch: userFetch }) + const userFetch: NonNullable< + import('openai').ClientOptions['fetch'] + > = async () => new Response() + const out = withBedrockDefaults({ + auth: 'sigv4', + region: 'us-east-1', + fetch: userFetch, + }) expect(out.fetch).toBe(userFetch) }) }) @@ -72,11 +101,16 @@ describe('resolveBedrockAuth', () => { it("auth: 'apikey' with no key throws an actionable error", () => { vi.stubEnv('BEDROCK_API_KEY', '') vi.stubEnv('AWS_BEARER_TOKEN_BEDROCK', '') - expect(() => resolveBedrockAuth({ auth: 'apikey' }, 'runtime')).toThrowError(/BEDROCK_API_KEY/) + expect(() => + resolveBedrockAuth({ auth: 'apikey' }, 'runtime'), + ).toThrowError(/BEDROCK_API_KEY/) }) it("auth: 'sigv4' returns a signing fetch and a placeholder apiKey", () => { - const r = resolveBedrockAuth({ auth: 'sigv4', region: 'us-east-1' }, 'runtime') + const r = resolveBedrockAuth( + { auth: 'sigv4', region: 'us-east-1' }, + 'runtime', + ) expect(typeof r.fetch).toBe('function') expect(r.apiKey.length).toBeGreaterThan(0) }) diff --git a/packages/ai-bedrock/tests/model-meta.test.ts b/packages/ai-bedrock/tests/model-meta.test.ts index 9b0adfa38..c1b867adc 100644 --- a/packages/ai-bedrock/tests/model-meta.test.ts +++ b/packages/ai-bedrock/tests/model-meta.test.ts @@ -12,7 +12,9 @@ describe('bedrock model-meta', () => { it('responses catalog is non-empty and unique', () => { expect(BEDROCK_RESPONSES_MODELS.length).toBeGreaterThan(0) - expect(new Set(BEDROCK_RESPONSES_MODELS).size).toBe(BEDROCK_RESPONSES_MODELS.length) + expect(new Set(BEDROCK_RESPONSES_MODELS).size).toBe( + BEDROCK_RESPONSES_MODELS.length, + ) }) it('every responses model is also a chat model (Responses subset of Chat reach)', () => { diff --git a/packages/ai-bedrock/tests/sigv4.test.ts b/packages/ai-bedrock/tests/sigv4.test.ts index 8339f7679..0efa56523 100644 --- a/packages/ai-bedrock/tests/sigv4.test.ts +++ b/packages/ai-bedrock/tests/sigv4.test.ts @@ -3,14 +3,18 @@ import { resolveSigV4Params } from '../src/sigv4/index' describe('resolveSigV4Params', () => { it('uses service "bedrock" and the given region', () => { - expect(resolveSigV4Params({ region: 'us-east-1', endpoint: 'runtime' })).toEqual({ + expect( + resolveSigV4Params({ region: 'us-east-1', endpoint: 'runtime' }), + ).toEqual({ service: 'bedrock', region: 'us-east-1', }) }) it('uses service "bedrock-mantle" for the mantle endpoint', () => { - expect(resolveSigV4Params({ region: 'eu-west-1', endpoint: 'mantle' })).toEqual({ + expect( + resolveSigV4Params({ region: 'eu-west-1', endpoint: 'mantle' }), + ).toEqual({ service: 'bedrock-mantle', region: 'eu-west-1', }) diff --git a/packages/ai-bedrock/vite.config.ts b/packages/ai-bedrock/vite.config.ts index f6ab00f03..81a76cc07 100644 --- a/packages/ai-bedrock/vite.config.ts +++ b/packages/ai-bedrock/vite.config.ts @@ -13,7 +13,14 @@ const config = defineConfig({ coverage: { provider: 'v8', reporter: ['text', 'json', 'html', 'lcov'], - exclude: ['node_modules/', 'dist/', 'tests/', '**/*.test.ts', '**/*.config.ts', '**/types.ts'], + exclude: [ + 'node_modules/', + 'dist/', + 'tests/', + '**/*.test.ts', + '**/*.config.ts', + '**/types.ts', + ], include: ['src/**/*.ts'], }, }, diff --git a/packages/ai/skills/ai-core/adapter-configuration/SKILL.md b/packages/ai/skills/ai-core/adapter-configuration/SKILL.md index 9d0b7f0f8..295ae3a67 100644 --- a/packages/ai/skills/ai-core/adapter-configuration/SKILL.md +++ b/packages/ai/skills/ai-core/adapter-configuration/SKILL.md @@ -287,16 +287,16 @@ Source: docs/migration/migration.md Each provider uses a specific env var name. Using the wrong one causes a runtime error: -| Provider | Correct Env Var | Common Mistake | -| ---------- | ------------------------------------ | ------------------------------------------------------------------------ | -| OpenAI | `OPENAI_API_KEY` | | -| Anthropic | `ANTHROPIC_API_KEY` | | -| Gemini | `GOOGLE_API_KEY` or `GEMINI_API_KEY` | `GOOGLE_GENAI_API_KEY` (does not work) | -| Grok (xAI) | `XAI_API_KEY` | `GROK_API_KEY` (does not work) | -| Groq | `GROQ_API_KEY` | | -| OpenRouter | `OPENROUTER_API_KEY` | | -| Ollama | `OLLAMA_HOST` | No API key needed, just the host URL (default: `http://localhost:11434`) | -| Bedrock | `BEDROCK_API_KEY` / `AWS_BEARER_TOKEN_BEDROCK` | Falls back to SigV4 credentials when no API key is set | +| Provider | Correct Env Var | Common Mistake | +| ---------- | ---------------------------------------------- | ------------------------------------------------------------------------ | +| OpenAI | `OPENAI_API_KEY` | | +| Anthropic | `ANTHROPIC_API_KEY` | | +| Gemini | `GOOGLE_API_KEY` or `GEMINI_API_KEY` | `GOOGLE_GENAI_API_KEY` (does not work) | +| Grok (xAI) | `XAI_API_KEY` | `GROK_API_KEY` (does not work) | +| Groq | `GROQ_API_KEY` | | +| OpenRouter | `OPENROUTER_API_KEY` | | +| Ollama | `OLLAMA_HOST` | No API key needed, just the host URL (default: `http://localhost:11434`) | +| Bedrock | `BEDROCK_API_KEY` / `AWS_BEARER_TOKEN_BEDROCK` | Falls back to SigV4 credentials when no API key is set | Source: adapter source code (`utils/client.ts` in each adapter package). diff --git a/testing/e2e/src/lib/providers.ts b/testing/e2e/src/lib/providers.ts index 3352f7f9e..8409c0038 100644 --- a/testing/e2e/src/lib/providers.ts +++ b/testing/e2e/src/lib/providers.ts @@ -117,18 +117,26 @@ export function createTextAdapter( }), bedrock: () => createChatOptions({ - adapter: createBedrockText(model as 'openai.gpt-oss-120b-1:0', DUMMY_KEY, { - baseURL: openaiUrl, - defaultHeaders: testHeaders, - }), + adapter: createBedrockText( + model as 'openai.gpt-oss-120b-1:0', + DUMMY_KEY, + { + baseURL: openaiUrl, + defaultHeaders: testHeaders, + }, + ), }), 'bedrock-responses': () => createChatOptions({ - adapter: createBedrockText(model as 'openai.gpt-oss-120b-1:0', DUMMY_KEY, { - baseURL: openaiUrl, - defaultHeaders: testHeaders, - api: 'responses', - }), + adapter: createBedrockText( + model as 'openai.gpt-oss-120b-1:0', + DUMMY_KEY, + { + baseURL: openaiUrl, + defaultHeaders: testHeaders, + api: 'responses', + }, + ), }), openrouter: () => { // OpenRouter SDK exposes an HTTPClient with beforeRequest hooks. Use From 6ccd207185c68d0f20a5f875c5943465ee016176 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 21:39:51 +0200 Subject: [PATCH 23/46] =?UTF-8?q?fix(ai-bedrock):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20lazy=20auth=20in=20bedrockText,=20authoritative=20a?= =?UTF-8?q?piKey,=20SigV4=20error=20passthrough,=20skill+script=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-bedrock/src/adapters/responses-text.ts | 2 +- packages/ai-bedrock/src/adapters/text.ts | 2 +- packages/ai-bedrock/src/index.ts | 36 ++++++++++--------- packages/ai-bedrock/src/sigv4/index.ts | 23 +++++++++--- packages/ai-bedrock/tests/adapter.test.ts | 12 +++++++ .../ai-core/adapter-configuration/SKILL.md | 3 +- scripts/fetch-bedrock-models.ts | 35 ++++++++++++++---- 7 files changed, 84 insertions(+), 29 deletions(-) diff --git a/packages/ai-bedrock/src/adapters/responses-text.ts b/packages/ai-bedrock/src/adapters/responses-text.ts index 28f9f2d37..a85be08ed 100644 --- a/packages/ai-bedrock/src/adapters/responses-text.ts +++ b/packages/ai-bedrock/src/adapters/responses-text.ts @@ -70,5 +70,5 @@ export function createBedrockResponsesText< apiKey: string, config?: Omit, ): BedrockResponsesTextAdapter { - return new BedrockResponsesTextAdapter({ apiKey, ...config }, model) + return new BedrockResponsesTextAdapter({ ...config, apiKey }, model) } diff --git a/packages/ai-bedrock/src/adapters/text.ts b/packages/ai-bedrock/src/adapters/text.ts index 4c7326db5..102b610b7 100644 --- a/packages/ai-bedrock/src/adapters/text.ts +++ b/packages/ai-bedrock/src/adapters/text.ts @@ -94,5 +94,5 @@ export function createBedrockChat( apiKey: string, config?: Omit, ): BedrockTextAdapter { - return new BedrockTextAdapter({ apiKey, ...config }, model) + return new BedrockTextAdapter({ ...config, apiKey }, model) } diff --git a/packages/ai-bedrock/src/index.ts b/packages/ai-bedrock/src/index.ts index ac911a195..c10aaf8fe 100644 --- a/packages/ai-bedrock/src/index.ts +++ b/packages/ai-bedrock/src/index.ts @@ -5,15 +5,12 @@ * The public `bedrockText` / `createBedrockText` factory branches between the * Chat Completions adapter (default) and the Responses adapter via `api`. */ -import { createBedrockChat } from './adapters/text' -import { createBedrockResponsesText } from './adapters/responses-text' -import { getBedrockApiKeyFromEnv } from './utils' +import { BedrockTextAdapter } from './adapters/text' +import { BedrockResponsesTextAdapter } from './adapters/responses-text' import { BEDROCK_RESPONSES_MODELS } from './model-meta' -import type { BedrockTextAdapter, BedrockTextConfig } from './adapters/text' -import type { - BedrockResponsesConfig, - BedrockResponsesTextAdapter, -} from './adapters/responses-text' +import type { BedrockTextConfig } from './adapters/text' +import type { BedrockResponsesConfig } from './adapters/responses-text' +import type { BedrockClientConfig } from './utils' import type { BedrockChatModels, BedrockResponsesModels } from './model-meta' /** Config for the branching factory's chat mode (api omitted or 'chat'). */ @@ -42,11 +39,15 @@ function stripApi(config: T): Omit { return rest } -/** Shared branching used by both public factories. */ +/** + * Shared branching used by both public factories. Constructs the adapter + * classes directly so their constructors run the full auth cascade lazily + * (config.apiKey → BEDROCK_API_KEY → AWS_BEARER_TOKEN_BEDROCK → SigV4). No + * eager env-key fetch here, so `auth: 'sigv4'` never throws for a missing key. + */ function build( model: BedrockChatModels, - apiKey: string, - config?: BedrockChatApiConfig | BedrockResponsesApiConfig, + config?: BedrockClientConfig & { api?: 'chat' | 'responses' }, ): AnyBedrockAdapter { if (config?.api === 'responses') { const rest = stripApi(config) @@ -56,10 +57,10 @@ function build( `Responses-capable models: ${BEDROCK_RESPONSES_MODELS.join(', ')}.`, ) } - return createBedrockResponsesText(model, apiKey, rest) + return new BedrockResponsesTextAdapter(rest, model) } - const rest = config ? stripApi(config) : undefined - return createBedrockChat(model, apiKey, rest) + const rest = config ? stripApi(config) : {} + return new BedrockTextAdapter(rest, model) } // --- createBedrockText: explicit key, overloaded on `api` --- @@ -78,7 +79,8 @@ export function createBedrockText( apiKey: string, config?: BedrockChatApiConfig | BedrockResponsesApiConfig, ): AnyBedrockAdapter { - return build(model, apiKey, config) + // Explicit apiKey is authoritative — spread config first so it can't override. + return build(model, { ...config, apiKey }) } // --- bedrockText: env-key counterpart, same overloads --- @@ -94,7 +96,9 @@ export function bedrockText( model: BedrockChatModels, config?: BedrockChatApiConfig | BedrockResponsesApiConfig, ): AnyBedrockAdapter { - return build(model, getBedrockApiKeyFromEnv(), config) + // No eager env-key fetch: the adapter constructor resolves auth lazily so + // SigV4 (and the env-key fallback) work without a forced API key here. + return build(model, config) } // --- Re-exports --- diff --git a/packages/ai-bedrock/src/sigv4/index.ts b/packages/ai-bedrock/src/sigv4/index.ts index d212d6618..73763e527 100644 --- a/packages/ai-bedrock/src/sigv4/index.ts +++ b/packages/ai-bedrock/src/sigv4/index.ts @@ -49,16 +49,31 @@ export function bedrockSigV4Fetch( const fn: NonNullable = async (url, init) => { if (!signedFetch) { - let createSignedFetcher: CreateSignedFetcher + let mod: { createSignedFetcher: CreateSignedFetcher } try { - const mod = await import('aws-sigv4-fetch') - createSignedFetcher = mod.createSignedFetcher - } catch { + mod = await import('aws-sigv4-fetch') + } catch (err) { + const code = + typeof err === 'object' && + err !== null && + 'code' in err && + typeof err.code === 'string' + ? err.code + : undefined + const message = err instanceof Error ? err.message : '' + const isMissing = + code === 'ERR_MODULE_NOT_FOUND' || + code === 'MODULE_NOT_FOUND' || + /cannot find (module|package)|failed to resolve/i.test(message) + // Only remap the genuine module-not-found case; surface real errors + // (e.g. an installed package that throws on evaluation) untouched. + if (!isMissing) throw err throw new Error( 'SigV4 auth for @tanstack/ai-bedrock requires the optional "aws-sigv4-fetch" ' + 'package. Install it (`pnpm add aws-sigv4-fetch`) or use API-key auth via BEDROCK_API_KEY.', ) } + const createSignedFetcher = mod.createSignedFetcher signedFetch = createSignedFetcher({ service, region }) } const fetcher = signedFetch diff --git a/packages/ai-bedrock/tests/adapter.test.ts b/packages/ai-bedrock/tests/adapter.test.ts index ee257a2d0..68af2216c 100644 --- a/packages/ai-bedrock/tests/adapter.test.ts +++ b/packages/ai-bedrock/tests/adapter.test.ts @@ -121,4 +121,16 @@ describe('bedrockText (env-key branching factory)', () => { }), ).toBeInstanceOf(RespAdapter) }) + + it('does not require an API key when auth is sigv4', () => { + vi.stubEnv('BEDROCK_API_KEY', '') + vi.stubEnv('AWS_BEARER_TOKEN_BEDROCK', '') + // Must NOT throw — SigV4 path resolves lazily. + expect(() => + bedrockText('openai.gpt-oss-120b-1:0', { + region: 'us-east-1', + auth: 'sigv4', + }), + ).not.toThrow() + }) }) diff --git a/packages/ai/skills/ai-core/adapter-configuration/SKILL.md b/packages/ai/skills/ai-core/adapter-configuration/SKILL.md index 295ae3a67..5157209b5 100644 --- a/packages/ai/skills/ai-core/adapter-configuration/SKILL.md +++ b/packages/ai/skills/ai-core/adapter-configuration/SKILL.md @@ -6,7 +6,8 @@ description: > Per-model type safety with modelOptions, reasoning/thinking configuration, runtime adapter switching, extendAdapter() for custom models, createModel(). API key env vars: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY/GEMINI_API_KEY, - XAI_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY, OLLAMA_HOST, BEDROCK_API_KEY. + XAI_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY, OLLAMA_HOST, + BEDROCK_API_KEY (or AWS_BEARER_TOKEN_BEDROCK). type: sub-skill library: tanstack-ai library_version: '0.10.0' diff --git a/scripts/fetch-bedrock-models.ts b/scripts/fetch-bedrock-models.ts index 5e8277788..21a77a9f7 100644 --- a/scripts/fetch-bedrock-models.ts +++ b/scripts/fetch-bedrock-models.ts @@ -24,12 +24,35 @@ async function main() { const region = process.env['AWS_REGION'] ?? 'us-east-1' const client = new BedrockClient({ region }) - const models = await client.send( - new ListFoundationModelsCommand({ byOutputModality: 'TEXT' }), - ) - const profiles = await client.send(new ListInferenceProfilesCommand({})) + // ListFoundationModels typically returns no nextToken, but loop on it anyway + // so we stay correct if it ever paginates. + const modelSummaries = [] + let modelsToken: string | undefined + do { + const page = await client.send( + new ListFoundationModelsCommand({ + byOutputModality: 'TEXT', + ...(modelsToken ? { nextToken: modelsToken } : {}), + }), + ) + modelSummaries.push(...(page.modelSummaries ?? [])) + modelsToken = page.nextToken + } while (modelsToken) + + // ListInferenceProfiles is paginated — collect every page via nextToken. + const inferenceProfileSummaries = [] + let profilesToken: string | undefined + do { + const page = await client.send( + new ListInferenceProfilesCommand( + profilesToken ? { nextToken: profilesToken } : {}, + ), + ) + inferenceProfileSummaries.push(...(page.inferenceProfileSummaries ?? [])) + profilesToken = page.nextToken + } while (profilesToken) - const textModels = (models.modelSummaries ?? []) + const textModels = modelSummaries .filter((m) => (m.outputModalities ?? []).includes('TEXT')) .map((m) => ({ id: m.modelId ?? '', @@ -37,7 +60,7 @@ async function main() { })) .filter((m) => m.id.length > 0) - const inferenceProfileIds = (profiles.inferenceProfileSummaries ?? []) + const inferenceProfileIds = inferenceProfileSummaries .map((p) => p.inferenceProfileId ?? '') .filter((id) => id.length > 0) From 6ac6e40c2b4c81cad404bd7158e60e65536c5c24 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 21:42:58 +0200 Subject: [PATCH 24/46] docs(ai-bedrock): keep config.json diff to just the Bedrock nav entry --- docs/config.json | 89 +++++++++++++++--------------------------------- 1 file changed, 28 insertions(+), 61 deletions(-) diff --git a/docs/config.json b/docs/config.json index 2af4f4006..198a22ee1 100644 --- a/docs/config.json +++ b/docs/config.json @@ -21,10 +21,6 @@ "label": "Devtools", "to": "getting-started/devtools" }, - { - "label": "Quick Start: React Native", - "to": "getting-started/quick-start-react-native" - }, { "label": "Quick Start: Vue", "to": "getting-started/quick-start-vue" @@ -43,6 +39,15 @@ } ] }, + { + "label": "Comparison", + "children": [ + { + "label": "TanStack AI vs Vercel AI SDK", + "to": "comparison/vercel-ai-sdk" + } + ] + }, { "label": "Tools", "children": [ @@ -92,7 +97,7 @@ "to": "chat/connection-adapters" }, { - "label": "Structured Outputs (Moved)", + "label": "Structured Outputs", "to": "chat/structured-outputs" }, { @@ -141,6 +146,10 @@ "label": "Transcription", "to": "media/transcription" }, + { + "label": "Audio Generation", + "to": "media/audio-generation" + }, { "label": "Image Generation", "to": "media/image-generation" @@ -152,10 +161,6 @@ { "label": "Generation Hooks", "to": "media/generation-hooks" - }, - { - "label": "Audio Generation", - "to": "media/audio-generation" } ] }, @@ -166,22 +171,22 @@ "label": "Middleware", "to": "advanced/middleware" }, - { - "label": "Observability", - "to": "advanced/observability" - }, { "label": "Debug Logging", "to": "advanced/debug-logging" }, - { - "label": "Multimodal Content", - "to": "advanced/multimodal-content" - }, { "label": "OpenTelemetry", "to": "advanced/otel" }, + { + "label": "Observability", + "to": "advanced/observability" + }, + { + "label": "Multimodal Content", + "to": "advanced/multimodal-content" + }, { "label": "Per-Model Type Safety", "to": "advanced/per-model-type-safety" @@ -197,10 +202,6 @@ { "label": "Extend Adapter", "to": "advanced/extend-adapter" - }, - { - "label": "Typed Pre-Configured Options", - "to": "advanced/typed-options" } ] }, @@ -212,11 +213,11 @@ "to": "migration/migration" }, { - "label": "Migration from Vercel AI SDK", + "label": "From Vercel AI SDK", "to": "migration/migration-from-vercel-ai" }, { - "label": "Migrating to AG-UI Client-to-Server Compliance", + "label": "AG-UI Client Compliance", "to": "migration/ag-ui-compliance" } ] @@ -281,10 +282,6 @@ "label": "Groq", "to": "adapters/groq" }, - { - "label": "Amazon Bedrock", - "to": "adapters/bedrock" - }, { "label": "ElevenLabs", "to": "adapters/elevenlabs" @@ -296,6 +293,10 @@ { "label": "OpenRouter Adapter", "to": "adapters/openrouter" + }, + { + "label": "Amazon Bedrock", + "to": "adapters/bedrock" } ] }, @@ -328,40 +329,6 @@ } ] }, - { - "label": "Comparison", - "children": [ - { - "label": "TanStack AI vs Vercel AI SDK", - "to": "comparison/vercel-ai-sdk" - } - ] - }, - { - "label": "Structured Outputs", - "children": [ - { - "label": "Structured Outputs Overview", - "to": "structured-outputs/overview" - }, - { - "label": "One-Shot Extraction", - "to": "structured-outputs/one-shot" - }, - { - "label": "Streaming Structured Output UIs", - "to": "structured-outputs/streaming" - }, - { - "label": "Multi-Turn Structured Chat", - "to": "structured-outputs/multi-turn" - }, - { - "label": "Structured Outputs With Tools", - "to": "structured-outputs/with-tools" - } - ] - }, { "label": "Class References", "collapsible": true, From 7b3632ba5f5f18266aef811c0945fb87a96ccd8c Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 15:02:03 +0200 Subject: [PATCH 25/46] chore(ai-bedrock): add @aws-sdk client, drop aws-sigv4-fetch optional peer --- packages/ai-bedrock/package.json | 6 +- pnpm-lock.yaml | 513 +++++++++++++++++++++++++++++++ 2 files changed, 515 insertions(+), 4 deletions(-) diff --git a/packages/ai-bedrock/package.json b/packages/ai-bedrock/package.json index 60a425854..673fdd679 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -16,10 +16,6 @@ ".": { "types": "./dist/esm/index.d.ts", "import": "./dist/esm/index.js" - }, - "./sigv4": { - "types": "./dist/esm/sigv4/index.d.ts", - "import": "./dist/esm/sigv4/index.js" } }, "files": [ @@ -57,6 +53,8 @@ "zod": "^4.0.0" }, "dependencies": { + "@aws-sdk/client-bedrock-runtime": "^3.1057.0", + "@aws-sdk/credential-providers": "^3.1057.0", "@tanstack/ai-utils": "workspace:*", "@tanstack/openai-base": "workspace:*", "openai": "^6.9.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a1293161..c5197e4d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1028,6 +1028,12 @@ importers: packages/ai-bedrock: dependencies: + '@aws-sdk/client-bedrock-runtime': + specifier: ^3.1057.0 + version: 3.1057.0 + '@aws-sdk/credential-providers': + specifier: ^3.1057.0 + version: 3.1057.0 '@tanstack/ai': specifier: workspace:^ version: link:../ai @@ -2117,6 +2123,119 @@ 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.1057.0': + resolution: {integrity: sha512-TqnYAhAEk45+w3JmS5uHc05AAxfQ7NDyfuARzBv/Y5WuDftRPJMm6FBHCEH7dqcDCcAHmI+XyCYaBI7g7EgweQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-cognito-identity@3.1057.0': + resolution: {integrity: sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.15': + resolution: {integrity: sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-cognito-identity@3.972.38': + resolution: {integrity: sha512-OHkK6xOx/IHkSbQdDWxnVCLU+j28EFl8wyWgBILQDFAPY8n240C/O4gjmFx+zFU12lL8njgJQ5GWAIWq88CnSQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.41': + resolution: {integrity: sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.43': + resolution: {integrity: sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.46': + resolution: {integrity: sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.45': + resolution: {integrity: sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.47': + resolution: {integrity: sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.41': + resolution: {integrity: sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.45': + resolution: {integrity: sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.45': + resolution: {integrity: sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-providers@3.1057.0': + resolution: {integrity: sha512-rbrEHtz11g0kxsSkYr3fx2HABNNblp4AhB2MgPvJHgYOWfJ2eBviU7Mvoaef0PW8QH6lbZDfJcnM7eKvtvz3sw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.18': + resolution: {integrity: sha512-QPQhwY/fstR8fMZFWrsJRNoTP6D1RjRPHGRX7u9/VkF3opCsvD0oXPz6qzkX94SchzvuS5vyFZbJbPcMEs2Jeg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.14': + resolution: {integrity: sha512-DoZ4djVj/74XQ6M/IwxuKh543tTvLCL7u1Dx+VDHMgW9yGNrFSJJ1l0LrUQRaekic5CB12wUiiOoHL0VI6H0gg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.23': + resolution: {integrity: sha512-F0d4A9pJFiwljyKgSwU1Z5n+CXSv8bp+V5SthbS2rftB8wBN9z1K2Yyv3xbeK0AM2T0g4q6Ptf0shFF+oQZyiA==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.13': + resolution: {integrity: sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.30': + resolution: {integrity: sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1056.0': + resolution: {integrity: sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1057.0': + resolution: {integrity: sha512-nIypx3Pvn9l7XoCi1a1ruY/FdUyfQW0LXk/2BdazRzs7rOAZeoSdZx9E1A6bmXIDedrG+09hFb8QlxhEk40jfA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.9': + resolution: {integrity: sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.26': + resolution: {integrity: sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -4223,6 +4342,9 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@nodable/entities@2.1.1': + resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -6229,6 +6351,42 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@smithy/core@3.24.5': + resolution: {integrity: sha512-Kt8phUg45M15EjhYAbZ+fFikYneijLu9Liugz8ZsYz2i8j0hzGv27LWKpEHYRfvj+LyCOSijpcR/2i8RouV+cA==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.3.6': + resolution: {integrity: sha512-tHhdiWZfG1ZIh2YcRfPJmY2gHcBmqbAzqm3ER4TIDFYsSEqTD5tICT7cgQ/kI8LRakxp12myOYyK68XPn7MnHw==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.4.5': + resolution: {integrity: sha512-SK3VMeH0fibgdTg2QeB+O4p7Yy/2E5HBOHJeC58FshkDdeuX8lOgO7PfjYfLyPLP1ch55j91cQqKBzDS0mRjSQ==} + 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/node-http-handler@4.7.5': + resolution: {integrity: sha512-3dA9TQ+ybRSZ/m0wnbZhiBy4Dezjgq1Ib/ZZrYTpJDBgpoLLU/SDzZc/g0x0MNAdOJe1wPcM+x2PBRmoOur+Sw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.4.5': + resolution: {integrity: sha512-QBJKWGqIknH0dc9LWpfH1mkdokAx6iXYN3UcQ3eY6uIEyScuoQAhfl94ge7ozUy9WgFUdE8xsvwBjaYBbWmPNA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.2': + resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} + 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-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@solid-devtools/debugger@0.28.1': resolution: {integrity: sha512-6qIUI6VYkXoRnL8oF5bvh2KgH71qlJ18hNw/mwSyY6v48eb80ZR48/5PDXufUa3q+MBSuYa1uqTMwLewpay9eg==} peerDependencies: @@ -8035,6 +8193,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + boxen@7.1.1: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} @@ -9225,6 +9386,13 @@ packages: fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -11323,6 +11491,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -12402,6 +12574,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@2.3.0: + resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} @@ -13709,6 +13884,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xml2js@0.6.0: resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==} engines: {node: '>=4.0.0'} @@ -13827,6 +14006,270 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.9 + 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.973.9 + '@aws-sdk/util-locate-window': 3.965.5 + '@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.973.9 + 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.973.9 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1057.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.15 + '@aws-sdk/credential-provider-node': 3.972.47 + '@aws-sdk/eventstream-handler-node': 3.972.18 + '@aws-sdk/middleware-eventstream': 3.972.14 + '@aws-sdk/middleware-websocket': 3.972.23 + '@aws-sdk/token-providers': 3.1057.0 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/fetch-http-handler': 5.4.5 + '@smithy/node-http-handler': 4.7.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/client-cognito-identity@3.1057.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.15 + '@aws-sdk/credential-provider-node': 3.972.47 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/fetch-http-handler': 5.4.5 + '@smithy/node-http-handler': 4.7.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.15': + dependencies: + '@aws-sdk/types': 3.973.9 + '@aws-sdk/xml-builder': 3.972.26 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.5 + '@smithy/signature-v4': 5.4.5 + '@smithy/types': 4.14.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-cognito-identity@3.972.38': + dependencies: + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.41': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.43': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/fetch-http-handler': 5.4.5 + '@smithy/node-http-handler': 4.7.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.46': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/credential-provider-env': 3.972.41 + '@aws-sdk/credential-provider-http': 3.972.43 + '@aws-sdk/credential-provider-login': 3.972.45 + '@aws-sdk/credential-provider-process': 3.972.41 + '@aws-sdk/credential-provider-sso': 3.972.45 + '@aws-sdk/credential-provider-web-identity': 3.972.45 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/credential-provider-imds': 4.3.6 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.45': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.47': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.41 + '@aws-sdk/credential-provider-http': 3.972.43 + '@aws-sdk/credential-provider-ini': 3.972.46 + '@aws-sdk/credential-provider-process': 3.972.41 + '@aws-sdk/credential-provider-sso': 3.972.45 + '@aws-sdk/credential-provider-web-identity': 3.972.45 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/credential-provider-imds': 4.3.6 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.41': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.45': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/token-providers': 3.1056.0 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.45': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-providers@3.1057.0': + dependencies: + '@aws-sdk/client-cognito-identity': 3.1057.0 + '@aws-sdk/core': 3.974.15 + '@aws-sdk/credential-provider-cognito-identity': 3.972.38 + '@aws-sdk/credential-provider-env': 3.972.41 + '@aws-sdk/credential-provider-http': 3.972.43 + '@aws-sdk/credential-provider-ini': 3.972.46 + '@aws-sdk/credential-provider-login': 3.972.45 + '@aws-sdk/credential-provider-node': 3.972.47 + '@aws-sdk/credential-provider-process': 3.972.41 + '@aws-sdk/credential-provider-sso': 3.972.45 + '@aws-sdk/credential-provider-web-identity': 3.972.45 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/credential-provider-imds': 4.3.6 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.18': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.14': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.23': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/fetch-http-handler': 5.4.5 + '@smithy/signature-v4': 5.4.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.13': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.15 + '@aws-sdk/signature-v4-multi-region': 3.996.30 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/fetch-http-handler': 5.4.5 + '@smithy/node-http-handler': 4.7.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.30': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/signature-v4': 5.4.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1056.0': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1057.0': + dependencies: + '@aws-sdk/core': 3.974.15 + '@aws-sdk/nested-clients': 3.997.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.9': + dependencies: + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.26': + dependencies: + '@smithy/types': 4.14.2 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -15998,6 +16441,8 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@nodable/entities@2.1.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -17677,6 +18122,54 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@smithy/core@3.24.5': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.3.6': + dependencies: + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.4.5': + dependencies: + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.5': + dependencies: + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/signature-v4@5.4.5': + dependencies: + '@smithy/core': 3.24.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/types@4.14.2': + 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-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@solid-devtools/debugger@0.28.1(solid-js@1.9.10)': dependencies: '@nothing-but/utils': 0.17.0 @@ -20468,6 +20961,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.14.1: {} + boxen@7.1.1: dependencies: ansi-align: 3.0.1 @@ -21848,6 +22343,18 @@ snapshots: fast-sha256@1.3.0: {} + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 + + fast-xml-parser@5.7.3: + dependencies: + '@nodable/entities': 2.1.1 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.3.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -24674,6 +25181,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.5.0: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -26019,6 +26528,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + strnum@2.3.0: {} + structured-headers@0.4.1: {} style-to-js@1.1.21: @@ -27249,6 +27760,8 @@ snapshots: xml-name-validator@5.0.0: {} + xml-naming@0.1.0: {} + xml2js@0.6.0: dependencies: sax: 1.6.0 From 43d973f84010ab3d941fdaab5443bb4a7a6f6859 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 15:07:14 +0200 Subject: [PATCH 26/46] feat(ai-bedrock): unified discriminated auth resolver (bearer | sigv4) --- packages/ai-bedrock/src/utils/auth.ts | 65 ++++++++++++++++++++++++++ packages/ai-bedrock/tests/auth.test.ts | 36 ++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 packages/ai-bedrock/src/utils/auth.ts create mode 100644 packages/ai-bedrock/tests/auth.test.ts diff --git a/packages/ai-bedrock/src/utils/auth.ts b/packages/ai-bedrock/src/utils/auth.ts new file mode 100644 index 000000000..32edca490 --- /dev/null +++ b/packages/ai-bedrock/src/utils/auth.ts @@ -0,0 +1,65 @@ +import { getApiKeyFromEnv } from '@tanstack/ai-utils' +import { fromNodeProviderChain } from '@aws-sdk/credential-providers' + +export type BedrockEndpoint = 'runtime' | 'mantle' + +/** SigV4 service name differs per endpoint. */ +export function sigv4Service(endpoint: BedrockEndpoint): string { + return endpoint === 'mantle' ? 'bedrock-mantle' : 'bedrock' +} + +export type ResolvedBedrockAuth = + | { kind: 'bearer'; token: string } + | { + kind: 'sigv4' + region: string + service: string + credentials: ReturnType + } + +const DEFAULT_REGION = 'us-east-1' + +function readApiKeyFromEnv(): string | undefined { + try { + return getApiKeyFromEnv('BEDROCK_API_KEY') + } catch { + try { + return getApiKeyFromEnv('AWS_BEARER_TOKEN_BEDROCK') + } catch { + return undefined + } + } +} + +export interface BedrockAuthConfig { + apiKey?: string + region?: string + auth?: 'apikey' | 'sigv4' | 'auto' +} + +/** apiKey -> BEDROCK_API_KEY -> AWS_BEARER_TOKEN_BEDROCK -> SigV4 (credential chain). */ +export function resolveBedrockAuth( + config: BedrockAuthConfig, + endpoint: BedrockEndpoint, +): ResolvedBedrockAuth { + const mode = config.auth ?? 'auto' + const region = config.region ?? DEFAULT_REGION + + if (mode !== 'sigv4') { + const token = config.apiKey ?? readApiKeyFromEnv() + if (token) return { kind: 'bearer', token } + if (mode === 'apikey') { + throw new Error( + 'No Bedrock API key found. Set BEDROCK_API_KEY (or ' + + 'AWS_BEARER_TOKEN_BEDROCK), pass `apiKey`, or use auth: "sigv4".', + ) + } + } + + return { + kind: 'sigv4', + region, + service: sigv4Service(endpoint), + credentials: fromNodeProviderChain(), + } +} diff --git a/packages/ai-bedrock/tests/auth.test.ts b/packages/ai-bedrock/tests/auth.test.ts new file mode 100644 index 000000000..3fd31f640 --- /dev/null +++ b/packages/ai-bedrock/tests/auth.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' +import { resolveBedrockAuth } from '../src/utils/auth' + +describe('resolveBedrockAuth', () => { + it('returns bearer when an explicit apiKey is given', () => { + const r = resolveBedrockAuth({ apiKey: 'k', region: 'us-east-1' }, 'runtime') + expect(r).toEqual({ kind: 'bearer', token: 'k' }) + }) + + it('returns bearer from BEDROCK_API_KEY env', () => { + process.env.BEDROCK_API_KEY = 'envkey' + try { + const r = resolveBedrockAuth({ region: 'us-east-1' }, 'runtime') + expect(r).toEqual({ kind: 'bearer', token: 'envkey' }) + } finally { + delete process.env.BEDROCK_API_KEY + } + }) + + it('returns sigv4 with service+region when auth forced sigv4', () => { + const r = resolveBedrockAuth({ auth: 'sigv4', region: 'us-west-2' }, 'mantle') + expect(r.kind).toBe('sigv4') + if (r.kind === 'sigv4') { + expect(r.region).toBe('us-west-2') + expect(r.service).toBe('bedrock-mantle') + } + }) + + it('throws in apikey mode with no key available', () => { + delete process.env.BEDROCK_API_KEY + delete process.env.AWS_BEARER_TOKEN_BEDROCK + expect(() => resolveBedrockAuth({ auth: 'apikey' }, 'runtime')).toThrow( + /No Bedrock API key/, + ) + }) +}) From b9fabb566f1481db1be8fe535348a93fb875be19 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 15:20:38 +0200 Subject: [PATCH 27/46] refactor(ai-bedrock): sign openai-SDK path with @aws-sdk signer; remove src/sigv4 Replace the optional aws-sigv4-fetch peer dependency with createSigV4Fetch using @smithy/signature-v4 + @aws-crypto/sha256-js (both ship transitively with @aws-sdk/client-bedrock-runtime). Delete src/sigv4/ and its ambient type declaration. Rewrite client.ts onto the unified auth resolver from auth.ts; remove the now-redundant duplicate auth logic. Update knip.json, vite.config.ts, utils/index.ts, and src/index.ts to reflect removed symbols. --- knip.json | 4 +- packages/ai-bedrock/package.json | 3 + packages/ai-bedrock/src/index.ts | 1 - .../ai-bedrock/src/sigv4/aws-sigv4-fetch.d.ts | 10 -- packages/ai-bedrock/src/sigv4/index.ts | 83 ------------- packages/ai-bedrock/src/utils/client.ts | 109 ++++-------------- packages/ai-bedrock/src/utils/index.ts | 1 - .../src/utils/openai-sigv4-fetch.ts | 49 ++++++++ packages/ai-bedrock/tests/client.test.ts | 27 +++-- .../tests/openai-sigv4-fetch.test.ts | 30 +++++ packages/ai-bedrock/tests/sigv4.test.ts | 22 ---- packages/ai-bedrock/vite.config.ts | 9 +- pnpm-lock.yaml | 9 ++ 13 files changed, 128 insertions(+), 229 deletions(-) delete mode 100644 packages/ai-bedrock/src/sigv4/aws-sigv4-fetch.d.ts delete mode 100644 packages/ai-bedrock/src/sigv4/index.ts create mode 100644 packages/ai-bedrock/src/utils/openai-sigv4-fetch.ts create mode 100644 packages/ai-bedrock/tests/openai-sigv4-fetch.test.ts delete mode 100644 packages/ai-bedrock/tests/sigv4.test.ts diff --git a/knip.json b/knip.json index 40927f9ed..71927cd48 100644 --- a/knip.json +++ b/knip.json @@ -44,8 +44,6 @@ "packages/ai-vue-ui": { "ignore": ["src/use-chat-context.ts"] }, - "packages/ai-bedrock": { - "ignoreDependencies": ["aws-sigv4-fetch"] - } + "packages/ai-bedrock": {} } } diff --git a/packages/ai-bedrock/package.json b/packages/ai-bedrock/package.json index 673fdd679..4dfb5578c 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -53,8 +53,11 @@ "zod": "^4.0.0" }, "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/client-bedrock-runtime": "^3.1057.0", "@aws-sdk/credential-providers": "^3.1057.0", + "@smithy/signature-v4": "^5.4.5", + "@smithy/types": "^4.14.2", "@tanstack/ai-utils": "workspace:*", "@tanstack/openai-base": "workspace:*", "openai": "^6.9.1" diff --git a/packages/ai-bedrock/src/index.ts b/packages/ai-bedrock/src/index.ts index c10aaf8fe..8d95365cc 100644 --- a/packages/ai-bedrock/src/index.ts +++ b/packages/ai-bedrock/src/index.ts @@ -115,7 +115,6 @@ export { type BedrockResponsesProviderOptions, } from './adapters/responses-text' export { - getBedrockApiKeyFromEnv, resolveBedrockAuth, withBedrockDefaults, type BedrockClientConfig, diff --git a/packages/ai-bedrock/src/sigv4/aws-sigv4-fetch.d.ts b/packages/ai-bedrock/src/sigv4/aws-sigv4-fetch.d.ts deleted file mode 100644 index 3c0eb3f58..000000000 --- a/packages/ai-bedrock/src/sigv4/aws-sigv4-fetch.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// aws-sigv4-fetch is an optional, user-installed dependency (NOT in our -// manifest — see package docs). This ambient declaration lets `tsc` resolve -// the dynamic `import('aws-sigv4-fetch')` without the package being present. -// No `any`, no cast. -declare module 'aws-sigv4-fetch' { - export function createSignedFetcher(opts: { - service: string - region: string - }): (input: string | URL | Request, init?: RequestInit) => Promise -} diff --git a/packages/ai-bedrock/src/sigv4/index.ts b/packages/ai-bedrock/src/sigv4/index.ts deleted file mode 100644 index 73763e527..000000000 --- a/packages/ai-bedrock/src/sigv4/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { ClientOptions } from 'openai' -import type { BedrockEndpoint } from '../utils/client' - -export interface BedrockSigV4Options { - region: string - endpoint: BedrockEndpoint - /** Override the SigV4 service name (default 'bedrock'). */ - service?: string -} - -interface SigV4Params { - service: string - region: string -} - -// Mirrors the createSignedFetcher signature from `aws-sigv4-fetch` (see -// aws-sigv4-fetch.d.ts). Defined here so we can type the variable without -// using `import()` in a type annotation (forbidden by consistent-type-imports). -type SignedFetcher = ( - input: string | URL | Request, - init?: RequestInit, -) => Promise - -type CreateSignedFetcher = (opts: { - service: string - region: string -}) => SignedFetcher - -/** Pure resolver — testable without network or credentials. */ -export function resolveSigV4Params(options: BedrockSigV4Options): SigV4Params { - const defaultService = - options.endpoint === 'mantle' ? 'bedrock-mantle' : 'bedrock' - return { service: options.service ?? defaultService, region: options.region } -} - -/** - * Builds a fetch that signs each request with AWS SigV4, suitable for the - * OpenAI SDK `fetch` option against Bedrock's OpenAI-compatible endpoints. - * - * Requires the optional `aws-sigv4-fetch` dependency (install it yourself: - * `pnpm add aws-sigv4-fetch`). AWS credentials are resolved from the standard - * provider chain. Throws an actionable error if the dep is absent. - */ -export function bedrockSigV4Fetch( - options: BedrockSigV4Options, -): NonNullable { - const { service, region } = resolveSigV4Params(options) - let signedFetch: SignedFetcher | undefined - - const fn: NonNullable = async (url, init) => { - if (!signedFetch) { - let mod: { createSignedFetcher: CreateSignedFetcher } - try { - mod = await import('aws-sigv4-fetch') - } catch (err) { - const code = - typeof err === 'object' && - err !== null && - 'code' in err && - typeof err.code === 'string' - ? err.code - : undefined - const message = err instanceof Error ? err.message : '' - const isMissing = - code === 'ERR_MODULE_NOT_FOUND' || - code === 'MODULE_NOT_FOUND' || - /cannot find (module|package)|failed to resolve/i.test(message) - // Only remap the genuine module-not-found case; surface real errors - // (e.g. an installed package that throws on evaluation) untouched. - if (!isMissing) throw err - throw new Error( - 'SigV4 auth for @tanstack/ai-bedrock requires the optional "aws-sigv4-fetch" ' + - 'package. Install it (`pnpm add aws-sigv4-fetch`) or use API-key auth via BEDROCK_API_KEY.', - ) - } - const createSignedFetcher = mod.createSignedFetcher - signedFetch = createSignedFetcher({ service, region }) - } - const fetcher = signedFetch - return fetcher(url, init) - } - return fn -} diff --git a/packages/ai-bedrock/src/utils/client.ts b/packages/ai-bedrock/src/utils/client.ts index d236e2223..9e1317945 100644 --- a/packages/ai-bedrock/src/utils/client.ts +++ b/packages/ai-bedrock/src/utils/client.ts @@ -1,7 +1,11 @@ -import { getApiKeyFromEnv } from '@tanstack/ai-utils' import type { ClientOptions } from 'openai' +import { resolveBedrockAuth } from './auth' +import { createSigV4Fetch } from './openai-sigv4-fetch' +import type { BedrockEndpoint } from './auth' -export type BedrockEndpoint = 'runtime' | 'mantle' +export type { BedrockEndpoint } from './auth' +export { resolveBedrockAuth } from './auth' +export type { ResolvedBedrockAuth } from './auth' export interface BedrockClientConfig extends Omit< ClientOptions, @@ -29,85 +33,6 @@ function buildBaseURL(region: string, endpoint: BedrockEndpoint): string { : `https://bedrock-runtime.${region}.amazonaws.com/openai/v1` } -/** Reads BEDROCK_API_KEY, then AWS_BEARER_TOKEN_BEDROCK. Returns undefined if neither is set. */ -function readApiKeyFromEnv(): string | undefined { - try { - return getApiKeyFromEnv('BEDROCK_API_KEY') - } catch { - try { - return getApiKeyFromEnv('AWS_BEARER_TOKEN_BEDROCK') - } catch { - return undefined - } - } -} - -/** Throws if no Bedrock API key is available via config or env. */ -export function getBedrockApiKeyFromEnv(): string { - const key = readApiKeyFromEnv() - if (!key) { - throw new Error( - 'No Bedrock API key found. Set BEDROCK_API_KEY (or AWS_BEARER_TOKEN_BEDROCK) in your ' + - 'environment, pass `apiKey` to the factory, or use SigV4 auth (set auth: "sigv4" with ' + - 'AWS credentials configured).', - ) - } - return key -} - -export interface ResolvedBedrockAuth { - apiKey: string - /** Present only for the SigV4 path — a signing fetch for the OpenAI SDK. */ - fetch?: ClientOptions['fetch'] -} - -/** - * Resolves auth per the cascade: explicit apiKey → BEDROCK_API_KEY → - * AWS_BEARER_TOKEN_BEDROCK → SigV4. `auth: 'apikey'` forces the bearer path - * (throws with no key); `auth: 'sigv4'` forces signing. - */ -export function resolveBedrockAuth( - config: BedrockClientConfig, - endpoint: BedrockEndpoint, -): ResolvedBedrockAuth { - const mode = config.auth ?? 'auto' - - if (mode !== 'sigv4') { - const key = config.apiKey ?? readApiKeyFromEnv() - if (key) return { apiKey: key } - if (mode === 'apikey') { - // No key and apikey mode forced — throw the canonical error (terminal). - return { apiKey: getBedrockApiKeyFromEnv() } - } - } - - // SigV4 path — build a lazily-imported signing fetch. - const region = config.region ?? DEFAULT_REGION - return { - apiKey: SIGV4_PLACEHOLDER_KEY, - fetch: createLazySigV4Fetch(region, endpoint), - } -} - -/** - * Returns a fetch that, on first call, dynamically imports the SigV4 signer - * from the `./sigv4` subpath (which holds the optional `aws-sigv4-fetch` dep) - * and delegates to it. Keeps the AWS signing code out of the default bundle. - */ -function createLazySigV4Fetch( - region: string, - endpoint: BedrockEndpoint, -): NonNullable { - let signed: NonNullable | undefined - return async (url, init) => { - if (!signed) { - const { bedrockSigV4Fetch } = await import('../sigv4/index') - signed = bedrockSigV4Fetch({ region, endpoint }) - } - return signed(url, init) - } -} - /** Builds OpenAI ClientOptions for the requested endpoint. `forced` pins the endpoint (responses → 'mantle'). */ export function withBedrockDefaults( config: BedrockClientConfig, @@ -116,16 +41,22 @@ export function withBedrockDefaults( const { region, endpoint, auth, apiKey, baseURL, fetch, ...rest } = config const resolvedRegion = region ?? DEFAULT_REGION const resolvedEndpoint = forced ?? endpoint ?? 'runtime' - const resolvedAuth = resolveBedrockAuth(config, resolvedEndpoint) + const resolved = resolveBedrockAuth( + { apiKey, region: resolvedRegion, auth }, + resolvedEndpoint, + ) + if (resolved.kind === 'bearer') { + return { + ...rest, + baseURL: baseURL ?? buildBaseURL(resolvedRegion, resolvedEndpoint), + apiKey: resolved.token, + ...(fetch ? { fetch } : {}), + } + } return { ...rest, baseURL: baseURL ?? buildBaseURL(resolvedRegion, resolvedEndpoint), - apiKey: resolvedAuth.apiKey, - // A user-supplied fetch wins over the SigV4 signer. - ...(fetch - ? { fetch } - : resolvedAuth.fetch - ? { fetch: resolvedAuth.fetch } - : {}), + apiKey: SIGV4_PLACEHOLDER_KEY, + fetch: fetch ?? createSigV4Fetch(resolved), } } diff --git a/packages/ai-bedrock/src/utils/index.ts b/packages/ai-bedrock/src/utils/index.ts index b47d6a9b1..aa73872fb 100644 --- a/packages/ai-bedrock/src/utils/index.ts +++ b/packages/ai-bedrock/src/utils/index.ts @@ -1,5 +1,4 @@ export { - getBedrockApiKeyFromEnv, resolveBedrockAuth, withBedrockDefaults, type BedrockClientConfig, diff --git a/packages/ai-bedrock/src/utils/openai-sigv4-fetch.ts b/packages/ai-bedrock/src/utils/openai-sigv4-fetch.ts new file mode 100644 index 000000000..06c5d808b --- /dev/null +++ b/packages/ai-bedrock/src/utils/openai-sigv4-fetch.ts @@ -0,0 +1,49 @@ +import { SignatureV4 } from '@smithy/signature-v4' +import { Sha256 } from '@aws-crypto/sha256-js' +import type { HttpRequest } from '@smithy/types' +import type { ResolvedBedrockAuth } from './auth' + +type FetchLike = typeof fetch + +/** + * Wraps a fetch so each request is SigV4-signed via the AWS signer that ships + * with `@aws-sdk/client-bedrock-runtime`. Replaces the old aws-sigv4-fetch peer. + */ +export function createSigV4Fetch( + auth: Extract, + baseFetch: FetchLike = fetch, +): FetchLike { + const signer = new SignatureV4({ + service: auth.service, + region: auth.region, + credentials: auth.credentials, + sha256: Sha256, + }) + + return async (input, init) => { + const url = new URL(typeof input === 'string' ? input : input.toString()) + const headers: Record = {} + new Headers(init?.headers).forEach((v, k) => (headers[k] = v)) + headers['host'] = url.host + + const body = + typeof init?.body === 'string' ? init.body : init?.body ?? undefined + + // Construct a plain object satisfying the @smithy/types HttpRequest interface — + // no @smithy/protocol-http needed. + const request: HttpRequest = { + method: init?.method ?? 'GET', + protocol: url.protocol, + hostname: url.hostname, + path: url.pathname + url.search, + headers, + body, + } + + const signed = await signer.sign(request) + return baseFetch(url.toString(), { + ...init, + headers: signed.headers as Record, + }) + } +} diff --git a/packages/ai-bedrock/tests/client.test.ts b/packages/ai-bedrock/tests/client.test.ts index 373d0c943..ac17e5e15 100644 --- a/packages/ai-bedrock/tests/client.test.ts +++ b/packages/ai-bedrock/tests/client.test.ts @@ -81,21 +81,21 @@ describe('withBedrockDefaults', () => { }) describe('resolveBedrockAuth', () => { - it('uses an explicit apiKey', () => { + it('uses an explicit apiKey — returns bearer', () => { const r = resolveBedrockAuth({ apiKey: 'explicit' }, 'runtime') - expect(r).toEqual({ apiKey: 'explicit' }) + expect(r).toEqual({ kind: 'bearer', token: 'explicit' }) }) - it('falls back to BEDROCK_API_KEY', () => { + it('falls back to BEDROCK_API_KEY — returns bearer', () => { vi.stubEnv('BEDROCK_API_KEY', 'from-bedrock-env') const r = resolveBedrockAuth({}, 'runtime') - expect(r).toEqual({ apiKey: 'from-bedrock-env' }) + expect(r).toEqual({ kind: 'bearer', token: 'from-bedrock-env' }) }) - it('falls back to AWS_BEARER_TOKEN_BEDROCK', () => { + it('falls back to AWS_BEARER_TOKEN_BEDROCK — returns bearer', () => { vi.stubEnv('AWS_BEARER_TOKEN_BEDROCK', 'from-aws-env') const r = resolveBedrockAuth({}, 'runtime') - expect(r).toEqual({ apiKey: 'from-aws-env' }) + expect(r).toEqual({ kind: 'bearer', token: 'from-aws-env' }) }) it("auth: 'apikey' with no key throws an actionable error", () => { @@ -103,22 +103,25 @@ describe('resolveBedrockAuth', () => { vi.stubEnv('AWS_BEARER_TOKEN_BEDROCK', '') expect(() => resolveBedrockAuth({ auth: 'apikey' }, 'runtime'), - ).toThrowError(/BEDROCK_API_KEY/) + ).toThrowError(/No Bedrock API key/) }) - it("auth: 'sigv4' returns a signing fetch and a placeholder apiKey", () => { + it("auth: 'sigv4' returns kind:'sigv4' with region and service", () => { const r = resolveBedrockAuth( { auth: 'sigv4', region: 'us-east-1' }, 'runtime', ) - expect(typeof r.fetch).toBe('function') - expect(r.apiKey.length).toBeGreaterThan(0) + expect(r.kind).toBe('sigv4') + if (r.kind === 'sigv4') { + expect(r.region).toBe('us-east-1') + expect(r.service).toBe('bedrock') + } }) - it("'auto' with no key falls through to SigV4", () => { + it("'auto' with no key falls through to SigV4 — returns kind:'sigv4'", () => { vi.stubEnv('BEDROCK_API_KEY', '') vi.stubEnv('AWS_BEARER_TOKEN_BEDROCK', '') const r = resolveBedrockAuth({ region: 'us-east-1' }, 'runtime') - expect(typeof r.fetch).toBe('function') + expect(r.kind).toBe('sigv4') }) }) diff --git a/packages/ai-bedrock/tests/openai-sigv4-fetch.test.ts b/packages/ai-bedrock/tests/openai-sigv4-fetch.test.ts new file mode 100644 index 000000000..5482fac74 --- /dev/null +++ b/packages/ai-bedrock/tests/openai-sigv4-fetch.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import { createSigV4Fetch } from '../src/utils/openai-sigv4-fetch' + +describe('createSigV4Fetch', () => { + it('signs the request and adds an Authorization header', async () => { + let seen: Headers | undefined + const fakeFetch: typeof fetch = async (_url, init) => { + seen = new Headers(init?.headers) + return new Response('{}', { status: 200 }) + } + const signed = createSigV4Fetch( + { + kind: 'sigv4', + region: 'us-east-1', + service: 'bedrock', + credentials: async () => ({ + accessKeyId: 'AKIA', + secretAccessKey: 'secret', + }), + }, + fakeFetch, + ) + await signed('https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1/chat/completions', { + method: 'POST', + body: '{}', + headers: { 'content-type': 'application/json' }, + }) + expect(seen?.get('authorization')).toMatch(/AWS4-HMAC-SHA256/) + }) +}) diff --git a/packages/ai-bedrock/tests/sigv4.test.ts b/packages/ai-bedrock/tests/sigv4.test.ts deleted file mode 100644 index 0efa56523..000000000 --- a/packages/ai-bedrock/tests/sigv4.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { resolveSigV4Params } from '../src/sigv4/index' - -describe('resolveSigV4Params', () => { - it('uses service "bedrock" and the given region', () => { - expect( - resolveSigV4Params({ region: 'us-east-1', endpoint: 'runtime' }), - ).toEqual({ - service: 'bedrock', - region: 'us-east-1', - }) - }) - - it('uses service "bedrock-mantle" for the mantle endpoint', () => { - expect( - resolveSigV4Params({ region: 'eu-west-1', endpoint: 'mantle' }), - ).toEqual({ - service: 'bedrock-mantle', - region: 'eu-west-1', - }) - }) -}) diff --git a/packages/ai-bedrock/vite.config.ts b/packages/ai-bedrock/vite.config.ts index 81a76cc07..77bcc2e60 100644 --- a/packages/ai-bedrock/vite.config.ts +++ b/packages/ai-bedrock/vite.config.ts @@ -29,15 +29,8 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: ['./src/index.ts', './src/sigv4/index.ts'], + entry: ['./src/index.ts'], srcDir: './src', cjs: false, - // `aws-sigv4-fetch` is an optional, user-installed dependency that the - // `/sigv4` subpath dynamically imports. It is intentionally NOT declared in - // package.json (pnpm v11 autoInstallPeers + trust-policy interaction), so - // externalizeDeps (which reads the manifest) does not pick it up. Externalize - // it explicitly so Rollup leaves the dynamic import in place instead of - // trying — and failing — to bundle it. - externalDeps: ['aws-sigv4-fetch'], }), ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5197e4d9..2fc5a0bfa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1028,12 +1028,21 @@ importers: packages/ai-bedrock: dependencies: + '@aws-crypto/sha256-js': + specifier: ^5.2.0 + version: 5.2.0 '@aws-sdk/client-bedrock-runtime': specifier: ^3.1057.0 version: 3.1057.0 '@aws-sdk/credential-providers': specifier: ^3.1057.0 version: 3.1057.0 + '@smithy/signature-v4': + specifier: ^5.4.5 + version: 5.4.5 + '@smithy/types': + specifier: ^4.14.2 + version: 4.14.2 '@tanstack/ai': specifier: workspace:^ version: link:../ai From 63d67fe0185ef087e41d3fd9770c13148a722537 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 15:29:16 +0200 Subject: [PATCH 28/46] fix(ai-bedrock): handle Request input in createSigV4Fetch; simplify body assignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Request.toString() returns '[object Request]', not the URL; use input.url when input is a Request object so new URL() does not throw. Also remove the redundant string-type ternary on body — init?.body ?? undefined covers all BodyInit cases. --- packages/ai-bedrock/src/utils/openai-sigv4-fetch.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/ai-bedrock/src/utils/openai-sigv4-fetch.ts b/packages/ai-bedrock/src/utils/openai-sigv4-fetch.ts index 06c5d808b..b0fcb01ef 100644 --- a/packages/ai-bedrock/src/utils/openai-sigv4-fetch.ts +++ b/packages/ai-bedrock/src/utils/openai-sigv4-fetch.ts @@ -21,13 +21,19 @@ export function createSigV4Fetch( }) return async (input, init) => { - const url = new URL(typeof input === 'string' ? input : input.toString()) + // Request.toString() returns '[object Request]', not the URL — use .url instead. + const href = + typeof input === 'string' + ? input + : input instanceof Request + ? input.url + : input.toString() + const url = new URL(href) const headers: Record = {} new Headers(init?.headers).forEach((v, k) => (headers[k] = v)) headers['host'] = url.host - const body = - typeof init?.body === 'string' ? init.body : init?.body ?? undefined + const body = init?.body ?? undefined // Construct a plain object satisfying the @smithy/types HttpRequest interface — // no @smithy/protocol-http needed. From 57e15ceba4ac265720cde87de5b9ca441fcd3854 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 15:35:17 +0200 Subject: [PATCH 29/46] feat(ai-bedrock): generate model catalog with per-API support flags --- package.json | 1 + .../ai-bedrock/src/model-catalog.generated.ts | 88 ++++++++++ packages/ai-bedrock/tests/auth.test.ts | 10 +- .../tests/openai-sigv4-fetch.test.ts | 13 +- pnpm-lock.yaml | 21 +++ scripts/bedrock-api-compatibility.json | 59 +++++++ scripts/fetch-bedrock-models.ts | 150 +++++++++++++++--- 7 files changed, 316 insertions(+), 26 deletions(-) create mode 100644 packages/ai-bedrock/src/model-catalog.generated.ts create mode 100644 scripts/bedrock-api-compatibility.json diff --git a/package.json b/package.json index 53669156c..d6c9e18d4 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ ] }, "devDependencies": { + "@aws-sdk/client-bedrock": "^3.1057.0", "@changesets/changelog-github": "^0.7.0", "@changesets/cli": "^2.30.0", "@faker-js/faker": "^10.1.0", diff --git a/packages/ai-bedrock/src/model-catalog.generated.ts b/packages/ai-bedrock/src/model-catalog.generated.ts new file mode 100644 index 000000000..f2d3dafda --- /dev/null +++ b/packages/ai-bedrock/src/model-catalog.generated.ts @@ -0,0 +1,88 @@ +// GENERATED by scripts/fetch-bedrock-models.ts — do not edit by hand. +// Refresh: AWS_REGION=us-east-1 pnpm tsx scripts/fetch-bedrock-models.ts +export const GENERATED_BEDROCK_MODELS = [ + { + id: 'openai.gpt-oss-120b-1:0', + input: ['text'], + output: ['text'], + apis: { converse: true, chat: true, responses: true }, + }, + { + id: 'openai.gpt-oss-20b-1:0', + input: ['text'], + output: ['text'], + apis: { converse: true, chat: true, responses: true }, + }, + { + id: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', + input: ['text', 'image', 'document'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.anthropic.claude-haiku-4-5-20251001-v1:0', + input: ['text', 'image', 'document'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + input: ['text', 'image', 'document'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0', + input: ['text', 'image', 'document'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.anthropic.claude-3-5-haiku-20241022-v1:0', + input: ['text'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.amazon.nova-pro-v1:0', + input: ['text', 'image', 'document'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.amazon.nova-lite-v1:0', + input: ['text', 'image', 'document'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.amazon.nova-micro-v1:0', + input: ['text'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.meta.llama3-3-70b-instruct-v1:0', + input: ['text'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.meta.llama4-maverick-17b-instruct-v1:0', + input: ['text', 'image'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.mistral.pixtral-large-2502-v1:0', + input: ['text', 'image'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, + { + id: 'us.deepseek.r1-v1:0', + input: ['text'], + output: ['text'], + apis: { converse: true, chat: false, responses: false }, + }, +] as const diff --git a/packages/ai-bedrock/tests/auth.test.ts b/packages/ai-bedrock/tests/auth.test.ts index 3fd31f640..0ac7e0867 100644 --- a/packages/ai-bedrock/tests/auth.test.ts +++ b/packages/ai-bedrock/tests/auth.test.ts @@ -3,7 +3,10 @@ import { resolveBedrockAuth } from '../src/utils/auth' describe('resolveBedrockAuth', () => { it('returns bearer when an explicit apiKey is given', () => { - const r = resolveBedrockAuth({ apiKey: 'k', region: 'us-east-1' }, 'runtime') + const r = resolveBedrockAuth( + { apiKey: 'k', region: 'us-east-1' }, + 'runtime', + ) expect(r).toEqual({ kind: 'bearer', token: 'k' }) }) @@ -18,7 +21,10 @@ describe('resolveBedrockAuth', () => { }) it('returns sigv4 with service+region when auth forced sigv4', () => { - const r = resolveBedrockAuth({ auth: 'sigv4', region: 'us-west-2' }, 'mantle') + const r = resolveBedrockAuth( + { auth: 'sigv4', region: 'us-west-2' }, + 'mantle', + ) expect(r.kind).toBe('sigv4') if (r.kind === 'sigv4') { expect(r.region).toBe('us-west-2') diff --git a/packages/ai-bedrock/tests/openai-sigv4-fetch.test.ts b/packages/ai-bedrock/tests/openai-sigv4-fetch.test.ts index 5482fac74..43e73d854 100644 --- a/packages/ai-bedrock/tests/openai-sigv4-fetch.test.ts +++ b/packages/ai-bedrock/tests/openai-sigv4-fetch.test.ts @@ -20,11 +20,14 @@ describe('createSigV4Fetch', () => { }, fakeFetch, ) - await signed('https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1/chat/completions', { - method: 'POST', - body: '{}', - headers: { 'content-type': 'application/json' }, - }) + await signed( + 'https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1/chat/completions', + { + method: 'POST', + body: '{}', + headers: { 'content-type': 'application/json' }, + }, + ) expect(seen?.get('authorization')).toMatch(/AWS4-HMAC-SHA256/) }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fc5a0bfa..459751e88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: .: devDependencies: + '@aws-sdk/client-bedrock': + specifier: ^3.1057.0 + version: 3.1057.0 '@changesets/changelog-github': specifier: ^0.7.0 version: 0.7.0 @@ -2153,6 +2156,10 @@ packages: resolution: {integrity: sha512-TqnYAhAEk45+w3JmS5uHc05AAxfQ7NDyfuARzBv/Y5WuDftRPJMm6FBHCEH7dqcDCcAHmI+XyCYaBI7g7EgweQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/client-bedrock@3.1057.0': + resolution: {integrity: sha512-3Vn/bXD6Ohcx8HO8awru4IKxKITEFR3/xRDmkX5StdU4wT1ZTQGLpDjzisJbPy1UmcGwo/Zp9EzeGzZu4xLDkw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/client-cognito-identity@3.1057.0': resolution: {integrity: sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg==} engines: {node: '>=20.0.0'} @@ -14064,6 +14071,20 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 + '@aws-sdk/client-bedrock@3.1057.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.15 + '@aws-sdk/credential-provider-node': 3.972.47 + '@aws-sdk/token-providers': 3.1057.0 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.5 + '@smithy/fetch-http-handler': 5.4.5 + '@smithy/node-http-handler': 4.7.5 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/client-cognito-identity@3.1057.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 diff --git a/scripts/bedrock-api-compatibility.json b/scripts/bedrock-api-compatibility.json new file mode 100644 index 000000000..a85990bde --- /dev/null +++ b/scripts/bedrock-api-compatibility.json @@ -0,0 +1,59 @@ +[ + { + "match": "openai.gpt-oss", + "converse": true, + "chat": true, + "responses": true + }, + { + "match": "anthropic.claude", + "converse": true, + "chat": false, + "responses": false + }, + { + "match": "amazon.nova", + "converse": true, + "chat": false, + "responses": false + }, + { + "match": "meta.llama", + "converse": true, + "chat": false, + "responses": false + }, + { + "match": "ai21.jamba", + "converse": true, + "chat": false, + "responses": false + }, + { + "match": "cohere.command", + "converse": true, + "chat": false, + "responses": false + }, + { + "match": "deepseek.r1", + "converse": true, + "chat": false, + "responses": false + }, + { "match": "deepseek", "converse": true, "chat": true, "responses": false }, + { + "match": "mistral.pixtral", + "converse": true, + "chat": false, + "responses": false + }, + { "match": "mistral", "converse": true, "chat": true, "responses": false }, + { "match": "qwen", "converse": true, "chat": true, "responses": false }, + { + "match": "google.gemma", + "converse": true, + "chat": true, + "responses": false + } +] diff --git a/scripts/fetch-bedrock-models.ts b/scripts/fetch-bedrock-models.ts index 21a77a9f7..a4df76e82 100644 --- a/scripts/fetch-bedrock-models.ts +++ b/scripts/fetch-bedrock-models.ts @@ -1,28 +1,99 @@ /** - * Fetches the Bedrock foundation-model + inference-profile catalog and prints - * the chat-capable invocation IDs and cross-region inference-profile IDs so a - * maintainer can refresh packages/ai-bedrock/src/model-meta.ts. + * Fetches the Bedrock foundation-model + inference-profile catalog and WRITES + * packages/ai-bedrock/src/model-catalog.generated.ts so the committed file + * stays fresh without manual editing. * * MAINTAINER-ONLY. Not run in CI. Requires AWS credentials (standard provider * chain) with bedrock:List* permissions, and the AWS SDK: * pnpm add -Dw @aws-sdk/client-bedrock # if not already installed * AWS_REGION=us-east-1 pnpm tsx scripts/fetch-bedrock-models.ts * - * Why manual: ListFoundationModels carries modalities + inference types but no - * pricing, and per-account/region availability varies. The committed model-meta - * is a hand-transcribed seed; this script is the long-term source of truth. - * Responses-capable models are those with Responses=Yes in + * Per-API flags (converse / chat / responses) come from the static seed file + * scripts/bedrock-api-compatibility.json, transcribed from: * https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html + * Update the JSON seed to add new providers/models before re-running the script. + * + * Why manual: ListFoundationModels carries modalities + inference types but no + * pricing, and per-account/region availability varies. The committed model-catalog + * is the long-term source of truth; this script regenerates it automatically. */ import { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand, } from '@aws-sdk/client-bedrock' +import { readFileSync, writeFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { join, dirname } from 'node:path' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const ROOT = join(__dirname, '..') + +interface CompatibilityRule { + match: string + converse: boolean + chat: boolean + responses: boolean +} + +function loadCompatibilitySeed(): CompatibilityRule[] { + const seedPath = join(__dirname, 'bedrock-api-compatibility.json') + return JSON.parse(readFileSync(seedPath, 'utf-8')) as CompatibilityRule[] +} + +function lookupApis( + id: string, + rules: CompatibilityRule[], +): { converse: boolean; chat: boolean; responses: boolean } { + for (const rule of rules) { + if (id.includes(rule.match)) { + return { + converse: rule.converse, + chat: rule.chat, + responses: rule.responses, + } + } + } + // Default: Converse is supported by virtually all text models; chat/responses are opt-in. + return { converse: true, chat: false, responses: false } +} + +function emitCatalog( + entries: Array<{ + id: string + profileId?: string + input: string[] + output: string[] + apis: { converse: boolean; chat: boolean; responses: boolean } + }>, +): string { + const lines: string[] = [ + `// GENERATED by scripts/fetch-bedrock-models.ts — do not edit by hand.`, + `// Refresh: AWS_REGION=us-east-1 pnpm tsx scripts/fetch-bedrock-models.ts`, + `export const GENERATED_BEDROCK_MODELS = [`, + ] + + for (const entry of entries) { + const inputLiteral = entry.input.map((m) => `'${m}'`).join(', ') + const outputLiteral = entry.output.map((m) => `'${m}'`).join(', ') + const apis = entry.apis + + const profilePart = + entry.profileId !== undefined ? `profileId: '${entry.profileId}', ` : '' + + lines.push( + ` { id: '${entry.id}', ${profilePart}input: [${inputLiteral}], output: [${outputLiteral}], apis: { converse: ${apis.converse}, chat: ${apis.chat}, responses: ${apis.responses} } },`, + ) + } + + lines.push(`] as const`, ``) + return lines.join('\n') +} async function main() { const region = process.env['AWS_REGION'] ?? 'us-east-1' const client = new BedrockClient({ region }) + const compatRules = loadCompatibilitySeed() // ListFoundationModels typically returns no nextToken, but loop on it anyway // so we stay correct if it ever paginates. @@ -52,24 +123,65 @@ async function main() { profilesToken = page.nextToken } while (profilesToken) + // Build a lookup: base model id → preferred cross-region inference profile id. + // A cross-region profile id typically looks like "us.". + const profileByBaseId = new Map() + for (const profile of inferenceProfileSummaries) { + const profileId = profile.inferenceProfileId + if (!profileId) continue + // Strip the leading region prefix (e.g. "us.", "eu.", "ap.") to get the base id. + const baseId = profileId.replace(/^(us|eu|ap)\./, '') + if (!profileByBaseId.has(baseId)) { + profileByBaseId.set(baseId, profileId) + } + } + const textModels = modelSummaries .filter((m) => (m.outputModalities ?? []).includes('TEXT')) - .map((m) => ({ - id: m.modelId ?? '', - input: (m.inputModalities ?? []).map((x) => x.toLowerCase()), - })) + .map((m) => { + const id = m.modelId ?? '' + return { + id, + input: (m.inputModalities ?? []).map((x) => x.toLowerCase()), + } + }) .filter((m) => m.id.length > 0) - const inferenceProfileIds = inferenceProfileSummaries - .map((p) => p.inferenceProfileId ?? '') - .filter((id) => id.length > 0) + const entries = textModels.map((m) => { + const profileId = profileByBaseId.get(m.id) + const resolvedId = profileId ?? m.id + const apis = lookupApis(resolvedId, compatRules) + + const entry: { + id: string + profileId?: string + input: string[] + output: string[] + apis: { converse: boolean; chat: boolean; responses: boolean } + } = { + id: resolvedId, + input: m.input, + output: ['text'], + apis, + } + + if (profileId !== undefined) { + entry.profileId = profileId + } + + return entry + }) - console.log('# Base foundation text models:') - for (const m of textModels) console.log(`${m.id}\tinput=${m.input.join(',')}`) - console.log( - '\n# Cross-region inference profile IDs (use as `model` for runtime chat):', + const outPath = join( + ROOT, + 'packages', + 'ai-bedrock', + 'src', + 'model-catalog.generated.ts', ) - for (const id of inferenceProfileIds.sort()) console.log(id) + const content = emitCatalog(entries) + writeFileSync(outPath, content, 'utf-8') + console.log(`Wrote ${entries.length} models to ${outPath}`) } main().catch((err) => { From 63b0bc956e601acc6cedb90cb55edd512920399c Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 15:39:50 +0200 Subject: [PATCH 30/46] feat(ai-bedrock): three per-API catalogs (converse default) + curated overrides --- packages/ai-bedrock/src/model-meta.ts | 289 +++------------------ packages/ai-bedrock/src/model-overrides.ts | 29 +++ 2 files changed, 66 insertions(+), 252 deletions(-) create mode 100644 packages/ai-bedrock/src/model-overrides.ts diff --git a/packages/ai-bedrock/src/model-meta.ts b/packages/ai-bedrock/src/model-meta.ts index 59fb70e12..88c0f936e 100644 --- a/packages/ai-bedrock/src/model-meta.ts +++ b/packages/ai-bedrock/src/model-meta.ts @@ -1,265 +1,50 @@ +import { GENERATED_BEDROCK_MODELS } from './model-catalog.generated' import type { BedrockTextProviderOptions } from './text/text-provider-options' -/** Bedrock model metadata. `pricing` is intentionally optional and unpopulated initially. */ -interface ModelMeta { - name: string - context_window?: number - max_completion_tokens?: number - pricing?: { - input?: { normal: number; cached?: number } - output?: { normal: number } - } - supports: { - input: Array<'text' | 'image' | 'document'> - output: Array<'text'> - endpoints: Array<'chat' | 'responses'> - features: Array< - 'streaming' | 'tools' | 'reasoning' | 'json_schema' | 'vision' - > - tools: ReadonlyArray - } -} - -// --- OpenAI gpt-oss (text-only; chat + responses) --- -// Both IDs use AWS's canonical versioned Model IDs (`-1:0`). The mantle/Responses -// endpoint may also accept an unversioned alias; that is reconciled by the refresh script. -const GPT_OSS_120B = { - name: 'openai.gpt-oss-120b-1:0', - context_window: 128_000, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat', 'responses'], - features: ['streaming', 'tools', 'reasoning'], - tools: [] as const, - }, -} as const satisfies ModelMeta -const GPT_OSS_20B = { - name: 'openai.gpt-oss-20b-1:0', - context_window: 128_000, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat', 'responses'], - features: ['streaming', 'tools', 'reasoning'], - tools: [] as const, - }, -} as const satisfies ModelMeta - -// --- Anthropic Claude (US cross-region inference profiles; chat) --- -const CLAUDE_SONNET_4_5 = { - name: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', - context_window: 200_000, - supports: { - input: ['text', 'image', 'document'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'vision', 'reasoning'], - tools: [] as const, - }, -} as const satisfies ModelMeta -const CLAUDE_HAIKU_4_5 = { - name: 'us.anthropic.claude-haiku-4-5-20251001-v1:0', - context_window: 200_000, - supports: { - input: ['text', 'image', 'document'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'vision'], - tools: [] as const, - }, -} as const satisfies ModelMeta -const CLAUDE_3_7_SONNET = { - name: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', - context_window: 200_000, - supports: { - input: ['text', 'image', 'document'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'vision', 'reasoning'], - tools: [] as const, - }, -} as const satisfies ModelMeta -const CLAUDE_3_5_SONNET_V2 = { - name: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0', - context_window: 200_000, - supports: { - input: ['text', 'image', 'document'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'vision'], - tools: [] as const, - }, -} as const satisfies ModelMeta -const CLAUDE_3_5_HAIKU = { - name: 'us.anthropic.claude-3-5-haiku-20241022-v1:0', - context_window: 200_000, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools'], - tools: [] as const, - }, -} as const satisfies ModelMeta - -// --- Amazon Nova (US profiles; chat) --- -const NOVA_PRO = { - name: 'us.amazon.nova-pro-v1:0', - context_window: 300_000, - supports: { - input: ['text', 'image', 'document'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'vision'], - tools: [] as const, - }, -} as const satisfies ModelMeta -const NOVA_LITE = { - name: 'us.amazon.nova-lite-v1:0', - context_window: 300_000, - supports: { - input: ['text', 'image', 'document'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'vision'], - tools: [] as const, - }, -} as const satisfies ModelMeta -const NOVA_MICRO = { - name: 'us.amazon.nova-micro-v1:0', - context_window: 128_000, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools'], - tools: [] as const, - }, -} as const satisfies ModelMeta - -// --- Meta Llama (US profiles; chat) --- -const LLAMA_3_3_70B = { - name: 'us.meta.llama3-3-70b-instruct-v1:0', - context_window: 128_000, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools'], - tools: [] as const, - }, -} as const satisfies ModelMeta -const LLAMA_4_MAVERICK = { - name: 'us.meta.llama4-maverick-17b-instruct-v1:0', - context_window: 128_000, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'vision'], - tools: [] as const, - }, -} as const satisfies ModelMeta - -// --- Mistral / DeepSeek (US profiles; chat) --- -const MISTRAL_PIXTRAL_LARGE = { - name: 'us.mistral.pixtral-large-2502-v1:0', - context_window: 128_000, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'vision'], - tools: [] as const, - }, -} as const satisfies ModelMeta -const DEEPSEEK_R1 = { - name: 'us.deepseek.r1-v1:0', - context_window: 128_000, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'reasoning'], - tools: [] as const, - }, -} as const satisfies ModelMeta - -const CHAT_MODELS = [ - GPT_OSS_20B, - GPT_OSS_120B, - CLAUDE_SONNET_4_5, - CLAUDE_HAIKU_4_5, - CLAUDE_3_7_SONNET, - CLAUDE_3_5_SONNET_V2, - CLAUDE_3_5_HAIKU, - NOVA_PRO, - NOVA_LITE, - NOVA_MICRO, - LLAMA_3_3_70B, - LLAMA_4_MAVERICK, - MISTRAL_PIXTRAL_LARGE, - DEEPSEEK_R1, -] as const - -// Cast-free: explicit `.name` lists with `as const` (the ai-groq pattern). -export const BEDROCK_CHAT_MODELS = [ - GPT_OSS_20B.name, - GPT_OSS_120B.name, - CLAUDE_SONNET_4_5.name, - CLAUDE_HAIKU_4_5.name, - CLAUDE_3_7_SONNET.name, - CLAUDE_3_5_SONNET_V2.name, - CLAUDE_3_5_HAIKU.name, - NOVA_PRO.name, - NOVA_LITE.name, - NOVA_MICRO.name, - LLAMA_3_3_70B.name, - LLAMA_4_MAVERICK.name, - MISTRAL_PIXTRAL_LARGE.name, - DEEPSEEK_R1.name, -] as const -export const BEDROCK_RESPONSES_MODELS = [ - GPT_OSS_20B.name, - GPT_OSS_120B.name, -] as const - -export type BedrockChatModels = (typeof BEDROCK_CHAT_MODELS)[number] -export type BedrockResponsesModels = (typeof BEDROCK_RESPONSES_MODELS)[number] - -// Mapped types keyed off the model-constant tuple union. The `as M['name']` -// is mapped-type KEY REMAPPING (legal syntax), NOT a value cast. -type ChatModelMeta = (typeof CHAT_MODELS)[number] - -// Compile-time guard: CHAT_MODELS (drives the per-model type maps) and -// BEDROCK_CHAT_MODELS (the public runtime catalog) must list the same models. -// If they diverge, the type argument to `_AssertTrue` stops satisfying -// `extends true` and tsc fails with a readable message. -// The `declare const` form has no runtime cost and avoids a `noUnusedLocals` -// error on a `const` whose value is never read. -type _AssertTrue = TResult -declare const _chatModelsInSync: _AssertTrue< - ChatModelMeta['name'] extends BedrockChatModels - ? BedrockChatModels extends ChatModelMeta['name'] - ? true - : ['BEDROCK_CHAT_MODELS has a name missing from CHAT_MODELS'] - : ['CHAT_MODELS has a name missing from BEDROCK_CHAT_MODELS'] -> - -/** Per-model input modalities (drives type-safe multimodal content). */ +type Entry = (typeof GENERATED_BEDROCK_MODELS)[number] + +/** + * Type-level per-API filter over the generated catalog. Because the catalog is + * `as const`, `Extract` preserves literal `id` unions (no widening to `string`). + */ +type IdsWhere = Extract< + Entry, + { apis: Record } +>['id'] + +export type BedrockConverseModels = IdsWhere<'converse'> +export type BedrockChatModels = IdsWhere<'chat'> +export type BedrockResponsesModels = IdsWhere<'responses'> + +/** Runtime catalogs. Cast-free narrowing via a type predicate (the ai-bedrock pattern). */ +export const BEDROCK_CONVERSE_MODELS: ReadonlyArray = + GENERATED_BEDROCK_MODELS.filter( + (m): m is Extract => m.apis.converse, + ).map((m) => m.id) + +export const BEDROCK_CHAT_MODELS: ReadonlyArray = + GENERATED_BEDROCK_MODELS.filter( + (m): m is Extract => m.apis.chat, + ).map((m) => m.id) + +export const BEDROCK_RESPONSES_MODELS: ReadonlyArray = + GENERATED_BEDROCK_MODELS.filter( + (m): m is Extract => m.apis.responses, + ).map((m) => m.id) + +/** Per-model input modalities (drives type-safe multimodal content). Covers ALL models. */ export type BedrockModelInputModalitiesByName = { - [M in ChatModelMeta as M['name']]: M['supports']['input'] + [E in Entry as E['id']]: E['input'] } -/** Provider options per model — mapped type (ai-grok pattern). */ +/** Provider options per model. Same options for every model; keyed over the full catalog. */ export type BedrockChatModelProviderOptionsByName = { - [K in BedrockChatModels]: BedrockTextProviderOptions + [E in Entry as E['id']]: BedrockTextProviderOptions } /** No provider-specific tools — empty tuple makes cross-provider ProviderTool a compile error. */ export type BedrockChatModelToolCapabilitiesByName = { - [M in ChatModelMeta as M['name']]: M['supports']['tools'] + [E in Entry as E['id']]: readonly [] } export type ResolveProviderOptions = diff --git a/packages/ai-bedrock/src/model-overrides.ts b/packages/ai-bedrock/src/model-overrides.ts new file mode 100644 index 000000000..c0b98e8de --- /dev/null +++ b/packages/ai-bedrock/src/model-overrides.ts @@ -0,0 +1,29 @@ +/** + * Capabilities `ListFoundationModels` does not report (tool & reasoning support). + * Hand-maintained; merged with the generated catalog at runtime. Keyed by model + * id / inference-profile id. + */ +export interface ModelOverride { + features?: Array<'tools' | 'reasoning' | 'json_schema'> +} + +export const BEDROCK_MODEL_OVERRIDES: Record = { + 'openai.gpt-oss-120b-1:0': { features: ['tools', 'reasoning'] }, + 'openai.gpt-oss-20b-1:0': { features: ['tools', 'reasoning'] }, + 'us.anthropic.claude-sonnet-4-5-20250929-v1:0': { + features: ['tools', 'reasoning'], + }, + 'us.anthropic.claude-haiku-4-5-20251001-v1:0': { features: ['tools'] }, + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': { + features: ['tools', 'reasoning'], + }, + 'us.anthropic.claude-3-5-sonnet-20241022-v2:0': { features: ['tools'] }, + 'us.anthropic.claude-3-5-haiku-20241022-v1:0': { features: ['tools'] }, + 'us.amazon.nova-pro-v1:0': { features: ['tools'] }, + 'us.amazon.nova-lite-v1:0': { features: ['tools'] }, + 'us.amazon.nova-micro-v1:0': { features: ['tools'] }, + 'us.meta.llama3-3-70b-instruct-v1:0': { features: ['tools'] }, + 'us.meta.llama4-maverick-17b-instruct-v1:0': { features: ['tools'] }, + 'us.mistral.pixtral-large-2502-v1:0': { features: ['tools'] }, + 'us.deepseek.r1-v1:0': { features: ['reasoning'] }, +} From 8602e82cb0fed83ced187938507a6e6c9bf48315 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 15:51:52 +0200 Subject: [PATCH 31/46] feat(ai-bedrock): Converse message converter (system, role merge, tools, multimodal) TDD-implemented toConverseMessages: lifts system prompts via normalizeSystemPrompts, maps tool/user/assistant roles with consecutive-role merging, handles text/image/document content parts, and emits toolUse/toolResult blocks. Guards: JSON.parse try/catch, missing toolCallId throw, empty-block skip, URL-source throw. --- .../src/converse/message-converter.ts | 255 ++++++++++++++++++ .../tests/converse/message-converter.test.ts | 84 ++++++ 2 files changed, 339 insertions(+) create mode 100644 packages/ai-bedrock/src/converse/message-converter.ts create mode 100644 packages/ai-bedrock/tests/converse/message-converter.test.ts diff --git a/packages/ai-bedrock/src/converse/message-converter.ts b/packages/ai-bedrock/src/converse/message-converter.ts new file mode 100644 index 000000000..d37285156 --- /dev/null +++ b/packages/ai-bedrock/src/converse/message-converter.ts @@ -0,0 +1,255 @@ +import { normalizeSystemPrompts } from '@tanstack/ai' +import type { + ContentPart, + ModelMessage, + SystemPrompt, + TextPart, + ImagePart, + DocumentPart, + ContentPartDataSource, +} from '@tanstack/ai' +import type { + ContentBlock, + Message, + SystemContentBlock, + ToolResultContentBlock, +} from '@aws-sdk/client-bedrock-runtime' +import type { DocumentType } from '@smithy/types' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function base64ToBytes(b64: string): Uint8Array { + return new Uint8Array(Buffer.from(b64, 'base64')) +} + +function imageFormat( + mime: string, +): 'png' | 'jpeg' | 'gif' | 'webp' { + switch (mime) { + case 'image/png': + return 'png' + case 'image/jpeg': + case 'image/jpg': + return 'jpeg' + case 'image/gif': + return 'gif' + case 'image/webp': + return 'webp' + default: + throw new Error( + `Bedrock Converse: unsupported image MIME type "${mime}". Supported types: image/png, image/jpeg, image/gif, image/webp.`, + ) + } +} + +function documentFormat( + mime: string, +): 'pdf' | 'csv' | 'doc' | 'docx' | 'xls' | 'xlsx' | 'html' | 'txt' | 'md' { + switch (mime) { + case 'application/pdf': + return 'pdf' + case 'text/csv': + return 'csv' + case 'application/msword': + return 'doc' + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + return 'docx' + case 'application/vnd.ms-excel': + return 'xls' + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + return 'xlsx' + case 'text/html': + return 'html' + case 'text/plain': + return 'txt' + case 'text/markdown': + case 'text/x-markdown': + return 'md' + default: + throw new Error( + `Bedrock Converse: unsupported document MIME type "${mime}". Supported types: pdf, csv, doc, docx, xls, xlsx, html, txt, md.`, + ) + } +} + +function stringContent( + content: string | null | ContentPart[], +): string { + if (content === null) return '' + if (typeof content === 'string') return content + return content + .filter((p): p is TextPart => p.type === 'text') + .map((p) => p.content) + .join('') +} + +function isTextPart(p: ContentPart): p is TextPart { + return p.type === 'text' +} + +function isImagePart(p: ContentPart): p is ImagePart { + return p.type === 'image' +} + +function isDocumentPart(p: ContentPart): p is DocumentPart { + return p.type === 'document' +} + +function isDataSource( + source: ImagePart['source'] | DocumentPart['source'], +): source is ContentPartDataSource { + return source.type === 'data' +} + +function contentPartToBlock(part: ContentPart): ContentBlock { + if (isTextPart(part)) { + return { text: part.content } + } + + if (isImagePart(part)) { + const { source } = part + if (!isDataSource(source)) { + throw new Error( + 'Bedrock Converse requires inline image bytes; URL image sources are not supported.', + ) + } + return { + image: { + format: imageFormat(source.mimeType), + source: { bytes: base64ToBytes(source.value) }, + }, + } + } + + if (isDocumentPart(part)) { + const { source } = part + if (!isDataSource(source)) { + throw new Error( + 'Bedrock Converse requires inline document bytes; URL document sources are not supported.', + ) + } + return { + document: { + format: documentFormat(source.mimeType), + name: 'document', + source: { bytes: base64ToBytes(source.value) }, + }, + } + } + + // Fail loud for unsupported part types (audio, video, etc.) + const unsupported = (part as ContentPart).type + throw new Error( + `Bedrock Converse does not support content part type "${String(unsupported)}".`, + ) +} + +function messageToBlocks(msg: ModelMessage): ContentBlock[] { + const blocks: ContentBlock[] = [] + + if (msg.role === 'tool') { + if (!msg.toolCallId) { + throw new Error( + 'Bedrock Converse: tool message is missing toolCallId. Every tool result must reference the tool use ID it is responding to.', + ) + } + const textContent = stringContent(msg.content) + const toolResult: ToolResultContentBlock = { text: textContent } + blocks.push({ + toolResult: { + toolUseId: msg.toolCallId, + content: [toolResult], + status: 'success', + }, + }) + return blocks + } + + // Map content field to text/image/document blocks + if (typeof msg.content === 'string') { + if (msg.content !== '') { + blocks.push({ text: msg.content }) + } + } else if (Array.isArray(msg.content)) { + for (const part of msg.content) { + blocks.push(contentPartToBlock(part)) + } + } + // null → no text blocks + + // Append toolUse blocks for assistant tool calls + if (msg.role === 'assistant' && msg.toolCalls) { + for (const call of msg.toolCalls) { + let input: DocumentType = {} + try { + const parsed = JSON.parse(call.function.arguments || '{}') as unknown + if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) { + input = parsed as DocumentType + } + } catch { + // Malformed / partial JSON — fall back to empty object so the call + // can still be forwarded rather than crashing the whole request. + input = {} + } + blocks.push({ + toolUse: { + toolUseId: call.id, + name: call.function.name, + input, + }, + }) + } + } + + return blocks +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Convert TanStack AI messages + system prompts into the Converse API format. + * + * - System prompts are lifted into `SystemContentBlock[]`. + * - `tool` role messages are remapped to `user` role `toolResult` blocks. + * - Consecutive messages with the same Converse role are merged (Converse + * requires strict user/assistant alternation). + */ +export function toConverseMessages( + messages: ModelMessage[], + systemPrompts?: Array, +): { system: SystemContentBlock[]; messages: Message[] } { + // Build system blocks (uses normalizeSystemPrompts for runtime validation) + const system: SystemContentBlock[] = normalizeSystemPrompts(systemPrompts).map( + (p) => ({ text: p.content }), + ) + + // Convert each ModelMessage to a Converse Message, merging same-role pairs + const converseMessages: Message[] = [] + + for (const msg of messages) { + // Map TanStack roles to Converse roles + const converseRole: 'user' | 'assistant' = + msg.role === 'assistant' ? 'assistant' : 'user' + + const blocks = messageToBlocks(msg) + + // Skip messages that produce no content blocks (e.g. assistant with + // null content and no toolCalls). Pushing an empty-content message to + // Converse triggers a ValidationException. + if (blocks.length === 0) continue + + const last = converseMessages[converseMessages.length - 1] + if (last && last.role === converseRole) { + // Merge into the previous message's content array + last.content = [...(last.content ?? []), ...blocks] + } else { + converseMessages.push({ role: converseRole, content: blocks }) + } + } + + return { system, messages: converseMessages } +} diff --git a/packages/ai-bedrock/tests/converse/message-converter.test.ts b/packages/ai-bedrock/tests/converse/message-converter.test.ts new file mode 100644 index 000000000..10a10514a --- /dev/null +++ b/packages/ai-bedrock/tests/converse/message-converter.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest' +import { toConverseMessages } from '../../src/converse/message-converter' +import type { ModelMessage } from '@tanstack/ai' + +describe('toConverseMessages', () => { + it('lifts system prompts into the Converse system field', () => { + const { system, messages } = toConverseMessages( + [{ role: 'user', content: 'hi' }], + ['be terse'], + ) + expect(system).toEqual([{ text: 'be terse' }]) + expect(messages).toEqual([{ role: 'user', content: [{ text: 'hi' }] }]) + }) + + it('normalizes object system prompts and joins multiple', () => { + const { system } = toConverseMessages( + [{ role: 'user', content: 'hi' }], + ['a', { content: 'b' }], + ) + expect(system).toEqual([{ text: 'a' }, { text: 'b' }]) + }) + + it('merges consecutive same-role messages (Converse requires alternation)', () => { + const { messages } = toConverseMessages([ + { role: 'user', content: 'a' }, + { role: 'user', content: 'b' }, + ]) + expect(messages).toEqual([ + { role: 'user', content: [{ text: 'a' }, { text: 'b' }] }, + ]) + }) + + it('maps assistant tool calls to toolUse and tool results to a user toolResult', () => { + const msgs: ModelMessage[] = [ + { + role: 'assistant', + content: '', + toolCalls: [ + { id: 't1', type: 'function', function: { name: 'getX', arguments: '{"a":1}' } }, + ], + }, + { role: 'tool', content: '{"ok":true}', toolCallId: 't1' }, + ] + const { messages } = toConverseMessages(msgs) + expect(messages[0]).toEqual({ + role: 'assistant', + content: [{ toolUse: { toolUseId: 't1', name: 'getX', input: { a: 1 } } }], + }) + expect(messages[1]).toEqual({ + role: 'user', + content: [ + { toolResult: { toolUseId: 't1', content: [{ text: '{"ok":true}' }], status: 'success' } }, + ], + }) + }) + + it('maps a data-source image part to a Converse image block', () => { + const { messages } = toConverseMessages([ + { + role: 'user', + content: [ + { type: 'text', content: 'look' }, + { type: 'image', source: { type: 'data', value: btoa('xy'), mimeType: 'image/png' } }, + ], + }, + ]) + const content = messages[0]!.content! + const textBlock = content[0]! + const imageBlock = content[1]! + expect(textBlock).toEqual({ text: 'look' }) + expect(imageBlock).toMatchObject({ image: { format: 'png' } }) + // bytes decoded from base64 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((imageBlock as any).image.source.bytes).toEqual(new Uint8Array([120, 121])) + }) + + it('throws on a URL image source (Converse needs inline bytes)', () => { + expect(() => + toConverseMessages([ + { role: 'user', content: [{ type: 'image', source: { type: 'url', value: 'https://x/y.png' } }] }, + ]), + ).toThrow(/inline|bytes|URL/i) + }) +}) From f345d0891b8e7bbd6262226959e731092af53892 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 15:59:35 +0200 Subject: [PATCH 32/46] feat(ai-bedrock): Converse tool & tool-choice converter --- .../ai-bedrock/src/converse/tool-converter.ts | 39 +++++++++++++++++ .../tests/converse/tool-converter.test.ts | 42 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 packages/ai-bedrock/src/converse/tool-converter.ts create mode 100644 packages/ai-bedrock/tests/converse/tool-converter.test.ts diff --git a/packages/ai-bedrock/src/converse/tool-converter.ts b/packages/ai-bedrock/src/converse/tool-converter.ts new file mode 100644 index 000000000..1b202caf4 --- /dev/null +++ b/packages/ai-bedrock/src/converse/tool-converter.ts @@ -0,0 +1,39 @@ +import type { ToolConfiguration, ToolChoice } from '@aws-sdk/client-bedrock-runtime' +import type { DocumentType } from '@smithy/types' + +export interface ConverseToolInput { + name: string + description?: string + inputSchema: unknown +} + +export type ToolChoiceInput = + | 'auto' + | 'required' + | 'none' + | { type: 'tool'; name: string } + +export function toToolConfig( + tools: ConverseToolInput[], + choice: ToolChoiceInput | undefined, +): ToolConfiguration | undefined { + if (!tools.length) return undefined + const toolChoice = mapChoice(choice) + return { + tools: tools.map((t) => ({ + toolSpec: { + name: t.name, + ...(t.description ? { description: t.description } : {}), + inputSchema: { json: t.inputSchema as DocumentType }, + }, + })), + ...(toolChoice ? { toolChoice } : {}), + } +} + +function mapChoice(choice: ToolChoiceInput | undefined): ToolChoice | undefined { + if (!choice || choice === 'auto') return { auto: {} } + if (choice === 'required') return { any: {} } + if (choice === 'none') return undefined + return { tool: { name: choice.name } } +} diff --git a/packages/ai-bedrock/tests/converse/tool-converter.test.ts b/packages/ai-bedrock/tests/converse/tool-converter.test.ts new file mode 100644 index 000000000..95a63b852 --- /dev/null +++ b/packages/ai-bedrock/tests/converse/tool-converter.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { toToolConfig } from '../../src/converse/tool-converter' + +describe('toToolConfig', () => { + it('maps JSON-schema tools to Converse toolSpec', () => { + const cfg = toToolConfig( + [{ name: 'getX', description: 'd', inputSchema: { type: 'object', properties: {} } }], + 'auto', + ) + expect(cfg?.tools?.[0]).toEqual({ + toolSpec: { + name: 'getX', + description: 'd', + inputSchema: { json: { type: 'object', properties: {} } }, + }, + }) + expect(cfg?.toolChoice).toEqual({ auto: {} }) + }) + + it('maps required -> any and a named tool -> tool', () => { + expect(toToolConfig([{ name: 'a', inputSchema: {} }], 'required')?.toolChoice).toEqual({ any: {} }) + expect( + toToolConfig([{ name: 'a', inputSchema: {} }], { type: 'tool', name: 'a' })?.toolChoice, + ).toEqual({ tool: { name: 'a' } }) + }) + + it('omits description when not provided', () => { + const cfg = toToolConfig([{ name: 'a', inputSchema: {} }], 'auto') + expect(cfg?.tools?.[0]).toEqual({ + toolSpec: { name: 'a', inputSchema: { json: {} } }, + }) + }) + + it('returns undefined when there are no tools', () => { + expect(toToolConfig([], 'auto')).toBeUndefined() + }) + + it('returns undefined toolChoice for "none" (caller omits tools instead)', () => { + const cfg = toToolConfig([{ name: 'a', inputSchema: {} }], 'none') + expect(cfg?.toolChoice).toBeUndefined() + }) +}) From 0357dabff1db3dbb4dfaf5059d0defc832f1a6df Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 16:06:33 +0200 Subject: [PATCH 33/46] feat(ai-bedrock): Converse stream -> AG-UI StreamChunk processor --- .../src/converse/stream-processor.ts | 267 ++++++++++++++++++ .../tests/converse/stream-processor.test.ts | 120 ++++++++ 2 files changed, 387 insertions(+) create mode 100644 packages/ai-bedrock/src/converse/stream-processor.ts create mode 100644 packages/ai-bedrock/tests/converse/stream-processor.test.ts diff --git a/packages/ai-bedrock/src/converse/stream-processor.ts b/packages/ai-bedrock/src/converse/stream-processor.ts new file mode 100644 index 000000000..3e69ca2f0 --- /dev/null +++ b/packages/ai-bedrock/src/converse/stream-processor.ts @@ -0,0 +1,267 @@ +import { EventType } from '@tanstack/ai' +import type { RunFinishedEvent, StreamChunk } from '@tanstack/ai' +import type { ConverseStreamOutput } from '@aws-sdk/client-bedrock-runtime' + +/** + * Maps a Bedrock Converse `ConverseStreamOutput` event stream to the TanStack + * AG-UI `StreamChunk` lifecycle. This mirrors, field-for-field, how + * `openai-base`'s `processStreamChunks` constructs each event so the activity + * layer / agent loop behave identically across providers. + * + * Lifecycle ownership matches openai-base: this processor emits the full + * success-path lifecycle itself — `RUN_STARTED` lazily before the first event, + * `TEXT_MESSAGE_*` / `TOOL_CALL_*` / `REASONING_*` for content, and a single + * terminal `RUN_FINISHED` once the iterator is exhausted (so the trailing + * `metadata` usage event is folded into the finish event regardless of arrival + * order). The calling adapter only owns the catch/`RUN_ERROR` path. + * + * Converse streams tool-call arguments as partial-JSON string fragments inside + * `contentBlockDelta.delta.toolUse.input`; each fragment is emitted as a + * `TOOL_CALL_ARGS` `delta`, mirroring OpenAI's `function.arguments` deltas. + * + * @param stream - The Converse event stream from `ConverseStreamCommand`. + * @param newMessageId - Factory for fresh message/tool-call ids (the adapter + * passes `() => this.generateId()`). + */ +export async function* processConverseStream( + stream: AsyncIterable, + newMessageId: () => string, +): AsyncIterable { + const runId = newMessageId() + const threadId = newMessageId() + const messageId = newMessageId() + + let hasEmittedRunStarted = false + + // Text lifecycle + let accumulatedContent = '' + let hasEmittedTextMessageStart = false + + // Reasoning lifecycle + let reasoningMessageId: string | undefined + let hasClosedReasoning = false + + // Tool-call lifecycle, keyed by Converse contentBlockIndex. Converse opens a + // tool-use block with `contentBlockStart`, streams arg fragments via + // `contentBlockDelta`, and closes it with `contentBlockStop`. + const toolCallsByIndex = new Map< + number, + { id: string; name: string; started: boolean } + >() + + // Usage + finish-reason are captured during iteration and folded into the + // single terminal RUN_FINISHED, matching openai-base's deferred-finish + // contract (usage may arrive after the finish signal). + let usage: + | { promptTokens: number; completionTokens: number; totalTokens: number } + | undefined + let finishReason: NonNullable | undefined + + // Lazily emit RUN_STARTED exactly once, before the first content event. + function* ensureRunStarted(): Generator { + if (hasEmittedRunStarted) return + hasEmittedRunStarted = true + yield { + type: EventType.RUN_STARTED, + runId, + threadId, + timestamp: Date.now(), + } + } + + // Close an open reasoning message before text/tool content begins, mirroring + // openai-base which always emits REASONING_MESSAGE_END before TEXT_MESSAGE_START. + function* closeReasoning(): Generator { + if (reasoningMessageId && !hasClosedReasoning) { + hasClosedReasoning = true + yield { + type: EventType.REASONING_MESSAGE_END, + messageId: reasoningMessageId, + timestamp: Date.now(), + } + } + } + + for await (const ev of stream) { + yield* ensureRunStarted() + + // messageStart carries only the role; no AG-UI event maps to it. + if ('messageStart' in ev) continue + + if ('contentBlockStart' in ev) { + const start = ev.contentBlockStart + const toolUse = start?.start?.toolUse + if (start && toolUse) { + yield* closeReasoning() + const id = toolUse.toolUseId ?? newMessageId() + const name = toolUse.name ?? '' + const index = start.contentBlockIndex ?? 0 + toolCallsByIndex.set(index, { + id, + name, + started: true, + }) + yield { + type: EventType.TOOL_CALL_START, + toolCallId: id, + toolCallName: name, + toolName: name, + timestamp: Date.now(), + index, + } + } + continue + } + + if ('contentBlockDelta' in ev) { + const block = ev.contentBlockDelta + const delta = block?.delta + const index = block?.contentBlockIndex ?? 0 + + // Tool-call argument fragments (partial JSON). + if (delta && 'toolUse' in delta && delta.toolUse?.input !== undefined) { + const toolCall = toolCallsByIndex.get(index) + if (toolCall?.started) { + yield { + type: EventType.TOOL_CALL_ARGS, + toolCallId: toolCall.id, + timestamp: Date.now(), + delta: delta.toolUse.input, + } + } + continue + } + + // Reasoning content. + if ( + delta && + 'reasoningContent' in delta && + delta.reasoningContent && + 'text' in delta.reasoningContent && + delta.reasoningContent.text !== undefined + ) { + if (!reasoningMessageId) { + reasoningMessageId = newMessageId() + yield { + type: EventType.REASONING_MESSAGE_START, + messageId: reasoningMessageId, + role: 'reasoning', + timestamp: Date.now(), + } + } + yield { + type: EventType.REASONING_MESSAGE_CONTENT, + messageId: reasoningMessageId, + delta: delta.reasoningContent.text, + timestamp: Date.now(), + } + continue + } + + // Text content. + if (delta && 'text' in delta && delta.text !== undefined) { + yield* closeReasoning() + if (!hasEmittedTextMessageStart) { + hasEmittedTextMessageStart = true + yield { + type: EventType.TEXT_MESSAGE_START, + messageId, + role: 'assistant', + timestamp: Date.now(), + } + } + accumulatedContent += delta.text + yield { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId, + delta: delta.text, + content: accumulatedContent, + timestamp: Date.now(), + } + } + continue + } + + if ('contentBlockStop' in ev) { + const stopIndex = ev.contentBlockStop?.contentBlockIndex ?? 0 + const toolCall = toolCallsByIndex.get(stopIndex) + if (toolCall?.started) { + yield { + type: EventType.TOOL_CALL_END, + toolCallId: toolCall.id, + toolCallName: toolCall.name, + toolName: toolCall.name, + timestamp: Date.now(), + } + toolCallsByIndex.delete(stopIndex) + } + continue + } + + if ('messageStop' in ev) { + const stopReason = ev.messageStop?.stopReason + // Map Converse stopReason to AG-UI's narrower finishReason vocabulary. + finishReason = + stopReason === 'tool_use' + ? 'tool_calls' + : stopReason === 'max_tokens' + ? 'length' + : stopReason === 'content_filtered' + ? 'content_filter' + : 'stop' + continue + } + + if ('metadata' in ev) { + const u = ev.metadata?.usage + if (u) { + usage = { + promptTokens: u.inputTokens ?? 0, + completionTokens: u.outputTokens ?? 0, + totalTokens: u.totalTokens ?? 0, + } + } + continue + } + } + + // Stream ended (possibly without any content) — still emit RUN_STARTED so + // consumers always see a run lifecycle. + yield* ensureRunStarted() + + // Drain any tool call that opened but never received contentBlockStop. + for (const [index, toolCall] of toolCallsByIndex) { + if (!toolCall.started) continue + yield { + type: EventType.TOOL_CALL_END, + toolCallId: toolCall.id, + toolCallName: toolCall.name, + toolName: toolCall.name, + timestamp: Date.now(), + } + toolCallsByIndex.delete(index) + } + + // Close the text message lifecycle if it was opened. + if (hasEmittedTextMessageStart) { + yield { + type: EventType.TEXT_MESSAGE_END, + messageId, + timestamp: Date.now(), + } + } + + // Close any reasoning lifecycle that text never closed. + yield* closeReasoning() + + // Single terminal RUN_FINISHED. Conditional `usage` spread keeps the wire + // shape spec-compliant (AG-UI's `usage` is optional with no `| undefined`). + yield { + type: EventType.RUN_FINISHED, + runId, + threadId, + timestamp: Date.now(), + finishReason: finishReason ?? 'stop', + ...(usage && { usage }), + } +} diff --git a/packages/ai-bedrock/tests/converse/stream-processor.test.ts b/packages/ai-bedrock/tests/converse/stream-processor.test.ts new file mode 100644 index 000000000..5c310ce5c --- /dev/null +++ b/packages/ai-bedrock/tests/converse/stream-processor.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from 'vitest' +import { EventType } from '@tanstack/ai' +import { processConverseStream } from '../../src/converse/stream-processor' +import type { ConverseStreamOutput } from '@aws-sdk/client-bedrock-runtime' + +// Test fixtures use the minimal field subset the processor reads. Cast at the +// generator boundary to the SDK union type — the SDK marks every field as +// `T | undefined` and requires sibling fields (e.g. `metrics` on metadata) the +// processor never touches, so supplying full Smithy shapes would only add noise. +type ConverseStreamFixture = { + [K in keyof ConverseStreamOutput]?: unknown +} + +async function* gen(...e: Array) { + for (const x of e) yield x as ConverseStreamOutput +} + +describe('processConverseStream', () => { + it('emits the text lifecycle and finishes', async () => { + const types: Array = [] + for await (const c of processConverseStream( + gen( + { messageStart: { role: 'assistant' } }, + { contentBlockDelta: { delta: { text: 'Hel' }, contentBlockIndex: 0 } }, + { contentBlockDelta: { delta: { text: 'lo' }, contentBlockIndex: 0 } }, + { contentBlockStop: { contentBlockIndex: 0 } }, + { messageStop: { stopReason: 'end_turn' } }, + { + metadata: { + usage: { inputTokens: 3, outputTokens: 2, totalTokens: 5 }, + }, + }, + ), + () => 'msg-1', + )) { + types.push(c.type) + } + expect(types).toContain(EventType.RUN_STARTED) + expect(types).toContain(EventType.TEXT_MESSAGE_START) + expect(types).toContain(EventType.TEXT_MESSAGE_CONTENT) + expect(types).toContain(EventType.TEXT_MESSAGE_END) + expect(types).toContain(EventType.RUN_FINISHED) + }) + + it('accumulates text content across deltas', async () => { + const contents: Array = [] + for await (const c of processConverseStream( + gen( + { messageStart: { role: 'assistant' } }, + { contentBlockDelta: { delta: { text: 'Hel' }, contentBlockIndex: 0 } }, + { contentBlockDelta: { delta: { text: 'lo' }, contentBlockIndex: 0 } }, + { messageStop: { stopReason: 'end_turn' } }, + ), + () => 'msg-1', + )) { + if (c.type === EventType.TEXT_MESSAGE_CONTENT) + contents.push((c as { delta: string }).delta) + } + expect(contents).toEqual(['Hel', 'lo']) + }) + + it('emits TOOL_CALL_* for a toolUse block with streamed args', async () => { + const types: Array = [] + const argDeltas: Array = [] + for await (const c of processConverseStream( + gen( + { messageStart: { role: 'assistant' } }, + { + contentBlockStart: { + start: { toolUse: { toolUseId: 't1', name: 'getX' } }, + contentBlockIndex: 0, + }, + }, + { + contentBlockDelta: { + delta: { toolUse: { input: '{"a":' } }, + contentBlockIndex: 0, + }, + }, + { + contentBlockDelta: { + delta: { toolUse: { input: '1}' } }, + contentBlockIndex: 0, + }, + }, + { contentBlockStop: { contentBlockIndex: 0 } }, + { messageStop: { stopReason: 'tool_use' } }, + ), + () => 'msg-2', + )) { + types.push(c.type) + if (c.type === EventType.TOOL_CALL_ARGS) + argDeltas.push((c as { delta: string }).delta) + } + expect(types).toContain(EventType.TOOL_CALL_START) + expect(types).toContain(EventType.TOOL_CALL_ARGS) + expect(types).toContain(EventType.TOOL_CALL_END) + expect(argDeltas.join('')).toBe('{"a":1}') + }) + + it('emits reasoning content', async () => { + const types: Array = [] + for await (const c of processConverseStream( + gen( + { messageStart: { role: 'assistant' } }, + { + contentBlockDelta: { + delta: { reasoningContent: { text: 'thinking' } }, + contentBlockIndex: 0, + }, + }, + { messageStop: { stopReason: 'end_turn' } }, + ), + () => 'msg-3', + )) { + types.push(c.type) + } + expect(types).toContain(EventType.REASONING_MESSAGE_CONTENT) + }) +}) From 878b6155d8ba1fd483cf87ee03ad7f8390251269 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 16:08:29 +0200 Subject: [PATCH 34/46] feat(ai-bedrock): Converse structured output via forced single-tool --- .../src/converse/structured-output.ts | 24 +++++++++++++++++++ .../tests/converse/structured-output.test.ts | 20 ++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 packages/ai-bedrock/src/converse/structured-output.ts create mode 100644 packages/ai-bedrock/tests/converse/structured-output.test.ts diff --git a/packages/ai-bedrock/src/converse/structured-output.ts b/packages/ai-bedrock/src/converse/structured-output.ts new file mode 100644 index 000000000..c818b22aa --- /dev/null +++ b/packages/ai-bedrock/src/converse/structured-output.ts @@ -0,0 +1,24 @@ +import type { ToolConfiguration } from '@aws-sdk/client-bedrock-runtime' +import type { DocumentType } from '@smithy/types' + +export const STRUCTURED_TOOL_NAME = 'structured_output' + +/** + * Converse has no native json_schema response_format. Structured output is + * achieved by forcing a single tool whose input schema is the requested output + * schema; the model's tool-use `input` is the structured result. + */ +export function buildStructuredToolConfig(schema: unknown): ToolConfiguration { + return { + tools: [ + { + toolSpec: { + name: STRUCTURED_TOOL_NAME, + description: 'Return the final answer as structured JSON.', + inputSchema: { json: schema as DocumentType }, + }, + }, + ], + toolChoice: { tool: { name: STRUCTURED_TOOL_NAME } }, + } +} diff --git a/packages/ai-bedrock/tests/converse/structured-output.test.ts b/packages/ai-bedrock/tests/converse/structured-output.test.ts new file mode 100644 index 000000000..673d92afc --- /dev/null +++ b/packages/ai-bedrock/tests/converse/structured-output.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest' +import { + STRUCTURED_TOOL_NAME, + buildStructuredToolConfig, +} from '../../src/converse/structured-output' + +describe('buildStructuredToolConfig', () => { + it('wraps the output schema as a single forced tool', () => { + const schema = { type: 'object', properties: { n: { type: 'number' } } } + const cfg = buildStructuredToolConfig(schema) + expect(cfg.tools?.[0]).toEqual({ + toolSpec: { + name: STRUCTURED_TOOL_NAME, + description: 'Return the final answer as structured JSON.', + inputSchema: { json: schema }, + }, + }) + expect(cfg.toolChoice).toEqual({ tool: { name: STRUCTURED_TOOL_NAME } }) + }) +}) From b1a3b4b28820d35540adb026dd39104425730b5d Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 16:19:10 +0200 Subject: [PATCH 35/46] feat(ai-bedrock): BedrockConverseTextAdapter (streaming, tools, structured output) --- .../ai-bedrock/src/adapters/converse-text.ts | 521 ++++++++++++++++++ .../ai-bedrock/tests/converse/adapter.test.ts | 172 ++++++ 2 files changed, 693 insertions(+) create mode 100644 packages/ai-bedrock/src/adapters/converse-text.ts create mode 100644 packages/ai-bedrock/tests/converse/adapter.test.ts diff --git a/packages/ai-bedrock/src/adapters/converse-text.ts b/packages/ai-bedrock/src/adapters/converse-text.ts new file mode 100644 index 000000000..aac61c6ea --- /dev/null +++ b/packages/ai-bedrock/src/adapters/converse-text.ts @@ -0,0 +1,521 @@ +import { + BedrockRuntimeClient, + ConverseCommand, + ConverseStreamCommand, +} from '@aws-sdk/client-bedrock-runtime' +import { EventType, convertSchemaToJsonSchema } from '@tanstack/ai' +import { BaseTextAdapter } from '@tanstack/ai/adapters' +import { toRunErrorPayload } from '@tanstack/ai/adapter-internals' +import { resolveBedrockAuth } from '../utils/auth' +import { toConverseMessages } from '../converse/message-converter' +import { toToolConfig } from '../converse/tool-converter' +import { processConverseStream } from '../converse/stream-processor' +import { + STRUCTURED_TOOL_NAME, + buildStructuredToolConfig, +} from '../converse/structured-output' +import type { ConverseToolInput } from '../converse/tool-converter' +import type { + ContentBlock, + ConverseCommandInput, + ConverseCommandOutput, + ConverseStreamCommandInput, + ConverseStreamOutput, +} from '@aws-sdk/client-bedrock-runtime' +import type { + JSONSchema, + Modality, + StreamChunk, + TextOptions, + Tool, +} from '@tanstack/ai' +import type { + StructuredOutputOptions, + StructuredOutputResult, +} from '@tanstack/ai/adapters' +import type { BedrockClientConfig } from '../utils/client' +import type { BedrockMessageMetadataByModality } from '../message-types' +import type { + BedrockConverseModels, + ResolveInputModalities, + ResolveProviderOptions, +} from '../model-meta' + +/** Config for the Converse adapter — same client config as the chat adapter. */ +export interface BedrockConverseConfig extends BedrockClientConfig {} + +/** + * Bedrock Converse text adapter. Wires the Converse translation modules (message + * converter, tool converter, stream processor, structured-output forced-tool + * builder) onto `@tanstack/ai`'s `BaseTextAdapter` and the + * `@aws-sdk/client-bedrock-runtime` `BedrockRuntimeClient`. + * + * The success-path AG-UI lifecycle (`RUN_STARTED`..`RUN_FINISHED`) is owned by + * `processConverseStream` (per C3); this adapter only owns the catch/`RUN_ERROR` + * path, mirroring openai-base's `chatStream`. + * + * The actual SDK calls live behind two protected seams (`sendStream` / `send`) + * so tests can subclass and inject canned Converse SDK shapes without a real + * AWS request. + */ +export class BedrockConverseTextAdapter< + TModel extends BedrockConverseModels, + // Constraint mirrors the chat adapter (text.ts): the base parameterises + // `TProviderOptions extends Record`, but our default + // `ResolveProviderOptions` resolves to an interface lacking an + // implicit index signature. `Record` is the only constraint + // that accepts that interface AND satisfies the base. Confined to the + // generic constraint — no value `as` cast is introduced. + TProviderOptions extends Record = ResolveProviderOptions, + TInputModalities extends ReadonlyArray = + ResolveInputModalities, +> extends BaseTextAdapter< + TModel, + TProviderOptions, + TInputModalities, + BedrockMessageMetadataByModality +> { + override readonly kind = 'text' as const + override readonly name = 'bedrock-converse' as const + protected client: BedrockRuntimeClient + + constructor(config: BedrockConverseConfig, model: TModel) { + super({}, model) + const region = config.region ?? 'us-east-1' + const resolved = resolveBedrockAuth( + { apiKey: config.apiKey, region, auth: config.auth }, + 'runtime', + ) + // The installed @aws-sdk/client-bedrock-runtime (v3.1057) exposes a + // first-class `token: TokenIdentity | TokenIdentityProvider` config field + // (HttpAuthSchemeInputConfig) for Bedrock API-key bearer auth — no custom + // requestHandler/middleware needed. SigV4 uses the credential provider. + if (resolved.kind === 'bearer') { + this.client = new BedrockRuntimeClient({ + region, + token: { token: resolved.token }, + ...(config.baseURL ? { endpoint: config.baseURL } : {}), + }) + } else { + this.client = new BedrockRuntimeClient({ + region: resolved.region, + credentials: resolved.credentials, + ...(config.baseURL ? { endpoint: config.baseURL } : {}), + }) + } + } + + // --------------------------------------------------------------------------- + // SDK seams (overridden in tests so no real AWS call happens) + // --------------------------------------------------------------------------- + + protected async sendStream( + input: ConverseStreamCommandInput, + ): Promise> { + const res = await this.client.send(new ConverseStreamCommand(input)) + if (!res.stream) { + throw new Error('Bedrock Converse: empty stream response') + } + return res.stream + } + + protected async send( + input: ConverseCommandInput, + ): Promise { + return this.client.send(new ConverseCommand(input)) + } + + // --------------------------------------------------------------------------- + // Public adapter surface + // --------------------------------------------------------------------------- + + async *chatStream( + options: TextOptions, + ): AsyncIterable { + try { + options.logger.request( + `activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`, + { provider: this.name, model: this.model }, + ) + const input = this.buildInput(options) + const stream = await this.sendStream(input) + yield* processConverseStream(stream, () => this.generateId()) + } catch (error: unknown) { + const errorPayload = toRunErrorPayload( + error, + `${this.name}.chatStream failed`, + ) + options.logger.errors(`${this.name}.chatStream fatal`, { + error: errorPayload, + source: `${this.name}.chatStream`, + }) + // Conditional `code` spread keeps the wire shape spec-compliant under + // `exactOptionalPropertyTypes` (AG-UI's `RunErrorEvent.code` is optional). + yield { + type: EventType.RUN_ERROR, + model: options.model, + timestamp: Date.now(), + message: errorPayload.message, + ...(errorPayload.code !== undefined && { code: errorPayload.code }), + error: { + message: errorPayload.message, + ...(errorPayload.code !== undefined && { code: errorPayload.code }), + }, + } + } + } + + /** + * Structured output via the forced-tool strategy. Converse has no native + * json_schema response_format, so we force a single tool whose input schema + * is the requested output schema and read the model's `toolUse.input` back as + * the structured result. + */ + async structuredOutput( + options: StructuredOutputOptions, + ): Promise> { + const { chatOptions, outputSchema } = options + try { + chatOptions.logger.request( + `activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`, + { provider: this.name, model: this.model }, + ) + const input: ConverseCommandInput = { + ...this.buildInput(chatOptions), + toolConfig: buildStructuredToolConfig(outputSchema), + } + const res = await this.send(input) + const structured = extractStructuredToolInput(res) + if (structured === undefined) { + throw new Error( + `${this.name}.structuredOutput: response contained no forced-tool output`, + ) + } + return { + data: structured, + rawText: JSON.stringify(structured), + } + } catch (error: unknown) { + chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, { + error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`), + source: `${this.name}.structuredOutput`, + }) + throw error + } + } + + /** + * Streaming structured output. Same forced-tool strategy as + * `structuredOutput`, but streamed: the forced tool's `toolUse.input` JSON + * fragments are accumulated from the Converse stream and a terminal + * `CUSTOM 'structured-output.complete'` event carries `{ object, raw }`, + * mirroring openai-base's `structuredOutputStream` contract exactly. + */ + async *structuredOutputStream( + options: StructuredOutputOptions, + ): AsyncIterable { + const { chatOptions, outputSchema } = options + const timestamp = Date.now() + const runId = this.generateId() + const threadId = chatOptions.threadId ?? this.generateId() + const messageId = this.generateId() + + let hasEmittedRunStarted = false + let hasEmittedTextMessageStart = false + let accumulatedRaw = '' + let finishReason: 'stop' | 'tool_calls' | 'length' | 'content_filter' = + 'stop' + + try { + chatOptions.logger.request( + `activity=structuredOutputStream provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`, + { provider: this.name, model: this.model }, + ) + const input: ConverseStreamCommandInput = { + ...this.buildInput(chatOptions), + toolConfig: buildStructuredToolConfig(outputSchema), + } + const stream = await this.sendStream(input) + + // The forced tool streams its `input` as partial-JSON fragments inside + // `contentBlockDelta.delta.toolUse.input`. We surface them as + // TEXT_MESSAGE_CONTENT deltas (raw JSON text), matching openai-base which + // carries the structured JSON as text deltas. + for await (const ev of stream) { + if (!hasEmittedRunStarted) { + hasEmittedRunStarted = true + yield { + type: EventType.RUN_STARTED, + runId, + threadId, + model: chatOptions.model, + timestamp, + parentRunId: chatOptions.parentRunId, + } + } + + if ('contentBlockDelta' in ev) { + const delta = ev.contentBlockDelta?.delta + const fragment = + delta && 'toolUse' in delta ? delta.toolUse?.input : undefined + if (fragment !== undefined) { + if (!hasEmittedTextMessageStart) { + hasEmittedTextMessageStart = true + yield { + type: EventType.TEXT_MESSAGE_START, + messageId, + role: 'assistant', + model: chatOptions.model, + timestamp, + } + } + accumulatedRaw += fragment + yield { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId, + delta: fragment, + content: accumulatedRaw, + model: chatOptions.model, + timestamp, + } + } + continue + } + + if ('messageStop' in ev) { + const stopReason = ev.messageStop?.stopReason + finishReason = + stopReason === 'max_tokens' + ? 'length' + : stopReason === 'content_filtered' + ? 'content_filter' + : 'tool_calls' + continue + } + } + + if (!hasEmittedRunStarted) { + hasEmittedRunStarted = true + yield { + type: EventType.RUN_STARTED, + runId, + threadId, + model: chatOptions.model, + timestamp, + parentRunId: chatOptions.parentRunId, + } + } + + if (hasEmittedTextMessageStart) { + yield { + type: EventType.TEXT_MESSAGE_END, + messageId, + model: chatOptions.model, + timestamp, + } + } + + if (accumulatedRaw.length === 0) { + yield { + type: EventType.RUN_ERROR, + runId, + model: chatOptions.model, + timestamp, + message: `${this.name}.structuredOutputStream: response contained no content`, + code: 'empty-response', + error: { + message: `${this.name}.structuredOutputStream: response contained no content`, + code: 'empty-response', + }, + } + return + } + + let parsed: unknown + try { + parsed = JSON.parse(accumulatedRaw) + } catch { + yield { + type: EventType.RUN_ERROR, + runId, + model: chatOptions.model, + timestamp, + message: `Failed to parse structured output as JSON. Content: ${accumulatedRaw.slice(0, 200)}${accumulatedRaw.length > 200 ? '...' : ''}`, + code: 'parse-error', + error: { + message: 'Failed to parse structured output as JSON', + code: 'parse-error', + }, + } + return + } + + yield { + type: EventType.CUSTOM, + name: 'structured-output.complete', + value: { + object: parsed, + raw: accumulatedRaw, + }, + model: chatOptions.model, + timestamp, + } + + yield { + type: EventType.RUN_FINISHED, + runId, + threadId, + model: chatOptions.model, + timestamp, + finishReason, + } + } catch (error: unknown) { + if (!hasEmittedRunStarted) { + hasEmittedRunStarted = true + yield { + type: EventType.RUN_STARTED, + runId, + threadId, + model: chatOptions.model, + timestamp, + parentRunId: chatOptions.parentRunId, + } + } + const errorPayload = toRunErrorPayload( + error, + `${this.name}.structuredOutputStream failed`, + ) + chatOptions.logger.errors(`${this.name}.structuredOutputStream fatal`, { + error: errorPayload, + source: `${this.name}.structuredOutputStream`, + }) + yield { + type: EventType.RUN_ERROR, + runId, + model: chatOptions.model, + timestamp, + message: errorPayload.message, + ...(errorPayload.code !== undefined && { code: errorPayload.code }), + error: { + message: errorPayload.message, + ...(errorPayload.code !== undefined && { code: errorPayload.code }), + }, + } + } + } + + /** + * Converse sends `tools` and a forced structured-output tool via two separate + * mechanisms, never together. Declaring `false` makes the engine run the + * agent loop without `outputSchema` and finalize via `structuredOutput` / + * `structuredOutputStream`. + */ + supportsCombinedToolsAndSchema(): boolean { + return false + } + + // --------------------------------------------------------------------------- + // Request construction + // --------------------------------------------------------------------------- + + /** + * Translate `TextOptions` into a `ConverseCommandInput`. Shared by chatStream, + * structuredOutput, and structuredOutputStream (the latter two override + * `toolConfig` with the forced structured tool afterwards). + */ + protected buildInput( + options: TextOptions, + ): ConverseCommandInput { + const { system, messages } = toConverseMessages( + options.messages, + options.systemPrompts, + ) + + const toolConfig = options.tools + ? toToolConfig(convertTools(options.tools), 'auto') + : undefined + + const inferenceConfig = + options.temperature !== undefined || + options.topP !== undefined || + options.maxTokens !== undefined + ? { + ...(options.temperature !== undefined && { + temperature: options.temperature, + }), + ...(options.topP !== undefined && { topP: options.topP }), + ...(options.maxTokens !== undefined && { + maxTokens: options.maxTokens, + }), + } + : undefined + + return { + modelId: this.model, + messages, + ...(system.length > 0 && { system }), + ...(toolConfig && { toolConfig }), + ...(inferenceConfig && { inferenceConfig }), + } + } +} + +/** + * Convert TanStack `Tool[]` to the Converse tool-converter input shape. Reuses + * the SAME `convertSchemaToJsonSchema` the other adapters use so the Converse + * tool input schemas match what every other provider sends. + */ +function convertTools(tools: Array): Array { + return tools.map((tool) => { + const inputSchema: JSONSchema = convertSchemaToJsonSchema( + tool.inputSchema, + ) ?? { type: 'object', properties: {}, required: [] } + return { + name: tool.name, + description: tool.description, + inputSchema, + } + }) +} + +/** + * Find the forced structured-output tool's `input` in a non-streaming Converse + * response. SDK-boundary narrowing only — `ConverseOutput` is a tagged union + * (`{ message }`) and a tool-use block is `{ toolUse: { input } }`. + */ +function extractStructuredToolInput( + res: ConverseCommandOutput, +): unknown | undefined { + const message = + res.output && 'message' in res.output ? res.output.message : undefined + const content: Array = message?.content ?? [] + for (const block of content) { + if ('toolUse' in block && block.toolUse) { + // Prefer the forced tool by name, but fall back to any toolUse block so a + // provider that omits/renames the tool name still yields its structured + // input rather than failing loud on a name mismatch. + if ( + block.toolUse.name === STRUCTURED_TOOL_NAME || + block.toolUse.name === undefined + ) { + return block.toolUse.input + } + } + } + // Second pass: accept the first toolUse block regardless of name. + for (const block of content) { + if ('toolUse' in block && block.toolUse) { + return block.toolUse.input + } + } + return undefined +} + +/** Converse adapter with an explicit API key (low-level; mirrors createBedrockChat). */ +export function createBedrockConverse( + model: TModel, + apiKey: string, + config?: Omit, +): BedrockConverseTextAdapter { + return new BedrockConverseTextAdapter({ ...config, apiKey }, model) +} diff --git a/packages/ai-bedrock/tests/converse/adapter.test.ts b/packages/ai-bedrock/tests/converse/adapter.test.ts new file mode 100644 index 000000000..535df2548 --- /dev/null +++ b/packages/ai-bedrock/tests/converse/adapter.test.ts @@ -0,0 +1,172 @@ +import { describe, expect, it } from 'vitest' +import { EventType } from '@tanstack/ai' +import { resolveDebugOption } from '@tanstack/ai/adapter-internals' +import { BedrockConverseTextAdapter } from '../../src/adapters/converse-text' +import type { + ConverseCommandOutput, + ConverseStreamOutput, +} from '@aws-sdk/client-bedrock-runtime' +import type { StreamChunk, TextOptions } from '@tanstack/ai' + +/** + * Subclass that overrides the protected SDK seams so no real AWS call happens. + * The adapter's translation logic (buildInput, lifecycle wiring) is exercised + * end-to-end against canned Converse SDK shapes. + */ +class StubAdapter extends BedrockConverseTextAdapter< + 'us.amazon.nova-pro-v1:0' +> { + streamEvents: Array = [] + nonStreamOutput: ConverseCommandOutput = {} as unknown as ConverseCommandOutput + + protected override async sendStream(): Promise< + AsyncIterable + > { + const evs = this.streamEvents + return (async function* () { + for (const e of evs) yield e + })() + } + + protected override async send(): Promise { + return this.nonStreamOutput + } +} + +const testLogger = resolveDebugOption(false) + +/** Minimal TextOptions for the stub. */ +function textOptions(overrides: Partial = {}): TextOptions { + return { + model: 'us.amazon.nova-pro-v1:0', + messages: [{ role: 'user', content: 'hi' }], + logger: testLogger, + ...overrides, + } +} + +describe('BedrockConverseTextAdapter', () => { + it('exposes name "bedrock-converse" and kind "text"', () => { + const a = new BedrockConverseTextAdapter( + { apiKey: 'k' }, + 'us.amazon.nova-pro-v1:0', + ) + expect(a.name).toBe('bedrock-converse') + expect(a.kind).toBe('text') + }) + + it('streams text through chatStream', async () => { + const a = new StubAdapter({ apiKey: 'k' }, 'us.amazon.nova-pro-v1:0') + a.streamEvents = [ + { messageStart: { role: 'assistant' } }, + { contentBlockDelta: { delta: { text: 'hi' }, contentBlockIndex: 0 } }, + { messageStop: { stopReason: 'end_turn' } }, + // SDK boundary: the metadata event requires `metrics` too — narrow the + // canned shape through `unknown` rather than spell out every field. + { + metadata: { + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + }, + } as unknown as ConverseStreamOutput, + ] + const types: Array = [] + for await (const c of a.chatStream(textOptions())) { + types.push(c.type) + } + expect(types).toContain(EventType.TEXT_MESSAGE_CONTENT) + expect(types).toContain(EventType.RUN_FINISHED) + }) + + it('emits RUN_ERROR when the stream seam throws', async () => { + class ThrowingAdapter extends BedrockConverseTextAdapter< + 'us.amazon.nova-pro-v1:0' + > { + protected override async sendStream(): Promise< + AsyncIterable + > { + throw new Error('boom') + } + protected override async send(): Promise { + return {} as unknown as ConverseCommandOutput + } + } + const a = new ThrowingAdapter({ apiKey: 'k' }, 'us.amazon.nova-pro-v1:0') + const types: Array = [] + for await (const c of a.chatStream(textOptions())) { + types.push(c.type) + } + expect(types).toContain(EventType.RUN_ERROR) + }) + + it('returns parsed object from structuredOutput (forced tool)', async () => { + const a = new StubAdapter({ apiKey: 'k' }, 'us.amazon.nova-pro-v1:0') + a.nonStreamOutput = { + output: { + message: { + role: 'assistant', + content: [ + { + toolUse: { + toolUseId: 's', + name: 'structured_output', + input: { n: 5 }, + }, + }, + ], + }, + }, + // SDK boundary: a real ConverseCommandOutput also carries stopReason / + // usage / metrics / $metadata — narrow through `unknown` for the fixture. + } as unknown as ConverseCommandOutput + const res = await a.structuredOutput({ + chatOptions: textOptions({ messages: [{ role: 'user', content: 'go' }] }), + outputSchema: { type: 'object', properties: { n: { type: 'number' } } }, + }) + expect(res.data).toEqual({ n: 5 }) + expect(JSON.parse(res.rawText)).toEqual({ n: 5 }) + }) + + it('streams structured output through structuredOutputStream', async () => { + const a = new StubAdapter({ apiKey: 'k' }, 'us.amazon.nova-pro-v1:0') + a.streamEvents = [ + { messageStart: { role: 'assistant' } }, + { + contentBlockStart: { + start: { toolUse: { toolUseId: 's', name: 'structured_output' } }, + contentBlockIndex: 0, + }, + }, + { + contentBlockDelta: { + delta: { toolUse: { input: '{"n":5}' } }, + contentBlockIndex: 0, + }, + }, + { contentBlockStop: { contentBlockIndex: 0 } }, + { messageStop: { stopReason: 'tool_use' } }, + ] + const events: Array = [] + for await (const c of a.structuredOutputStream({ + chatOptions: textOptions({ messages: [{ role: 'user', content: 'go' }] }), + outputSchema: { type: 'object', properties: { n: { type: 'number' } } }, + })) { + events.push(c) + } + const complete = events.find( + (e): e is Extract => + e.type === EventType.CUSTOM && + 'name' in e && + e.name === 'structured-output.complete', + ) + expect(complete).toBeDefined() + expect((complete?.value as { object: unknown }).object).toEqual({ n: 5 }) + }) + + it('declares it does not support combined tools and schema', () => { + const a = new BedrockConverseTextAdapter( + { apiKey: 'k' }, + 'us.amazon.nova-pro-v1:0', + ) + expect(a.supportsCombinedToolsAndSchema()).toBe(false) + }) +}) From 4b628f8d241c245079495aaa7fda976a40e3cac6 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 16:23:57 +0200 Subject: [PATCH 36/46] feat(ai-bedrock): converse is the default bedrockText path; chat/responses opt-in --- packages/ai-bedrock/src/index.ts | 74 ++++++++++++++++------- packages/ai-bedrock/tests/adapter.test.ts | 30 ++++++--- packages/ai-bedrock/tests/factory.test.ts | 25 ++++++++ 3 files changed, 101 insertions(+), 28 deletions(-) create mode 100644 packages/ai-bedrock/tests/factory.test.ts diff --git a/packages/ai-bedrock/src/index.ts b/packages/ai-bedrock/src/index.ts index 8d95365cc..03807d183 100644 --- a/packages/ai-bedrock/src/index.ts +++ b/packages/ai-bedrock/src/index.ts @@ -1,29 +1,40 @@ /** * @module @tanstack/ai-bedrock * - * Amazon Bedrock adapter for TanStack AI via Bedrock's OpenAI-compatible APIs. - * The public `bedrockText` / `createBedrockText` factory branches between the - * Chat Completions adapter (default) and the Responses adapter via `api`. + * Amazon Bedrock adapter for TanStack AI via Bedrock's OpenAI-compatible APIs + * and the native Converse API. The public `bedrockText` / `createBedrockText` + * factory branches between the Converse adapter (DEFAULT), the Chat Completions + * adapter (`api: 'chat'`), and the Responses adapter (`api: 'responses'`). */ import { BedrockTextAdapter } from './adapters/text' import { BedrockResponsesTextAdapter } from './adapters/responses-text' +import { BedrockConverseTextAdapter } from './adapters/converse-text' import { BEDROCK_RESPONSES_MODELS } from './model-meta' import type { BedrockTextConfig } from './adapters/text' import type { BedrockResponsesConfig } from './adapters/responses-text' +import type { BedrockConverseConfig } from './adapters/converse-text' import type { BedrockClientConfig } from './utils' -import type { BedrockChatModels, BedrockResponsesModels } from './model-meta' +import type { + BedrockChatModels, + BedrockResponsesModels, + BedrockConverseModels, +} from './model-meta' -/** Config for the branching factory's chat mode (api omitted or 'chat'). */ -export type BedrockChatApiConfig = Omit & { - api?: 'chat' +/** Config for the branching factory's converse mode (default, or api: 'converse'). */ +export type BedrockConverseApiConfig = BedrockConverseConfig & { + api?: 'converse' +} +/** Config for the branching factory's chat mode (api: 'chat' required). */ +export type BedrockChatApiConfig = BedrockTextConfig & { + api: 'chat' } /** Config for the branching factory's responses mode (api: 'responses' required). */ -export type BedrockResponsesApiConfig = Omit< - BedrockResponsesConfig, - 'apiKey' -> & { api: 'responses' } +export type BedrockResponsesApiConfig = BedrockResponsesConfig & { + api: 'responses' +} type AnyBedrockAdapter = + | BedrockConverseTextAdapter | BedrockTextAdapter | BedrockResponsesTextAdapter @@ -44,10 +55,12 @@ function stripApi(config: T): Omit { * classes directly so their constructors run the full auth cascade lazily * (config.apiKey → BEDROCK_API_KEY → AWS_BEARER_TOKEN_BEDROCK → SigV4). No * eager env-key fetch here, so `auth: 'sigv4'` never throws for a missing key. + * + * Default path → Converse adapter; opt-in via `api: 'chat'` or `api: 'responses'`. */ function build( - model: BedrockChatModels, - config?: BedrockClientConfig & { api?: 'chat' | 'responses' }, + model: BedrockConverseModels, + config?: BedrockClientConfig & { api?: 'converse' | 'chat' | 'responses' }, ): AnyBedrockAdapter { if (config?.api === 'responses') { const rest = stripApi(config) @@ -59,15 +72,23 @@ function build( } return new BedrockResponsesTextAdapter(rest, model) } - const rest = config ? stripApi(config) : {} - return new BedrockTextAdapter(rest, model) + if (config?.api === 'chat') { + return new BedrockTextAdapter(stripApi(config), model as BedrockChatModels) + } + // Default + explicit 'converse' + return new BedrockConverseTextAdapter(config ? stripApi(config) : {}, model) } // --- createBedrockText: explicit key, overloaded on `api` --- +export function createBedrockText( + model: TModel, + apiKey: string, + config?: BedrockConverseApiConfig, +): BedrockConverseTextAdapter export function createBedrockText( model: TModel, apiKey: string, - config?: BedrockChatApiConfig, + config: BedrockChatApiConfig, ): BedrockTextAdapter export function createBedrockText( model: TModel, @@ -75,26 +96,30 @@ export function createBedrockText( config: BedrockResponsesApiConfig, ): BedrockResponsesTextAdapter export function createBedrockText( - model: BedrockChatModels, + model: BedrockConverseModels, apiKey: string, - config?: BedrockChatApiConfig | BedrockResponsesApiConfig, + config?: BedrockConverseApiConfig | BedrockChatApiConfig | BedrockResponsesApiConfig, ): AnyBedrockAdapter { // Explicit apiKey is authoritative — spread config first so it can't override. return build(model, { ...config, apiKey }) } // --- bedrockText: env-key counterpart, same overloads --- +export function bedrockText( + model: TModel, + config?: BedrockConverseApiConfig, +): BedrockConverseTextAdapter export function bedrockText( model: TModel, - config?: BedrockChatApiConfig, + config: BedrockChatApiConfig, ): BedrockTextAdapter export function bedrockText( model: TModel, config: BedrockResponsesApiConfig, ): BedrockResponsesTextAdapter export function bedrockText( - model: BedrockChatModels, - config?: BedrockChatApiConfig | BedrockResponsesApiConfig, + model: BedrockConverseModels, + config?: BedrockConverseApiConfig | BedrockChatApiConfig | BedrockResponsesApiConfig, ): AnyBedrockAdapter { // No eager env-key fetch: the adapter constructor resolves auth lazily so // SigV4 (and the env-key fallback) work without a forced API key here. @@ -114,6 +139,11 @@ export { type BedrockResponsesConfig, type BedrockResponsesProviderOptions, } from './adapters/responses-text' +export { + BedrockConverseTextAdapter, + createBedrockConverse, + type BedrockConverseConfig, +} from './adapters/converse-text' export { resolveBedrockAuth, withBedrockDefaults, @@ -124,8 +154,10 @@ export { export { BEDROCK_CHAT_MODELS, BEDROCK_RESPONSES_MODELS, + BEDROCK_CONVERSE_MODELS, type BedrockChatModels, type BedrockResponsesModels, + type BedrockConverseModels, type BedrockChatModelProviderOptionsByName, type BedrockChatModelToolCapabilitiesByName, type BedrockModelInputModalitiesByName, diff --git a/packages/ai-bedrock/tests/adapter.test.ts b/packages/ai-bedrock/tests/adapter.test.ts index 68af2216c..a9bc3a7aa 100644 --- a/packages/ai-bedrock/tests/adapter.test.ts +++ b/packages/ai-bedrock/tests/adapter.test.ts @@ -7,6 +7,7 @@ import { import { bedrockText, createBedrockText } from '../src/index' import { BedrockTextAdapter as ChatAdapter } from '../src/adapters/text' import { BedrockResponsesTextAdapter as RespAdapter } from '../src/adapters/responses-text' +import { BedrockConverseTextAdapter as ConverseAdapter } from '../src/adapters/converse-text' afterEach(() => vi.unstubAllEnvs()) @@ -74,12 +75,21 @@ describe('BedrockResponsesTextAdapter', () => { }) describe('createBedrockText (branching factory)', () => { - it('defaults to the chat adapter', () => { - const a = createBedrockText('openai.gpt-oss-120b-1:0', 'k', { + it('defaults to the Converse adapter', () => { + const a = createBedrockText('us.amazon.nova-pro-v1:0', 'k', { region: 'us-east-1', }) - expect(a).toBeInstanceOf(ChatAdapter) - expect(a.name).toBe('bedrock') + expect(a).toBeInstanceOf(ConverseAdapter) + expect(a.name).toBe('bedrock-converse') + }) + + it("explicit api: 'converse' returns the Converse adapter", () => { + const a = createBedrockText('us.amazon.nova-pro-v1:0', 'k', { + region: 'us-east-1', + api: 'converse', + }) + expect(a).toBeInstanceOf(ConverseAdapter) + expect(a.name).toBe('bedrock-converse') }) it("returns the responses adapter when api: 'responses'", () => { @@ -109,17 +119,23 @@ describe('createBedrockText (branching factory)', () => { }) describe('bedrockText (env-key branching factory)', () => { - it('reads the key from BEDROCK_API_KEY and branches on api', () => { + it('reads the key from BEDROCK_API_KEY and defaults to Converse', () => { vi.stubEnv('BEDROCK_API_KEY', 'env-key') expect( - bedrockText('openai.gpt-oss-120b-1:0', { region: 'us-east-1' }), - ).toBeInstanceOf(ChatAdapter) + bedrockText('us.amazon.nova-pro-v1:0', { region: 'us-east-1' }), + ).toBeInstanceOf(ConverseAdapter) expect( bedrockText('openai.gpt-oss-120b-1:0', { region: 'us-east-1', api: 'responses', }), ).toBeInstanceOf(RespAdapter) + expect( + bedrockText('openai.gpt-oss-120b-1:0', { + region: 'us-east-1', + api: 'chat', + }), + ).toBeInstanceOf(ChatAdapter) }) it('does not require an API key when auth is sigv4', () => { diff --git a/packages/ai-bedrock/tests/factory.test.ts b/packages/ai-bedrock/tests/factory.test.ts new file mode 100644 index 000000000..6e2d097f5 --- /dev/null +++ b/packages/ai-bedrock/tests/factory.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' +import { bedrockText, createBedrockText } from '../src/index' + +describe('bedrockText branching', () => { + it('defaults to the Converse adapter', () => { + const a = bedrockText('us.anthropic.claude-haiku-4-5-20251001-v1:0', { apiKey: 'k' }) + expect(a.name).toBe('bedrock-converse') + }) + it('api "converse" is explicit Converse', () => { + const a = bedrockText('us.amazon.nova-pro-v1:0', { apiKey: 'k', api: 'converse' }) + expect(a.name).toBe('bedrock-converse') + }) + it('api "chat" returns the Chat Completions adapter', () => { + const a = bedrockText('openai.gpt-oss-120b-1:0', { apiKey: 'k', api: 'chat' }) + expect(a.name).toBe('bedrock') + }) + it('api "responses" returns the Responses adapter', () => { + const a = bedrockText('openai.gpt-oss-120b-1:0', { apiKey: 'k', api: 'responses' }) + expect(a.name).toBe('bedrock-responses') + }) + it('createBedrockText defaults to Converse with an explicit key', () => { + const a = createBedrockText('us.amazon.nova-lite-v1:0', 'k') + expect(a.name).toBe('bedrock-converse') + }) +}) From 8cac582fac487ea29e05d5319af7f8f3d1b8228a Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 18:17:40 +0200 Subject: [PATCH 37/46] docs(e2e): document Bedrock Converse coverage gap (aimock lacks AWS event-stream) --- testing/e2e/README.md | 10 ++++++++++ testing/e2e/src/lib/providers.ts | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/testing/e2e/README.md b/testing/e2e/README.md index ab0f13479..09fec08cc 100644 --- a/testing/e2e/README.md +++ b/testing/e2e/README.md @@ -184,6 +184,16 @@ await waitForAssistantText(page, 'Fender Stratocaster') 3. **Add to `tests/test-matrix.ts`** — mirror the support matrix 4. **No fixture changes needed** — aimock translates to correct wire format +### Bedrock Converse coverage gap + +The `bedrock` and `bedrock-responses` providers in this matrix use `createBedrockText` with a `baseURL` pointing at aimock — they speak Bedrock's **OpenAI-compatible** endpoint, which aimock's OpenAI replay handles fine. + +The default `bedrock-converse` adapter (introduced later) uses `@aws-sdk/client-bedrock-runtime` and speaks AWS's **binary event-stream (`vnd.amazon.eventstream`) Converse protocol**, which `@copilotkit/aimock` does not currently mock. Adding `bedrock-converse` to the live matrix would fail without a Converse-capable aimock provider. + +**Coverage today:** the Converse translation layer (message converter, tool converter, stream processor, structured output, adapter) is covered by unit tests in `packages/ai-bedrock/tests/converse/` (64 tests). The OpenAI-compatible `bedrock` and `bedrock-responses` entries remain in the E2E matrix as-is. + +**Follow-up:** a Bedrock/Converse provider will be added to aimock to close this gap and enable full E2E coverage of the Converse path. + **SDK baseURL notes:** - OpenAI, Grok: `LLMOCK_OPENAI` (with `/v1`) + `defaultHeaders` diff --git a/testing/e2e/src/lib/providers.ts b/testing/e2e/src/lib/providers.ts index 8409c0038..c421ff317 100644 --- a/testing/e2e/src/lib/providers.ts +++ b/testing/e2e/src/lib/providers.ts @@ -115,6 +115,11 @@ export function createTextAdapter( defaultHeaders: testHeaders, }), }), + // NOTE: Only the OpenAI-compatible Bedrock paths are E2E-covered here. + // The default `bedrock-converse` adapter uses the AWS binary event-stream + // (vnd.amazon.eventstream) Converse protocol, which aimock cannot replay — + // that path is covered by unit tests in packages/ai-bedrock/tests/converse/ + // instead. See testing/e2e/README.md § "Bedrock Converse coverage gap". bedrock: () => createChatOptions({ adapter: createBedrockText( From bc75c305e0074a1fe056bf84eb27f7c72f6f658c Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 18:19:39 +0200 Subject: [PATCH 38/46] docs: restore nav entries dropped by stale config.json rewrite; keep Bedrock --- docs/config.json | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/docs/config.json b/docs/config.json index 198a22ee1..8e498eedb 100644 --- a/docs/config.json +++ b/docs/config.json @@ -17,6 +17,10 @@ "label": "Quick Start: React", "to": "getting-started/quick-start" }, + { + "label": "Quick Start: React Native", + "to": "getting-started/quick-start-react-native" + }, { "label": "Devtools", "to": "getting-started/devtools" @@ -96,16 +100,37 @@ "label": "Connection Adapters", "to": "chat/connection-adapters" }, - { - "label": "Structured Outputs", - "to": "chat/structured-outputs" - }, { "label": "Thinking & Reasoning", "to": "chat/thinking-content" } ] }, + { + "label": "Structured Outputs", + "children": [ + { + "label": "Overview", + "to": "structured-outputs/overview" + }, + { + "label": "One-Shot Extraction", + "to": "structured-outputs/one-shot" + }, + { + "label": "Streaming UIs", + "to": "structured-outputs/streaming" + }, + { + "label": "Multi-Turn Chat", + "to": "structured-outputs/multi-turn" + }, + { + "label": "With Tools", + "to": "structured-outputs/with-tools" + } + ] + }, { "label": "Code Mode", "children": [ @@ -202,6 +227,10 @@ { "label": "Extend Adapter", "to": "advanced/extend-adapter" + }, + { + "label": "Typed Pre-Configured Options", + "to": "advanced/typed-options" } ] }, From f0bd676e7381e4f9ee8eab173631ab5f7386773d Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 18:22:41 +0200 Subject: [PATCH 39/46] docs(ai-bedrock): document Converse-as-default + opt-in chat/responses --- .changeset/ai-bedrock-adapter.md | 2 +- .claude/skills/gap-analysis/SKILL.md | 10 +- .../references/provider-doc-urls.md | 3 +- docs/adapters/bedrock.md | 124 ++++++++++++++---- .../ai-core/adapter-configuration/SKILL.md | 14 +- 5 files changed, 112 insertions(+), 41 deletions(-) diff --git a/.changeset/ai-bedrock-adapter.md b/.changeset/ai-bedrock-adapter.md index 0315785ec..197ba8acd 100644 --- a/.changeset/ai-bedrock-adapter.md +++ b/.changeset/ai-bedrock-adapter.md @@ -2,4 +2,4 @@ '@tanstack/ai-bedrock': minor --- -Add `@tanstack/ai-bedrock`: an Amazon Bedrock adapter built on Bedrock's OpenAI-compatible Chat Completions and Responses APIs. A single branching `bedrockText` factory (`api: 'chat' | 'responses'`) supports streaming, tools, and reasoning, with API-key or SigV4 authentication and configurable `runtime`/`mantle` endpoints. +Add `@tanstack/ai-bedrock`: an Amazon Bedrock adapter. The default `bedrockText` path uses Bedrock's **Converse** API (`@aws-sdk/client-bedrock-runtime`), reaching the broad chat catalog including Anthropic Claude, Amazon Nova, and Meta Llama, with streaming, tools, reasoning, and structured output. Opt into Bedrock's OpenAI-compatible endpoints with `api: 'chat'` (Chat Completions) or `api: 'responses'` (gpt-oss Responses). Authentication supports Bedrock API keys or SigV4 via the AWS credential chain. diff --git a/.claude/skills/gap-analysis/SKILL.md b/.claude/skills/gap-analysis/SKILL.md index 0cc605785..aafb9c3f8 100644 --- a/.claude/skills/gap-analysis/SKILL.md +++ b/.claude/skills/gap-analysis/SKILL.md @@ -74,11 +74,13 @@ markdown report under `.agent/gap-analysis/`. **Do not edit source files.** ## Known providers `openai`, `anthropic`, `gemini`, `ollama`, `grok`, `groq`, `openrouter`, -`bedrock` (`@tanstack/ai-bedrock`; adapter names `bedrock` / -`bedrock-responses`), `fal` (media-only), `elevenlabs` (TTS-only). The +`bedrock` (`@tanstack/ai-bedrock`; three-API surface — Converse default +(adapter name `bedrock-converse`), Chat Completions opt-in (`api: 'chat'`, +adapter name `bedrock`), Responses opt-in (`api: 'responses'`, adapter name +`bedrock-responses`)), `fal` (media-only), `elevenlabs` (TTS-only). The feature matrix tracks `openai`, `anthropic`, `gemini`, `ollama`, `grok`, -`groq`, `openrouter`, `bedrock`, and `bedrock-responses`; `fal` and -`elevenlabs` only appear in model/media audits. +`groq`, `openrouter`, `bedrock`, `bedrock-converse`, and `bedrock-responses`; +`fal` and `elevenlabs` only appear in model/media audits. ## Known features (19) diff --git a/.claude/skills/gap-analysis/references/provider-doc-urls.md b/.claude/skills/gap-analysis/references/provider-doc-urls.md index 3f733a0c3..a19c0ae48 100644 --- a/.claude/skills/gap-analysis/references/provider-doc-urls.md +++ b/.claude/skills/gap-analysis/references/provider-doc-urls.md @@ -73,11 +73,12 @@ WebFetch — call `resolve-library-id` with the SDK npm name, then `query-docs`. ## bedrock (Amazon Bedrock) - Models / API compatibility: https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html +- Converse API reference (default path): https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html - OpenAI-compatible Chat Completions: https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions-mantle.html - Responses API (mantle): https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html - Cross-region inference profiles: https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html - API keys: https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html -- (Uses OpenAI-compatible API; SDK is the openai package. Adapters: `bedrock` (chat) / `bedrock-responses`.) +- (Default path uses Converse API via `@aws-sdk/client-bedrock-runtime` (adapter `bedrock-converse`). Opt-in paths: `api: 'chat'` → OpenAI-compatible Chat Completions (adapter `bedrock`); `api: 'responses'` → Responses API (adapter `bedrock-responses`).) ## fal (media-only) diff --git a/docs/adapters/bedrock.md b/docs/adapters/bedrock.md index 0111867e4..b4c99ec97 100644 --- a/docs/adapters/bedrock.md +++ b/docs/adapters/bedrock.md @@ -2,12 +2,13 @@ title: Amazon Bedrock id: bedrock-adapter order: 7 -description: "Use Amazon Bedrock's OpenAI-compatible Chat Completions and Responses APIs with TanStack AI — streaming, tools, reasoning, API-key or SigV4 auth, and configurable runtime/mantle endpoints." +description: "Use Amazon Bedrock with TanStack AI — the Converse API is the default, reaching Claude, Nova, Llama, Mistral, DeepSeek, and more. Opt into OpenAI-compatible Chat Completions or Responses for open-weight and gpt-oss models. Supports streaming, tools, reasoning, and API-key or SigV4 auth." keywords: - tanstack ai - amazon bedrock - aws - bedrock + - converse api - openai compatible - chat completions - responses api @@ -18,7 +19,13 @@ keywords: - adapter --- -The Bedrock adapter connects TanStack AI to [Amazon Bedrock](https://aws.amazon.com/bedrock/) via Bedrock's OpenAI-compatible Chat Completions and Responses APIs. It is built on top of the `openai` SDK and supports streaming, client-side tool calling, and reasoning. +The Bedrock adapter connects TanStack AI to [Amazon Bedrock](https://aws.amazon.com/bedrock/) with three API paths: + +- **Converse** (default) — Bedrock's model-agnostic API built on `@aws-sdk/client-bedrock-runtime`. Reaches the broad chat catalog including Anthropic Claude, Amazon Nova, Meta Llama, Mistral, DeepSeek, Cohere, AI21, and OpenAI gpt-oss models. +- **Chat Completions** (`api: 'chat'`) — Bedrock's OpenAI-compatible Chat Completions endpoint. Reaches open-weight models only (gpt-oss, DeepSeek V3.x, Gemma, Qwen, Mistral open models, GLM, etc.). Does NOT reach Claude, Nova, or Llama. +- **Responses** (`api: 'responses'`) — Bedrock's OpenAI-compatible Responses API, mantle-only. Currently the OpenAI gpt-oss family. + +All paths support streaming, client-side tool calling, and reasoning. ## Installation @@ -26,13 +33,29 @@ The Bedrock adapter connects TanStack AI to [Amazon Bedrock](https://aws.amazon. pnpm add @tanstack/ai-bedrock ``` -If you want to use **SigV4 authentication** (AWS credentials instead of an API key), also install the optional peer: +No additional packages are required. SigV4 authentication is handled by `@aws-sdk/client-bedrock-runtime`, which is a direct dependency. -```bash -pnpm add aws-sigv4-fetch +## Quick Start (Converse — default) + +The default `bedrockText` call uses the Converse API and reaches the broad model catalog: + +```typescript +import { bedrockText } from '@tanstack/ai-bedrock' +import { chat } from '@tanstack/ai' + +const adapter = bedrockText('us.anthropic.claude-haiku-4-5-20251001-v1:0', { + region: 'us-east-1', +}) + +for await (const chunk of chat({ + adapter, + messages: [{ role: 'user', content: 'What is the capital of France?' }], +})) { + if (chunk.type === 'content') process.stdout.write(chunk.delta) +} ``` -`aws-sigv4-fetch` is not bundled with `@tanstack/ai-bedrock` — it is an optional install you only need when `auth: 'sigv4'` (or `auth: 'auto'` with no API key in the environment). +Equivalent to passing `{ api: 'converse' }` explicitly. Returns a `bedrock-converse` adapter. ## Authentication @@ -52,7 +75,7 @@ AWS_BEARER_TOKEN_BEDROCK=your-bedrock-api-key ### SigV4 (AWS credential chain) -For workloads that use IAM roles, instance profiles, or `~/.aws/credentials`, set `auth: 'sigv4'`. The adapter uses the standard AWS credential chain (environment variables, shared credential file, instance metadata, etc.) via `aws-sigv4-fetch`. +For workloads using IAM roles, instance profiles, or `~/.aws/credentials`, set `auth: 'sigv4'` (or leave it as `'auto'` with no API key in the environment). SigV4 works out of the box via `@aws-sdk/client-bedrock-runtime` — no additional packages required. ```bash AWS_ACCESS_KEY_ID=... @@ -65,7 +88,7 @@ AWS_SESSION_TOKEN=... # optional, for temporary credentials 1. Explicit `apiKey` passed to the factory 2. `BEDROCK_API_KEY` environment variable 3. `AWS_BEARER_TOKEN_BEDROCK` environment variable -4. SigV4 via the AWS credential chain (requires `aws-sigv4-fetch`) +4. SigV4 via the standard AWS credential chain ## Configuration @@ -73,49 +96,81 @@ AWS_SESSION_TOKEN=... # optional, for temporary credentials | Option | Type | Default | Description | |--------|------|---------|-------------| -| `region` | `string` | `'us-east-1'` | Full AWS region string (e.g. `'us-west-2'`) | -| `endpoint` | `'runtime' \| 'mantle'` | `'runtime'` | Bedrock endpoint to target (Chat API only) | +| `api` | `'converse' \| 'chat' \| 'responses'` | `'converse'` | Bedrock API to use | +| `region` | `string` | `'us-east-1'` | AWS region string (e.g. `'us-west-2'`) | | `auth` | `'apikey' \| 'sigv4' \| 'auto'` | `'auto'` | Authentication mode | | `apiKey` | `string` | — | Explicit API key (overrides env vars) | | `baseURL` | `string` | — | Override the computed base URL entirely | +| `endpoint` | `'runtime' \| 'mantle'` | `'runtime'` | Bedrock endpoint to target (Chat Completions path only) | + +The `endpoint` option only applies when `api: 'chat'`. The `runtime` endpoint (`bedrock-runtime`) hosts the broad open-weight catalog; `mantle` is an alternative. The Responses API always targets mantle. -The `endpoint` option applies only when `api: 'chat'` (or omitted). The `runtime` endpoint (`bedrock-runtime`) hosts the broad model catalog; `mantle` is an optional alternative. The Responses API always targets mantle. +## Converse API (default) -## Chat Completions (default) +`bedrockText(model)` or `bedrockText(model, { api: 'converse' })` returns a `bedrock-converse` adapter backed by `@aws-sdk/client-bedrock-runtime`. This is Bedrock's model-agnostic conversational API and is the recommended path for most use cases. -Use `bedrockText` with no `api` option, or `api: 'chat'`, to call Bedrock's Chat Completions endpoint. This gives you access to the broadest model catalog: Claude, Amazon Nova, Meta Llama, Mistral, DeepSeek, and OpenAI gpt-oss models. +**Model scope:** Anthropic Claude, Amazon Nova, Meta Llama, Mistral, DeepSeek, Cohere, AI21, OpenAI gpt-oss, and other models accessible in your account. See [Model availability](#model-availability) below. ```typescript import { bedrockText } from '@tanstack/ai-bedrock' import { chat } from '@tanstack/ai' -const adapter = bedrockText('us.anthropic.claude-3-7-sonnet-20250219-v1:0', { +// Claude via Converse +const claudeAdapter = bedrockText('us.anthropic.claude-haiku-4-5-20251001-v1:0', { region: 'us-east-1', }) -for await (const chunk of chat({ - adapter, - messages: [{ role: 'user', content: 'What is the capital of France?' }], -})) { - if (chunk.type === 'content') process.stdout.write(chunk.delta) -} +// Amazon Nova via Converse +const novaAdapter = bedrockText('us.amazon.nova-pro-v1:0', { + region: 'us-east-1', +}) + +// Meta Llama via Converse +const llamaAdapter = bedrockText('us.meta.llama3-3-70b-instruct-v1:0', { + region: 'us-east-1', +}) ``` -### Explicit API key +### Explicit API key (Converse) ```typescript import { createBedrockText } from '@tanstack/ai-bedrock' const adapter = createBedrockText( - 'us.amazon.nova-pro-v1:0', + 'us.anthropic.claude-haiku-4-5-20251001-v1:0', 'your-bedrock-api-key', { region: 'us-west-2' }, ) ``` -## Responses API +## Chat Completions API (`api: 'chat'`) + +Set `api: 'chat'` to use Bedrock's OpenAI-compatible Chat Completions endpoint. Returns a `bedrock` adapter. + +**Model scope:** Open-weight models only — gpt-oss, DeepSeek V3.x, Gemma, Qwen, Mistral open models, GLM, and similar. Claude, Nova, and Llama are NOT available on this endpoint. See the [AWS API compatibility matrix](https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html) for the current list. + +```typescript +import { bedrockText } from '@tanstack/ai-bedrock' +import { chat } from '@tanstack/ai' + +const adapter = bedrockText('openai.gpt-oss-mini-1:0', { + region: 'us-east-1', + api: 'chat', +}) + +for await (const chunk of chat({ + adapter, + messages: [{ role: 'user', content: 'What is the capital of France?' }], +})) { + if (chunk.type === 'content') process.stdout.write(chunk.delta) +} +``` + +## Responses API (`api: 'responses'`) + +Set `api: 'responses'` to use Bedrock's OpenAI-compatible Responses API. Returns a `bedrock-responses` adapter. This API is mantle-only. -Set `api: 'responses'` to use Bedrock's Responses API. This API is mantle-only, supports a narrower model set (currently the OpenAI gpt-oss family), and is stateful — you can pass `previous_response_id` and `store` through `modelOptions` to continue a conversation server-side. +**Model scope:** Currently the OpenAI gpt-oss family. The Responses API is stateful — pass `previous_response_id` and `store` through `modelOptions` to continue a conversation server-side. ```typescript import { bedrockText } from '@tanstack/ai-bedrock' @@ -136,7 +191,11 @@ for await (const chunk of chat({ ## Model Availability -The model catalog (`BEDROCK_CHAT_MODELS`, `BEDROCK_RESPONSES_MODELS`) is a hand-seeded snapshot of cross-region inference profile IDs. **Actual model availability depends on your AWS account's model access configuration and the region you are targeting.** Enable model access in the [Amazon Bedrock console](https://console.aws.amazon.com/bedrock/home#/modelaccess) before use. A maintainer refresh script (`scripts/fetch-bedrock-models.ts`) can regenerate the catalog. +The adapter ships with a hand-seeded snapshot catalog (`src/model-catalog.generated.ts`) of confirmed model IDs. This catalog can be refreshed by the maintainer script `scripts/fetch-bedrock-models.ts`, which calls `ListFoundationModels` with AWS credentials. + +**Actual model availability depends on your AWS account's model access configuration and the region you are targeting.** Enable model access in the [Amazon Bedrock console](https://console.aws.amazon.com/bedrock/home#/modelaccess) before use. + +For the full list of models and which API endpoints they support, see the [AWS API compatibility matrix](https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html). ## Supported Capabilities @@ -152,15 +211,22 @@ The model catalog (`BEDROCK_CHAT_MODELS`, `BEDROCK_RESPONSES_MODELS`) is a hand- Creates a Bedrock adapter using environment-variable auth. -- `model` — Model ID (e.g. `'us.anthropic.claude-3-7-sonnet-20250219-v1:0'`) -- `config.api` — `'chat'` (default) or `'responses'` +- `model` — Model ID (e.g. `'us.anthropic.claude-haiku-4-5-20251001-v1:0'`) +- `config.api` — `'converse'` (default), `'chat'`, or `'responses'` - `config.region` — AWS region string (default `'us-east-1'`) -- `config.endpoint` — `'runtime'` (default) or `'mantle'` (Chat API only) - `config.auth` — `'auto'` (default), `'apikey'`, or `'sigv4'` +- `config.apiKey` — Explicit API key (overrides env vars) - `config.baseURL` — Override base URL +- `config.endpoint` — `'runtime'` (default) or `'mantle'` (Chat Completions path only) Returns a chat adapter for use with `chat()` or `generate()`. +| `api` value | Adapter name | Underlying SDK | +|---|---|---| +| `'converse'` (default) | `bedrock-converse` | `@aws-sdk/client-bedrock-runtime` | +| `'chat'` | `bedrock` | `openai` (OpenAI-compatible) | +| `'responses'` | `bedrock-responses` | `openai` (OpenAI-compatible) | + ### `createBedrockText(model, apiKey, config?)` Creates a Bedrock adapter with an explicit API key, bypassing the environment-variable lookup. @@ -169,5 +235,7 @@ Creates a Bedrock adapter with an explicit API key, bypassing the environment-va - [Amazon Bedrock API keys](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html) — Create and manage API keys - [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) — Enable models in your account +- [AWS API compatibility matrix](https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html) — Which models work with which APIs +- [Converse API reference](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html) — Native Converse API docs - [Streaming Guide](../chat/streaming) — Learn about streaming responses - [Tools Guide](../tools/tools) — Learn about tool calling diff --git a/packages/ai/skills/ai-core/adapter-configuration/SKILL.md b/packages/ai/skills/ai-core/adapter-configuration/SKILL.md index 5157209b5..a3c20a96d 100644 --- a/packages/ai/skills/ai-core/adapter-configuration/SKILL.md +++ b/packages/ai/skills/ai-core/adapter-configuration/SKILL.md @@ -99,13 +99,13 @@ const adapterWithKey = openaiText('gpt-5.2', { }) ``` -`@tanstack/ai-bedrock` (Amazon Bedrock, via Bedrock's OpenAI-compatible -APIs) branches on `config.api`: `bedrockText(model, { api: 'chat' })` (the -default) targets the Chat Completions endpoint (adapter name `bedrock`), -while `bedrockText(model, { api: 'responses' })` targets the Responses API -(adapter name `bedrock-responses`). Use `createBedrockText(model, apiKey, -config?)` to pass the key explicitly. Auth resolves from `BEDROCK_API_KEY` -/ `AWS_BEARER_TOKEN_BEDROCK`, or SigV4 credentials. +`@tanstack/ai-bedrock` (Amazon Bedrock) branches on `config.api`: + +- `bedrockText(model)` or `bedrockText(model, { api: 'converse' })` (the default) — Bedrock's native Converse API via `@aws-sdk/client-bedrock-runtime` (adapter name `bedrock-converse`). Reaches the broad catalog: Claude, Nova, Llama, Mistral, DeepSeek, and more. +- `bedrockText(model, { api: 'chat' })` — OpenAI-compatible Chat Completions endpoint (adapter name `bedrock`). Open-weight models only (gpt-oss, DeepSeek V3.x, Gemma, Qwen, etc.). Does NOT reach Claude, Nova, or Llama. +- `bedrockText(model, { api: 'responses' })` — OpenAI-compatible Responses API, mantle-only (adapter name `bedrock-responses`). Currently gpt-oss family. + +Use `createBedrockText(model, apiKey, config?)` to pass the key explicitly. Auth resolves from `BEDROCK_API_KEY` / `AWS_BEARER_TOKEN_BEDROCK`, or SigV4 via the standard AWS credential chain (no extra packages needed — handled by `@aws-sdk/client-bedrock-runtime`). ### 2. Runtime Adapter Switching From 9bb60111c798c9fadce7875d2802199755187868 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 18:30:33 +0200 Subject: [PATCH 40/46] chore(ai-bedrock): satisfy PR quality gate (lint, knip, format); drop unused overrides --- knip.json | 2 +- .../src/converse/message-converter.ts | 36 +++++++++--------- .../ai-bedrock/src/converse/tool-converter.ts | 11 ++++-- packages/ai-bedrock/src/index.ts | 12 ++++-- packages/ai-bedrock/src/model-meta.ts | 6 +-- packages/ai-bedrock/src/model-overrides.ts | 29 --------------- packages/ai-bedrock/src/utils/client.ts | 2 +- .../ai-bedrock/tests/converse/adapter.test.ts | 11 ++---- .../tests/converse/message-converter.test.ts | 37 ++++++++++++++++--- .../tests/converse/tool-converter.test.ts | 17 +++++++-- packages/ai-bedrock/tests/factory.test.ts | 19 ++++++++-- 11 files changed, 104 insertions(+), 78 deletions(-) delete mode 100644 packages/ai-bedrock/src/model-overrides.ts diff --git a/knip.json b/knip.json index 71927cd48..77dafff1e 100644 --- a/knip.json +++ b/knip.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "ignoreDependencies": ["@faker-js/faker"], + "ignoreDependencies": ["@faker-js/faker", "@aws-sdk/client-bedrock"], "ignoreWorkspaces": [ "examples/**", "testing/**", diff --git a/packages/ai-bedrock/src/converse/message-converter.ts b/packages/ai-bedrock/src/converse/message-converter.ts index d37285156..6fbf0b12b 100644 --- a/packages/ai-bedrock/src/converse/message-converter.ts +++ b/packages/ai-bedrock/src/converse/message-converter.ts @@ -1,12 +1,12 @@ import { normalizeSystemPrompts } from '@tanstack/ai' import type { ContentPart, + ContentPartDataSource, + DocumentPart, + ImagePart, ModelMessage, SystemPrompt, TextPart, - ImagePart, - DocumentPart, - ContentPartDataSource, } from '@tanstack/ai' import type { ContentBlock, @@ -24,9 +24,7 @@ function base64ToBytes(b64: string): Uint8Array { return new Uint8Array(Buffer.from(b64, 'base64')) } -function imageFormat( - mime: string, -): 'png' | 'jpeg' | 'gif' | 'webp' { +function imageFormat(mime: string): 'png' | 'jpeg' | 'gif' | 'webp' { switch (mime) { case 'image/png': return 'png' @@ -74,9 +72,7 @@ function documentFormat( } } -function stringContent( - content: string | null | ContentPart[], -): string { +function stringContent(content: string | null | Array): string { if (content === null) return '' if (typeof content === 'string') return content return content @@ -146,8 +142,8 @@ function contentPartToBlock(part: ContentPart): ContentBlock { ) } -function messageToBlocks(msg: ModelMessage): ContentBlock[] { - const blocks: ContentBlock[] = [] +function messageToBlocks(msg: ModelMessage): Array { + const blocks: Array = [] if (msg.role === 'tool') { if (!msg.toolCallId) { @@ -185,7 +181,11 @@ function messageToBlocks(msg: ModelMessage): ContentBlock[] { let input: DocumentType = {} try { const parsed = JSON.parse(call.function.arguments || '{}') as unknown - if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) { + if ( + parsed !== null && + typeof parsed === 'object' && + !Array.isArray(parsed) + ) { input = parsed as DocumentType } } catch { @@ -219,16 +219,16 @@ function messageToBlocks(msg: ModelMessage): ContentBlock[] { * requires strict user/assistant alternation). */ export function toConverseMessages( - messages: ModelMessage[], + messages: Array, systemPrompts?: Array, -): { system: SystemContentBlock[]; messages: Message[] } { +): { system: Array; messages: Array } { // Build system blocks (uses normalizeSystemPrompts for runtime validation) - const system: SystemContentBlock[] = normalizeSystemPrompts(systemPrompts).map( - (p) => ({ text: p.content }), - ) + const system: Array = normalizeSystemPrompts( + systemPrompts, + ).map((p) => ({ text: p.content })) // Convert each ModelMessage to a Converse Message, merging same-role pairs - const converseMessages: Message[] = [] + const converseMessages: Array = [] for (const msg of messages) { // Map TanStack roles to Converse roles diff --git a/packages/ai-bedrock/src/converse/tool-converter.ts b/packages/ai-bedrock/src/converse/tool-converter.ts index 1b202caf4..2f41ff308 100644 --- a/packages/ai-bedrock/src/converse/tool-converter.ts +++ b/packages/ai-bedrock/src/converse/tool-converter.ts @@ -1,4 +1,7 @@ -import type { ToolConfiguration, ToolChoice } from '@aws-sdk/client-bedrock-runtime' +import type { + ToolChoice, + ToolConfiguration, +} from '@aws-sdk/client-bedrock-runtime' import type { DocumentType } from '@smithy/types' export interface ConverseToolInput { @@ -14,7 +17,7 @@ export type ToolChoiceInput = | { type: 'tool'; name: string } export function toToolConfig( - tools: ConverseToolInput[], + tools: Array, choice: ToolChoiceInput | undefined, ): ToolConfiguration | undefined { if (!tools.length) return undefined @@ -31,7 +34,9 @@ export function toToolConfig( } } -function mapChoice(choice: ToolChoiceInput | undefined): ToolChoice | undefined { +function mapChoice( + choice: ToolChoiceInput | undefined, +): ToolChoice | undefined { if (!choice || choice === 'auto') return { auto: {} } if (choice === 'required') return { any: {} } if (choice === 'none') return undefined diff --git a/packages/ai-bedrock/src/index.ts b/packages/ai-bedrock/src/index.ts index 03807d183..73bc89006 100644 --- a/packages/ai-bedrock/src/index.ts +++ b/packages/ai-bedrock/src/index.ts @@ -16,8 +16,8 @@ import type { BedrockConverseConfig } from './adapters/converse-text' import type { BedrockClientConfig } from './utils' import type { BedrockChatModels, - BedrockResponsesModels, BedrockConverseModels, + BedrockResponsesModels, } from './model-meta' /** Config for the branching factory's converse mode (default, or api: 'converse'). */ @@ -98,7 +98,10 @@ export function createBedrockText( export function createBedrockText( model: BedrockConverseModels, apiKey: string, - config?: BedrockConverseApiConfig | BedrockChatApiConfig | BedrockResponsesApiConfig, + config?: + | BedrockConverseApiConfig + | BedrockChatApiConfig + | BedrockResponsesApiConfig, ): AnyBedrockAdapter { // Explicit apiKey is authoritative — spread config first so it can't override. return build(model, { ...config, apiKey }) @@ -119,7 +122,10 @@ export function bedrockText( ): BedrockResponsesTextAdapter export function bedrockText( model: BedrockConverseModels, - config?: BedrockConverseApiConfig | BedrockChatApiConfig | BedrockResponsesApiConfig, + config?: + | BedrockConverseApiConfig + | BedrockChatApiConfig + | BedrockResponsesApiConfig, ): AnyBedrockAdapter { // No eager env-key fetch: the adapter constructor resolves auth lazily so // SigV4 (and the env-key fallback) work without a forced API key here. diff --git a/packages/ai-bedrock/src/model-meta.ts b/packages/ai-bedrock/src/model-meta.ts index 88c0f936e..46da36c97 100644 --- a/packages/ai-bedrock/src/model-meta.ts +++ b/packages/ai-bedrock/src/model-meta.ts @@ -17,10 +17,10 @@ export type BedrockChatModels = IdsWhere<'chat'> export type BedrockResponsesModels = IdsWhere<'responses'> /** Runtime catalogs. Cast-free narrowing via a type predicate (the ai-bedrock pattern). */ +// Every catalog entry advertises `converse: true` (Converse is the universal +// Bedrock surface), so the id list is the full catalog — no runtime filter needed. export const BEDROCK_CONVERSE_MODELS: ReadonlyArray = - GENERATED_BEDROCK_MODELS.filter( - (m): m is Extract => m.apis.converse, - ).map((m) => m.id) + GENERATED_BEDROCK_MODELS.map((m) => m.id) export const BEDROCK_CHAT_MODELS: ReadonlyArray = GENERATED_BEDROCK_MODELS.filter( diff --git a/packages/ai-bedrock/src/model-overrides.ts b/packages/ai-bedrock/src/model-overrides.ts deleted file mode 100644 index c0b98e8de..000000000 --- a/packages/ai-bedrock/src/model-overrides.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Capabilities `ListFoundationModels` does not report (tool & reasoning support). - * Hand-maintained; merged with the generated catalog at runtime. Keyed by model - * id / inference-profile id. - */ -export interface ModelOverride { - features?: Array<'tools' | 'reasoning' | 'json_schema'> -} - -export const BEDROCK_MODEL_OVERRIDES: Record = { - 'openai.gpt-oss-120b-1:0': { features: ['tools', 'reasoning'] }, - 'openai.gpt-oss-20b-1:0': { features: ['tools', 'reasoning'] }, - 'us.anthropic.claude-sonnet-4-5-20250929-v1:0': { - features: ['tools', 'reasoning'], - }, - 'us.anthropic.claude-haiku-4-5-20251001-v1:0': { features: ['tools'] }, - 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': { - features: ['tools', 'reasoning'], - }, - 'us.anthropic.claude-3-5-sonnet-20241022-v2:0': { features: ['tools'] }, - 'us.anthropic.claude-3-5-haiku-20241022-v1:0': { features: ['tools'] }, - 'us.amazon.nova-pro-v1:0': { features: ['tools'] }, - 'us.amazon.nova-lite-v1:0': { features: ['tools'] }, - 'us.amazon.nova-micro-v1:0': { features: ['tools'] }, - 'us.meta.llama3-3-70b-instruct-v1:0': { features: ['tools'] }, - 'us.meta.llama4-maverick-17b-instruct-v1:0': { features: ['tools'] }, - 'us.mistral.pixtral-large-2502-v1:0': { features: ['tools'] }, - 'us.deepseek.r1-v1:0': { features: ['reasoning'] }, -} diff --git a/packages/ai-bedrock/src/utils/client.ts b/packages/ai-bedrock/src/utils/client.ts index 9e1317945..75fee20ef 100644 --- a/packages/ai-bedrock/src/utils/client.ts +++ b/packages/ai-bedrock/src/utils/client.ts @@ -1,6 +1,6 @@ -import type { ClientOptions } from 'openai' import { resolveBedrockAuth } from './auth' import { createSigV4Fetch } from './openai-sigv4-fetch' +import type { ClientOptions } from 'openai' import type { BedrockEndpoint } from './auth' export type { BedrockEndpoint } from './auth' diff --git a/packages/ai-bedrock/tests/converse/adapter.test.ts b/packages/ai-bedrock/tests/converse/adapter.test.ts index 535df2548..eba7ed3d1 100644 --- a/packages/ai-bedrock/tests/converse/adapter.test.ts +++ b/packages/ai-bedrock/tests/converse/adapter.test.ts @@ -13,11 +13,10 @@ import type { StreamChunk, TextOptions } from '@tanstack/ai' * The adapter's translation logic (buildInput, lifecycle wiring) is exercised * end-to-end against canned Converse SDK shapes. */ -class StubAdapter extends BedrockConverseTextAdapter< - 'us.amazon.nova-pro-v1:0' -> { +class StubAdapter extends BedrockConverseTextAdapter<'us.amazon.nova-pro-v1:0'> { streamEvents: Array = [] - nonStreamOutput: ConverseCommandOutput = {} as unknown as ConverseCommandOutput + nonStreamOutput: ConverseCommandOutput = + {} as unknown as ConverseCommandOutput protected override async sendStream(): Promise< AsyncIterable @@ -78,9 +77,7 @@ describe('BedrockConverseTextAdapter', () => { }) it('emits RUN_ERROR when the stream seam throws', async () => { - class ThrowingAdapter extends BedrockConverseTextAdapter< - 'us.amazon.nova-pro-v1:0' - > { + class ThrowingAdapter extends BedrockConverseTextAdapter<'us.amazon.nova-pro-v1:0'> { protected override async sendStream(): Promise< AsyncIterable > { diff --git a/packages/ai-bedrock/tests/converse/message-converter.test.ts b/packages/ai-bedrock/tests/converse/message-converter.test.ts index 10a10514a..1b14536e6 100644 --- a/packages/ai-bedrock/tests/converse/message-converter.test.ts +++ b/packages/ai-bedrock/tests/converse/message-converter.test.ts @@ -36,7 +36,11 @@ describe('toConverseMessages', () => { role: 'assistant', content: '', toolCalls: [ - { id: 't1', type: 'function', function: { name: 'getX', arguments: '{"a":1}' } }, + { + id: 't1', + type: 'function', + function: { name: 'getX', arguments: '{"a":1}' }, + }, ], }, { role: 'tool', content: '{"ok":true}', toolCallId: 't1' }, @@ -44,12 +48,20 @@ describe('toConverseMessages', () => { const { messages } = toConverseMessages(msgs) expect(messages[0]).toEqual({ role: 'assistant', - content: [{ toolUse: { toolUseId: 't1', name: 'getX', input: { a: 1 } } }], + content: [ + { toolUse: { toolUseId: 't1', name: 'getX', input: { a: 1 } } }, + ], }) expect(messages[1]).toEqual({ role: 'user', content: [ - { toolResult: { toolUseId: 't1', content: [{ text: '{"ok":true}' }], status: 'success' } }, + { + toolResult: { + toolUseId: 't1', + content: [{ text: '{"ok":true}' }], + status: 'success', + }, + }, ], }) }) @@ -60,7 +72,10 @@ describe('toConverseMessages', () => { role: 'user', content: [ { type: 'text', content: 'look' }, - { type: 'image', source: { type: 'data', value: btoa('xy'), mimeType: 'image/png' } }, + { + type: 'image', + source: { type: 'data', value: btoa('xy'), mimeType: 'image/png' }, + }, ], }, ]) @@ -71,13 +86,23 @@ describe('toConverseMessages', () => { expect(imageBlock).toMatchObject({ image: { format: 'png' } }) // bytes decoded from base64 // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((imageBlock as any).image.source.bytes).toEqual(new Uint8Array([120, 121])) + expect((imageBlock as any).image.source.bytes).toEqual( + new Uint8Array([120, 121]), + ) }) it('throws on a URL image source (Converse needs inline bytes)', () => { expect(() => toConverseMessages([ - { role: 'user', content: [{ type: 'image', source: { type: 'url', value: 'https://x/y.png' } }] }, + { + role: 'user', + content: [ + { + type: 'image', + source: { type: 'url', value: 'https://x/y.png' }, + }, + ], + }, ]), ).toThrow(/inline|bytes|URL/i) }) diff --git a/packages/ai-bedrock/tests/converse/tool-converter.test.ts b/packages/ai-bedrock/tests/converse/tool-converter.test.ts index 95a63b852..b2b3c9626 100644 --- a/packages/ai-bedrock/tests/converse/tool-converter.test.ts +++ b/packages/ai-bedrock/tests/converse/tool-converter.test.ts @@ -4,7 +4,13 @@ import { toToolConfig } from '../../src/converse/tool-converter' describe('toToolConfig', () => { it('maps JSON-schema tools to Converse toolSpec', () => { const cfg = toToolConfig( - [{ name: 'getX', description: 'd', inputSchema: { type: 'object', properties: {} } }], + [ + { + name: 'getX', + description: 'd', + inputSchema: { type: 'object', properties: {} }, + }, + ], 'auto', ) expect(cfg?.tools?.[0]).toEqual({ @@ -18,9 +24,14 @@ describe('toToolConfig', () => { }) it('maps required -> any and a named tool -> tool', () => { - expect(toToolConfig([{ name: 'a', inputSchema: {} }], 'required')?.toolChoice).toEqual({ any: {} }) expect( - toToolConfig([{ name: 'a', inputSchema: {} }], { type: 'tool', name: 'a' })?.toolChoice, + toToolConfig([{ name: 'a', inputSchema: {} }], 'required')?.toolChoice, + ).toEqual({ any: {} }) + expect( + toToolConfig([{ name: 'a', inputSchema: {} }], { + type: 'tool', + name: 'a', + })?.toolChoice, ).toEqual({ tool: { name: 'a' } }) }) diff --git a/packages/ai-bedrock/tests/factory.test.ts b/packages/ai-bedrock/tests/factory.test.ts index 6e2d097f5..c4ae697a0 100644 --- a/packages/ai-bedrock/tests/factory.test.ts +++ b/packages/ai-bedrock/tests/factory.test.ts @@ -3,19 +3,30 @@ import { bedrockText, createBedrockText } from '../src/index' describe('bedrockText branching', () => { it('defaults to the Converse adapter', () => { - const a = bedrockText('us.anthropic.claude-haiku-4-5-20251001-v1:0', { apiKey: 'k' }) + const a = bedrockText('us.anthropic.claude-haiku-4-5-20251001-v1:0', { + apiKey: 'k', + }) expect(a.name).toBe('bedrock-converse') }) it('api "converse" is explicit Converse', () => { - const a = bedrockText('us.amazon.nova-pro-v1:0', { apiKey: 'k', api: 'converse' }) + const a = bedrockText('us.amazon.nova-pro-v1:0', { + apiKey: 'k', + api: 'converse', + }) expect(a.name).toBe('bedrock-converse') }) it('api "chat" returns the Chat Completions adapter', () => { - const a = bedrockText('openai.gpt-oss-120b-1:0', { apiKey: 'k', api: 'chat' }) + const a = bedrockText('openai.gpt-oss-120b-1:0', { + apiKey: 'k', + api: 'chat', + }) expect(a.name).toBe('bedrock') }) it('api "responses" returns the Responses adapter', () => { - const a = bedrockText('openai.gpt-oss-120b-1:0', { apiKey: 'k', api: 'responses' }) + const a = bedrockText('openai.gpt-oss-120b-1:0', { + apiKey: 'k', + api: 'responses', + }) expect(a.name).toBe('bedrock-responses') }) it('createBedrockText defaults to Converse with an explicit key', () => { From b5a15e7ec3ce0f73cc70289df5ac576108ba4a3f Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 18:39:06 +0200 Subject: [PATCH 41/46] fix(ai-bedrock): give Converse document blocks unique names --- .../src/converse/message-converter.ts | 17 +++-- .../tests/converse/message-converter.test.ts | 74 +++++++++++++++++++ 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/packages/ai-bedrock/src/converse/message-converter.ts b/packages/ai-bedrock/src/converse/message-converter.ts index 6fbf0b12b..df6f55321 100644 --- a/packages/ai-bedrock/src/converse/message-converter.ts +++ b/packages/ai-bedrock/src/converse/message-converter.ts @@ -99,7 +99,7 @@ function isDataSource( return source.type === 'data' } -function contentPartToBlock(part: ContentPart): ContentBlock { +function contentPartToBlock(part: ContentPart, docIndex: number): ContentBlock { if (isTextPart(part)) { return { text: part.content } } @@ -129,7 +129,7 @@ function contentPartToBlock(part: ContentPart): ContentBlock { return { document: { format: documentFormat(source.mimeType), - name: 'document', + name: `document-${docIndex}`, source: { bytes: base64ToBytes(source.value) }, }, } @@ -142,7 +142,10 @@ function contentPartToBlock(part: ContentPart): ContentBlock { ) } -function messageToBlocks(msg: ModelMessage): Array { +function messageToBlocks( + msg: ModelMessage, + docCounter: { value: number }, +): Array { const blocks: Array = [] if (msg.role === 'tool') { @@ -170,7 +173,8 @@ function messageToBlocks(msg: ModelMessage): Array { } } else if (Array.isArray(msg.content)) { for (const part of msg.content) { - blocks.push(contentPartToBlock(part)) + const docIndex = isDocumentPart(part) ? ++docCounter.value : 0 + blocks.push(contentPartToBlock(part, docIndex)) } } // null → no text blocks @@ -229,13 +233,16 @@ export function toConverseMessages( // Convert each ModelMessage to a Converse Message, merging same-role pairs const converseMessages: Array = [] + // Global document counter: ensures every document block across all messages + // gets a unique name, preventing Bedrock ValidationException for duplicate names. + const docCounter = { value: 0 } for (const msg of messages) { // Map TanStack roles to Converse roles const converseRole: 'user' | 'assistant' = msg.role === 'assistant' ? 'assistant' : 'user' - const blocks = messageToBlocks(msg) + const blocks = messageToBlocks(msg, docCounter) // Skip messages that produce no content blocks (e.g. assistant with // null content and no toolCalls). Pushing an empty-content message to diff --git a/packages/ai-bedrock/tests/converse/message-converter.test.ts b/packages/ai-bedrock/tests/converse/message-converter.test.ts index 1b14536e6..4f05bb1f3 100644 --- a/packages/ai-bedrock/tests/converse/message-converter.test.ts +++ b/packages/ai-bedrock/tests/converse/message-converter.test.ts @@ -106,4 +106,78 @@ describe('toConverseMessages', () => { ]), ).toThrow(/inline|bytes|URL/i) }) + + it('gives distinct names to multiple document parts in one message', () => { + const { messages } = toConverseMessages([ + { + role: 'user', + content: [ + { + type: 'document', + source: { + type: 'data', + value: btoa('doc1'), + mimeType: 'application/pdf', + }, + }, + { + type: 'document', + source: { + type: 'data', + value: btoa('doc2'), + mimeType: 'text/plain', + }, + }, + ], + }, + ]) + const content = messages[0]!.content! + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const name0 = (content[0] as any).document.name as string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const name1 = (content[1] as any).document.name as string + expect(name0).not.toBe(name1) + expect(name0).toMatch(/document-\d+/) + expect(name1).toMatch(/document-\d+/) + }) + + it('gives distinct names to document parts across multiple messages', () => { + const { messages } = toConverseMessages([ + { + role: 'user', + content: [ + { + type: 'document', + source: { + type: 'data', + value: btoa('doc1'), + mimeType: 'application/pdf', + }, + }, + ], + }, + { + role: 'assistant', + content: 'noted', + }, + { + role: 'user', + content: [ + { + type: 'document', + source: { + type: 'data', + value: btoa('doc2'), + mimeType: 'text/plain', + }, + }, + ], + }, + ]) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const name0 = (messages[0]!.content![0] as any).document.name as string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const name1 = (messages[2]!.content![0] as any).document.name as string + expect(name0).not.toBe(name1) + }) }) From 6a4e71e2bf186123892d49593fa79fad2ae421cd Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 19:20:37 +0200 Subject: [PATCH 42/46] fix(ai-bedrock): keep AWS SDK out of the browser bundle via non-literal lazy imports; PR feedback --- packages/ai-bedrock/package.json | 2 +- .../ai-bedrock/src/adapters/converse-text.ts | 97 +++++++++++++------ packages/ai-bedrock/src/utils/auth.ts | 20 +++- pnpm-lock.yaml | 2 +- 4 files changed, 87 insertions(+), 34 deletions(-) diff --git a/packages/ai-bedrock/package.json b/packages/ai-bedrock/package.json index 4dfb5578c..0550c6360 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -49,7 +49,7 @@ "vite": "^7.3.3" }, "peerDependencies": { - "@tanstack/ai": "workspace:^", + "@tanstack/ai": "workspace:*", "zod": "^4.0.0" }, "dependencies": { diff --git a/packages/ai-bedrock/src/adapters/converse-text.ts b/packages/ai-bedrock/src/adapters/converse-text.ts index aac61c6ea..fab5b869b 100644 --- a/packages/ai-bedrock/src/adapters/converse-text.ts +++ b/packages/ai-bedrock/src/adapters/converse-text.ts @@ -1,8 +1,3 @@ -import { - BedrockRuntimeClient, - ConverseCommand, - ConverseStreamCommand, -} from '@aws-sdk/client-bedrock-runtime' import { EventType, convertSchemaToJsonSchema } from '@tanstack/ai' import { BaseTextAdapter } from '@tanstack/ai/adapters' import { toRunErrorPayload } from '@tanstack/ai/adapter-internals' @@ -15,7 +10,9 @@ import { buildStructuredToolConfig, } from '../converse/structured-output' import type { ConverseToolInput } from '../converse/tool-converter' +import type * as BedrockRuntime from '@aws-sdk/client-bedrock-runtime' import type { + BedrockRuntimeClient, ContentBlock, ConverseCommandInput, ConverseCommandOutput, @@ -77,32 +74,70 @@ export class BedrockConverseTextAdapter< > { override readonly kind = 'text' as const override readonly name = 'bedrock-converse' as const - protected client: BedrockRuntimeClient + private clientPromise?: Promise + private readonly clientConfig: BedrockConverseConfig constructor(config: BedrockConverseConfig, model: TModel) { super({}, model) - const region = config.region ?? 'us-east-1' - const resolved = resolveBedrockAuth( - { apiKey: config.apiKey, region, auth: config.auth }, - 'runtime', - ) - // The installed @aws-sdk/client-bedrock-runtime (v3.1057) exposes a - // first-class `token: TokenIdentity | TokenIdentityProvider` config field - // (HttpAuthSchemeInputConfig) for Bedrock API-key bearer auth — no custom - // requestHandler/middleware needed. SigV4 uses the credential provider. - if (resolved.kind === 'bearer') { - this.client = new BedrockRuntimeClient({ - region, - token: { token: resolved.token }, - ...(config.baseURL ? { endpoint: config.baseURL } : {}), - }) - } else { - this.client = new BedrockRuntimeClient({ - region: resolved.region, - credentials: resolved.credentials, - ...(config.baseURL ? { endpoint: config.baseURL } : {}), - }) + // Defer client construction and auth resolution: the AWS SDK is Node/ + // server-only, so we must not pull it into the static graph here. The + // client (and its dynamic import) is built lazily on first SDK call. + this.clientConfig = config + } + + /** + * Dynamically import `@aws-sdk/client-bedrock-runtime`. The specifier is held + * in a variable (not a string literal) so bundler dep scanners (e.g. Vite/ + * esbuild optimizeDeps) cannot statically discover the AWS SDK and try to + * pre-bundle it for the browser — it would fail on the SDK's Node-only + * `fromTokenFile` export chain. The SDK is Node/server-only and is only + * reached on a real request. `typeof import(...)` is a type-only reference + * (erased at emit) so the imported members keep full typing. + */ + protected importBedrockRuntime(): Promise { + const mod = '@aws-sdk/client-bedrock-runtime' + return import(/* @vite-ignore */ mod) as Promise + } + + /** + * Lazily construct the `BedrockRuntimeClient`. The dynamic import keeps + * `@aws-sdk/client-bedrock-runtime` out of the static/browser graph and + * defers `resolveBedrockAuth` until a real request is made. + */ + protected async getClient(): Promise { + if (!this.clientPromise) { + this.clientPromise = (async () => { + const { BedrockRuntimeClient } = await this.importBedrockRuntime() + const region = this.clientConfig.region ?? 'us-east-1' + const resolved = resolveBedrockAuth( + { + apiKey: this.clientConfig.apiKey, + region, + auth: this.clientConfig.auth, + }, + 'runtime', + ) + const endpoint = this.clientConfig.baseURL + // The installed @aws-sdk/client-bedrock-runtime (v3.1057) exposes a + // first-class `token: TokenIdentity | TokenIdentityProvider` config + // field (HttpAuthSchemeInputConfig) for Bedrock API-key bearer auth — + // no custom requestHandler/middleware needed. SigV4 uses the credential + // provider. + if (resolved.kind === 'bearer') { + return new BedrockRuntimeClient({ + region, + token: { token: resolved.token }, + ...(endpoint ? { endpoint } : {}), + }) + } + return new BedrockRuntimeClient({ + region: resolved.region, + credentials: resolved.credentials, + ...(endpoint ? { endpoint } : {}), + }) + })() } + return this.clientPromise } // --------------------------------------------------------------------------- @@ -112,7 +147,9 @@ export class BedrockConverseTextAdapter< protected async sendStream( input: ConverseStreamCommandInput, ): Promise> { - const res = await this.client.send(new ConverseStreamCommand(input)) + const { ConverseStreamCommand } = await this.importBedrockRuntime() + const client = await this.getClient() + const res = await client.send(new ConverseStreamCommand(input)) if (!res.stream) { throw new Error('Bedrock Converse: empty stream response') } @@ -122,7 +159,9 @@ export class BedrockConverseTextAdapter< protected async send( input: ConverseCommandInput, ): Promise { - return this.client.send(new ConverseCommand(input)) + const { ConverseCommand } = await this.importBedrockRuntime() + const client = await this.getClient() + return client.send(new ConverseCommand(input)) } // --------------------------------------------------------------------------- diff --git a/packages/ai-bedrock/src/utils/auth.ts b/packages/ai-bedrock/src/utils/auth.ts index 32edca490..75cc19321 100644 --- a/packages/ai-bedrock/src/utils/auth.ts +++ b/packages/ai-bedrock/src/utils/auth.ts @@ -1,5 +1,6 @@ import { getApiKeyFromEnv } from '@tanstack/ai-utils' -import { fromNodeProviderChain } from '@aws-sdk/credential-providers' +import type { AwsCredentialIdentityProvider } from '@smithy/types' +import type * as CredentialProviders from '@aws-sdk/credential-providers' export type BedrockEndpoint = 'runtime' | 'mantle' @@ -14,7 +15,7 @@ export type ResolvedBedrockAuth = kind: 'sigv4' region: string service: string - credentials: ReturnType + credentials: AwsCredentialIdentityProvider } const DEFAULT_REGION = 'us-east-1' @@ -60,6 +61,19 @@ export function resolveBedrockAuth( kind: 'sigv4', region, service: sigv4Service(endpoint), - credentials: fromNodeProviderChain(), + // Lazy credential provider: the AWS SDK is Node/server-only, so we defer the + // dynamic import until SigV4 actually needs to resolve credentials. The + // specifier is held in a variable (not a string literal) so bundler dep + // scanners (e.g. Vite/esbuild optimizeDeps) cannot statically discover the + // AWS SDK and try to pre-bundle it for the browser — it would fail on the + // SDK's Node-only `fromTokenFile` export chain. `typeof import(...)` is a + // type-only reference (erased at emit) so we keep full typing. + credentials: async (...args) => { + const mod = '@aws-sdk/credential-providers' + const { fromNodeProviderChain } = (await import( + /* @vite-ignore */ mod + )) as typeof CredentialProviders + return fromNodeProviderChain()(...args) + }, } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 459751e88..ffe16d271 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1047,7 +1047,7 @@ importers: specifier: ^4.14.2 version: 4.14.2 '@tanstack/ai': - specifier: workspace:^ + specifier: workspace:* version: link:../ai '@tanstack/ai-utils': specifier: workspace:* From ee3173c43db69bc42146b402a25a1c7dbc3fb6c2 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sun, 31 May 2026 19:41:23 +0200 Subject: [PATCH 43/46] fix(e2e): pin bedrock matrix entry to api: 'chat' (Converse is now the default) --- testing/e2e/src/lib/providers.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testing/e2e/src/lib/providers.ts b/testing/e2e/src/lib/providers.ts index c421ff317..adee1b350 100644 --- a/testing/e2e/src/lib/providers.ts +++ b/testing/e2e/src/lib/providers.ts @@ -128,6 +128,9 @@ export function createTextAdapter( { baseURL: openaiUrl, defaultHeaders: testHeaders, + // Converse is now the default; this matrix entry exercises the + // OpenAI-compatible Chat Completions path, so pin api: 'chat'. + api: 'chat', }, ), }), From 6b8a81d84c8ba0b47b5deb488b54cbc9b0e0a7b8 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Thu, 25 Jun 2026 12:45:41 +0200 Subject: [PATCH 44/46] fix(bedrock): align openai dep and adapt to post-merge API changes - Bump openai to ^6.41.0 to match openai-base/ai-openai (fixes sherif dual-version error and the resulting dual-package type clash that broke ai-bedrock build/test:types, which cascaded to E2E and Preview). - converse-text buildInput now reads sampling options (temperature, top_p, max_completion_tokens) from modelOptions instead of removed top-level TextOptions fields, mapping them to Converse inferenceConfig. - Make bedrock.md stream-consumption snippets type-check under kiira (ignore direct chat() iteration like other adapter docs; fix invalid model id), and exclude docs/superpowers/** planning artifacts from kiira. --- docs/adapters/bedrock.md | 26 ++++++++++++++----- kiira.config.ts | 4 +++ packages/ai-bedrock/package.json | 2 +- .../ai-bedrock/src/adapters/converse-text.ts | 22 +++++++++------- pnpm-lock.yaml | 2 +- 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/docs/adapters/bedrock.md b/docs/adapters/bedrock.md index b4c99ec97..35bb42ae9 100644 --- a/docs/adapters/bedrock.md +++ b/docs/adapters/bedrock.md @@ -39,7 +39,13 @@ No additional packages are required. SigV4 authentication is handled by `@aws-sd The default `bedrockText` call uses the Converse API and reaches the broad model catalog: -```typescript +```typescript ignore +// ignore: iterating a chat() stream and reading chunk.type/chunk.delta needs the +// AG-UI base event fields, which come from @ag-ui/core. It's a transitive dep of +// @tanstack/ai, so kiira (resolving @tanstack/ai from source under the dist->src +// heuristic) can't follow it and those base fields drop off StreamChunk. The code +// is correct (the same pattern is used throughout ai-client); see +// getting-started/quick-start-server for the type-checked consumption shape. import { bedrockText } from '@tanstack/ai-bedrock' import { chat } from '@tanstack/ai' @@ -51,7 +57,7 @@ for await (const chunk of chat({ adapter, messages: [{ role: 'user', content: 'What is the capital of France?' }], })) { - if (chunk.type === 'content') process.stdout.write(chunk.delta) + if (chunk.type === 'TEXT_MESSAGE_CONTENT') process.stdout.write(chunk.delta ?? '') } ``` @@ -149,11 +155,14 @@ Set `api: 'chat'` to use Bedrock's OpenAI-compatible Chat Completions endpoint. **Model scope:** Open-weight models only — gpt-oss, DeepSeek V3.x, Gemma, Qwen, Mistral open models, GLM, and similar. Claude, Nova, and Llama are NOT available on this endpoint. See the [AWS API compatibility matrix](https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html) for the current list. -```typescript +```typescript ignore +// ignore: see the Converse quick-start above — iterating a chat() stream and +// reading chunk.type/chunk.delta needs @ag-ui/core base fields kiira can't +// resolve transitively through @tanstack/ai source. import { bedrockText } from '@tanstack/ai-bedrock' import { chat } from '@tanstack/ai' -const adapter = bedrockText('openai.gpt-oss-mini-1:0', { +const adapter = bedrockText('openai.gpt-oss-20b-1:0', { region: 'us-east-1', api: 'chat', }) @@ -162,7 +171,7 @@ for await (const chunk of chat({ adapter, messages: [{ role: 'user', content: 'What is the capital of France?' }], })) { - if (chunk.type === 'content') process.stdout.write(chunk.delta) + if (chunk.type === 'TEXT_MESSAGE_CONTENT') process.stdout.write(chunk.delta ?? '') } ``` @@ -172,7 +181,10 @@ Set `api: 'responses'` to use Bedrock's OpenAI-compatible Responses API. Returns **Model scope:** Currently the OpenAI gpt-oss family. The Responses API is stateful — pass `previous_response_id` and `store` through `modelOptions` to continue a conversation server-side. -```typescript +```typescript ignore +// ignore: see the Converse quick-start above — iterating a chat() stream and +// reading chunk.type/chunk.delta needs @ag-ui/core base fields kiira can't +// resolve transitively through @tanstack/ai source. import { bedrockText } from '@tanstack/ai-bedrock' import { chat } from '@tanstack/ai' @@ -185,7 +197,7 @@ for await (const chunk of chat({ adapter, messages: [{ role: 'user', content: 'Summarize the Bedrock pricing page.' }], })) { - if (chunk.type === 'content') process.stdout.write(chunk.delta) + if (chunk.type === 'TEXT_MESSAGE_CONTENT') process.stdout.write(chunk.delta ?? '') } ``` diff --git a/kiira.config.ts b/kiira.config.ts index 16eecbd27..07fb98dce 100644 --- a/kiira.config.ts +++ b/kiira.config.ts @@ -14,6 +14,10 @@ export default defineConfig({ // ai-angular exposes a source-resolvable entry. 'docs/api/ai-angular.md', 'docs/getting-started/quick-start-angular.md', + // docs/superpowers/** are internal planning/spec artifacts (design docs and + // implementation plans), not published, curated examples. Their snippets are + // illustrative pseudo-code, not meant to compile against package source. + 'docs/superpowers/**', ], defaultValidate: 'type', languages: ['ts', 'tsx', 'js', 'jsx'], diff --git a/packages/ai-bedrock/package.json b/packages/ai-bedrock/package.json index 0550c6360..19ad3fe1f 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -60,6 +60,6 @@ "@smithy/types": "^4.14.2", "@tanstack/ai-utils": "workspace:*", "@tanstack/openai-base": "workspace:*", - "openai": "^6.9.1" + "openai": "^6.41.0" } } diff --git a/packages/ai-bedrock/src/adapters/converse-text.ts b/packages/ai-bedrock/src/adapters/converse-text.ts index fab5b869b..c4862becf 100644 --- a/packages/ai-bedrock/src/adapters/converse-text.ts +++ b/packages/ai-bedrock/src/adapters/converse-text.ts @@ -474,18 +474,20 @@ export class BedrockConverseTextAdapter< ? toToolConfig(convertTools(options.tools), 'auto') : undefined + // Sampling options live on `modelOptions` (Bedrock surfaces the OpenAI + // Chat Completions field names); translate them into Converse's + // `inferenceConfig`, which uses AWS-native camelCase keys. + const modelOptions = options.modelOptions + const temperature = modelOptions?.temperature + const topP = modelOptions?.top_p + const maxTokens = modelOptions?.max_completion_tokens + const inferenceConfig = - options.temperature !== undefined || - options.topP !== undefined || - options.maxTokens !== undefined + temperature != null || topP != null || maxTokens != null ? { - ...(options.temperature !== undefined && { - temperature: options.temperature, - }), - ...(options.topP !== undefined && { topP: options.topP }), - ...(options.maxTokens !== undefined && { - maxTokens: options.maxTokens, - }), + ...(temperature != null && { temperature }), + ...(topP != null && { topP }), + ...(maxTokens != null && { maxTokens }), } : undefined diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2cbd83a3..0d19602d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1190,7 +1190,7 @@ importers: specifier: workspace:* version: link:../openai-base openai: - specifier: ^6.9.1 + specifier: ^6.41.0 version: 6.41.0(ws@8.20.1)(zod@4.3.6) zod: specifier: ^4.0.0 From 16a73d6110813bd1988756638e9f9186ecc15de6 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Thu, 25 Jun 2026 12:55:37 +0200 Subject: [PATCH 45/46] fix(bedrock): address review feedback (tool choice none, client retry, docs) - toToolConfig now omits the tool config entirely for choice: 'none' (Bedrock treats a present config with no toolChoice as auto), and update its unit test to assert the new contract. - getClient clears the cached client promise on failure so a later call can retry instead of replaying a permanently rejected promise. - testing/e2e/README: list bedrock/bedrock-responses under providers tested. --- packages/ai-bedrock/src/adapters/converse-text.ts | 7 ++++++- packages/ai-bedrock/src/converse/tool-converter.ts | 5 +++++ .../ai-bedrock/tests/converse/tool-converter.test.ts | 9 ++++++--- testing/e2e/README.md | 2 +- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/ai-bedrock/src/adapters/converse-text.ts b/packages/ai-bedrock/src/adapters/converse-text.ts index c4862becf..ac6ce827e 100644 --- a/packages/ai-bedrock/src/adapters/converse-text.ts +++ b/packages/ai-bedrock/src/adapters/converse-text.ts @@ -135,7 +135,12 @@ export class BedrockConverseTextAdapter< credentials: resolved.credentials, ...(endpoint ? { endpoint } : {}), }) - })() + })().catch((error: unknown) => { + // Don't cache a rejected promise — clear it so a later call can retry + // (e.g. after a transient import failure or fixed auth config). + this.clientPromise = undefined + throw error + }) } return this.clientPromise } diff --git a/packages/ai-bedrock/src/converse/tool-converter.ts b/packages/ai-bedrock/src/converse/tool-converter.ts index 2f41ff308..05367062c 100644 --- a/packages/ai-bedrock/src/converse/tool-converter.ts +++ b/packages/ai-bedrock/src/converse/tool-converter.ts @@ -21,6 +21,9 @@ export function toToolConfig( choice: ToolChoiceInput | undefined, ): ToolConfiguration | undefined { if (!tools.length) return undefined + // `none` means "don't call tools" — omit the tool config entirely, since + // Bedrock treats a present tool config with no `toolChoice` as auto. + if (choice === 'none') return undefined const toolChoice = mapChoice(choice) return { tools: tools.map((t) => ({ @@ -39,6 +42,8 @@ function mapChoice( ): ToolChoice | undefined { if (!choice || choice === 'auto') return { auto: {} } if (choice === 'required') return { any: {} } + // `none` is handled earlier in toToolConfig (omits the tool config); this + // branch keeps the string union narrowed so `choice.name` type-checks. if (choice === 'none') return undefined return { tool: { name: choice.name } } } diff --git a/packages/ai-bedrock/tests/converse/tool-converter.test.ts b/packages/ai-bedrock/tests/converse/tool-converter.test.ts index b2b3c9626..9c9d04ecf 100644 --- a/packages/ai-bedrock/tests/converse/tool-converter.test.ts +++ b/packages/ai-bedrock/tests/converse/tool-converter.test.ts @@ -46,8 +46,11 @@ describe('toToolConfig', () => { expect(toToolConfig([], 'auto')).toBeUndefined() }) - it('returns undefined toolChoice for "none" (caller omits tools instead)', () => { - const cfg = toToolConfig([{ name: 'a', inputSchema: {} }], 'none') - expect(cfg?.toolChoice).toBeUndefined() + it('returns undefined for "none" so no tool config is sent', () => { + // Bedrock treats a present tool config with no toolChoice as auto, so + // honoring "none" means omitting the whole config, not just toolChoice. + expect( + toToolConfig([{ name: 'a', inputSchema: {} }], 'none'), + ).toBeUndefined() }) }) diff --git a/testing/e2e/README.md b/testing/e2e/README.md index 0b146a6c5..aa8045e0c 100644 --- a/testing/e2e/README.md +++ b/testing/e2e/README.md @@ -4,7 +4,7 @@ End-to-end tests for TanStack AI using Playwright and [aimock](https://github.co **Architecture:** Playwright drives a TanStack Start app (`testing/e2e/`) which routes requests through provider adapters pointing at aimock. Fixtures define mock responses. No real API keys needed. All scenarios (including tool execution flows) use aimock fixtures. Tests run in parallel with per-test `X-Test-Id` isolation. -**Providers tested:** openai, anthropic, gemini, ollama, groq, grok, openrouter +**Providers tested:** openai, anthropic, gemini, ollama, groq, grok, openrouter, bedrock, bedrock-responses ## What's tested From 4bb126ac6f61f5affbd79ae946381b67ef719e91 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Thu, 25 Jun 2026 13:17:01 +0200 Subject: [PATCH 46/46] fix(bedrock): address maintainer review (structured output, model guard, lifecycle, auth, docs) - extractStructuredToolInput: only accept the forced structured tool (or an unnamed block); drop the second pass that returned any tool's input, so a hallucinated/leftover tool call no longer masquerades as the result. - build(): guard the api:'chat' branch with isChatModel instead of an unchecked `as BedrockChatModels` cast (mirrors isResponsesModel). - processConverseStream: thread incoming threadId/parentRunId/model onto RUN_STARTED/RUN_FINISHED so the chat path matches sibling adapters. - auth: read env directly (empty treated as absent) instead of nested blind try/catch; drop now-unused @tanstack/ai-utils dependency. - message-converter: log malformed/non-object tool-call args instead of silently coercing to {}; remove an unnecessary cast. - README: mention Converse (default), drop removed aws-sigv4-fetch peer steps, use current claude-haiku-4-5 id, fix stream-loop snippet. - Add unit tests for finishReason/usage folding, orphaned tool-call drain, reasoning->text ordering, lifecycle threading, and structuredOutputStream empty-response/parse-error paths. --- packages/ai-bedrock/README.md | 19 ++- packages/ai-bedrock/package.json | 1 - .../ai-bedrock/src/adapters/converse-text.ts | 21 +-- .../src/converse/message-converter.ts | 40 ++++-- .../src/converse/stream-processor.ts | 10 +- packages/ai-bedrock/src/index.ts | 15 ++- packages/ai-bedrock/src/utils/auth.ts | 18 ++- .../ai-bedrock/tests/converse/adapter.test.ts | 79 +++++++++++ .../tests/converse/stream-processor.test.ts | 127 ++++++++++++++++++ pnpm-lock.yaml | 3 - 10 files changed, 288 insertions(+), 45 deletions(-) diff --git a/packages/ai-bedrock/README.md b/packages/ai-bedrock/README.md index c4e2a269d..56492d805 100644 --- a/packages/ai-bedrock/README.md +++ b/packages/ai-bedrock/README.md @@ -1,6 +1,6 @@ # @tanstack/ai-bedrock -Amazon Bedrock adapter for TanStack AI — OpenAI-compatible Chat Completions and Responses APIs with streaming, tool calling, and reasoning. +Amazon Bedrock adapter for TanStack AI — the native Converse API (default) plus the OpenAI-compatible Chat Completions and Responses APIs, with streaming, tool calling, and reasoning. ## Installation @@ -28,7 +28,7 @@ Alternatively, configure AWS credentials for SigV4 auth (see below). import { bedrockText } from '@tanstack/ai-bedrock' import { chat } from '@tanstack/ai' -const adapter = bedrockText('us.anthropic.claude-3-7-sonnet-20250219-v1:0', { +const adapter = bedrockText('us.anthropic.claude-haiku-4-5-20251001-v1:0', { region: 'us-east-1', }) @@ -36,7 +36,8 @@ for await (const chunk of chat({ adapter, messages: [{ role: 'user', content: 'Hello from Bedrock!' }], })) { - if (chunk.type === 'content') process.stdout.write(chunk.delta) + if (chunk.type === 'TEXT_MESSAGE_CONTENT') + process.stdout.write(chunk.delta ?? '') } ``` @@ -70,16 +71,14 @@ Auth is resolved in this order: 1. Explicit `apiKey` passed to the factory 2. `BEDROCK_API_KEY` environment variable 3. `AWS_BEARER_TOKEN_BEDROCK` environment variable -4. SigV4 via the AWS credential chain (requires `pnpm add aws-sigv4-fetch`) +4. SigV4 via the AWS credential chain -To use SigV4, install the optional peer dependency and set `auth: 'sigv4'`: - -```bash -pnpm add aws-sigv4-fetch -``` +SigV4 signing is built in (via the bundled `@smithy/signature-v4`) — no +additional packages required. Set `auth: 'sigv4'` and provide AWS credentials +through the standard credential chain (env vars, shared config, instance role): ```typescript -const adapter = bedrockText('us.anthropic.claude-3-7-sonnet-20250219-v1:0', { +const adapter = bedrockText('us.anthropic.claude-haiku-4-5-20251001-v1:0', { auth: 'sigv4', region: 'us-east-1', }) diff --git a/packages/ai-bedrock/package.json b/packages/ai-bedrock/package.json index 19ad3fe1f..7d4672de4 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -58,7 +58,6 @@ "@aws-sdk/credential-providers": "^3.1057.0", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", - "@tanstack/ai-utils": "workspace:*", "@tanstack/openai-base": "workspace:*", "openai": "^6.41.0" } diff --git a/packages/ai-bedrock/src/adapters/converse-text.ts b/packages/ai-bedrock/src/adapters/converse-text.ts index ac6ce827e..5d450203a 100644 --- a/packages/ai-bedrock/src/adapters/converse-text.ts +++ b/packages/ai-bedrock/src/adapters/converse-text.ts @@ -183,7 +183,11 @@ export class BedrockConverseTextAdapter< ) const input = this.buildInput(options) const stream = await this.sendStream(input) - yield* processConverseStream(stream, () => this.generateId()) + yield* processConverseStream(stream, () => this.generateId(), { + threadId: options.threadId, + parentRunId: options.parentRunId, + model: options.model, + }) } catch (error: unknown) { const errorPayload = toRunErrorPayload( error, @@ -473,6 +477,7 @@ export class BedrockConverseTextAdapter< const { system, messages } = toConverseMessages( options.messages, options.systemPrompts, + options.logger, ) const toolConfig = options.tools @@ -537,9 +542,11 @@ function extractStructuredToolInput( const content: Array = message?.content ?? [] for (const block of content) { if ('toolUse' in block && block.toolUse) { - // Prefer the forced tool by name, but fall back to any toolUse block so a - // provider that omits/renames the tool name still yields its structured - // input rather than failing loud on a name mismatch. + // Only accept the forced structured tool (an unnamed block is allowed, + // since the forced tool is the only one configured). A differently-named + // tool-use block is a hallucinated/leftover call whose arbitrary input + // must not be returned as the validated result — leave it to the caller's + // `throw` so the failure is accurate instead of silently wrong. if ( block.toolUse.name === STRUCTURED_TOOL_NAME || block.toolUse.name === undefined @@ -548,12 +555,6 @@ function extractStructuredToolInput( } } } - // Second pass: accept the first toolUse block regardless of name. - for (const block of content) { - if ('toolUse' in block && block.toolUse) { - return block.toolUse.input - } - } return undefined } diff --git a/packages/ai-bedrock/src/converse/message-converter.ts b/packages/ai-bedrock/src/converse/message-converter.ts index df6f55321..da3088c97 100644 --- a/packages/ai-bedrock/src/converse/message-converter.ts +++ b/packages/ai-bedrock/src/converse/message-converter.ts @@ -1,4 +1,5 @@ import { normalizeSystemPrompts } from '@tanstack/ai' +import type { InternalLogger } from '@tanstack/ai/adapter-internals' import type { ContentPart, ContentPartDataSource, @@ -136,15 +137,15 @@ function contentPartToBlock(part: ContentPart, docIndex: number): ContentBlock { } // Fail loud for unsupported part types (audio, video, etc.) - const unsupported = (part as ContentPart).type throw new Error( - `Bedrock Converse does not support content part type "${String(unsupported)}".`, + `Bedrock Converse does not support content part type "${String(part.type)}".`, ) } function messageToBlocks( msg: ModelMessage, docCounter: { value: number }, + logger?: InternalLogger, ): Array { const blocks: Array = [] @@ -183,19 +184,41 @@ function messageToBlocks( if (msg.role === 'assistant' && msg.toolCalls) { for (const call of msg.toolCalls) { let input: DocumentType = {} + const rawArguments = call.function.arguments || '{}' try { - const parsed = JSON.parse(call.function.arguments || '{}') as unknown + const parsed = JSON.parse(rawArguments) as unknown if ( parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ) { input = parsed as DocumentType + } else { + // Parsed, but not a JSON object (array/number/string). These args came + // from a prior assistant turn the engine already accepted, so this + // usually signals an upstream bug; don't silently coerce without a trace. + logger?.errors( + `bedrock-converse: tool call "${call.function.name}" arguments are not a JSON object; forwarding empty input`, + { + source: 'bedrock-converse.messageToBlocks', + toolName: call.function.name, + rawArguments, + }, + ) } - } catch { - // Malformed / partial JSON — fall back to empty object so the call - // can still be forwarded rather than crashing the whole request. - input = {} + } catch (error: unknown) { + // Malformed / partial JSON — fall back to empty object so the call can + // still be forwarded rather than crashing the whole request, but log it + // since truncated/invalid args are almost always a real upstream issue. + logger?.errors( + `bedrock-converse: tool call "${call.function.name}" has malformed JSON arguments; forwarding empty input`, + { + source: 'bedrock-converse.messageToBlocks', + toolName: call.function.name, + rawArguments, + error: String(error), + }, + ) } blocks.push({ toolUse: { @@ -225,6 +248,7 @@ function messageToBlocks( export function toConverseMessages( messages: Array, systemPrompts?: Array, + logger?: InternalLogger, ): { system: Array; messages: Array } { // Build system blocks (uses normalizeSystemPrompts for runtime validation) const system: Array = normalizeSystemPrompts( @@ -242,7 +266,7 @@ export function toConverseMessages( const converseRole: 'user' | 'assistant' = msg.role === 'assistant' ? 'assistant' : 'user' - const blocks = messageToBlocks(msg, docCounter) + const blocks = messageToBlocks(msg, docCounter, logger) // Skip messages that produce no content blocks (e.g. assistant with // null content and no toolCalls). Pushing an empty-content message to diff --git a/packages/ai-bedrock/src/converse/stream-processor.ts b/packages/ai-bedrock/src/converse/stream-processor.ts index 3e69ca2f0..194994582 100644 --- a/packages/ai-bedrock/src/converse/stream-processor.ts +++ b/packages/ai-bedrock/src/converse/stream-processor.ts @@ -22,13 +22,19 @@ import type { ConverseStreamOutput } from '@aws-sdk/client-bedrock-runtime' * @param stream - The Converse event stream from `ConverseStreamCommand`. * @param newMessageId - Factory for fresh message/tool-call ids (the adapter * passes `() => this.generateId()`). + * @param lifecycle - Incoming run lifecycle ids, threaded onto the emitted + * `RUN_STARTED`/`RUN_FINISHED` so the chat path matches every sibling adapter + * (openai-base reuses `options.threadId`/`parentRunId`). Defaults preserve the + * previous behaviour (fresh `threadId`, no `parentRunId`). */ export async function* processConverseStream( stream: AsyncIterable, newMessageId: () => string, + lifecycle: { threadId?: string; parentRunId?: string; model?: string } = {}, ): AsyncIterable { const runId = newMessageId() - const threadId = newMessageId() + const threadId = lifecycle.threadId ?? newMessageId() + const { parentRunId, model } = lifecycle const messageId = newMessageId() let hasEmittedRunStarted = false @@ -65,6 +71,8 @@ export async function* processConverseStream( type: EventType.RUN_STARTED, runId, threadId, + parentRunId, + ...(model && { model }), timestamp: Date.now(), } } diff --git a/packages/ai-bedrock/src/index.ts b/packages/ai-bedrock/src/index.ts index 73bc89006..e75e5b74b 100644 --- a/packages/ai-bedrock/src/index.ts +++ b/packages/ai-bedrock/src/index.ts @@ -9,7 +9,7 @@ import { BedrockTextAdapter } from './adapters/text' import { BedrockResponsesTextAdapter } from './adapters/responses-text' import { BedrockConverseTextAdapter } from './adapters/converse-text' -import { BEDROCK_RESPONSES_MODELS } from './model-meta' +import { BEDROCK_CHAT_MODELS, BEDROCK_RESPONSES_MODELS } from './model-meta' import type { BedrockTextConfig } from './adapters/text' import type { BedrockResponsesConfig } from './adapters/responses-text' import type { BedrockConverseConfig } from './adapters/converse-text' @@ -43,6 +43,11 @@ function isResponsesModel(model: string): model is BedrockResponsesModels { return BEDROCK_RESPONSES_MODELS.some((m) => m === model) } +/** Cast-free runtime guard: is this model in the Chat-capable subset? */ +function isChatModel(model: string): model is BedrockChatModels { + return BEDROCK_CHAT_MODELS.some((m) => m === model) +} + /** Strip the `api` discriminator from a config without an unused-var lint error. */ function stripApi(config: T): Omit { const { api, ...rest } = config @@ -73,7 +78,13 @@ function build( return new BedrockResponsesTextAdapter(rest, model) } if (config?.api === 'chat') { - return new BedrockTextAdapter(stripApi(config), model as BedrockChatModels) + if (!isChatModel(model)) { + throw new Error( + `Model "${model}" is not available on the Bedrock Chat Completions API. ` + + `Chat-capable models: ${BEDROCK_CHAT_MODELS.join(', ')}.`, + ) + } + return new BedrockTextAdapter(stripApi(config), model) } // Default + explicit 'converse' return new BedrockConverseTextAdapter(config ? stripApi(config) : {}, model) diff --git a/packages/ai-bedrock/src/utils/auth.ts b/packages/ai-bedrock/src/utils/auth.ts index 75cc19321..a8fa7e997 100644 --- a/packages/ai-bedrock/src/utils/auth.ts +++ b/packages/ai-bedrock/src/utils/auth.ts @@ -1,4 +1,3 @@ -import { getApiKeyFromEnv } from '@tanstack/ai-utils' import type { AwsCredentialIdentityProvider } from '@smithy/types' import type * as CredentialProviders from '@aws-sdk/credential-providers' @@ -21,15 +20,14 @@ export type ResolvedBedrockAuth = const DEFAULT_REGION = 'us-east-1' function readApiKeyFromEnv(): string | undefined { - try { - return getApiKeyFromEnv('BEDROCK_API_KEY') - } catch { - try { - return getApiKeyFromEnv('AWS_BEARER_TOKEN_BEDROCK') - } catch { - return undefined - } - } + // Bedrock is server-only (the AWS SDK is Node-only), so the key always comes + // from process.env. Read it directly: a property access never throws, so — + // unlike the previous try/catch around getApiKeyFromEnv — we no longer swallow + // unrelated errors as "key not set". Empty/whitespace values are treated as + // absent so a present-but-blank var falls through to the next auth source. + const env = typeof process !== 'undefined' ? process.env : undefined + const key = env?.BEDROCK_API_KEY ?? env?.AWS_BEARER_TOKEN_BEDROCK + return key && key.trim() !== '' ? key : undefined } export interface BedrockAuthConfig { diff --git a/packages/ai-bedrock/tests/converse/adapter.test.ts b/packages/ai-bedrock/tests/converse/adapter.test.ts index eba7ed3d1..c15f358c4 100644 --- a/packages/ai-bedrock/tests/converse/adapter.test.ts +++ b/packages/ai-bedrock/tests/converse/adapter.test.ts @@ -159,6 +159,85 @@ describe('BedrockConverseTextAdapter', () => { expect((complete?.value as { object: unknown }).object).toEqual({ n: 5 }) }) + it('rejects a non-forced tool-use block in structuredOutput', async () => { + const a = new StubAdapter({ apiKey: 'k' }, 'us.amazon.nova-pro-v1:0') + // The model emitted a different (hallucinated/leftover) tool — its input + // must NOT be returned as the structured result; structuredOutput throws. + a.nonStreamOutput = { + output: { + message: { + role: 'assistant', + content: [ + { + toolUse: { + toolUseId: 'x', + name: 'some_other_tool', + input: { wrong: true }, + }, + }, + ], + }, + }, + } as unknown as ConverseCommandOutput + await expect( + a.structuredOutput({ + chatOptions: textOptions({ + messages: [{ role: 'user', content: 'go' }], + }), + outputSchema: { type: 'object', properties: { n: { type: 'number' } } }, + }), + ).rejects.toThrow(/no forced-tool output/) + }) + + it('emits RUN_ERROR(empty-response) when structuredOutputStream yields no content', async () => { + const a = new StubAdapter({ apiKey: 'k' }, 'us.amazon.nova-pro-v1:0') + a.streamEvents = [ + { messageStart: { role: 'assistant' } }, + { messageStop: { stopReason: 'end_turn' } }, + ] + const errors = [] as Array<{ code?: string }> + for await (const c of a.structuredOutputStream({ + chatOptions: textOptions({ messages: [{ role: 'user', content: 'go' }] }), + outputSchema: { type: 'object', properties: { n: { type: 'number' } } }, + })) { + if (c.type === EventType.RUN_ERROR) + errors.push(c as unknown as { code?: string }) + } + expect(errors).toHaveLength(1) + expect(errors[0]?.code).toBe('empty-response') + }) + + it('emits RUN_ERROR(parse-error) when structuredOutputStream content is invalid JSON', async () => { + const a = new StubAdapter({ apiKey: 'k' }, 'us.amazon.nova-pro-v1:0') + a.streamEvents = [ + { messageStart: { role: 'assistant' } }, + { + contentBlockStart: { + start: { toolUse: { toolUseId: 's', name: 'structured_output' } }, + contentBlockIndex: 0, + }, + }, + { + contentBlockDelta: { + // Truncated/invalid JSON fragment — JSON.parse will throw. + delta: { toolUse: { input: '{"n":' } }, + contentBlockIndex: 0, + }, + }, + { messageStop: { stopReason: 'tool_use' } }, + ] + const errors = [] as Array<{ code?: string }> + for await (const c of a.structuredOutputStream({ + chatOptions: textOptions({ messages: [{ role: 'user', content: 'go' }] }), + outputSchema: { type: 'object', properties: { n: { type: 'number' } } }, + })) { + if (c.type === EventType.RUN_ERROR) + errors.push(c as unknown as { code?: string }) + } + expect(errors).toHaveLength(1) + expect(errors[0]?.code).toBe('parse-error') + }) + it('declares it does not support combined tools and schema', () => { const a = new BedrockConverseTextAdapter( { apiKey: 'k' }, diff --git a/packages/ai-bedrock/tests/converse/stream-processor.test.ts b/packages/ai-bedrock/tests/converse/stream-processor.test.ts index 5c310ce5c..a4c34f0d6 100644 --- a/packages/ai-bedrock/tests/converse/stream-processor.test.ts +++ b/packages/ai-bedrock/tests/converse/stream-processor.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { EventType } from '@tanstack/ai' +import type { StreamChunk } from '@tanstack/ai' import { processConverseStream } from '../../src/converse/stream-processor' import type { ConverseStreamOutput } from '@aws-sdk/client-bedrock-runtime' @@ -117,4 +118,130 @@ describe('processConverseStream', () => { } expect(types).toContain(EventType.REASONING_MESSAGE_CONTENT) }) + + // Unique-id factory so RUN_STARTED/TEXT_MESSAGE/etc. carry distinct ids and + // payload assertions aren't ambiguous. + function counter() { + let n = 0 + return () => `id-${n++}` + } + + async function collect( + ...events: Array + ): Promise> { + const out: Array = [] + for await (const c of processConverseStream(gen(...events), counter())) { + out.push(c) + } + return out + } + + it('folds usage and maps each stopReason into the terminal RUN_FINISHED', async () => { + const cases: Array<[string, string]> = [ + ['tool_use', 'tool_calls'], + ['max_tokens', 'length'], + ['content_filtered', 'content_filter'], + ['end_turn', 'stop'], + ['some_unknown_reason', 'stop'], + ] + for (const [stopReason, expected] of cases) { + const events = await collect( + { contentBlockDelta: { delta: { text: 'hi' }, contentBlockIndex: 0 } }, + { messageStop: { stopReason } }, + { + metadata: { + usage: { inputTokens: 7, outputTokens: 11, totalTokens: 18 }, + }, + }, + ) + const finished = events.filter((e) => e.type === EventType.RUN_FINISHED) + expect(finished).toHaveLength(1) + const evt = finished[0] as { + finishReason: string + usage?: { + promptTokens: number + completionTokens: number + totalTokens: number + } + } + expect(evt.finishReason).toBe(expected) + // Usage arrives in the trailing metadata event (after messageStop) yet is + // folded into the single terminal RUN_FINISHED. + expect(evt.usage).toEqual({ + promptTokens: 7, + completionTokens: 11, + totalTokens: 18, + }) + } + }) + + it('drains a tool call that never received contentBlockStop (truncated stream)', async () => { + const events = await collect( + { + contentBlockStart: { + start: { toolUse: { toolUseId: 't9', name: 'getX' } }, + contentBlockIndex: 0, + }, + }, + { + contentBlockDelta: { + delta: { toolUse: { input: '{"a":1}' } }, + contentBlockIndex: 0, + }, + }, + // No contentBlockStop, no messageStop — stream just ends. + ) + const ends = events.filter((e) => e.type === EventType.TOOL_CALL_END) + expect(ends).toHaveLength(1) + expect((ends[0] as { toolCallId: string }).toolCallId).toBe('t9') + // The terminal RUN_FINISHED is still emitted after the drain. + expect(events.at(-1)?.type).toBe(EventType.RUN_FINISHED) + }) + + it('closes reasoning before opening the text message (ordering)', async () => { + const events = await collect( + { + contentBlockDelta: { + delta: { reasoningContent: { text: 'think' } }, + contentBlockIndex: 0, + }, + }, + { + contentBlockDelta: { delta: { text: 'answer' }, contentBlockIndex: 1 }, + }, + { messageStop: { stopReason: 'end_turn' } }, + ) + const order = events.map((e) => e.type) + const reasoningEnd = order.indexOf(EventType.REASONING_MESSAGE_END) + const textStart = order.indexOf(EventType.TEXT_MESSAGE_START) + expect(reasoningEnd).toBeGreaterThanOrEqual(0) + expect(textStart).toBeGreaterThanOrEqual(0) + expect(reasoningEnd).toBeLessThan(textStart) + }) + + it('threads incoming threadId/parentRunId/model onto the lifecycle', async () => { + const out: Array = [] + for await (const c of processConverseStream( + gen( + { contentBlockDelta: { delta: { text: 'hi' }, contentBlockIndex: 0 } }, + { messageStop: { stopReason: 'end_turn' } }, + ), + counter(), + { threadId: 'thread-x', parentRunId: 'parent-y', model: 'model-z' }, + )) { + out.push(c) + } + const started = out.find((e) => e.type === EventType.RUN_STARTED) as { + threadId: string + parentRunId?: string + model?: string + } + expect(started.threadId).toBe('thread-x') + expect(started.parentRunId).toBe('parent-y') + expect(started.model).toBe('model-z') + const finished = out.find((e) => e.type === EventType.RUN_FINISHED) as { + threadId: string + } + expect(finished.threadId).toBe('thread-x') + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d19602d2..d702da12d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1183,9 +1183,6 @@ importers: '@tanstack/ai': specifier: workspace:* version: link:../ai - '@tanstack/ai-utils': - specifier: workspace:* - version: link:../ai-utils '@tanstack/openai-base': specifier: workspace:* version: link:../openai-base