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:
@@ -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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user