From 885b05b8cc6f200f5b7da9489b3c5de8b73ebc58 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Sat, 30 May 2026 19:05:11 +0200 Subject: [PATCH 01/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] =?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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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', }, ), }),