feat(ai): added AWS auth and Bedrock stream decoding with SigV4

- Removed deprecated AWS/Google client and proxy dependencies from root and AI package manifests.
- Added AWS credential chaining, SigV4 signing, and Bedrock stream decoding with region fallback and CRC checks.
- Added local Google type mirrors and migrated providers to request plans with SSE fetch and token-based auth.
- Updated AI changelog with auth/stream fixes, and added fetch-stub and SigV4/event-stream tests.
This commit is contained in:
can1357
2026-05-17 02:36:43 +02:00
parent 42d86254df
commit 4e24a79002
17 changed files with 1904 additions and 674 deletions
+11 -281
View File
@@ -36,15 +36,10 @@
},
"dependencies": {
"@anthropic-ai/sdk": "catalog:",
"@aws-sdk/client-bedrock-runtime": "catalog:",
"@aws-sdk/credential-provider-node": "catalog:",
"@bufbuild/protobuf": "catalog:",
"@google/genai": "catalog:",
"@oh-my-pi/pi-utils": "catalog:",
"@smithy/node-http-handler": "catalog:",
"openai": "catalog:",
"partial-json": "catalog:",
"proxy-agent": "catalog:",
"zod": "catalog:",
},
"devDependencies": {
@@ -208,8 +203,6 @@
"catalog": {
"@agentclientprotocol/sdk": "0.21.0",
"@anthropic-ai/sdk": "^0.94.0",
"@aws-sdk/client-bedrock-runtime": "^3.1043.0",
"@aws-sdk/credential-provider-node": "^3.972.39",
"@babel/generator": "^7.29.1",
"@babel/parser": "^7.29.3",
"@babel/traverse": "^7.29.0",
@@ -217,7 +210,6 @@
"@biomejs/biome": "^2.4.14",
"@bufbuild/protobuf": "^2.12.0",
"@bufbuild/protoc-gen-es": "^2.12.0",
"@google/genai": "^1.52.0",
"@mozilla/readability": "^0.6.0",
"@napi-rs/cli": "3.6.2",
"@oh-my-pi/omp-stats": "15.1.2",
@@ -231,7 +223,6 @@
"@opentelemetry/context-async-hooks": "^2.0.0",
"@opentelemetry/sdk-trace-base": "^2.0.0",
"@puppeteer/browsers": "^2.13.0",
"@smithy/node-http-handler": "^4.6.1",
"@tailwindcss/node": "^4.2.4",
"@tailwindcss/vite": "^4.2.4",
"@types/babel__generator": "^7.27.0",
@@ -259,7 +250,6 @@
"partial-json": "^0.1.7",
"postcss": "^8.5.14",
"prettier": "^3.8.3",
"proxy-agent": "^8.0.1",
"puppeteer-core": "^24.42.0",
"react": "19.2.5",
"react-chartjs-2": "^5.3.1",
@@ -281,78 +271,6 @@
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.94.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-OVlCttk5MyeTGtrWX5+F3MJOfEMDuEjK8+rm9aQMDfRPWndVMbhk37QG8WLnVbcc7huyUGngVMjT7iMN2llySA=="],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/sha256-browser": ["@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.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1045.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.8", "@aws-sdk/credential-provider-node": "^3.972.39", "@aws-sdk/eventstream-handler-node": "^3.972.14", "@aws-sdk/middleware-eventstream": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/middleware-websocket": "^3.972.16", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/token-providers": "3.1045.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/eventstream-serde-config-resolver": "^4.3.14", "@smithy/eventstream-serde-node": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.49", "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.6", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-aPC6gAz9uKRiwfnKB7peTs6yD0FpSzmVnSkx0f2QtJfosFM6J6KtBvR1lMKby050K4C4PAyEScwA5YTsGfTcGA=="],
"@aws-sdk/core": ["@aws-sdk/core@3.974.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.22", "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw=="],
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.34", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ=="],
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.36", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.6.1", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" } }, "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg=="],
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/credential-provider-env": "^3.972.34", "@aws-sdk/credential-provider-http": "^3.972.36", "@aws-sdk/credential-provider-login": "^3.972.38", "@aws-sdk/credential-provider-process": "^3.972.34", "@aws-sdk/credential-provider-sso": "^3.972.38", "@aws-sdk/credential-provider-web-identity": "^3.972.38", "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ=="],
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww=="],
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.39", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.34", "@aws-sdk/credential-provider-http": "^3.972.36", "@aws-sdk/credential-provider-ini": "^3.972.38", "@aws-sdk/credential-provider-process": "^3.972.34", "@aws-sdk/credential-provider-sso": "^3.972.38", "@aws-sdk/credential-provider-web-identity": "^3.972.38", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg=="],
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.34", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q=="],
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/token-providers": "3.1041.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA=="],
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw=="],
"@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.14", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/eventstream-codec": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg=="],
"@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ=="],
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg=="],
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ=="],
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ=="],
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.37", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA=="],
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@smithy/core": "^3.23.17", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-retry": "^4.3.6", "tslib": "^2.6.2" } }, "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A=="],
"@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.16", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-format-url": "^3.972.10", "@smithy/eventstream-codec": "^4.2.14", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.6", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.8", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/signature-v4-multi-region": "^3.996.25", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.49", "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w=="],
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.13", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/config-resolver": "^4.4.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A=="],
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.25", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.37", "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw=="],
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1045.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-/o4qcty0DmQola0DBniRVeBakYY6ALOvKEFo1AtJpTmMn/cJ+Fk3RWGe5ieT/f/eYbHG9k5E7poKge/E+WGv4Q=="],
"@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="],
"@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="],
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-endpoints": "^3.4.2", "tslib": "^2.6.2" } }, "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g=="],
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ=="],
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="],
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g=="],
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.24", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw=="],
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.22", "", { "dependencies": { "@nodable/entities": "2.1.0", "@smithy/types": "^4.14.1", "fast-xml-parser": "5.7.2", "tslib": "^2.6.2" } }, "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA=="],
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
@@ -469,8 +387,6 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="],
"@inquirer/ansi": ["@inquirer/ansi@2.0.5", "", {}, "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw=="],
"@inquirer/checkbox": ["@inquirer/checkbox@5.1.5", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/core": "^11.1.10", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Jmf9tgBHIEK5SAOB7swYfStqmtkZb00xOTpSQmkoGEpdxOTpJi9RS0A8bkfDPHTTItZRJrRdZrEMu25wyj0VfQ=="],
@@ -679,26 +595,6 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.1", "", {}, "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="],
"@puppeteer/browsers": ["@puppeteer/browsers@2.13.2", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.3", "", { "os": "android", "cpu": "arm" }, "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw=="],
@@ -751,84 +647,6 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA=="],
"@smithy/config-resolver": ["@smithy/config-resolver@4.5.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-m5PNfr7xKdIegNG8DlLz+Gf/DlAhHWFGmFbe0DZo9pnvBwuZ3P/9OMtQU0UyWMYy8zjl+HDFVS7rdD9p2xEFjQ=="],
"@smithy/core": ["@smithy/core@3.24.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-rZ5YfycIXX6puoGjthnDiMpUgtKNOq3c7CndQYkCNYQTv26AiCrZQOJPy7ANSfZ6Okk3UvCRnmO1OYWlLnYZgg=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-5gi+28FH+RurB2+tcRH1CK7KiLJ0dVnabjWLY3DgeFLiU45dbyrsq7NOYvMUcHgu9LVZH5F7G+Qk1GdXF0y6jg=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-vBxRIMKUGxS6sifVJOhV50PY1w+4esgSgS6cgEa/EB0lJL3BuRP1oP6A1yTOX9j9eEwHi4bRHC94A2yhG/l0+Q=="],
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-JlY17/ZwBJ2O7FK/bKt8PZR+HBkyFwvgssgT6LiB0xYtz5/E5XG/HeKr5q2NMaVm8u8xjFfGk/6DVlbBe1qNkA=="],
"@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-1Pg7aqxIdMilTbGJKCHTx0toIkKSrHdO6VHCh9oCncWJG+1wkJa90O/xb9mmRPuoOFCg2DLZAqnRyuBiUQnNIA=="],
"@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-Xte1Td6CQpc/D0WnPZ2k98CvF7y1GopylMoGY/r26a9wbRHV5xusRbT6O9vouSeZlvtxoVb4ON/1fLRofO7m4Q=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-yxurumLvHfgYgM0FVtjOVIyBSJXfno4xKKOgD43wOk9Qh+2lTKfP9Qhu4JHU7IUwrqVPa888byUzomHMgvKVMg=="],
"@smithy/hash-node": ["@smithy/hash-node@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-4a+KoVqr1SZtw7cZvY24XU1S5OL+c23MdDQ3jFmMCQ5s9diBFdMG/UIgp5dNqlwvDrWA0U5KO+z3Gzq1ize+LA=="],
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-TaoGtqi2ZNdGzxUgYcLczjW8rb/h5DQ8vlCMYDSdZ4LRzGQrrEYgUjlZVM9dAagTsLK5gZx1f7+44sFTjz5vuQ=="],
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-IbSiS/3nOxsimCthzElEoBrjQo+Na4bsQ63qyC8qSI8lkMjOv9+VlosDQd8gfNolAD9XmC5tLqYTI0bJGJsscg=="],
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.5.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-ux8LgN/m/X7ET2ISRc8G4aKFI1QhINZtkKpoayNPTrhwpsCVxb47mlpYFuWceTlesc0Wmb0S9y6DP195ReQoXA=="],
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.6.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-8CtxY9aHT4f3UvZUbU2O0bccRckqTDfTKk3t1DawUZa5DWRZdV2AMABLsdMTdj7KE1uumhzEaT0X7/jTcOtoBw=="],
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-c+V02hZlIStscI4ie2VllJjM4DLxdI2SymIBvXmqCqicrNb0NAbgDXDTBiwcMiruaBOqEFYxpKXbz6JjsNEN3Q=="],
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-KtYcs+sJn7AiT0YdM53/6MT0dKsaW2MSAr9MpprRVSfwN9qyKQf2dBIuCXt18/nEZaWerol/bGaQ63G949aovw=="],
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-5RutFJsYoqK4tWYZOjGQrPLowGf2Ku8rbNuVeGkNJ5axIDO4LV/fydBojPtwcDz2zf87YNCOXfNyuEyAwYgI7A=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-PxF57Jr3dPm+RgZWekOL+o96FPdaT62xZUyDfi47uMRFi5rHpwO/ewFbrztrASQ/7H8moNi1sspIHihHpfoKsQ=="],
"@smithy/property-provider": ["@smithy/property-provider@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-/YBWtO2SdvPSAUk/Ke1Xpdg1E1lfaNGblla7mnIVGtaGkSQ5bK7KBZqpuj5IokHlU9UcLDvt2QwTLV7oRzBUTA=="],
"@smithy/protocol-http": ["@smithy/protocol-http@5.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-WG0LgSZg+WbvWYD04uwIYVyMEpyd0cPx1lkqx61JxunxiFti+wGoFiDKr6wswun1r25Z2f8yUoMQWyxjMnnXtw=="],
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-w1EVgJXg1R/f5iJlQatMBt7sP9tHhEscvK0lv62j/esnqRgdoQqlkcgHotfOJpg1CTtY8eUvze3v3EU91631IQ=="],
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.5.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-xATpw6gcurFztdsUrMNaKb2ugqk3545Whhqg7ZD4sxTg+zI27THjg3IY+InXsVWturOWdCdV+UHQx11g9Sp5Kw=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-nkdB9T8JS6iD5PukE5TB8KqcvMEPVPHVUY7J0odYJgyIM40Du2msUhBdoPNRqRArDDcGQqVQcbzu0CZA7b+Nkw=="],
"@smithy/smithy-client": ["@smithy/smithy-client@4.13.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lysfoRCr7PdD9CsPp9VQuJYRGI5mWYb8FRkbdBSQttxpQmW7tZsFgmpBNKVcgvBsAgBCkYX/UQs0NmznuBcZQQ=="],
"@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="],
"@smithy/url-parser": ["@smithy/url-parser@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-I5tCWs/ndLrJrbvlnsN1cOt8PVAbQEqg0nNeQqebD5ynQcbhgch9uA7KmpX9vfq/vEudq0iVYAOxt+4aBkUlWA=="],
"@smithy/util-base64": ["@smithy/util-base64@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-puJITyefgQ9a5F+wKylCLkf0VCwesWbaN4O3YCEalRin4N0CTPQu/XA3kz/QsMOTgd3knhd0BQwGCBm/tv0Y1A=="],
"@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-83U8xa8EmdExGzFuqBzgXvtmbLQIYcCuCNm5no4rlPqpGdOPGUufzMvLdlw+sPTb01qHIsDDNwOecm4s8ROOPw=="],
"@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-Ok2v9zPFfd6uOJMTIIJ8HFdCpARD77q4OHYhwhG9y5X1Y9oeQ0CHUQVJD6LhT6l8FUkFYisqcUaZSg7SArFUTA=="],
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-kAC6/UB9qW9r2xQAOko2iDxAXmRD2VGMZjnXSEacAhQySdJs58CwvoOE0tHWdtc/lWF4g78X6Z9ucLanJnuVUw=="],
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-jKezW5Taa+N2gbkB02UVijH1rFlEJC+cskZzwasFqFJMBBi/bcVgHqcYOX0WOnUk6MDZfHf0gEsr5Br4XMHiAg=="],
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-xYRuNHHIztu5AzruMJ8kTyA1JsBL/yZKvX5z/A7OHUxsf+rkEESZFZWJDcAj5dDWSu6brWFe5KH6qJNTVztX/w=="],
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.5.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-pcvTCp9Wch/9UnWWfRGoG5GJogDXFPjevE+CqALxtPFGA4GqFQRD6eUtgJhHN+NPtohcozI12u1skF2/iubGrQ=="],
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-ZkAHu0SAsXPkVpaP6dhzu+DO/i4mlAMmwa4tejbGv9shozy/m4a2vIAk6HjPy7fKuGpANE1tZczGfCSLgyw5jA=="],
"@smithy/util-middleware": ["@smithy/util-middleware@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-X/DNQxgUCbjjs3HosLmt5Yi1NocxjRFiiOgHml4tVV3w4mIbqZxPR8kq7apGPEMnhIpyxeTgFyypMrfxfn2DlQ=="],
"@smithy/util-retry": ["@smithy/util-retry@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-pV/Kq4jUuP9raOqwSPeBiut2IWmwbc9vM+nE3ly4YUkzPHbBZvfhikwMOyudER+KHPjakuc8r4TecEPMsI7nVg=="],
"@smithy/util-stream": ["@smithy/util-stream@4.6.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-BlWg46UASokl3O5YqWmbLpINE5stmAxynXlyOe1nE4dx+tvwgqtT4ug/rPcRg0xVcBnj68XlcOqbXeaGGcH0DA=="],
"@smithy/util-utf8": ["@smithy/util-utf8@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-5hrmCc+dTgZkiFhX72Q16LemYPkvZ1M4pFMOhk0X9tQnLY7dn7zC1+C+aAJn0dw6CXldbqY/KMbMYCwm8yw14g=="],
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
"@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="],
@@ -887,8 +705,6 @@
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
"@types/turndown": ["@types/turndown@5.0.6", "", {}, "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg=="],
@@ -915,7 +731,7 @@
"@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="],
"agent-base": ["agent-base@9.0.0", "", {}, "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
@@ -957,20 +773,14 @@
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
@@ -1019,13 +829,13 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"data-uri-to-buffer": ["data-uri-to-buffer@8.0.0", "", {}, "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ=="],
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"degenerator": ["degenerator@7.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" }, "peerDependencies": { "quickjs-wasi": "^2.2.0" } }, "sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w=="],
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -1045,8 +855,6 @@
"duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.353", "", {}, "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w=="],
"elkjs": ["elkjs@0.11.1", "", {}, "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg=="],
@@ -1085,8 +893,6 @@
"exifr": ["exifr@7.1.3", "", {}, "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
@@ -1107,8 +913,6 @@
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-stream-rotator": ["file-stream-rotator@0.6.1", "", { "dependencies": { "moment": "^2.29.1" } }, "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ=="],
@@ -1117,14 +921,8 @@
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
@@ -1133,11 +931,7 @@
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"get-uri": ["get-uri@8.0.0", "", { "dependencies": { "basic-ftp": "^5.2.0", "data-uri-to-buffer": "8.0.0", "debug": "^4.3.4" } }, "sha512-CqtZlMKvfJeY0Zxv8wazDwXmSKmnMnsmNy8j8+wudi8EyG/pMUB1NqHc+Tv1QaNtpYsK9nOYjb7r7Ufu32RPSw=="],
"google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="],
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
"get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@@ -1149,9 +943,9 @@
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
"http-proxy-agent": ["http-proxy-agent@9.0.0", "", { "dependencies": { "agent-base": "9.0.0", "debug": "^4.3.4" } }, "sha512-FcF8VhXYLQcxWCnt/cCpT2apKsRDUGeVEeMqGu4HSTu29U8Yw0TLOjdYIlDsYk3IkUh+taX4IDWpPcCqKDhCjA=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@9.0.0", "", { "dependencies": { "agent-base": "9.0.0", "debug": "^4.3.4" } }, "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
@@ -1179,8 +973,6 @@
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
"json-with-bigint": ["json-with-bigint@3.5.8", "", {}, "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw=="],
@@ -1189,10 +981,6 @@
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
@@ -1231,8 +1019,6 @@
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="],
"lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="],
@@ -1273,10 +1059,6 @@
"netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"node-releases": ["node-releases@2.0.44", "", {}, "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
@@ -1295,11 +1077,9 @@
"option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="],
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
"pac-proxy-agent": ["pac-proxy-agent@9.0.1", "", { "dependencies": { "agent-base": "9.0.0", "debug": "^4.3.4", "get-uri": "8.0.0", "http-proxy-agent": "9.0.0", "https-proxy-agent": "9.0.0", "pac-resolver": "9.0.1", "quickjs-wasi": "^2.2.0", "socks-proxy-agent": "10.0.0" } }, "sha512-3ZOSpLboOlpW4yp8Cuv21KlTULRqyJ5Uuad3wXpSKFrxdNgcHEyoa22GRaZ2UlgCVuR6z+5BiavtYVvbajL/Yw=="],
"pac-resolver": ["pac-resolver@9.0.1", "", { "dependencies": { "degenerator": "7.0.1", "netmask": "^2.0.2" }, "peerDependencies": { "quickjs-wasi": "^2.2.0" } }, "sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw=="],
"pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
@@ -1325,18 +1105,14 @@
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"protobufjs": ["protobufjs@7.5.8", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA=="],
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
"proxy-agent": ["proxy-agent@8.0.1", "", { "dependencies": { "agent-base": "9.0.0", "debug": "^4.3.4", "http-proxy-agent": "9.0.0", "https-proxy-agent": "9.0.0", "lru-cache": "^7.14.1", "pac-proxy-agent": "9.0.1", "proxy-from-env": "^2.0.0", "socks-proxy-agent": "10.0.0" } }, "sha512-kccqGBqHZXR8onQhY/ganJjoO8QIKKRiFBhPOzbTZK16attzSZ/0XSmp9H7jrRxPKHjhGyx1q32lMPrJ3uLFgA=="],
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"puppeteer-core": ["puppeteer-core@24.43.1", "", { "dependencies": { "@puppeteer/browsers": "2.13.2", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1608973", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.20.0" } }, "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw=="],
"quickjs-wasi": ["quickjs-wasi@2.2.0", "", {}, "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw=="],
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
"react-chartjs-2": ["react-chartjs-2@5.3.1", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A=="],
@@ -1351,8 +1127,6 @@
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"robomp-web": ["robomp-web@workspace:python/robomp/web"],
@@ -1387,7 +1161,7 @@
"socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="],
"socks-proxy-agent": ["socks-proxy-agent@10.0.0", "", { "dependencies": { "agent-base": "9.0.0", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-pyp2YR3mNxAMu0mGLtzs4g7O3uT4/9sQOLAKcViAkaS9fJWkud7nmaf6ZREFqQEi24IPkBcjfHjXhPTUWjo3uA=="],
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="],
@@ -1471,8 +1245,6 @@
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.1", "", {}, "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw=="],
"win-guid": ["win-guid@0.2.1", "", {}, "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A=="],
@@ -1511,14 +1283,6 @@
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1041.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.8", "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw=="],
"@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.2", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.5", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
@@ -1527,8 +1291,6 @@
"@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@puppeteer/browsers/proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@@ -1553,20 +1315,12 @@
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"jwa/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"jws/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
@@ -1585,26 +1339,10 @@
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
"@puppeteer/browsers/proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"@puppeteer/browsers/proxy-agent/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"@puppeteer/browsers/proxy-agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"@puppeteer/browsers/proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"@puppeteer/browsers/proxy-agent/pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
"@puppeteer/browsers/proxy-agent/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"@puppeteer/browsers/proxy-agent/socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
@@ -1613,16 +1351,8 @@
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"@puppeteer/browsers/proxy-agent/pac-proxy-agent/get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
"@puppeteer/browsers/proxy-agent/pac-proxy-agent/pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
"cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"@puppeteer/browsers/proxy-agent/pac-proxy-agent/get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
"@puppeteer/browsers/proxy-agent/pac-proxy-agent/pac-resolver/degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
"cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
}
}
-5
View File
@@ -11,8 +11,6 @@
"catalog": {
"@agentclientprotocol/sdk": "0.21.0",
"@anthropic-ai/sdk": "^0.94.0",
"@aws-sdk/client-bedrock-runtime": "^3.1043.0",
"@aws-sdk/credential-provider-node": "^3.972.39",
"@babel/generator": "^7.29.1",
"@babel/parser": "^7.29.3",
"@babel/traverse": "^7.29.0",
@@ -20,7 +18,6 @@
"@biomejs/biome": "^2.4.14",
"@bufbuild/protobuf": "^2.12.0",
"@bufbuild/protoc-gen-es": "^2.12.0",
"@google/genai": "^1.52.0",
"@mozilla/readability": "^0.6.0",
"@napi-rs/cli": "3.6.2",
"@oh-my-pi/omp-stats": "15.1.2",
@@ -34,7 +31,6 @@
"@opentelemetry/context-async-hooks": "^2.0.0",
"@opentelemetry/sdk-trace-base": "^2.0.0",
"@puppeteer/browsers": "^2.13.0",
"@smithy/node-http-handler": "^4.6.1",
"@tailwindcss/node": "^4.2.4",
"@tailwindcss/vite": "^4.2.4",
"@types/babel__generator": "^7.27.0",
@@ -62,7 +58,6 @@
"partial-json": "^0.1.7",
"postcss": "^8.5.14",
"prettier": "^3.8.3",
"proxy-agent": "^8.0.1",
"puppeteer-core": "^24.42.0",
"react": "19.2.5",
"react-chartjs-2": "^5.3.1",
+6
View File
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Renamed public schema utilities in `@oh-my-pi/pi-ai/utils/schema` by replacing `sanitizeSchemaForGoogle`, `sanitizeSchemaForCCA`, `prepareSchemaForCCA`, and `sanitizeSchemaForMCP` with `normalizeSchemaForGoogle`, `normalizeSchemaForCCA`, and `normalizeSchemaForMCP`
@@ -10,6 +11,7 @@
### Added
- Added Vertex AI authentication via Google Application Default Credentials from `GOOGLE_APPLICATION_CREDENTIALS`, `~/.config/gcloud/application_default_credentials.json`, or metadata server tokens, with token caching and refresh skew control via `GOOGLE_VERTEX_REFRESH_SKEW_MS`
- Added support for Anthropic image message parts with `type: "url"` and `type: "file"` sources
- Added `stopSequences` and `frequencyPenalty` to shared stream options and wired them through to OpenAI request translation
- Added optional request cancellation support to auth-broker interactions by propagating `AbortSignal` into health, snapshot, usage, and refresh calls
@@ -38,6 +40,7 @@
### Changed
- Changed Bedrock credential resolution for streaming calls to prefer environment keys, AWS profile/SSO credentials, and IMDSv2 fallback when available
- Changed auth-gateway parsing for OpenAI chat-completions and Responses to ignore unsupported SDK-only fields instead of rejecting requests
- Changed auth-gateway protocol handling to include CORS headers on responses and support browser-origin requests
- Changed prompt-cache handling to resolve cache keys from request metadata and headers and preserve them through protocol translation
@@ -52,6 +55,9 @@
### Fixed
- Fixed Google Generative AI startup behavior to throw a clear API-key-required error when no key is configured
- Fixed AWS Bedrock image message serialization to preserve base64 `source.bytes` payloads instead of decoding and rebuilding them
- Fixed Google provider error handling to extract the API-reported `error.message` from JSON response bodies when available
- Fixed `RemoteAuthCredentialStore.getUsageReport` to return the matching credential-specific usage report and coalesce parallel callers into one broker `/v1/usage` fetch
- Fixed auth-broker credential upload validation to reject the remote refresh-token sentinel and prevent storing a non-refresh value
- Fixed OpenAI Responses streaming output to emit `reasoning_summary_text` events and parse/send `summary_text` reasoning payloads
-5
View File
@@ -42,15 +42,10 @@
},
"dependencies": {
"@anthropic-ai/sdk": "catalog:",
"@aws-sdk/client-bedrock-runtime": "catalog:",
"@aws-sdk/credential-provider-node": "catalog:",
"@bufbuild/protobuf": "catalog:",
"@google/genai": "catalog:",
"@oh-my-pi/pi-utils": "catalog:",
"@smithy/node-http-handler": "catalog:",
"openai": "catalog:",
"partial-json": "catalog:",
"proxy-agent": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
+277 -253
View File
@@ -1,28 +1,13 @@
import {
BedrockRuntimeClient,
type BedrockRuntimeClientConfig,
StopReason as BedrockStopReason,
type Tool as BedrockTool,
CachePointType,
CacheTTL,
type ContentBlock,
type ContentBlockDeltaEvent,
type ContentBlockStartEvent,
type ContentBlockStopEvent,
ConversationRole,
ConverseStreamCommand,
type ConverseStreamMetadataEvent,
ImageFormat,
type Message,
type SystemContentBlock,
type ToolChoice,
type ToolConfiguration,
ToolResultStatus,
} from "@aws-sdk/client-bedrock-runtime";
import { type DefaultProviderInit, defaultProvider } from "@aws-sdk/credential-provider-node";
import { $env, $flag } from "@oh-my-pi/pi-utils";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import { ProxyAgent } from "proxy-agent";
/**
* Amazon Bedrock Converse Stream provider.
*
* Talks directly to `bedrock-runtime.{region}.amazonaws.com` over HTTPS with
* SigV4 signing and decodes the `application/vnd.amazon.eventstream` response.
* No `@aws-sdk/*`, no `@smithy/*`, no `proxy-agent`. Proxies are honored via
* Bun's native `HTTPS_PROXY` support.
*/
import { $env, $flag, fetchWithRetry } from "@oh-my-pi/pi-utils";
import type { Effort } from "../model-thinking";
import { mapEffortToAnthropicAdaptiveEffort, requireSupportedEffort } from "../model-thinking";
import { calculateCost } from "../models";
@@ -47,6 +32,9 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
import { parseStreamingJson } from "../utils/json-parse";
import { toolWireSchema } from "../utils/schema/wire";
import { resolveAwsCredentials } from "./aws-credentials";
import { decodeEventStream } from "./aws-eventstream";
import { signRequest } from "./aws-sigv4";
import { transformMessages } from "./transform-messages";
export interface BedrockOptions extends StreamOptions {
@@ -63,49 +51,93 @@ export interface BedrockOptions extends StreamOptions {
type Block = (TextContent | ThinkingContent | ToolCall) & { index?: number; partialJson?: string };
const BEDROCK_PROXY_ENV_KEYS = ["HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "https_proxy", "http_proxy", "all_proxy"];
// ---------- Bedrock wire-format types ----------
// Mirrors only what we actually consume from `ConverseStreamRequest` /
// `ConverseStreamOutput`. Keeps us decoupled from `@aws-sdk/client-bedrock-runtime`.
function hasBedrockProxyEnvironment(): boolean {
return BEDROCK_PROXY_ENV_KEYS.some(key => Boolean($env[key]?.trim()));
interface CachePoint {
cachePoint: { type: "default"; ttl?: "5m" | "1h" };
}
interface TextBlockWire {
text: string;
}
interface ImageBlockWire {
image: { format: "jpeg" | "png" | "gif" | "webp"; source: { bytes: string } };
}
interface ToolUseBlockWire {
toolUse: { toolUseId: string; name: string; input: unknown };
}
interface ToolResultBlockWire {
toolResult: {
toolUseId: string;
content: Array<TextBlockWire | ImageBlockWire>;
status: "success" | "error";
};
}
interface ReasoningBlockWire {
reasoningContent: { reasoningText: { text: string; signature?: string } };
}
function installBedrockHttp1Transport(config: BedrockRuntimeClientConfig): void {
const requestHandler = createBedrockHttp1RequestHandler();
config.requestHandler = requestHandler;
type UserContent = TextBlockWire | ImageBlockWire | ToolResultBlockWire | CachePoint;
type AssistantContent = TextBlockWire | ToolUseBlockWire | ReasoningBlockWire;
type SystemContent = TextBlockWire | CachePoint;
if (hasBedrockProxyEnvironment()) {
config.credentialDefaultProvider = createBedrockCredentialDefaultProvider(requestHandler);
}
interface WireMessage {
role: "user" | "assistant";
content: Array<UserContent | AssistantContent>;
}
function createBedrockHttp1RequestHandler(): NodeHttpHandler {
if (!hasBedrockProxyEnvironment()) {
return new NodeHttpHandler();
}
const agent = new ProxyAgent();
return new NodeHttpHandler({
httpAgent: agent,
httpsAgent: agent,
});
interface WireToolSpec {
toolSpec: { name: string; description: string; inputSchema: { json: unknown } };
}
interface WireToolChoice {
auto?: Record<string, never>;
any?: Record<string, never>;
tool?: { name: string };
}
interface WireToolConfig {
tools: WireToolSpec[];
toolChoice?: WireToolChoice;
}
function createBedrockCredentialDefaultProvider(
requestHandler: NodeHttpHandler,
): NonNullable<BedrockRuntimeClientConfig["credentialDefaultProvider"]> {
return (init?: DefaultProviderInit) =>
defaultProvider({
...init,
clientConfig: {
...init?.clientConfig,
requestHandler,
},
});
interface ConverseStreamRequest {
messages: WireMessage[];
system?: SystemContent[];
inferenceConfig?: { maxTokens?: number; temperature?: number; topP?: number };
toolConfig?: WireToolConfig;
additionalModelRequestFields?: Record<string, unknown>;
}
function isHttp2ResponseError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return /\bhttp2\b|http\/2/i.test(message);
// Streaming events (snake_case matches the JSON envelope key, but Bedrock uses camelCase).
interface MessageStartEvent {
role: "user" | "assistant";
}
interface ContentBlockStartEvent {
contentBlockIndex: number;
start?: { toolUse?: { toolUseId?: string; name?: string } };
}
interface ContentBlockDeltaEvent {
contentBlockIndex: number;
delta?: {
text?: string;
toolUse?: { input?: string };
reasoningContent?: { text?: string; signature?: string };
};
}
interface ContentBlockStopEvent {
contentBlockIndex: number;
}
interface MessageStopEvent {
stopReason?: string;
}
interface MetadataEvent {
usage?: {
inputTokens?: number;
outputTokens?: number;
cacheReadInputTokens?: number;
cacheWriteInputTokens?: number;
totalTokens?: number;
};
}
export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
@@ -139,37 +171,10 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
const blocks = output.content as Block[];
let rawRequestDump: RawHttpRequestDump | undefined;
const config: BedrockRuntimeClientConfig = {
region: options.region,
profile: options.profile,
};
let usesHttp1RequestHandler = false;
let messageStarted = false;
// in Node.js/Bun environment only
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
config.region = config.region || $env.AWS_REGION || $env.AWS_DEFAULT_REGION;
// Support proxies that don't need authentication
if ($flag("AWS_BEDROCK_SKIP_AUTH")) {
config.credentials = {
accessKeyId: "dummy-access-key",
secretAccessKey: "dummy-secret-key",
};
}
if ($flag("AWS_BEDROCK_FORCE_HTTP1") || hasBedrockProxyEnvironment()) {
usesHttp1RequestHandler = true;
installBedrockHttp1Transport(config);
}
}
config.region = config.region || "us-east-1";
const region = options.region || $env.AWS_REGION || $env.AWS_DEFAULT_REGION || "us-east-1";
try {
const cacheRetention = resolveCacheRetention(options.cacheRetention);
const toolConfig = convertToolConfig(context.tools, options.toolChoice);
let additionalModelRequestFields = buildAdditionalModelRequestFields(model, options);
@@ -177,87 +182,142 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
// When tool_choice forces tool use, disable thinking to avoid API errors.
if (toolConfig?.toolChoice && additionalModelRequestFields) {
const tc = toolConfig.toolChoice;
if ("any" in tc || "tool" in tc) {
additionalModelRequestFields = undefined;
}
if (tc.any || tc.tool) additionalModelRequestFields = undefined;
}
const commandInput = {
modelId: model.id,
const commandInput: ConverseStreamRequest = {
messages: convertMessages(context, model, cacheRetention),
system: buildSystemPrompt(context.systemPrompt, model, cacheRetention),
inferenceConfig: { maxTokens: options.maxTokens, temperature: options.temperature, topP: options.topP },
inferenceConfig: {
maxTokens: options.maxTokens,
temperature: options.temperature,
topP: options.topP,
},
toolConfig,
additionalModelRequestFields,
};
options?.onPayload?.(commandInput);
const host = `bedrock-runtime.${region}.amazonaws.com`;
const url = `https://${host}/model/${encodeURIComponent(model.id)}/converse-stream`;
const urlPath = `/model/${encodeURIComponent(model.id)}/converse-stream`;
rawRequestDump = {
provider: model.provider,
api: output.api,
model: model.id,
method: "POST",
url: `https://bedrock-runtime.${config.region}.amazonaws.com/model/${model.id}/converse-stream`,
url,
body: commandInput,
};
while (true) {
const client = new BedrockRuntimeClient(config);
try {
const command = new ConverseStreamCommand(commandInput);
const response = await client.send(command, { abortSignal: options.signal });
let credentials: { accessKeyId: string; secretAccessKey: string; sessionToken?: string };
if ($flag("AWS_BEDROCK_SKIP_AUTH")) {
credentials = { accessKeyId: "dummy-access-key", secretAccessKey: "dummy-secret-key" };
} else {
credentials = await resolveAwsCredentials({
profile: options.profile,
region,
signal: options.signal,
});
}
for await (const item of response.stream!) {
if (item.messageStart) {
messageStarted = true;
if (item.messageStart.role !== ConversationRole.ASSISTANT) {
throw new Error("Unexpected assistant message start but got user message start instead");
}
stream.push({ type: "start", partial: output });
} else if (item.contentBlockStart) {
if (!firstTokenTime) firstTokenTime = Date.now();
handleContentBlockStart(item.contentBlockStart, blocks, output, stream);
} else if (item.contentBlockDelta) {
if (!firstTokenTime) firstTokenTime = Date.now();
handleContentBlockDelta(item.contentBlockDelta, blocks, output, stream);
} else if (item.contentBlockStop) {
handleContentBlockStop(item.contentBlockStop, blocks, output, stream);
} else if (item.messageStop) {
output.stopReason = mapStopReason(item.messageStop.stopReason);
} else if (item.metadata) {
handleMetadata(item.metadata, model, output);
} else if (item.internalServerException) {
throw new Error(`Internal server error: ${item.internalServerException.message}`);
} else if (item.modelStreamErrorException) {
throw new Error(`Model stream error: ${item.modelStreamErrorException.message}`);
} else if (item.validationException) {
throw withHttpStatus(new Error(`Validation error: ${item.validationException.message}`), 400);
} else if (item.throttlingException) {
throw new Error(`Throttling error: ${item.throttlingException.message}`);
} else if (item.serviceUnavailableException) {
throw new Error(`Service unavailable: ${item.serviceUnavailableException.message}`);
const bodyText = JSON.stringify(commandInput);
const body = new TextEncoder().encode(bodyText);
const baseHeaders: Record<string, string> = {
"content-type": "application/json",
accept: "application/vnd.amazon.eventstream",
};
const signed = await signRequest({
method: "POST",
host,
path: urlPath,
body,
region,
service: "bedrock",
credentials,
headers: baseHeaders,
});
const requestHeaders: Record<string, string> = { ...baseHeaders, ...signed };
const response = await fetchWithRetry(url, {
method: "POST",
headers: requestHeaders,
body,
signal: options.signal,
});
if (!response.ok) {
const errBody = await response.text().catch(() => "");
throw withHttpStatus(
new Error(`Bedrock HTTP ${response.status}: ${errBody.slice(0, 1000)}`),
response.status,
);
}
if (!response.body) throw new Error("Bedrock response has no body");
// Track first event for the abort/diagnostic path (currently informational).
for await (const message of decodeEventStream(response.body)) {
const messageType = message.headers[":message-type"];
const eventType = message.headers[":event-type"];
if (messageType === "exception") {
const exceptionType = message.headers[":exception-type"] || "Exception";
const payload = safeParsePayload(message.payload) as { message?: string } | undefined;
const errorMessage = payload?.message || new TextDecoder().decode(message.payload);
const status = exceptionType === "validationException" ? 400 : 0;
const err = new Error(`${exceptionType}: ${errorMessage}`);
throw status ? withHttpStatus(err, status) : err;
}
if (messageType === "error") {
const code = message.headers[":error-code"] || "UnknownError";
const errorMessage = message.headers[":error-message"] || new TextDecoder().decode(message.payload);
throw new Error(`${code}: ${errorMessage}`);
}
if (messageType !== "event") continue;
const payload = safeParsePayload(message.payload);
if (!payload) continue;
switch (eventType) {
case "messageStart": {
// no-op: first event marker is implicit by stream entry.
const ev = payload as MessageStartEvent;
if (ev.role !== "assistant") {
throw new Error("Unexpected assistant message start but got user message start instead");
}
stream.push({ type: "start", partial: output });
break;
}
break;
} catch (error) {
if (
!usesHttp1RequestHandler &&
!messageStarted &&
output.content.length === 0 &&
isHttp2ResponseError(error)
) {
usesHttp1RequestHandler = true;
installBedrockHttp1Transport(config);
continue;
case "contentBlockStart": {
if (!firstTokenTime) firstTokenTime = Date.now();
handleContentBlockStart(payload as ContentBlockStartEvent, blocks, output, stream);
break;
}
throw error;
} finally {
client.destroy();
case "contentBlockDelta": {
if (!firstTokenTime) firstTokenTime = Date.now();
handleContentBlockDelta(payload as ContentBlockDeltaEvent, blocks, output, stream);
break;
}
case "contentBlockStop": {
handleContentBlockStop(payload as ContentBlockStopEvent, blocks, output, stream);
break;
}
case "messageStop": {
const ev = payload as MessageStopEvent;
output.stopReason = mapStopReason(ev.stopReason);
break;
}
case "metadata": {
handleMetadata(payload as MetadataEvent, model, output);
break;
}
default:
// Unknown event types (Bedrock may add new ones) — ignore.
break;
}
}
if (options.signal?.aborted) {
throw new Error("Request was aborted");
}
if (options.signal?.aborted) throw new Error("Request was aborted");
if (output.stopReason === "error" || output.stopReason === "aborted") {
throw new Error(output.errorMessage ?? "An unknown error occurred");
@@ -305,13 +365,22 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
return stream;
};
function safeParsePayload(payload: Uint8Array): unknown {
if (payload.length === 0) return {};
try {
return JSON.parse(new TextDecoder().decode(payload));
} catch {
return undefined;
}
}
function handleContentBlockStart(
event: ContentBlockStartEvent,
blocks: Block[],
output: AssistantMessage,
stream: AssistantMessageEventStream,
): void {
const index = event.contentBlockIndex!;
const index = event.contentBlockIndex;
const start = event.start;
if (start?.toolUse) {
@@ -334,13 +403,13 @@ function handleContentBlockDelta(
output: AssistantMessage,
stream: AssistantMessageEventStream,
): void {
const contentBlockIndex = event.contentBlockIndex!;
const contentBlockIndex = event.contentBlockIndex;
const delta = event.delta;
let index = blocks.findIndex(b => b.index === contentBlockIndex);
let block = blocks[index];
if (delta?.text !== undefined) {
// If no text block exists yet, create one, as `handleContentBlockStart` is not sent for text blocks
// If no text block exists yet, create one — `handleContentBlockStart` is not sent for text blocks
if (!block) {
const newBlock: Block = { type: "text", text: "", index: contentBlockIndex };
output.content.push(newBlock);
@@ -386,11 +455,7 @@ function handleContentBlockDelta(
}
}
function handleMetadata(
event: ConverseStreamMetadataEvent,
model: Model<"bedrock-converse-stream">,
output: AssistantMessage,
): void {
function handleMetadata(event: MetadataEvent, model: Model<"bedrock-converse-stream">, output: AssistantMessage): void {
if (event.usage) {
output.usage.input = event.usage.inputTokens || 0;
output.usage.output = event.usage.outputTokens || 0;
@@ -468,16 +533,16 @@ function buildSystemPrompt(
systemPrompt: readonly string[] | undefined,
model: Model<"bedrock-converse-stream">,
cacheRetention: CacheRetention,
): SystemContentBlock[] | undefined {
): SystemContent[] | undefined {
const prompts = systemPrompt?.map(prompt => prompt.toWellFormed()).filter(prompt => prompt.length > 0) ?? [];
if (prompts.length === 0) return undefined;
const blocks: SystemContentBlock[] = prompts.map(prompt => ({ text: prompt }));
const blocks: SystemContent[] = prompts.map(prompt => ({ text: prompt }));
// Add cache point for supported Claude models
if (cacheRetention !== "none" && supportsPromptCaching(model)) {
blocks.push({
cachePoint: { type: CachePointType.DEFAULT, ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}) },
cachePoint: { type: "default", ...(cacheRetention === "long" ? { ttl: "1h" } : {}) },
});
}
@@ -488,8 +553,8 @@ function convertMessages(
context: Context,
model: Model<"bedrock-converse-stream">,
cacheRetention: CacheRetention,
): Message[] {
const result: Message[] = [];
): WireMessage[] {
const result: WireMessage[] = [];
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
for (let i = 0; i < transformedMessages.length; i++) {
@@ -501,44 +566,34 @@ function convertMessages(
if (typeof m.content === "string") {
// Skip empty user messages
if (!m.content || m.content.trim() === "") continue;
result.push({
role: ConversationRole.USER,
content: [{ text: m.content.toWellFormed() }],
});
result.push({ role: "user", content: [{ text: m.content.toWellFormed() }] });
} else {
const contentBlocks = m.content
.map(c => {
switch (c.type) {
case "text":
return { text: c.text.toWellFormed() };
case "image":
return { image: createImageBlock(c.mimeType, c.data) };
default:
throw new Error("Unknown user content type");
const contentBlocks: UserContent[] = [];
for (const c of m.content) {
switch (c.type) {
case "text": {
const text = c.text.toWellFormed();
if (text.trim().length === 0) continue;
contentBlocks.push({ text });
break;
}
})
.filter(block => {
// Filter out empty text blocks
if ("text" in block && block.text) {
return block.text.trim().length > 0;
}
return true; // Keep non-text blocks (images)
});
case "image":
contentBlocks.push({ image: createImageBlock(c.mimeType, c.data) });
break;
default:
throw new Error("Unknown user content type");
}
}
// Skip message if all blocks filtered out
if (contentBlocks.length === 0) continue;
result.push({
role: ConversationRole.USER,
content: contentBlocks,
});
result.push({ role: "user", content: contentBlocks });
}
break;
case "assistant": {
// Skip assistant messages with empty content (e.g., from aborted requests)
// Bedrock rejects messages with empty content arrays
if (m.content.length === 0) {
continue;
}
const contentBlocks: ContentBlock[] = [];
if (m.content.length === 0) continue;
const contentBlocks: AssistantContent[] = [];
for (const c of m.content) {
switch (c.type) {
case "text":
@@ -570,9 +625,7 @@ function convertMessages(
} else if (!supportsThinkingSignature(model)) {
// Model doesn't support signatures at all — send as unsigned reasoning
contentBlocks.push({
reasoningContent: {
reasoningText: { text: c.thinking.toWellFormed() },
},
reasoningContent: { reasoningText: { text: c.thinking.toWellFormed() } },
});
} else {
// Model requires signature but we don't have one — demote to text
@@ -584,21 +637,14 @@ function convertMessages(
}
}
// Skip if all content blocks were filtered out
if (contentBlocks.length === 0) {
continue;
}
result.push({
role: ConversationRole.ASSISTANT,
content: contentBlocks,
});
if (contentBlocks.length === 0) continue;
result.push({ role: "assistant", content: contentBlocks });
break;
}
case "toolResult": {
// Collect all consecutive toolResult messages into a single user message
// Bedrock requires all tool results to be in one message
const toolResults: ContentBlock.ToolResultMember[] = [];
// Add current tool result with all content blocks combined
// Collect all consecutive toolResult messages into a single user message —
// Bedrock requires all tool results to be in one message.
const toolResults: ToolResultBlockWire[] = [];
toolResults.push({
toolResult: {
toolUseId: normalizeToolCallId(m.toolCallId),
@@ -607,11 +653,10 @@ function convertMessages(
? { image: createImageBlock(c.mimeType, c.data) }
: { text: c.text.toWellFormed() },
),
status: m.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS,
status: m.isError ? "error" : "success",
},
});
// Look ahead for consecutive toolResult messages
let j = i + 1;
while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
const nextMsg = transformedMessages[j] as ToolResultMessage;
@@ -623,19 +668,14 @@ function convertMessages(
? { image: createImageBlock(c.mimeType, c.data) }
: { text: c.text.toWellFormed() },
),
status: nextMsg.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS,
status: nextMsg.isError ? "error" : "success",
},
});
j++;
}
// Skip the messages we've already processed
i = j - 1;
result.push({
role: ConversationRole.USER,
content: toolResults,
});
result.push({ role: "user", content: toolResults });
break;
}
default:
@@ -646,12 +686,9 @@ function convertMessages(
// Add cache point to the last user message for supported Claude models
if (cacheRetention !== "none" && supportsPromptCaching(model) && result.length > 0) {
const lastMessage = result[result.length - 1];
if (lastMessage.role === ConversationRole.USER && lastMessage.content) {
(lastMessage.content as ContentBlock[]).push({
cachePoint: {
type: CachePointType.DEFAULT,
...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}),
},
if (lastMessage.role === "user" && lastMessage.content) {
(lastMessage.content as UserContent[]).push({
cachePoint: { type: "default", ...(cacheRetention === "long" ? { ttl: "1h" } : {}) },
});
}
}
@@ -662,23 +699,18 @@ function convertMessages(
function convertToolConfig(
tools: Tool[] | undefined,
toolChoice: BedrockOptions["toolChoice"],
): ToolConfiguration | undefined {
): WireToolConfig | undefined {
if (!tools?.length || toolChoice === "none") return undefined;
const bedrockTools: BedrockTool[] = tools.map(tool => ({
const bedrockTools: WireToolSpec[] = tools.map(tool => ({
toolSpec: {
name: tool.name,
description: tool.description || "",
// Wire schema is structurally a JSON Schema document; the Bedrock SDK
// types it as the recursive `DocumentType` from `@smithy/types`, which
// `Record<string, unknown>` does not directly satisfy at the type
// level. Cast through `unknown` so the actual JSON value passes the
// type checker without changing runtime behavior.
inputSchema: { json: toolWireSchema(tool) as unknown as Record<string, never> },
inputSchema: { json: toolWireSchema(tool) },
},
}));
let bedrockToolChoice: ToolChoice | undefined;
let bedrockToolChoice: WireToolChoice | undefined;
switch (toolChoice) {
case "auto":
bedrockToolChoice = { auto: {} };
@@ -697,13 +729,13 @@ function convertToolConfig(
function mapStopReason(reason: string | undefined): StopReason {
switch (reason) {
case BedrockStopReason.END_TURN:
case BedrockStopReason.STOP_SEQUENCE:
case "end_turn":
case "stop_sequence":
return "stop";
case BedrockStopReason.MAX_TOKENS:
case BedrockStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED:
case "max_tokens":
case "model_context_window_exceeded":
return "length";
case BedrockStopReason.TOOL_USE:
case "tool_use":
return "toolUse";
default:
return "error";
@@ -713,11 +745,9 @@ function mapStopReason(reason: string | undefined): StopReason {
function buildAdditionalModelRequestFields(
model: Model<"bedrock-converse-stream">,
options: BedrockOptions,
): Record<string, any> | undefined {
): Record<string, unknown> | undefined {
const reasoning = options.reasoning;
if (!reasoning || !model.reasoning) {
return undefined;
}
if (!reasoning || !model.reasoning) return undefined;
const mode = model.thinking?.mode;
if (mode === "anthropic-adaptive") {
@@ -738,11 +768,8 @@ function buildAdditionalModelRequestFields(
};
const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[level];
const result: Record<string, any> = {
thinking: {
type: "enabled",
budget_tokens: budget,
},
const result: Record<string, unknown> = {
thinking: { type: "enabled", budget_tokens: budget },
};
if (options.interleavedThinking) {
@@ -752,31 +779,28 @@ function buildAdditionalModelRequestFields(
return result;
}
function createImageBlock(mimeType: string, data: string) {
let format: ImageFormat;
/**
* Bedrock's wire format expects the image as `{ source: { bytes: <base64-string> }, format }`.
* The caller already passes base64-encoded data, so no decode/re-encode round-trip is needed.
*/
function createImageBlock(mimeType: string, data: string): ImageBlockWire["image"] {
let format: "jpeg" | "png" | "gif" | "webp";
switch (mimeType) {
case "image/jpeg":
case "image/jpg":
format = ImageFormat.JPEG;
format = "jpeg";
break;
case "image/png":
format = ImageFormat.PNG;
format = "png";
break;
case "image/gif":
format = ImageFormat.GIF;
format = "gif";
break;
case "image/webp":
format = ImageFormat.WEBP;
format = "webp";
break;
default:
throw new Error(`Unknown image type: ${mimeType}`);
}
const binaryString = atob(data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return { source: { bytes }, format };
return { source: { bytes: data }, format };
}
@@ -0,0 +1,334 @@
/**
* AWS credential resolution for the Bedrock provider.
*
* Chain (first hit wins):
* 1. Static credentials from the environment
* (`AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` [+ `AWS_SESSION_TOKEN`]).
* 2. Profile in `~/.aws/credentials` (and `~/.aws/config` for SSO):
* - static `aws_access_key_id` / `aws_secret_access_key` / `aws_session_token`
* - SSO profile referencing a cached token in `~/.aws/sso/cache/*.json`,
* which we exchange for short-lived role credentials via
* `https://portal.sso.{region}.amazonaws.com/federation/credentials`.
* 3. EC2 IMDSv2 (only when `AWS_EC2_METADATA_DISABLED` is unset / falsey and
* `169.254.169.254` is reachable within a 1 s timeout).
*
* Resolved credentials are cached process-wide per profile and refreshed
* 60 s before `Expiration` to absorb clock skew.
*/
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
import type { AwsCredentials } from "./aws-sigv4";
export interface ResolvedCredentials extends AwsCredentials {
/** Absolute expiration timestamp in ms. `undefined` for non-expiring static creds. */
expiresAt?: number;
}
export interface CredentialResolveOptions {
/** Named profile from `~/.aws/credentials` / `~/.aws/config`. */
profile?: string;
/** Falls back to env (`AWS_REGION` / `AWS_DEFAULT_REGION`) and finally `us-east-1`. */
region?: string;
signal?: AbortSignal;
}
const REFRESH_SKEW_MS = 60_000;
interface CacheEntry {
creds: ResolvedCredentials;
expiresAt: number;
}
const cache: Map<string, CacheEntry> = new Map();
export async function resolveAwsCredentials(opts: CredentialResolveOptions = {}): Promise<ResolvedCredentials> {
const profile = opts.profile || $env.AWS_PROFILE || "default";
const region = opts.region || $env.AWS_REGION || $env.AWS_DEFAULT_REGION || "us-east-1";
const cacheKey = `${profile}\x00${region}`;
const hit = cache.get(cacheKey);
if (hit && hit.expiresAt - REFRESH_SKEW_MS > Date.now()) return hit.creds;
const creds = await resolveFresh(profile, region, opts.signal);
cache.set(cacheKey, { creds, expiresAt: creds.expiresAt ?? Number.POSITIVE_INFINITY });
return creds;
}
async function resolveFresh(profile: string, region: string, signal?: AbortSignal): Promise<ResolvedCredentials> {
// 1. Environment first — matches the AWS SDK chain order.
const envCreds = readEnvCredentials();
if (envCreds) return envCreds;
// 2. Profile (static or SSO).
const profileCreds = await readProfileCredentials(profile, region, signal);
if (profileCreds) return profileCreds;
// 3. EC2 IMDSv2.
if ($env.AWS_EC2_METADATA_DISABLED?.toLowerCase() !== "true") {
const imdsCreds = await readImdsCredentials(signal);
if (imdsCreds) return imdsCreds;
}
throw new Error(
`Unable to resolve AWS credentials. Set AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, ` +
`or configure profile '${profile}' in ~/.aws/credentials (or ~/.aws/config for SSO).`,
);
}
function readEnvCredentials(): ResolvedCredentials | undefined {
const ak = $env.AWS_ACCESS_KEY_ID;
const sk = $env.AWS_SECRET_ACCESS_KEY;
if (!ak || !sk) return undefined;
const token = $env.AWS_SESSION_TOKEN;
return token
? { accessKeyId: ak, secretAccessKey: sk, sessionToken: token }
: { accessKeyId: ak, secretAccessKey: sk };
}
// ---------- INI parsing ----------
/** Map of section name -> map of key -> value. Section names are stripped of
* any leading `profile ` (so `~/.aws/config` aligns with `~/.aws/credentials`). */
type IniFile = Record<string, Record<string, string>>;
function parseIni(text: string): IniFile {
const out: IniFile = {};
let current: Record<string, string> | null = null;
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
if (line.startsWith("[") && line.endsWith("]")) {
let name = line.slice(1, -1).trim();
if (name.startsWith("profile ")) name = name.slice(8).trim();
if (name.startsWith("sso-session ")) name = `sso-session:${name.slice(12).trim()}`;
let section = out[name];
if (!section) {
section = {};
out[name] = section;
}
current = section;
continue;
}
if (!current) continue;
const eq = line.indexOf("=");
if (eq === -1) continue;
current[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
}
return out;
}
async function readIniFile(p: string): Promise<IniFile | undefined> {
try {
const text = await fs.promises.readFile(p, "utf8");
return parseIni(text);
} catch (err) {
if (isEnoent(err)) return undefined;
throw err;
}
}
// ---------- Profile / SSO ----------
async function readProfileCredentials(
profile: string,
region: string,
signal: AbortSignal | undefined,
): Promise<ResolvedCredentials | undefined> {
const home = os.homedir();
const credentialsPath = $env.AWS_SHARED_CREDENTIALS_FILE || path.join(home, ".aws", "credentials");
const configPath = $env.AWS_CONFIG_FILE || path.join(home, ".aws", "config");
const credentialsIni = await readIniFile(credentialsPath);
const configIni = await readIniFile(configPath);
// Static credentials live in ~/.aws/credentials; SSO config lives in
// ~/.aws/config under `[profile foo]`. Merge into a single view.
const merged: Record<string, string> = { ...(configIni?.[profile] ?? {}), ...(credentialsIni?.[profile] ?? {}) };
if (Object.keys(merged).length === 0) return undefined;
if (merged.aws_access_key_id && merged.aws_secret_access_key) {
const out: ResolvedCredentials = {
accessKeyId: merged.aws_access_key_id,
secretAccessKey: merged.aws_secret_access_key,
};
if (merged.aws_session_token) out.sessionToken = merged.aws_session_token;
return out;
}
if (merged.sso_account_id && merged.sso_role_name) {
return readSsoCredentials(merged, configIni, region, signal);
}
return undefined;
}
interface SsoCachedToken {
accessToken?: string;
expiresAt?: string;
startUrl?: string;
region?: string;
}
async function readSsoCredentials(
profileCfg: Record<string, string>,
configIni: IniFile | undefined,
defaultRegion: string,
signal: AbortSignal | undefined,
): Promise<ResolvedCredentials | undefined> {
// Two SSO profile shapes:
// - legacy: `sso_start_url` + `sso_region` directly on the profile
// - sso-session: `sso_session = my-session` references a `[sso-session my-session]` block
let startUrl = profileCfg.sso_start_url;
let ssoRegion = profileCfg.sso_region;
const sessionName = profileCfg.sso_session;
if (sessionName && configIni) {
const session = configIni[`sso-session:${sessionName}`];
if (session) {
startUrl = startUrl || session.sso_start_url;
ssoRegion = ssoRegion || session.sso_region;
}
}
if (!startUrl || !ssoRegion) return undefined;
const token = await loadSsoCachedToken(startUrl, sessionName);
if (!token?.accessToken) {
throw new Error(`AWS SSO token for ${startUrl} not found in ~/.aws/sso/cache. Run 'aws sso login' first.`);
}
const expiresAt = token.expiresAt ? Date.parse(token.expiresAt) : Number.POSITIVE_INFINITY;
if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
throw new Error(`AWS SSO token for ${startUrl} has expired. Run 'aws sso login' to refresh.`);
}
const url =
`https://portal.sso.${ssoRegion}.amazonaws.com/federation/credentials` +
`?account_id=${encodeURIComponent(profileCfg.sso_account_id)}` +
`&role_name=${encodeURIComponent(profileCfg.sso_role_name)}`;
const response = await fetch(url, {
method: "GET",
headers: { "x-amz-sso_bearer_token": token.accessToken },
signal,
});
if (!response.ok) {
const body = await response.text().catch(() => "");
throw new Error(`AWS SSO GetRoleCredentials failed: ${response.status} ${body.slice(0, 200)}`);
}
const json = (await response.json()) as {
roleCredentials?: { accessKeyId: string; secretAccessKey: string; sessionToken: string; expiration: number };
};
const role = json.roleCredentials;
if (!role) throw new Error("AWS SSO GetRoleCredentials: missing roleCredentials in response");
// region is honored at the caller; we only consume defaultRegion to keep the
// param wired for symmetry with other resolution paths.
void defaultRegion;
return {
accessKeyId: role.accessKeyId,
secretAccessKey: role.secretAccessKey,
sessionToken: role.sessionToken,
expiresAt: role.expiration,
};
}
async function loadSsoCachedToken(
startUrl: string,
sessionName: string | undefined,
): Promise<SsoCachedToken | undefined> {
const cacheDir = path.join(os.homedir(), ".aws", "sso", "cache");
let entries: string[];
try {
entries = await fs.promises.readdir(cacheDir);
} catch (err) {
if (isEnoent(err)) return undefined;
throw err;
}
// Prefer the deterministic hash for legacy `sso_start_url` profiles or the
// session name for the newer `sso-session` shape; otherwise scan.
const candidates: string[] = [];
const hash = await sha1Hex(sessionName || startUrl);
candidates.push(`${hash}.json`);
for (const entry of entries) {
if (entry.endsWith(".json") && !candidates.includes(entry)) candidates.push(entry);
}
for (const file of candidates) {
if (!entries.includes(file)) continue;
try {
const text = await fs.promises.readFile(path.join(cacheDir, file), "utf8");
const parsed = JSON.parse(text) as SsoCachedToken;
if (parsed.startUrl === startUrl || (sessionName && file === `${hash}.json`)) {
return parsed;
}
} catch (err) {
logger.debug("aws-credentials: failed to read SSO cache", { file, err: String(err) });
}
}
return undefined;
}
async function sha1Hex(input: string): Promise<string> {
const digest = await globalThis.crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
const bytes = new Uint8Array(digest);
let out = "";
for (let i = 0; i < bytes.length; i++) out += bytes[i].toString(16).padStart(2, "0");
return out;
}
// ---------- IMDSv2 ----------
const IMDS_HOST = "169.254.169.254";
const IMDS_TIMEOUT_MS = 1000;
async function readImdsCredentials(parentSignal: AbortSignal | undefined): Promise<ResolvedCredentials | undefined> {
const timeout = AbortSignal.timeout(IMDS_TIMEOUT_MS);
const signal = parentSignal ? AbortSignal.any([parentSignal, timeout]) : timeout;
try {
const tokenRes = await fetch(`http://${IMDS_HOST}/latest/api/token`, {
method: "PUT",
headers: { "x-aws-ec2-metadata-token-ttl-seconds": "21600" },
signal,
});
if (!tokenRes.ok) return undefined;
const token = await tokenRes.text();
const roleRes = await fetch(`http://${IMDS_HOST}/latest/meta-data/iam/security-credentials/`, {
headers: { "x-aws-ec2-metadata-token": token },
signal,
});
if (!roleRes.ok) return undefined;
const role = (await roleRes.text()).trim();
if (!role) return undefined;
const credsRes = await fetch(
`http://${IMDS_HOST}/latest/meta-data/iam/security-credentials/${encodeURIComponent(role)}`,
{
headers: { "x-aws-ec2-metadata-token": token },
signal,
},
);
if (!credsRes.ok) return undefined;
const body = (await credsRes.json()) as {
AccessKeyId?: string;
SecretAccessKey?: string;
Token?: string;
Expiration?: string;
};
if (!body.AccessKeyId || !body.SecretAccessKey) return undefined;
const out: ResolvedCredentials = {
accessKeyId: body.AccessKeyId,
secretAccessKey: body.SecretAccessKey,
};
if (body.Token) out.sessionToken = body.Token;
if (body.Expiration) out.expiresAt = Date.parse(body.Expiration);
return out;
} catch {
return undefined;
}
}
/** Test/diagnostic helper — drops cached credentials. */
export function clearAwsCredentialCache(): void {
cache.clear();
}
@@ -0,0 +1,185 @@
/**
* `application/vnd.amazon.eventstream` decoder.
*
* Wire format (all integers big-endian):
*
* [total length u32]
* [headers length u32]
* [prelude CRC32 u32] <- CRC over the first 8 bytes
* [headers headers_length]
* [payload total_length - headers_length - 16]
* [message CRC32 u32] <- CRC over the entire message minus the trailing 4 bytes
*
* Headers: a sequence of `[name_len u8][name utf8][value_type u8][value …]`.
* We only need the typed values Bedrock emits (boolean true/false, byte, short,
* integer, long, byte-array, string, timestamp, uuid). All are surfaced as
* strings for ease of consumption — Bedrock only sets string-valued headers in
* practice (`:event-type`, `:message-type`, `:content-type`, `:exception-type`).
*/
const PRELUDE_LEN = 8;
const PRELUDE_CRC_LEN = 4;
const MESSAGE_CRC_LEN = 4;
const HEADER_BLOCK_OFFSET = PRELUDE_LEN + PRELUDE_CRC_LEN;
const MIN_MESSAGE_LEN = HEADER_BLOCK_OFFSET + MESSAGE_CRC_LEN;
export interface EventStreamMessage {
/** Lower-cased copy is *not* applied — Bedrock uses casing like `:event-type` verbatim. */
headers: Record<string, string>;
payload: Uint8Array;
}
/** CRC32 (IEEE / zlib polynomial 0xEDB88320), matches `@aws-crypto/crc32`. */
const CRC_TABLE = (() => {
const t = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
t[i] = c >>> 0;
}
return t;
})();
export function crc32(bytes: Uint8Array, seed = 0): number {
let c = (seed ^ 0xffffffff) >>> 0;
for (let i = 0; i < bytes.length; i++) c = (CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)) >>> 0;
return (c ^ 0xffffffff) >>> 0;
}
/**
* Decode a single, fully buffered eventstream message. Throws if the framing is
* malformed or either CRC mismatches. Used by both `decodeEventStream` (the
* streaming entry point) and the unit tests, which exercise it with hand-built
* frames.
*/
export function decodeMessage(frame: Uint8Array): EventStreamMessage {
if (frame.length < MIN_MESSAGE_LEN) throw new Error("eventstream: frame too short");
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
const total = view.getUint32(0, false);
if (total !== frame.length) throw new Error(`eventstream: framed length ${total} != buffer ${frame.length}`);
const headersLen = view.getUint32(4, false);
const preludeCrc = view.getUint32(8, false);
const computedPreludeCrc = crc32(frame.subarray(0, PRELUDE_LEN));
if (computedPreludeCrc !== preludeCrc) throw new Error("eventstream: prelude CRC mismatch");
const msgCrc = view.getUint32(total - MESSAGE_CRC_LEN, false);
const computedMsgCrc = crc32(frame.subarray(0, total - MESSAGE_CRC_LEN));
if (computedMsgCrc !== msgCrc) throw new Error("eventstream: message CRC mismatch");
const headersBytes = frame.subarray(HEADER_BLOCK_OFFSET, HEADER_BLOCK_OFFSET + headersLen);
const payload = frame.subarray(HEADER_BLOCK_OFFSET + headersLen, total - MESSAGE_CRC_LEN);
return { headers: parseHeaders(headersBytes), payload };
}
function parseHeaders(buf: Uint8Array): Record<string, string> {
const out: Record<string, string> = {};
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
const decoder = new TextDecoder();
let p = 0;
while (p < buf.length) {
const nameLen = view.getUint8(p);
p += 1;
const name = decoder.decode(buf.subarray(p, p + nameLen));
p += nameLen;
const type = view.getUint8(p);
p += 1;
switch (type) {
case 0: // bool true
out[name] = "true";
break;
case 1: // bool false
out[name] = "false";
break;
case 2: // byte
out[name] = String(view.getInt8(p));
p += 1;
break;
case 3: // short
out[name] = String(view.getInt16(p, false));
p += 2;
break;
case 4: // integer
out[name] = String(view.getInt32(p, false));
p += 4;
break;
case 5: // long — surface as decimal string to avoid precision loss
out[name] = bigIntFromBytes(buf.subarray(p, p + 8)).toString();
p += 8;
break;
case 6: {
// byte array — base64 for safe transport
const len = view.getUint16(p, false);
p += 2;
out[name] = Buffer.from(buf.buffer, buf.byteOffset + p, len).toString("base64");
p += len;
break;
}
case 7: {
// string
const len = view.getUint16(p, false);
p += 2;
out[name] = decoder.decode(buf.subarray(p, p + len));
p += len;
break;
}
case 8: // timestamp (ms since epoch as i64)
out[name] = new Date(Number(bigIntFromBytes(buf.subarray(p, p + 8)))).toISOString();
p += 8;
break;
case 9: {
// uuid
const u = buf.subarray(p, p + 16);
const hex: string[] = [];
for (let i = 0; i < 16; i++) hex.push(u[i].toString(16).padStart(2, "0"));
out[name] =
`${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
p += 16;
break;
}
default:
throw new Error(`eventstream: unknown header value type ${type}`);
}
}
return out;
}
function bigIntFromBytes(b: Uint8Array): bigint {
let v = 0n;
for (let i = 0; i < b.length; i++) v = (v << 8n) | BigInt(b[i]);
// sign-extend (two's complement)
if (b.length === 8 && b[0] & 0x80) v -= 1n << 64n;
return v;
}
/**
* Async generator that consumes a `ReadableStream<Uint8Array>` (e.g. a fetch
* response body) and yields fully-framed messages. Handles arbitrary chunk
* boundaries: messages may span multiple chunks, and a single chunk may carry
* many messages.
*/
export async function* decodeEventStream(source: ReadableStream<Uint8Array>): AsyncGenerator<EventStreamMessage> {
const reader = source.getReader();
// Single growable buffer; we slide a read cursor along it and compact when a
// complete prefix has been consumed. Avoids per-message Uint8Array copies.
let buf: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
try {
while (true) {
const { value, done } = await reader.read();
if (value && value.length > 0) buf = buf.length === 0 ? value : Buffer.concat([buf, value]);
let offset = 0;
while (buf.length - offset >= 4) {
const dv = new DataView(buf.buffer, buf.byteOffset + offset, buf.length - offset);
const total = dv.getUint32(0, false);
if (total < MIN_MESSAGE_LEN) throw new Error(`eventstream: total length ${total} below minimum`);
if (buf.length - offset < total) break;
const frame = buf.subarray(offset, offset + total);
yield decodeMessage(frame);
offset += total;
}
if (offset > 0) buf = buf.slice(offset);
if (done) break;
}
if (buf.length > 0) throw new Error("eventstream: truncated message at end of stream");
} finally {
reader.releaseLock();
}
}
+218
View File
@@ -0,0 +1,218 @@
/**
* AWS Signature V4 signing for HTTP requests. WebCrypto-only — no node:crypto.
*
* Matches `@smithy/signature-v4` for our usage: header-based signing with a
* full SHA-256 payload hash (Bedrock requires `applyChecksum: true`).
*
* Returns the set of headers to attach to the request:
* - `host`
* - `x-amz-date`
* - `x-amz-content-sha256`
* - `x-amz-security-token` (only when credentials carry a sessionToken)
* - `authorization`
*/
export interface AwsCredentials {
accessKeyId: string;
secretAccessKey: string;
sessionToken?: string;
}
export interface SignParams {
method: string;
/** Hostname only — used to build the `host` header and the canonical request. */
host: string;
/** URI path component, e.g. `/model/anthropic.claude/converse-stream`. */
path: string;
/** Optional pre-built query string (without leading `?`). */
query?: string;
/** Extra headers to sign in addition to `host`/`x-amz-*`. Names are case-insensitive. */
headers?: Record<string, string>;
body: Uint8Array;
region: string;
service: string;
credentials: AwsCredentials;
/** Override clock for deterministic tests. */
date?: Date;
}
const ALGORITHM = "AWS4-HMAC-SHA256";
const KEY_TYPE = "aws4_request";
// Headers the SDK never includes in the signature. Lowercased.
const UNSIGNABLE: Record<string, true> = {
authorization: true,
"cache-control": true,
connection: true,
expect: true,
from: true,
"keep-alive": true,
"max-forwards": true,
pragma: true,
referer: true,
te: true,
trailer: true,
"transfer-encoding": true,
upgrade: true,
"user-agent": true,
"x-amzn-trace-id": true,
};
/** Coerce a possibly-ArrayBufferLike-backed `Uint8Array` into one over a fresh
* `ArrayBuffer`, which is what `crypto.subtle.{digest,sign,importKey}` requires
* under the strict TS DOM typings. No-op when already strict.
*/
function asStrict(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
return bytes as Uint8Array<ArrayBuffer>;
}
const copy = new Uint8Array(bytes.byteLength);
copy.set(bytes);
return copy;
}
const subtle = globalThis.crypto.subtle;
const HEX = "0123456789abcdef";
export function toHex(bytes: Uint8Array): string {
let out = "";
for (let i = 0; i < bytes.length; i++) {
const b = bytes[i];
out += HEX[b >> 4] + HEX[b & 15];
}
return out;
}
export async function sha256(data: Uint8Array | string): Promise<Uint8Array> {
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : asStrict(data);
const digest = await subtle.digest("SHA-256", bytes);
return new Uint8Array(digest);
}
export async function sha256Hex(data: Uint8Array | string): Promise<string> {
return toHex(await sha256(data));
}
async function hmac(key: Uint8Array, data: string | Uint8Array): Promise<Uint8Array> {
const cryptoKey = await subtle.importKey("raw", asStrict(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : asStrict(data);
const sig = await subtle.sign("HMAC", cryptoKey, bytes);
return new Uint8Array(sig);
}
/**
* Derive a signing key: HMAC chain `kSecret → kDate → kRegion → kService → kSigning`.
*/
export async function getSigningKey(
secretAccessKey: string,
shortDate: string,
region: string,
service: string,
): Promise<Uint8Array> {
const kDate = await hmac(new TextEncoder().encode(`AWS4${secretAccessKey}`), shortDate);
const kRegion = await hmac(kDate, region);
const kService = await hmac(kRegion, service);
return hmac(kService, KEY_TYPE);
}
/** `YYYYMMDDTHHMMSSZ` + 8-char `YYYYMMDD`. */
export function formatAmzDate(d: Date): { longDate: string; shortDate: string } {
const iso = d.toISOString();
// `2025-05-17T12:34:56.789Z` -> `20250517T123456Z`
const longDate = `${iso.slice(0, 4)}${iso.slice(5, 7)}${iso.slice(8, 10)}T${iso.slice(11, 13)}${iso.slice(14, 16)}${iso.slice(17, 19)}Z`;
return { longDate, shortDate: longDate.slice(0, 8) };
}
/**
* Canonicalize a request path per RFC 3986: each segment is %-encoded but `/`
* stays literal. Matches the smithy default (`uriEscapePath: true`, then revert
* the double-encoding of `/`). Bedrock paths use no reserved characters in
* practice, but model IDs can include `:` and `.`.
*/
function canonicalPath(path: string): string {
const segments = path.split("/");
const escaped = segments.map(seg => (seg.length === 0 ? "" : encodeRfc3986(seg)));
return escaped.join("/");
}
function encodeRfc3986(str: string): string {
return encodeURIComponent(str).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
}
function canonicalQuery(query: string | undefined): string {
if (!query) return "";
const pairs: Array<[string, string]> = [];
for (const part of query.split("&")) {
if (!part) continue;
const eq = part.indexOf("=");
const k = eq === -1 ? part : part.slice(0, eq);
const v = eq === -1 ? "" : part.slice(eq + 1);
pairs.push([decodeURIComponent(k), decodeURIComponent(v)]);
}
pairs.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0));
return pairs.map(([k, v]) => `${encodeRfc3986(k)}=${encodeRfc3986(v)}`).join("&");
}
export interface SignedHeaders {
host: string;
"x-amz-date": string;
"x-amz-content-sha256": string;
authorization: string;
"x-amz-security-token"?: string;
}
export async function signRequest(params: SignParams): Promise<SignedHeaders> {
const { method, host, path, query, body, region, service, credentials } = params;
const date = params.date ?? new Date();
const { longDate, shortDate } = formatAmzDate(date);
const payloadHash = await sha256Hex(body);
// Assemble the headers that will be signed. Always include host, x-amz-date,
// x-amz-content-sha256, plus x-amz-security-token when present, plus
// caller-provided signable headers (e.g. content-type, accept).
const signed: Record<string, string> = {
host,
"x-amz-date": longDate,
"x-amz-content-sha256": payloadHash,
};
if (credentials.sessionToken) signed["x-amz-security-token"] = credentials.sessionToken;
const extraHeaders = params.headers;
if (extraHeaders) {
for (const k in extraHeaders) {
const lk = k.toLowerCase();
if (UNSIGNABLE[lk]) continue;
if (lk.startsWith("proxy-") || lk.startsWith("sec-")) continue;
signed[lk] = extraHeaders[k].trim().replace(/\s+/g, " ");
}
}
const sortedNames = Object.keys(signed).sort();
const canonicalHeaders = `${sortedNames.map(n => `${n}:${signed[n]}`).join("\n")}\n`;
const signedHeadersStr = sortedNames.join(";");
const canonicalRequest = [
method.toUpperCase(),
canonicalPath(path),
canonicalQuery(query),
canonicalHeaders,
signedHeadersStr,
payloadHash,
].join("\n");
const scope = `${shortDate}/${region}/${service}/${KEY_TYPE}`;
const stringToSign = [ALGORITHM, longDate, scope, await sha256Hex(canonicalRequest)].join("\n");
const signingKey = await getSigningKey(credentials.secretAccessKey, shortDate, region, service);
const signature = toHex(await hmac(signingKey, stringToSign));
const authorization =
`${ALGORITHM} Credential=${credentials.accessKeyId}/${scope}, ` +
`SignedHeaders=${signedHeadersStr}, Signature=${signature}`;
const out: SignedHeaders = {
host,
"x-amz-date": longDate,
"x-amz-content-sha256": payloadHash,
authorization,
};
if (credentials.sessionToken) out["x-amz-security-token"] = credentials.sessionToken;
return out;
}
+252
View File
@@ -0,0 +1,252 @@
/**
* Application Default Credentials (ADC) resolution for Vertex AI.
*
* Replaces `google-auth-library` with a direct WebCrypto + REST implementation.
* Sources, in priority order:
* 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange)
* or `type: "authorized_user"` (refresh-token exchange).
* 2. `~/.config/gcloud/application_default_credentials.json` (user ADC, same authorized_user flow).
* 3. GCE / Cloud Run metadata server (`metadata.google.internal`).
*
* Tokens are cached per source key and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` before expiry
* (default 60s). Concurrent callers waiting on a refresh share the same in-flight promise.
*/
import { Buffer } from "node:buffer";
import * as os from "node:os";
import * as path from "node:path";
import { $envpos, isEnoent, logger } from "@oh-my-pi/pi-utils";
import type { FetchImpl } from "../types";
const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
const METADATA_TOKEN_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
const CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
const JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
interface CachedToken {
token: string;
expiresAtMs: number;
}
interface ServiceAccountCredentials {
type: "service_account";
client_email: string;
private_key: string;
private_key_id?: string;
}
interface AuthorizedUserCredentials {
type: "authorized_user";
client_id: string;
client_secret: string;
refresh_token: string;
}
type AdcFileCredentials = ServiceAccountCredentials | AuthorizedUserCredentials;
interface TokenResponse {
access_token: string;
expires_in: number;
token_type?: string;
}
const tokenCache = new Map<string, CachedToken>();
const inflight = new Map<string, Promise<string>>();
function getRefreshSkewMs(): number {
return $envpos("GOOGLE_VERTEX_REFRESH_SKEW_MS", 60_000);
}
function userAdcPath(): string {
return path.join(os.homedir(), ".config", "gcloud", "application_default_credentials.json");
}
async function readJsonFile<T>(filePath: string): Promise<T | undefined> {
try {
return (await Bun.file(filePath).json()) as T;
} catch (err) {
if (isEnoent(err)) return undefined;
throw err;
}
}
async function loadAdcCredentials(): Promise<{ source: string; creds: AdcFileCredentials } | undefined> {
const gacPath = Bun.env.GOOGLE_APPLICATION_CREDENTIALS;
if (gacPath) {
const creds = await readJsonFile<AdcFileCredentials>(gacPath);
if (!creds) {
throw new Error(`GOOGLE_APPLICATION_CREDENTIALS points to a missing file: ${gacPath}`);
}
return { source: `gac:${gacPath}`, creds };
}
const userPath = userAdcPath();
const creds = await readJsonFile<AdcFileCredentials>(userPath);
if (creds) return { source: `user:${userPath}`, creds };
return undefined;
}
function base64UrlEncode(bytes: Uint8Array | string): string {
const buf = typeof bytes === "string" ? Buffer.from(bytes, "utf8") : bytes;
return Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength).toString("base64url");
}
function pemToPkcs8(pem: string): Uint8Array<ArrayBuffer> {
const body = pem
.replace(/-----BEGIN [^-]+-----/g, "")
.replace(/-----END [^-]+-----/g, "")
.replace(/\s+/g, "");
if (!body) throw new Error("Invalid PEM: empty body");
return Uint8Array.fromBase64(body);
}
async function signJwtRs256(claims: Record<string, unknown>, privateKeyPem: string, keyId?: string): Promise<string> {
const header: Record<string, unknown> = { alg: "RS256", typ: "JWT" };
if (keyId) header.kid = keyId;
const payload = `${base64UrlEncode(JSON.stringify(header))}.${base64UrlEncode(JSON.stringify(claims))}`;
const key = await globalThis.crypto.subtle.importKey(
"pkcs8",
pemToPkcs8(privateKeyPem),
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["sign"],
);
const signature = new Uint8Array(
await globalThis.crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(payload)),
);
return `${payload}.${base64UrlEncode(signature)}`;
}
async function exchangeJwtForToken(
creds: ServiceAccountCredentials,
signal: AbortSignal | undefined,
fetchImpl: FetchImpl,
): Promise<TokenResponse> {
const now = Math.floor(Date.now() / 1000);
const assertion = await signJwtRs256(
{
iss: creds.client_email,
scope: CLOUD_PLATFORM_SCOPE,
aud: OAUTH_TOKEN_URL,
exp: now + 3600,
iat: now,
},
creds.private_key,
creds.private_key_id,
);
const body = new URLSearchParams({ grant_type: JWT_BEARER_GRANT, assertion });
return postForToken(OAUTH_TOKEN_URL, body, signal, fetchImpl);
}
async function exchangeRefreshToken(
creds: AuthorizedUserCredentials,
signal: AbortSignal | undefined,
fetchImpl: FetchImpl,
): Promise<TokenResponse> {
const body = new URLSearchParams({
client_id: creds.client_id,
client_secret: creds.client_secret,
refresh_token: creds.refresh_token,
grant_type: "refresh_token",
});
return postForToken(OAUTH_TOKEN_URL, body, signal, fetchImpl);
}
async function fetchMetadataToken(
signal: AbortSignal | undefined,
fetchImpl: FetchImpl,
): Promise<TokenResponse | undefined> {
const timeout = AbortSignal.timeout(2000);
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
try {
const response = await fetchImpl(METADATA_TOKEN_URL, {
method: "GET",
headers: { "Metadata-Flavor": "Google" },
signal: combined,
});
if (!response.ok) return undefined;
return (await response.json()) as TokenResponse;
} catch {
return undefined;
}
}
async function postForToken(
url: string,
body: URLSearchParams,
signal: AbortSignal | undefined,
fetchImpl: FetchImpl,
): Promise<TokenResponse> {
const response = await fetchImpl(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
signal,
});
if (!response.ok) {
const detail = await response.text().catch(() => "");
throw new Error(`Google OAuth token exchange failed (${response.status}): ${detail}`);
}
return (await response.json()) as TokenResponse;
}
async function resolveAccessTokenUncached(
signal: AbortSignal | undefined,
fetchImpl: FetchImpl,
): Promise<{ source: string; token: TokenResponse }> {
const adc = await loadAdcCredentials();
if (adc) {
const token =
adc.creds.type === "service_account"
? await exchangeJwtForToken(adc.creds, signal, fetchImpl)
: await exchangeRefreshToken(adc.creds, signal, fetchImpl);
return { source: adc.source, token };
}
const metadata = await fetchMetadataToken(signal, fetchImpl);
if (metadata) return { source: "metadata", token: metadata };
throw new Error(
"Vertex AI requires Application Default Credentials. Set GOOGLE_APPLICATION_CREDENTIALS, run `gcloud auth application-default login`, or run on a GCE/Cloud Run instance with a service account.",
);
}
/**
* Returns a Bearer access token suitable for the `Authorization` header on Vertex AI calls.
* The token is cached in module scope and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` ms before it expires.
*/
export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl }): Promise<string> {
const fetchImpl = options?.fetch ?? globalThis.fetch.bind(globalThis);
const skew = getRefreshSkewMs();
const now = Date.now();
// Best-effort cache key probe: we don't know the source until we resolve, but cached entries
// are keyed by their resolved source. Try every cached source first.
for (const [source, cached] of tokenCache) {
if (cached.expiresAtMs - skew > now) return cached.token;
// expired entry — drop and re-resolve
tokenCache.delete(source);
}
const cacheKey = "vertex-adc";
const existing = inflight.get(cacheKey);
if (existing) return existing;
const promise = (async () => {
try {
const { source, token } = await resolveAccessTokenUncached(options?.signal, fetchImpl);
const expiresAtMs = Date.now() + Math.max(0, token.expires_in * 1000);
tokenCache.set(source, { token: token.access_token, expiresAtMs });
logger.debug("vertex.adc acquired access token", { source, expiresInSec: token.expires_in });
return token.access_token;
} finally {
inflight.delete(cacheKey);
}
})();
inflight.set(cacheKey, promise);
return promise;
}
/** Test seam: clears every cached token. */
export function __resetVertexTokenCache(): void {
tokenCache.clear();
inflight.clear();
}
@@ -5,7 +5,6 @@
*/
import { createHash, randomBytes, randomUUID } from "node:crypto";
import { scheduler } from "node:timers/promises";
import type { Content, FunctionCallingConfigMode, ThinkingConfig } from "@google/genai";
import { fetchWithRetry, readSseJson } from "@oh-my-pi/pi-utils";
import { calculateCost } from "../models";
import type {
@@ -26,6 +25,7 @@ import { refreshAntigravityToken } from "../utils/oauth/google-antigravity";
import { refreshGoogleCloudToken } from "../utils/oauth/google-gemini-cli";
import { normalizeSchemaForCCA } from "../utils/schema";
import { ANTIGRAVITY_SYSTEM_INSTRUCTION, getAntigravityUserAgent, getGeminiCliHeaders } from "./google-gemini-headers";
import type { Content, FunctionCallingConfigMode, ThinkingConfig } from "./google-shared";
import {
convertMessages,
convertTools,
+135 -39
View File
@@ -1,23 +1,14 @@
/**
* Shared utilities for Google Generative AI and Google Cloud Code Assist providers.
*/
import {
type Content,
FinishReason,
FunctionCallingConfigMode,
type GenerateContentConfig,
type GenerateContentParameters,
type GenerateContentResponse,
type GoogleGenAI,
type Part,
type ThinkingConfig,
type ThinkingLevel,
} from "@google/genai";
import { readSseJson } from "@oh-my-pi/pi-utils";
import { calculateCost } from "../models";
import type {
Api,
AssistantMessage,
Context,
FetchImpl,
ImageContent,
Model,
StopReason,
@@ -29,11 +20,29 @@ import type {
} from "../types";
import { normalizeSystemPrompts } from "../utils";
import { AssistantMessageEventStream } from "../utils/event-stream";
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
import { finalizeErrorMessage, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
import { normalizeSchemaForCCA, normalizeSchemaForGoogle, toolWireSchema } from "../utils/schema";
import type {
Content,
FinishReason,
FunctionCallingConfigMode,
GenerateContentConfig,
GenerateContentParameters,
GenerateContentResponse,
Part,
ThinkingConfig,
ThinkingLevel,
} from "./google-types";
import { transformMessages } from "./transform-messages";
import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
export type {
Content,
FunctionCallingConfigMode,
GenerateContentParameters,
GenerateContentResponse,
ThinkingConfig,
} from "./google-types";
export { normalizeSchemaForGoogle };
type GoogleApiType = "google-generative-ai" | "google-gemini-cli" | "google-vertex";
@@ -353,13 +362,13 @@ export function convertTools(
export function mapToolChoice(choice: string): FunctionCallingConfigMode {
switch (choice) {
case "auto":
return FunctionCallingConfigMode.AUTO;
return "AUTO";
case "none":
return FunctionCallingConfigMode.NONE;
return "NONE";
case "any":
return FunctionCallingConfigMode.ANY;
return "ANY";
default:
return FunctionCallingConfigMode.AUTO;
return "AUTO";
}
}
@@ -368,25 +377,25 @@ export function mapToolChoice(choice: string): FunctionCallingConfigMode {
*/
export function mapStopReason(reason: FinishReason): StopReason {
switch (reason) {
case FinishReason.STOP:
case "STOP":
return "stop";
case FinishReason.MAX_TOKENS:
case "MAX_TOKENS":
return "length";
case FinishReason.BLOCKLIST:
case FinishReason.PROHIBITED_CONTENT:
case FinishReason.SPII:
case FinishReason.SAFETY:
case FinishReason.IMAGE_SAFETY:
case FinishReason.IMAGE_PROHIBITED_CONTENT:
case FinishReason.IMAGE_RECITATION:
case FinishReason.IMAGE_OTHER:
case FinishReason.RECITATION:
case FinishReason.FINISH_REASON_UNSPECIFIED:
case FinishReason.OTHER:
case FinishReason.LANGUAGE:
case FinishReason.MALFORMED_FUNCTION_CALL:
case FinishReason.UNEXPECTED_TOOL_CALL:
case FinishReason.NO_IMAGE:
case "BLOCKLIST":
case "PROHIBITED_CONTENT":
case "SPII":
case "SAFETY":
case "IMAGE_SAFETY":
case "IMAGE_PROHIBITED_CONTENT":
case "IMAGE_RECITATION":
case "IMAGE_OTHER":
case "RECITATION":
case "FINISH_REASON_UNSPECIFIED":
case "OTHER":
case "LANGUAGE":
case "MALFORMED_FUNCTION_CALL":
case "UNEXPECTED_TOOL_CALL":
case "NO_IMAGE":
return "error";
default: {
throw new Error(`Unhandled stop reason: ${reason satisfies never}`);
@@ -723,12 +732,19 @@ export function buildGoogleGenerateContentParams<T extends "google-generative-ai
* Caller-supplied `prepare()` runs inside the try-block so any failure (missing project,
* bad auth, etc.) is funneled through the same error path as a streaming failure.
*/
export interface GoogleGenAIRequestPlan {
params: GenerateContentParameters;
url: string;
headers: Record<string, string>;
fetch?: FetchImpl;
}
export function streamGoogleGenAI<T extends "google-generative-ai" | "google-vertex">(args: {
model: Model<T>;
options: GoogleSharedStreamOptions | undefined;
api: T;
retainTextSignature?: boolean;
prepare: () => { client: GoogleGenAI; params: GenerateContentParameters; url: string | undefined };
prepare: () => GoogleGenAIRequestPlan | Promise<GoogleGenAIRequestPlan>;
}): AssistantMessageEventStream {
const { model, options, api, retainTextSignature, prepare } = args;
const stream = new AssistantMessageEventStream();
@@ -757,17 +773,44 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
let rawRequestDump: RawHttpRequestDump | undefined;
try {
const { client, params, url } = prepare();
options?.onPayload?.(params);
const plan = await prepare();
let params = plan.params;
const replacement = await options?.onPayload?.(params, model);
if (replacement !== undefined) {
params = replacement as GenerateContentParameters;
}
rawRequestDump = {
provider: model.provider,
api: output.api,
model: model.id,
method: "POST",
url,
url: plan.url,
body: params,
headers: plan.headers,
};
const googleStream = await client.models.generateContentStream(params);
const wireBody = paramsToWireBody(params);
const fetchImpl = plan.fetch ?? options?.fetch ?? (globalThis.fetch.bind(globalThis) as FetchImpl);
const response = await fetchImpl(plan.url, {
method: "POST",
headers: { ...plan.headers, "Content-Type": "application/json", Accept: "text/event-stream" },
body: JSON.stringify(wireBody),
signal: options?.signal,
});
if (!response.ok) {
const errorText = await response.text().catch(() => "");
throw withHttpStatus(
new Error(`Google API error (${response.status}): ${extractGoogleErrorMessage(errorText)}`),
response.status,
);
}
if (!response.body) {
throw new Error("Google API returned an empty response body");
}
const googleStream = readSseJson<GenerateContentResponse>(response.body, options?.signal, event =>
options?.onSseEvent?.({ event: event.event, data: event.data, raw: [...event.raw] }, model),
);
stream.push({ type: "start", partial: output });
await consumeGoogleStream({
@@ -803,3 +846,56 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
return stream;
}
/**
* Lift the SDK's `params.config` fields out of `config` and place them where the
* Gemini / Vertex AI REST API expects them on the request body. Mirrors the
* generateContentParametersTo{Mldev,Vertex} transformation in @google/genai
* for the subset of fields this codebase actually sets.
*
* `abortSignal` is intentionally dropped — the SDK propagates it via `fetch.signal`,
* which our caller already wires up through `options.signal`.
*/
function paramsToWireBody(params: GenerateContentParameters): Record<string, unknown> {
const body: Record<string, unknown> = { contents: params.contents };
const config = params.config;
if (!config) return body;
if (config.systemInstruction !== undefined) body.systemInstruction = config.systemInstruction;
if (config.tools !== undefined) body.tools = config.tools;
if (config.toolConfig !== undefined) body.toolConfig = config.toolConfig;
if (config.safetySettings !== undefined) body.safetySettings = config.safetySettings;
if (config.cachedContent !== undefined) body.cachedContent = config.cachedContent;
const gen: Record<string, unknown> = {};
if (config.temperature !== undefined) gen.temperature = config.temperature;
if (config.maxOutputTokens !== undefined) gen.maxOutputTokens = config.maxOutputTokens;
if (config.topP !== undefined) gen.topP = config.topP;
if (config.topK !== undefined) gen.topK = config.topK;
if (config.candidateCount !== undefined) gen.candidateCount = config.candidateCount;
if (config.stopSequences !== undefined) gen.stopSequences = config.stopSequences;
if (config.presencePenalty !== undefined) gen.presencePenalty = config.presencePenalty;
if (config.frequencyPenalty !== undefined) gen.frequencyPenalty = config.frequencyPenalty;
if (config.seed !== undefined) gen.seed = config.seed;
if (config.responseMimeType !== undefined) gen.responseMimeType = config.responseMimeType;
if (config.responseSchema !== undefined) gen.responseSchema = config.responseSchema;
if (config.responseJsonSchema !== undefined) gen.responseJsonSchema = config.responseJsonSchema;
if (config.responseModalities !== undefined) gen.responseModalities = config.responseModalities;
if (config.thinkingConfig !== undefined) gen.thinkingConfig = config.thinkingConfig;
const generationConfig = config as unknown as { minP?: number; repetitionPenalty?: number };
if (generationConfig.minP !== undefined) gen.minP = generationConfig.minP;
if (generationConfig.repetitionPenalty !== undefined) gen.repetitionPenalty = generationConfig.repetitionPenalty;
if (Object.keys(gen).length > 0) body.generationConfig = gen;
return body;
}
function extractGoogleErrorMessage(errorText: string): string {
if (!errorText) return "Unknown error";
try {
const parsed = JSON.parse(errorText) as { error?: { message?: string } };
if (parsed.error?.message) return parsed.error.message;
} catch {
// fall through to raw text
}
return errorText;
}
+167
View File
@@ -0,0 +1,167 @@
/**
* Local mirror of the subset of `@google/genai` types this package consumes.
*
* Field shapes match Gemini / Vertex AI wire format 1:1. Enum-shaped values are
* modelled as string literal unions so they pass through `JSON.stringify` and
* `JSON.parse` unchanged.
*
* Keep this file in sync with the actual request/response surface of:
* - `POST {generativelanguage,aiplatform}.googleapis.com/.../models/{model}:streamGenerateContent?alt=sse`
* - The Cloud Code Assist endpoint used by `google-gemini-cli.ts`
*/
/** Mirror of `@google/genai`'s `FinishReason` string enum. */
export type FinishReason =
| "FINISH_REASON_UNSPECIFIED"
| "STOP"
| "MAX_TOKENS"
| "SAFETY"
| "RECITATION"
| "LANGUAGE"
| "OTHER"
| "BLOCKLIST"
| "PROHIBITED_CONTENT"
| "SPII"
| "MALFORMED_FUNCTION_CALL"
| "IMAGE_SAFETY"
| "IMAGE_PROHIBITED_CONTENT"
| "IMAGE_RECITATION"
| "IMAGE_OTHER"
| "UNEXPECTED_TOOL_CALL"
| "NO_IMAGE";
/** Mirror of `@google/genai`'s `FunctionCallingConfigMode` string enum. */
export type FunctionCallingConfigMode = "MODE_UNSPECIFIED" | "AUTO" | "NONE" | "ANY" | "VALIDATED";
/** Mirror of `@google/genai`'s `ThinkingLevel` string enum. */
export type ThinkingLevel = "THINKING_LEVEL_UNSPECIFIED" | "MINIMAL" | "LOW" | "MEDIUM" | "HIGH";
/** Inline base64-encoded data part. */
export interface InlineDataPart {
mimeType: string;
data: string;
}
/** Function call emitted by the model. */
export interface FunctionCallPart {
name?: string;
args?: Record<string, unknown>;
id?: string;
}
/** Tool execution result fed back to the model. */
export interface FunctionResponsePart {
name: string;
response: Record<string, unknown>;
parts?: Part[];
id?: string;
}
/**
* A single piece of a `Content` message. Mirrors the SDK's union by keeping
* every optional field — the model and the wire treat shape as discriminator.
*/
export interface Part {
text?: string;
thought?: boolean;
thoughtSignature?: string;
inlineData?: InlineDataPart;
functionCall?: FunctionCallPart;
functionResponse?: FunctionResponsePart;
}
/** Conversation turn. Roles: `"user"`, `"model"`, optionally absent for system instructions. */
export interface Content {
role?: string;
parts?: Part[];
}
/** Thinking/reasoning configuration shared by Gemini 2.x and 3.x models. */
export interface ThinkingConfig {
includeThoughts?: boolean;
thinkingBudget?: number;
thinkingLevel?: ThinkingLevel;
}
/** Function declaration entry inside `tools[].functionDeclarations`. */
export interface FunctionDeclaration {
name: string;
description?: string;
parameters?: Record<string, unknown>;
parametersJsonSchema?: Record<string, unknown>;
}
/** Tool group as accepted at the request top level. */
export interface ToolDeclaration {
functionDeclarations: Record<string, unknown>[];
}
/** Tool selection mode container. */
export interface ToolConfig {
functionCallingConfig?: {
mode: FunctionCallingConfigMode;
allowedFunctionNames?: string[];
};
}
/**
* Generation/sampling and request-shape options passed via the SDK's `config`.
*
* Fields that the wire format places at the request body root (systemInstruction,
* tools, toolConfig, safetySettings, cachedContent) live here too — the
* transformer in `google-shared.ts` lifts them out when serializing.
*/
export interface GenerateContentConfig {
temperature?: number;
maxOutputTokens?: number;
topP?: number;
topK?: number;
candidateCount?: number;
stopSequences?: string[];
presencePenalty?: number;
frequencyPenalty?: number;
seed?: number;
responseMimeType?: string;
responseSchema?: Record<string, unknown>;
responseJsonSchema?: Record<string, unknown>;
responseModalities?: string[];
systemInstruction?: Content | { role?: string; parts: { text: string }[] };
tools?: ToolDeclaration[];
toolConfig?: ToolConfig;
safetySettings?: Array<Record<string, unknown>>;
cachedContent?: string;
thinkingConfig?: ThinkingConfig;
abortSignal?: AbortSignal;
}
/** Top-level argument to `generateContentStream`. */
export interface GenerateContentParameters {
model: string;
contents: Content[];
config?: GenerateContentConfig;
}
/** Per-stream candidate envelope. */
export interface Candidate {
content?: Content;
finishReason?: FinishReason;
index?: number;
}
/** Cumulative token accounting attached to the trailing chunk. */
export interface UsageMetadata {
promptTokenCount?: number;
candidatesTokenCount?: number;
thoughtsTokenCount?: number;
totalTokenCount?: number;
cachedContentTokenCount?: number;
}
/** Single SSE chunk's parsed JSON body. */
export interface GenerateContentResponse {
candidates?: Candidate[];
usageMetadata?: UsageMetadata;
modelVersion?: string;
responseId?: string;
promptFeedback?: Record<string, unknown>;
}
+34 -55
View File
@@ -1,8 +1,13 @@
import { GoogleGenAI } from "@google/genai";
import { $env } from "@oh-my-pi/pi-utils";
import type { Context, FetchImpl, Model, StreamFunction } from "../types";
import type { Context, Model, StreamFunction } from "../types";
import type { AssistantMessageEventStream } from "../utils/event-stream";
import { buildGoogleGenerateContentParams, type GoogleSharedStreamOptions, streamGoogleGenAI } from "./google-shared";
import { getVertexAccessToken } from "./google-auth";
import {
buildGoogleGenerateContentParams,
type GoogleGenAIRequestPlan,
type GoogleSharedStreamOptions,
streamGoogleGenAI,
} from "./google-shared";
export interface GoogleVertexOptions extends GoogleSharedStreamOptions {
project?: string;
@@ -21,63 +26,37 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
options,
api: "google-vertex",
retainTextSignature: true,
prepare: () => {
prepare: async (): Promise<GoogleGenAIRequestPlan> => {
const apiKey = resolveApiKey(options);
const project = apiKey ? undefined : resolveProject(options);
const location = apiKey ? undefined : resolveLocation(options);
const client = apiKey
? createClientWithApiKey(model, apiKey, options?.fetch)
: createClient(model, project!, location!, options?.fetch);
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
const url = apiKey
? `https://aiplatform.googleapis.com/${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent`
: `https://${location}-aiplatform.googleapis.com/${API_VERSION}/projects/${project}/locations/${location}/publishers/google/models/${model.id}:streamGenerateContent`;
return { client, params, url };
const baseHeaders: Record<string, string> = {
...(model.headers ?? {}),
...(options?.headers ?? {}),
};
if (apiKey) {
const url = `https://aiplatform.googleapis.com/${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
return {
params,
url,
headers: { ...baseHeaders, "x-goog-api-key": apiKey },
fetch: options?.fetch,
};
}
const project = resolveProject(options);
const location = resolveLocation(options);
const accessToken = await getVertexAccessToken({ signal: options?.signal, fetch: options?.fetch });
const url = `https://${location}-aiplatform.googleapis.com/${API_VERSION}/projects/${project}/locations/${location}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
return {
params,
url,
headers: { ...baseHeaders, Authorization: `Bearer ${accessToken}` },
fetch: options?.fetch,
};
},
});
function buildHttpOptions(
model: Model<"google-vertex">,
fetchOverride: FetchImpl | undefined,
): { headers?: Record<string, string>; fetch?: FetchImpl } | undefined {
const options: { headers?: Record<string, string>; fetch?: FetchImpl } = {};
if (model.headers) {
options.headers = { ...model.headers };
}
if (fetchOverride) {
options.fetch = fetchOverride;
}
return Object.keys(options).length > 0 ? options : undefined;
}
function createClient(
model: Model<"google-vertex">,
project: string,
location: string,
fetchOverride: FetchImpl | undefined,
): GoogleGenAI {
return new GoogleGenAI({
vertexai: true,
project,
location,
apiVersion: API_VERSION,
httpOptions: buildHttpOptions(model, fetchOverride),
});
}
function createClientWithApiKey(
model: Model<"google-vertex">,
apiKey: string,
fetchOverride: FetchImpl | undefined,
): GoogleGenAI {
return new GoogleGenAI({
vertexai: true,
apiKey,
apiVersion: API_VERSION,
httpOptions: buildHttpOptions(model, fetchOverride),
});
}
function resolveApiKey(options?: GoogleVertexOptions): string | undefined {
// options.apiKey may contain sentinel values like "<authenticated>" or "N/A"
// leaked from the agent loop — only use it if it looks like a real API key.
+23 -31
View File
@@ -1,11 +1,17 @@
import { GoogleGenAI } from "@google/genai";
import { getEnvApiKey } from "../stream";
import type { Context, FetchImpl, Model, StreamFunction } from "../types";
import type { Context, Model, StreamFunction } from "../types";
import type { AssistantMessageEventStream } from "../utils/event-stream";
import { buildGoogleGenerateContentParams, type GoogleSharedStreamOptions, streamGoogleGenAI } from "./google-shared";
import {
buildGoogleGenerateContentParams,
type GoogleGenAIRequestPlan,
type GoogleSharedStreamOptions,
streamGoogleGenAI,
} from "./google-shared";
export type GoogleOptions = GoogleSharedStreamOptions;
const DEFAULT_GENERATIVE_LANGUAGE_BASE = "https://generativelanguage.googleapis.com/v1beta";
export const streamGoogle: StreamFunction<"google-generative-ai"> = (
model: Model<"google-generative-ai">,
context: Context,
@@ -15,35 +21,21 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
model,
options,
api: "google-generative-ai",
prepare: () => {
prepare: (): GoogleGenAIRequestPlan => {
const apiKey = options?.apiKey || getEnvApiKey(model.provider);
const client = createClient(model, apiKey, options?.fetch);
if (!apiKey) {
throw new Error("Google Generative AI requires an API key (GEMINI_API_KEY or options.apiKey).");
}
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
const url = model.baseUrl ? `${model.baseUrl}/models/${model.id}:streamGenerateContent` : undefined;
return { client, params, url };
// `model.baseUrl` already includes the API version segment when set (mirrors the
// `apiVersion: ""` reset that the SDK relied on for custom base URLs).
const base = model.baseUrl?.trim() || DEFAULT_GENERATIVE_LANGUAGE_BASE;
const url = `${base}/models/${model.id}:streamGenerateContent?alt=sse`;
const headers: Record<string, string> = {
"x-goog-api-key": apiKey,
...(model.headers ?? {}),
...(options?.headers ?? {}),
};
return { params, url, headers, fetch: options?.fetch };
},
});
function createClient(model: Model<"google-generative-ai">, apiKey?: string, fetchOverride?: FetchImpl): GoogleGenAI {
const httpOptions: {
baseUrl?: string;
apiVersion?: string;
headers?: Record<string, string>;
fetch?: FetchImpl;
} = {};
if (model.baseUrl) {
httpOptions.baseUrl = model.baseUrl;
httpOptions.apiVersion = ""; // baseUrl already includes version path, don't append
}
if (model.headers) {
httpOptions.headers = model.headers;
}
if (fetchOverride) {
httpOptions.fetch = fetchOverride;
}
return new GoogleGenAI({
apiKey,
httpOptions: Object.keys(httpOptions).length > 0 ? httpOptions : undefined,
});
}
+159
View File
@@ -0,0 +1,159 @@
import { describe, expect, test } from "bun:test";
import { crc32, decodeEventStream, decodeMessage } from "../src/providers/aws-eventstream";
// ---- Frame builder (mirrors @smithy/eventstream-codec but in-process so the
// test owns the bytes). The decoder is the production code; we encode here for
// fixture generation only.
function encodeStringHeader(name: string, value: string): Uint8Array {
const nameBytes = new TextEncoder().encode(name);
const valueBytes = new TextEncoder().encode(value);
if (nameBytes.length > 255) throw new Error("name too long");
const buf = new Uint8Array(1 + nameBytes.length + 1 + 2 + valueBytes.length);
const view = new DataView(buf.buffer);
let p = 0;
view.setUint8(p, nameBytes.length);
p += 1;
buf.set(nameBytes, p);
p += nameBytes.length;
view.setUint8(p, 7); // string type
p += 1;
view.setUint16(p, valueBytes.length, false);
p += 2;
buf.set(valueBytes, p);
return buf;
}
function encodeFrame(headers: Record<string, string>, payload: Uint8Array): Uint8Array {
const headerChunks: Uint8Array[] = [];
for (const name in headers) headerChunks.push(encodeStringHeader(name, headers[name]));
const headerLen = headerChunks.reduce((s, c) => s + c.length, 0);
const headerBytes = new Uint8Array(headerLen);
let off = 0;
for (const c of headerChunks) {
headerBytes.set(c, off);
off += c.length;
}
const total = 4 + 4 + 4 + headerLen + payload.length + 4;
const out = new Uint8Array(total);
const view = new DataView(out.buffer);
view.setUint32(0, total, false);
view.setUint32(4, headerLen, false);
const preludeCrc = crc32(out.subarray(0, 8));
view.setUint32(8, preludeCrc, false);
out.set(headerBytes, 12);
out.set(payload, 12 + headerLen);
const msgCrc = crc32(out.subarray(0, total - 4));
view.setUint32(total - 4, msgCrc, false);
return out;
}
function streamFrom(chunks: Uint8Array[]): ReadableStream<Uint8Array> {
let i = 0;
return new ReadableStream({
pull(controller) {
if (i < chunks.length) controller.enqueue(chunks[i++]);
else controller.close();
},
});
}
async function collect(
stream: ReadableStream<Uint8Array>,
): Promise<Array<{ headers: Record<string, string>; text: string }>> {
const out: Array<{ headers: Record<string, string>; text: string }> = [];
for await (const msg of decodeEventStream(stream)) {
out.push({ headers: msg.headers, text: new TextDecoder().decode(msg.payload) });
}
return out;
}
describe("aws-eventstream", () => {
test("CRC32 matches known vectors", () => {
// Standard CRC32 of "123456789" = 0xCBF43926 (zlib/IEEE).
const bytes = new TextEncoder().encode("123456789");
expect(crc32(bytes)).toBe(0xcbf43926);
expect(crc32(new Uint8Array(0))).toBe(0);
});
test("decodes a single full-message frame", async () => {
const payload = new TextEncoder().encode('{"messageStart":{"role":"assistant"}}');
const frame = encodeFrame(
{ ":message-type": "event", ":event-type": "messageStart", ":content-type": "application/json" },
payload,
);
const decoded = decodeMessage(frame);
expect(decoded.headers[":event-type"]).toBe("messageStart");
expect(new TextDecoder().decode(decoded.payload)).toBe('{"messageStart":{"role":"assistant"}}');
const collected = await collect(streamFrom([frame]));
expect(collected).toHaveLength(1);
expect(collected[0].headers[":message-type"]).toBe("event");
});
test("stitches a frame split across two chunks", async () => {
const payload = new TextEncoder().encode('{"contentBlockDelta":{"delta":{"text":"hi"}}}');
const frame = encodeFrame({ ":message-type": "event", ":event-type": "contentBlockDelta" }, payload);
const mid = Math.floor(frame.length / 2);
const chunks = [frame.subarray(0, mid), frame.subarray(mid)];
const collected = await collect(streamFrom(chunks.map(c => new Uint8Array(c))));
expect(collected).toHaveLength(1);
expect(collected[0].headers[":event-type"]).toBe("contentBlockDelta");
expect(collected[0].text).toContain('"hi"');
});
test("decodes multiple messages packed into one chunk", async () => {
const a = encodeFrame(
{ ":message-type": "event", ":event-type": "messageStart" },
new TextEncoder().encode('{"role":"assistant"}'),
);
const b = encodeFrame(
{ ":message-type": "event", ":event-type": "contentBlockDelta" },
new TextEncoder().encode('{"x":1}'),
);
const c = encodeFrame(
{ ":message-type": "event", ":event-type": "messageStop" },
new TextEncoder().encode('{"stopReason":"end_turn"}'),
);
const merged = new Uint8Array(a.length + b.length + c.length);
merged.set(a, 0);
merged.set(b, a.length);
merged.set(c, a.length + b.length);
const collected = await collect(streamFrom([merged]));
expect(collected.map(x => x.headers[":event-type"])).toEqual([
"messageStart",
"contentBlockDelta",
"messageStop",
]);
});
test("surfaces exception event headers and payload", async () => {
const payload = new TextEncoder().encode('{"message":"input too long"}');
const frame = encodeFrame(
{
":message-type": "exception",
":exception-type": "validationException",
":content-type": "application/json",
},
payload,
);
const collected = await collect(streamFrom([frame]));
expect(collected).toHaveLength(1);
expect(collected[0].headers[":message-type"]).toBe("exception");
expect(collected[0].headers[":exception-type"]).toBe("validationException");
expect(collected[0].text).toContain("input too long");
});
test("throws on prelude CRC mismatch", () => {
const frame = encodeFrame({ ":event-type": "x" }, new Uint8Array(0));
frame[8] ^= 0xff; // flip a byte in the prelude CRC
expect(() => decodeMessage(frame)).toThrow(/prelude CRC/);
});
test("throws on message CRC mismatch", () => {
const frame = encodeFrame({ ":event-type": "x" }, new TextEncoder().encode("{}"));
frame[frame.length - 1] ^= 0xff;
expect(() => decodeMessage(frame)).toThrow(/message CRC/);
});
});
+96
View File
@@ -0,0 +1,96 @@
import { describe, expect, test } from "bun:test";
import { formatAmzDate, getSigningKey, signRequest, toHex } from "../src/providers/aws-sigv4";
// Canonical AWS SigV4 test vectors. Sourced from the
// `aws-sig-v4-test-suite` published with the SigV4 spec.
//
// We hit the two most common shapes: a GET with no body and a POST with a JSON
// body. Each vector pins the expected signature so any drift in canonicalization
// is caught.
const CREDS = {
accessKeyId: "AKIDEXAMPLE",
secretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
};
const REGION = "us-east-1";
const SERVICE = "service";
// 2015-08-30T12:36:00Z -> longDate 20150830T123600Z, shortDate 20150830.
const DATE = new Date("2015-08-30T12:36:00Z");
describe("aws-sigv4 helpers", () => {
test("formatAmzDate", () => {
expect(formatAmzDate(DATE)).toEqual({ longDate: "20150830T123600Z", shortDate: "20150830" });
});
test("derived signing key matches spec sample", async () => {
// Reference value from AWS docs:
// https://docs.aws.amazon.com/IAM/latest/UserGuide/signature-v4-examples.html
const key = await getSigningKey(CREDS.secretAccessKey, "20150830", REGION, "iam");
expect(toHex(key)).toBe("c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9");
});
});
describe("aws-sigv4 signRequest", () => {
test("GET with empty body matches @smithy/signature-v4 reference", async () => {
// Reference signatures cross-verified once against `@smithy/signature-v4`
// (with `@aws-crypto/sha256-js` as the hash) signing the same request
// with identical credentials/date/region/service. Pinned here so the test
// runs without those SDK deps.
const signed = await signRequest({
method: "GET",
host: "example.amazonaws.com",
path: "/",
body: new Uint8Array(0),
region: REGION,
service: SERVICE,
credentials: CREDS,
date: DATE,
});
expect(signed["x-amz-date"]).toBe("20150830T123600Z");
// SHA-256("") = e3b0c44...
expect(signed["x-amz-content-sha256"]).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
expect(signed.authorization).toBe(
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, " +
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, " +
"Signature=726c5c4879a6b4ccbbd3b24edbd6b8826d34f87450fbbf4e85546fc7ba9c1642",
);
});
test("POST with JSON body matches @smithy/signature-v4 reference", async () => {
const body = new TextEncoder().encode('{"hello":"world"}');
const signed = await signRequest({
method: "POST",
host: "example.amazonaws.com",
path: "/",
body,
region: REGION,
service: SERVICE,
credentials: CREDS,
date: DATE,
headers: { "content-type": "application/json" },
});
// SHA-256('{"hello":"world"}') = 93a23971a914e5eacbf0a8d25154cda...
expect(signed["x-amz-content-sha256"]).toBe("93a23971a914e5eacbf0a8d25154cda309c3c1c72fbb9914d47c60f3cb681588");
expect(signed.authorization).toBe(
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, " +
"SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, " +
"Signature=e9744044f72be2a6e5082cdcebb673e0a1daf890c82cc130d46abd3769ca15e0",
);
});
test("session token is included when credentials carry one", async () => {
const signed = await signRequest({
method: "GET",
host: "example.amazonaws.com",
path: "/",
body: new Uint8Array(0),
region: REGION,
service: SERVICE,
credentials: { ...CREDS, sessionToken: "AQoDYXdzEJr..." },
date: DATE,
});
expect(signed["x-amz-security-token"]).toBe("AQoDYXdzEJr...");
// Token must appear in SignedHeaders too (it's signed).
expect(signed.authorization).toContain("x-amz-security-token");
});
});
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it, vi } from "bun:test";
import { Models } from "@google/genai";
import { streamGoogle } from "@oh-my-pi/pi-ai/providers/google";
import type { Context, Model } from "@oh-my-pi/pi-ai/types";
import { hookFetch } from "@oh-my-pi/pi-utils";
const model: Model<"google-generative-ai"> = {
id: "gemini-3-pro-preview",
@@ -20,9 +20,11 @@ async function captureGooglePayload(
context: Context,
): Promise<{ config: { systemInstruction?: unknown }; contents: unknown[] }> {
let captured: { config: { systemInstruction?: unknown }; contents: unknown[] } | undefined;
vi.spyOn(Models.prototype, "generateContentStream").mockImplementation(async function* () {
// No chunks needed; the test only validates the generated request payload.
} as never);
// Intercept the outgoing REST call so the streamGoogle promise resolves cleanly without
// hitting the network. The test only validates `onPayload` (which fires before fetch).
using _hook = hookFetch(
async () => new Response("", { status: 200, headers: { "content-type": "text/event-stream" } }),
);
await streamGoogle(model, context, {
apiKey: "test-key",