From b1be18d1ebda7139f3c43213a3a620c6ec3f1625 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:54:49 +0000 Subject: [PATCH 1/3] Initial plan From 2d04f4ce847aabd9bfa02fe587002422dfd3c0a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:57:51 +0000 Subject: [PATCH 2/3] Initial plan for fixing TPM throttling retry logic Co-authored-by: wenshao <1166785+wenshao@users.noreply.github.com> --- package-lock.json | 95 +++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff5a902d7c8..319d0d6593d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -416,6 +416,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -891,6 +892,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -914,6 +916,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2353,6 +2356,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3839,6 +3843,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4430,6 +4435,7 @@ "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4440,6 +4446,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4652,6 +4659,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -4912,6 +4920,7 @@ "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", @@ -5062,6 +5071,7 @@ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", @@ -5546,6 +5556,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5960,8 +5971,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/array-includes": { "version": "3.1.9", @@ -6562,6 +6572,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -7345,7 +7356,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -8501,6 +8511,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -9216,7 +9227,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -9278,7 +9288,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -9288,7 +9297,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -9298,7 +9306,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -9506,7 +9513,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -9525,7 +9531,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -9534,15 +9539,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/finalhandler/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -10650,6 +10653,7 @@ "resolved": "https://registry.npmjs.org/ink/-/ink-6.2.3.tgz", "integrity": "sha512-fQkfEJjKbLXIcVWEE3MvpYSnwtbbmRsmeNDNz1pIuOFlwE+UF2gsy228J36OXKZGWJWZJKUigphBSqCNMcARtg==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", @@ -11645,6 +11649,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12656,7 +12661,6 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -13984,8 +13988,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/path-type": { "version": "3.0.0", @@ -14111,6 +14114,7 @@ "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright-core": "1.57.0" }, @@ -14148,7 +14152,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -14193,6 +14196,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14380,6 +14384,7 @@ "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -14723,6 +14728,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14733,6 +14739,7 @@ "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -14810,6 +14817,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -16120,6 +16128,7 @@ "integrity": "sha512-fIQnFtpksRRgHR1CO1onGX3djaog4qsW/c5U8arqYTkUEr2TaWpn05mIJDOBoPJFlOdqFrB4Ttv0PZJxV7avhw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", @@ -17003,6 +17012,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17202,7 +17212,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -17210,6 +17221,7 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -17404,6 +17416,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17751,7 +17764,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -17807,6 +17819,7 @@ "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", @@ -17920,6 +17933,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17933,6 +17947,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -18469,6 +18484,7 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -18649,6 +18665,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -18748,6 +18765,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", @@ -19377,6 +19395,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", @@ -19771,6 +19790,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -20533,6 +20553,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -20702,39 +20723,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "packages/sdk-typescript/node_modules/@vitest/browser": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.6.1.tgz", - "integrity": "sha512-9ZYW6KQ30hJ+rIfJoGH4wAub/KAb4YrFzX0kVLASvTm7nJWVC5EAv5SlzlXVl3h3DaUq5aqHlZl77nmOPnALUQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vitest/utils": "1.6.1", - "magic-string": "^0.30.5", - "sirv": "^2.0.4" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "playwright": "*", - "vitest": "1.6.1", - "webdriverio": "*" - }, - "peerDependenciesMeta": { - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true - } - } - }, "packages/sdk-typescript/node_modules/@vitest/coverage-v8": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", @@ -21046,6 +21034,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -21549,7 +21538,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", @@ -22189,6 +22177,7 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", @@ -23870,6 +23859,7 @@ "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23884,6 +23874,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", From f4db361819c58283e9b26f408f1616f8f2cb4ced Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:04:31 +0000 Subject: [PATCH 3/3] Fix: Reorder TPM throttling check to respect Retry-After headers The TPM throttling error check now happens after the Retry-After header check. This ensures that server-specified retry delays take precedence over the hardcoded 1-minute TPM delay. Added a test case to verify the fix. Co-authored-by: wenshao <1166785+wenshao@users.noreply.github.com> --- packages/core/src/utils/retry.test.ts | 160 +++++++++++++++++++++++++- packages/core/src/utils/retry.ts | 51 ++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) diff --git a/packages/core/src/utils/retry.test.ts b/packages/core/src/utils/retry.test.ts index 490f2444800..fa7b0a0725c 100644 --- a/packages/core/src/utils/retry.test.ts +++ b/packages/core/src/utils/retry.test.ts @@ -7,7 +7,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { HttpError } from './retry.js'; -import { getErrorStatus, retryWithBackoff } from './retry.js'; +import { + getErrorStatus, + isTPMThrottlingError, + retryWithBackoff, +} from './retry.js'; import { setSimulate429 } from './testUtils.js'; import { AuthType } from '../core/contentGenerator.js'; @@ -532,3 +536,157 @@ describe('getErrorStatus', () => { expect(getErrorStatus({ error: {} })).toBeUndefined(); }); }); + +describe('isTPMThrottlingError', () => { + it('should detect TPM throttling error from string', () => { + const errorMessage = + '{"error":{"message":"Throttling: TPM(10680324/10000000)","type":"Throttling","code":"429"}}'; + expect(isTPMThrottlingError(errorMessage)).toBe(true); + }); + + it('should detect TPM throttling error from Error object', () => { + const error = new Error('Throttling: TPM(10680324/10000000)'); + expect(isTPMThrottlingError(error)).toBe(true); + }); + + it('should detect TPM throttling error from nested error object', () => { + const error = { + error: { + message: 'Throttling: TPM(10680324/10000000)', + type: 'Throttling', + code: '429', + }, + }; + expect(isTPMThrottlingError(error)).toBe(true); + }); + + it('should return false for non-TPM errors', () => { + expect(isTPMThrottlingError('Regular error message')).toBe(false); + expect(isTPMThrottlingError(new Error('Regular error'))).toBe(false); + expect( + isTPMThrottlingError({ + error: { message: 'Rate limit exceeded', code: '429' }, + }), + ).toBe(false); + }); + + it('should return false for non-string non-object values', () => { + expect(isTPMThrottlingError(null)).toBe(false); + expect(isTPMThrottlingError(undefined)).toBe(false); + expect(isTPMThrottlingError(429)).toBe(false); + expect(isTPMThrottlingError(true)).toBe(false); + }); +}); + +describe('TPM throttling retry handling', () => { + beforeEach(() => { + vi.useFakeTimers(); + setSimulate429(false); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + it('should wait 1 minute for TPM throttling errors before retrying', async () => { + const tpmError: HttpError = new Error('Throttling: TPM(10680324/10000000)'); + tpmError.status = 429; + + const fn = vi + .fn() + .mockRejectedValueOnce(tpmError) + .mockResolvedValue('success'); + + const promise = retryWithBackoff(fn, { + maxAttempts: 3, + initialDelayMs: 100, + maxDelayMs: 1000, + }); + + // Fast-forward 1 minute for TPM delay + await vi.advanceTimersByTimeAsync(60000); + + await expect(promise).resolves.toBe('success'); + + // Should be called twice (1 failure + 1 success) + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('should reset exponential backoff delay after TPM throttling error', async () => { + const tpmError: HttpError = new Error('Throttling: TPM(10680324/10000000)'); + tpmError.status = 429; + const normalError: HttpError = new Error('Server error'); + normalError.status = 500; + + const fn = vi + .fn() + .mockRejectedValueOnce(tpmError) // First: TPM error (1 minute delay) + .mockRejectedValueOnce(normalError) // Second: normal error (should use initialDelay) + .mockResolvedValue('success'); + + const setTimeoutSpy = vi.spyOn(global, 'setTimeout'); + + const promise = retryWithBackoff(fn, { + maxAttempts: 5, + initialDelayMs: 100, + maxDelayMs: 1000, + }); + + // Fast-forward 1 minute for TPM delay + await vi.advanceTimersByTimeAsync(60000); + + // Now handle the second error with exponential backoff + await vi.runAllTimersAsync(); + + await expect(promise).resolves.toBe('success'); + + // Should be called 3 times + expect(fn).toHaveBeenCalledTimes(3); + + // Check that the second delay (after TPM) uses initialDelayMs, not a doubled value + const delays = setTimeoutSpy.mock.calls.map((call) => call[1] as number); + // First delay should be 60000ms (1 minute for TPM) + // Second delay should be around initialDelayMs (100ms) with jitter + expect(delays[0]).toBe(60000); + expect(delays[1]).toBeGreaterThanOrEqual(100 * 0.7); + expect(delays[1]).toBeLessThanOrEqual(100 * 1.3); + }); + + it('should respect Retry-After header even for TPM throttling errors', async () => { + // Create an error that is both a TPM throttling error AND has a Retry-After header + const tpmErrorWithRetryAfter: HttpError & { + response?: { headers?: { 'retry-after'?: string } }; + } = new Error('Throttling: TPM(10680324/10000000)'); + tpmErrorWithRetryAfter.status = 429; + tpmErrorWithRetryAfter.response = { + headers: { + 'retry-after': '30', // Server says wait 30 seconds + }, + }; + + const fn = vi + .fn() + .mockRejectedValueOnce(tpmErrorWithRetryAfter) + .mockResolvedValue('success'); + + const setTimeoutSpy = vi.spyOn(global, 'setTimeout'); + + const promise = retryWithBackoff(fn, { + maxAttempts: 3, + initialDelayMs: 100, + maxDelayMs: 1000, + }); + + // Wait for all timers to complete + await vi.runAllTimersAsync(); + + await expect(promise).resolves.toBe('success'); + + // Check that the delay used was from Retry-After (30 seconds), not TPM (60 seconds) + const delays = setTimeoutSpy.mock.calls.map((call) => call[1] as number); + + // The Retry-After header should take precedence over TPM-specific delay + expect(delays[0]).toBe(30000); // Should use Retry-After header value (30 seconds) + }); +}); diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index fd9b5c0257c..429b6632167 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -131,6 +131,17 @@ export async function retryWithBackoff( await delay(retryAfterMs); // Reset currentDelay for next potential non-429 error, or if Retry-After is not present next time currentDelay = initialDelayMs; + } else if (isTPMThrottlingError(error)) { + // Check for TPM throttling error - use fixed 1 minute delay + // This check happens after Retry-After to respect server-specified delays + const tpmDelayMs = 60000; // 1 minute + debugLogger.warn( + `Attempt ${attempt} failed with TPM throttling error. Retrying after ${tpmDelayMs}ms (1 minute)...`, + error, + ); + await delay(tpmDelayMs); + // Reset currentDelay for next potential non-TPM error + currentDelay = initialDelayMs; } else { // Fallback to exponential backoff with jitter logRetryAttempt(attempt, error, errorStatus); @@ -147,6 +158,46 @@ export async function retryWithBackoff( throw new Error('Retry attempts exhausted'); } +/** + * Checks if an error is a TPM (Tokens Per Minute) throttling error. + * These errors occur when the API rate limit is exceeded for TPM. + * Example: {"error":{"message":"Throttling: TPM(10680324/10000000)","type":"Throttling","code":"429"}} + * @param error The error object. + * @returns True if the error is a TPM throttling error. + */ +export function isTPMThrottlingError(error: unknown): boolean { + const checkMessage = (message: string): boolean => + message.includes('Throttling: TPM('); + + if (typeof error === 'string') { + return checkMessage(error); + } + + if (typeof error === 'object' && error !== null) { + // Check error.message + if ('message' in error && typeof (error as Error).message === 'string') { + if (checkMessage((error as Error).message)) { + return true; + } + } + + // Check error.error.message (nested error) + if ( + 'error' in error && + typeof (error as { error?: { message?: string } }).error === 'object' && + (error as { error?: { message?: string } }).error !== null + ) { + const nestedMessage = (error as { error: { message?: string } }).error + .message; + if (typeof nestedMessage === 'string' && checkMessage(nestedMessage)) { + return true; + } + } + } + + return false; +} + /** * Extracts the HTTP status code from an error object. *