diff --git a/bun.lock b/bun.lock index b8f386392..77f0d212f 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], } } diff --git a/package.json b/package.json index 28dc1c033..36a99b45c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index c3e926885..682fea764 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -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 diff --git a/packages/ai/package.json b/packages/ai/package.json index 6905600b1..2243a32a6 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -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": { diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 880e623d0..0598c573f 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -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; + 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; } -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; + any?: Record; + tool?: { name: string }; +} +interface WireToolConfig { + tools: WireToolSpec[]; + toolChoice?: WireToolChoice; } -function createBedrockCredentialDefaultProvider( - requestHandler: NodeHttpHandler, -): NonNullable { - 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; } -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 = { + "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 = { ...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` 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 }, + 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 | undefined { +): Record | 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 = { - thinking: { - type: "enabled", - budget_tokens: budget, - }, + const result: Record = { + 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: }, 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 }; } diff --git a/packages/ai/src/providers/aws-credentials.ts b/packages/ai/src/providers/aws-credentials.ts new file mode 100644 index 000000000..831bc7fa2 --- /dev/null +++ b/packages/ai/src/providers/aws-credentials.ts @@ -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 = new Map(); + +export async function resolveAwsCredentials(opts: CredentialResolveOptions = {}): Promise { + 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 { + // 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>; + +function parseIni(text: string): IniFile { + const out: IniFile = {}; + let current: Record | 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 { + 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 { + 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 = { ...(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, + configIni: IniFile | undefined, + defaultRegion: string, + signal: AbortSignal | undefined, +): Promise { + // 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 { + 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 { + 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 { + 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(); +} diff --git a/packages/ai/src/providers/aws-eventstream.ts b/packages/ai/src/providers/aws-eventstream.ts new file mode 100644 index 000000000..2c9057f1a --- /dev/null +++ b/packages/ai/src/providers/aws-eventstream.ts @@ -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; + 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 { + const out: Record = {}; + 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` (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): AsyncGenerator { + 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 = 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(); + } +} diff --git a/packages/ai/src/providers/aws-sigv4.ts b/packages/ai/src/providers/aws-sigv4.ts new file mode 100644 index 000000000..f55576201 --- /dev/null +++ b/packages/ai/src/providers/aws-sigv4.ts @@ -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; + 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 = { + 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 { + if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) { + return bytes as Uint8Array; + } + 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 { + 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 { + return toHex(await sha256(data)); +} + +async function hmac(key: Uint8Array, data: string | Uint8Array): Promise { + 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 { + 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 { + 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 = { + 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; +} diff --git a/packages/ai/src/providers/google-auth.ts b/packages/ai/src/providers/google-auth.ts new file mode 100644 index 000000000..a8004508f --- /dev/null +++ b/packages/ai/src/providers/google-auth.ts @@ -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(); +const inflight = new Map>(); + +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(filePath: string): Promise { + 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(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(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 { + 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, privateKeyPem: string, keyId?: string): Promise { + const header: Record = { 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 { + 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 { + 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 { + 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 { + 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 { + 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(); +} diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index 1e9c29951..2bcd5821a 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -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, diff --git a/packages/ai/src/providers/google-shared.ts b/packages/ai/src/providers/google-shared.ts index 1226eef3b..8f65119aa 100644 --- a/packages/ai/src/providers/google-shared.ts +++ b/packages/ai/src/providers/google-shared.ts @@ -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; + fetch?: FetchImpl; +} + export function streamGoogleGenAI(args: { model: Model; options: GoogleSharedStreamOptions | undefined; api: T; retainTextSignature?: boolean; - prepare: () => { client: GoogleGenAI; params: GenerateContentParameters; url: string | undefined }; + prepare: () => GoogleGenAIRequestPlan | Promise; }): AssistantMessageEventStream { const { model, options, api, retainTextSignature, prepare } = args; const stream = new AssistantMessageEventStream(); @@ -757,17 +773,44 @@ export function streamGoogleGenAI ""); + 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(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 { + const body: Record = { 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 = {}; + 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; +} diff --git a/packages/ai/src/providers/google-types.ts b/packages/ai/src/providers/google-types.ts new file mode 100644 index 000000000..58b330275 --- /dev/null +++ b/packages/ai/src/providers/google-types.ts @@ -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; + id?: string; +} + +/** Tool execution result fed back to the model. */ +export interface FunctionResponsePart { + name: string; + response: Record; + 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; + parametersJsonSchema?: Record; +} + +/** Tool group as accepted at the request top level. */ +export interface ToolDeclaration { + functionDeclarations: Record[]; +} + +/** 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; + responseJsonSchema?: Record; + responseModalities?: string[]; + systemInstruction?: Content | { role?: string; parts: { text: string }[] }; + tools?: ToolDeclaration[]; + toolConfig?: ToolConfig; + safetySettings?: Array>; + 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; +} diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index ad8a21340..31ad3a676 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -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 => { 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 = { + ...(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; fetch?: FetchImpl } | undefined { - const options: { headers?: Record; 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 "" or "N/A" // leaked from the agent loop — only use it if it looks like a real API key. diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index b94386b8b..2d64c4199 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -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 = { + "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; - 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, - }); -} diff --git a/packages/ai/test/aws-eventstream.test.ts b/packages/ai/test/aws-eventstream.test.ts new file mode 100644 index 000000000..347660ca1 --- /dev/null +++ b/packages/ai/test/aws-eventstream.test.ts @@ -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, 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 { + let i = 0; + return new ReadableStream({ + pull(controller) { + if (i < chunks.length) controller.enqueue(chunks[i++]); + else controller.close(); + }, + }); +} + +async function collect( + stream: ReadableStream, +): Promise; text: string }>> { + const out: Array<{ headers: Record; 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/); + }); +}); diff --git a/packages/ai/test/aws-sigv4.test.ts b/packages/ai/test/aws-sigv4.test.ts new file mode 100644 index 000000000..bcb661f19 --- /dev/null +++ b/packages/ai/test/aws-sigv4.test.ts @@ -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"); + }); +}); diff --git a/packages/ai/test/google-system-prompt.test.ts b/packages/ai/test/google-system-prompt.test.ts index 42b8a29e9..f12d1e23b 100644 --- a/packages/ai/test/google-system-prompt.test.ts +++ b/packages/ai/test/google-system-prompt.test.ts @@ -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",