From 17129024f4c71a36b4cbbca84e91e1541b72ca41 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Fri, 19 Dec 2025 16:26:54 +0800 Subject: [PATCH] Add Gemini provider, remove legacy Google OAuth, and tune generation defaults --- package-lock.json | 2048 +++++++++++++---- package.json | 4 - packages/cli/package.json | 4 +- packages/cli/src/config/auth.ts | 18 + packages/cli/src/config/config.ts | 7 +- packages/cli/src/gemini.tsx | 14 +- .../io/BaseJsonOutputAdapter.ts | 2 - packages/cli/src/nonInteractiveCli.ts | 2 - .../cli/src/services/McpPromptLoader.test.ts | 2 +- packages/cli/src/services/McpPromptLoader.ts | 7 +- packages/cli/src/ui/AppContainer.test.tsx | 76 - packages/cli/src/ui/AppContainer.tsx | 22 +- packages/cli/src/ui/auth/AuthDialog.test.tsx | 8 +- packages/cli/src/ui/auth/useAuth.ts | 16 +- .../cli/src/ui/commands/ideCommand.test.ts | 1 - .../cli/src/ui/components/DialogManager.tsx | 10 - .../src/ui/components/ProQuotaDialog.test.tsx | 91 - .../cli/src/ui/components/ProQuotaDialog.tsx | 55 - .../cli/src/ui/contexts/UIActionsContext.tsx | 1 - .../cli/src/ui/contexts/UIStateContext.tsx | 10 - .../cli/src/ui/hooks/useGeminiStream.test.tsx | 8 +- packages/cli/src/ui/hooks/useGeminiStream.ts | 10 +- .../src/ui/hooks/useQuotaAndFallback.test.ts | 391 ---- .../cli/src/ui/hooks/useQuotaAndFallback.ts | 175 -- packages/cli/src/ui/hooks/useQwenAuth.test.ts | 2 +- .../cli/src/ui/hooks/useToolScheduler.test.ts | 2 +- .../cli/src/validateNonInterActiveAuth.ts | 7 + packages/core/package.json | 7 +- packages/core/src/code_assist/codeAssist.ts | 54 - .../core/src/code_assist/converter.test.ts | 456 ---- packages/core/src/code_assist/converter.ts | 285 --- .../oauth-credential-storage.test.ts | 217 -- .../code_assist/oauth-credential-storage.ts | 130 -- packages/core/src/code_assist/oauth2.test.ts | 1166 ---------- packages/core/src/code_assist/oauth2.ts | 563 ----- packages/core/src/code_assist/server.test.ts | 255 -- packages/core/src/code_assist/server.ts | 253 -- packages/core/src/code_assist/setup.test.ts | 224 -- packages/core/src/code_assist/setup.ts | 124 - packages/core/src/code_assist/types.ts | 201 -- packages/core/src/config/config.test.ts | 17 - packages/core/src/config/config.ts | 14 +- .../core/src/config/flashFallback.test.ts | 2 +- .../__tests__/openaiTimeoutHandling.test.ts | 3 + packages/core/src/core/baseLlmClient.test.ts | 12 +- packages/core/src/core/baseLlmClient.ts | 7 - packages/core/src/core/client.test.ts | 49 +- packages/core/src/core/client.ts | 40 +- .../core/src/core/contentGenerator.test.ts | 52 +- packages/core/src/core/contentGenerator.ts | 64 +- .../core/src/core/coreToolScheduler.test.ts | 26 +- packages/core/src/core/geminiChat.test.ts | 4 +- .../geminiContentGenerator.test.ts | 173 ++ .../geminiContentGenerator.ts | 140 ++ .../core/geminiContentGenerator/index.test.ts | 47 + .../src/core/geminiContentGenerator/index.ts | 55 + .../loggingContentGenerator.ts | 106 +- .../core/nonInteractiveToolExecutor.test.ts | 2 +- .../openaiContentGenerator.test.ts | 3 + .../openaiContentGenerator/pipeline.test.ts | 1 + .../core/openaiContentGenerator/pipeline.ts | 34 +- .../provider/dashscope.ts | 9 + .../provider/deepseek.ts | 7 + .../provider/default.ts | 8 + .../openaiContentGenerator/provider/types.ts | 2 + packages/core/src/fallback/handler.test.ts | 190 +- packages/core/src/fallback/handler.ts | 54 +- packages/core/src/index.ts | 5 - packages/core/src/qwen/qwenOAuth2.ts | 2 + .../clearcut-logger/clearcut-logger.test.ts | 44 +- .../clearcut-logger/clearcut-logger.ts | 21 +- packages/core/src/telemetry/loggers.test.ts | 35 +- packages/core/src/telemetry/loggers.ts | 4 - packages/core/src/tools/mcp-client.test.ts | 40 +- packages/core/src/utils/errorParsing.test.ts | 255 +- packages/core/src/utils/errorParsing.ts | 104 +- packages/core/src/utils/flashFallback.test.ts | 81 - packages/core/src/utils/retry.test.ts | 167 -- packages/core/src/utils/retry.ts | 83 +- .../core/src/utils/userAccountManager.test.ts | 340 --- packages/core/src/utils/userAccountManager.ts | 140 -- packages/sdk-typescript/package.json | 6 +- packages/sdk-typescript/src/mcp/tool.ts | 4 +- packages/sdk-typescript/tsconfig.json | 2 +- packages/vscode-ide-companion/NOTICES.txt | 531 +---- packages/vscode-ide-companion/package.json | 2 +- 86 files changed, 2409 insertions(+), 7506 deletions(-) delete mode 100644 packages/cli/src/ui/components/ProQuotaDialog.test.tsx delete mode 100644 packages/cli/src/ui/components/ProQuotaDialog.tsx delete mode 100644 packages/cli/src/ui/hooks/useQuotaAndFallback.test.ts delete mode 100644 packages/cli/src/ui/hooks/useQuotaAndFallback.ts delete mode 100644 packages/core/src/code_assist/codeAssist.ts delete mode 100644 packages/core/src/code_assist/converter.test.ts delete mode 100644 packages/core/src/code_assist/converter.ts delete mode 100644 packages/core/src/code_assist/oauth-credential-storage.test.ts delete mode 100644 packages/core/src/code_assist/oauth-credential-storage.ts delete mode 100644 packages/core/src/code_assist/oauth2.test.ts delete mode 100644 packages/core/src/code_assist/oauth2.ts delete mode 100644 packages/core/src/code_assist/server.test.ts delete mode 100644 packages/core/src/code_assist/server.ts delete mode 100644 packages/core/src/code_assist/setup.test.ts delete mode 100644 packages/core/src/code_assist/setup.ts delete mode 100644 packages/core/src/code_assist/types.ts create mode 100644 packages/core/src/core/geminiContentGenerator/geminiContentGenerator.test.ts create mode 100644 packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts create mode 100644 packages/core/src/core/geminiContentGenerator/index.test.ts create mode 100644 packages/core/src/core/geminiContentGenerator/index.ts rename packages/core/src/core/{ => geminiContentGenerator}/loggingContentGenerator.ts (63%) delete mode 100644 packages/core/src/utils/userAccountManager.test.ts delete mode 100644 packages/core/src/utils/userAccountManager.ts diff --git a/package-lock.json b/package-lock.json index 5526d051..6d6dbdcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,6 @@ "eslint-plugin-react-hooks": "^5.2.0", "glob": "^10.5.0", "globals": "^16.0.0", - "google-artifactregistry-auth": "^3.4.0", "husky": "^9.1.7", "json": "^11.0.0", "lint-staged": "^16.1.6", @@ -568,7 +567,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -592,7 +590,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1210,27 +1207,6 @@ "resolved": "packages/test-utils", "link": true }, - "node_modules/@google/genai": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.16.0.tgz", - "integrity": "sha512-hdTYu39QgDFxv+FB6BK2zi4UIJGWhx2iPc0pHQ0C5Q/RCi+m+4gsryIzTGO+riqWcUA8/WGYp6hpqckdOBNysw==", - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^9.14.2", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.11.4" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, "node_modules/@grpc/grpc-js": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", @@ -1262,6 +1238,18 @@ "node": ">=6" } }, + "node_modules/@hono/node-server": { + "version": "1.19.7", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", + "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1830,247 +1818,6 @@ "win32" ] }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz", - "integrity": "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.6", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@mswjs/interceptors": { "version": "0.39.5", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.5.tgz", @@ -2157,7 +1904,6 @@ "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" } @@ -2536,24 +2282,6 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/resource-detector-gcp": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.40.0.tgz", - "integrity": "sha512-uAsUV8K4R9OJ3cgPUGYDqQByxOMTz4StmzJyofIv7+W+c1dTSEc1WVjWpTS2PAmywik++JlSmd8O4rMRJZpO8Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "gcp-metadata": "^6.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, "node_modules/@opentelemetry/resources": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", @@ -3671,7 +3399,6 @@ "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", @@ -4142,7 +3869,6 @@ "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4153,7 +3879,6 @@ "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -4359,7 +4084,6 @@ "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", @@ -5135,7 +4859,6 @@ "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" }, @@ -5188,6 +4911,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5530,7 +5254,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/array-includes": { "version": "3.1.9", @@ -6865,6 +6590,7 @@ "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" }, @@ -7090,6 +6816,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -7982,7 +7717,6 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8518,6 +8252,7 @@ "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", @@ -8579,6 +8314,7 @@ "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" } @@ -8588,6 +8324,7 @@ "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" } @@ -8597,6 +8334,7 @@ "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" } @@ -8689,6 +8427,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -8732,6 +8471,29 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8763,6 +8525,7 @@ "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", @@ -8781,6 +8544,7 @@ "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" } @@ -8789,13 +8553,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "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" } @@ -8912,6 +8678,18 @@ "node": ">= 0.6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -9051,36 +8829,6 @@ "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", "license": "BSD-3-Clause" }, - "node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -9390,46 +9138,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/google-artifactregistry-auth": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/google-artifactregistry-auth/-/google-artifactregistry-auth-3.4.0.tgz", - "integrity": "sha512-Z2EmP7gbKtTmK5k846tfF7dQqeU2vREIcfCI79FKRTAdkbUZ/BFGJwwTvC2ss0vYSySvK7h6I1JsqBFqIXABBg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^9.14.0", - "yargs": "^17.1.1" - }, - "bin": { - "artifactregistry-auth": "src/main.js" - } - }, - "node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -9478,19 +9186,6 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, - "node_modules/gtoken": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "license": "MIT", - "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -9607,6 +9302,16 @@ "node": ">=12.0.0" } }, + "node_modules/hono": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.1.tgz", + "integrity": "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -9909,7 +9614,6 @@ "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", @@ -10647,6 +10351,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10908,6 +10613,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -11019,6 +10733,12 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -11864,6 +11584,7 @@ "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" } @@ -12199,46 +11920,24 @@ "license": "MIT", "optional": true }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + ], "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "engines": { + "node": ">=10.5.0" } }, "node_modules/node-pty": { @@ -13162,7 +12861,8 @@ "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" + "license": "MIT", + "peer": true }, "node_modules/path-type": { "version": "3.0.0", @@ -13652,6 +13352,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13821,7 +13522,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13832,7 +13532,6 @@ "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -13866,7 +13565,6 @@ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -15932,7 +15630,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16112,8 +15809,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.3", @@ -16121,7 +15817,6 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -16316,7 +16011,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16588,6 +16282,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -16623,6 +16318,7 @@ "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" } @@ -16678,7 +16374,6 @@ "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", @@ -16792,7 +16487,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16806,7 +16500,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -16900,6 +16593,15 @@ "node": ">=18" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -17485,27 +17187,17 @@ "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" } }, - "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - }, "packages/cli": { "name": "@qwen-code/qwen-code", "version": "0.5.1", "dependencies": { - "@google/genai": "1.16.0", + "@google/genai": "1.30.0", "@iarna/toml": "^2.2.5", - "@modelcontextprotocol/sdk": "^1.15.1", + "@modelcontextprotocol/sdk": "^1.25.1", "@qwen-code/qwen-code-core": "file:../core", "@types/update-notifier": "^6.0.8", "ansi-regex": "^6.2.2", @@ -17569,6 +17261,75 @@ "node": ">=20" } }, + "packages/cli/node_modules/@google/genai": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz", + "integrity": "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.20.1" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "packages/cli/node_modules/@modelcontextprotocol/sdk": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", + "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "packages/cli/node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, "packages/cli/node_modules/@testing-library/react": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", @@ -17597,6 +17358,436 @@ } } }, + "packages/cli/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/cli/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/cli/node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/cli/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/cli/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "packages/cli/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "packages/cli/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/cli/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/cli/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/cli/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "packages/cli/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/cli/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/cli/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "packages/cli/node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/cli/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/cli/node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/cli/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "packages/cli/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/cli/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/cli/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "packages/cli/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "packages/cli/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/cli/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "packages/cli/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/cli/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/cli/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "packages/cli/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -17614,13 +17805,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/cli/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "packages/core": { "name": "@qwen-code/qwen-code-core", "version": "0.5.1", "hasInstallScript": true, "dependencies": { - "@google/genai": "1.16.0", - "@modelcontextprotocol/sdk": "^1.11.0", + "@google/genai": "1.30.0", + "@modelcontextprotocol/sdk": "^1.25.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-logs-otlp-http": "^0.203.0", @@ -17629,7 +17834,6 @@ "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", "@opentelemetry/instrumentation-http": "^0.203.0", - "@opentelemetry/resource-detector-gcp": "^0.40.0", "@opentelemetry/sdk-node": "^0.203.0", "@types/html-to-text": "^9.0.4", "@xterm/headless": "5.5.0", @@ -17644,7 +17848,7 @@ "fdir": "^6.4.6", "fzf": "^0.5.2", "glob": "^10.5.0", - "google-auth-library": "^9.11.0", + "google-auth-library": "^10.5.0", "html-to-text": "^9.0.5", "https-proxy-agent": "^7.0.6", "ignore": "^7.0.0", @@ -17688,6 +17892,79 @@ "node-pty": "^1.0.0" } }, + "packages/core/node_modules/@google/genai": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz", + "integrity": "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.20.1" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "packages/core/node_modules/@modelcontextprotocol/sdk": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", + "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "packages/core/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "packages/core/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -17704,6 +17981,112 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "packages/core/node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/core/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/core/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "packages/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "packages/core/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "packages/core/node_modules/fdir": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", @@ -17718,6 +18101,141 @@ } } }, + "packages/core/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/core/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/core/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "packages/core/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/core/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/core/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "packages/core/node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/core/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/core/node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "packages/core/node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -17727,6 +18245,27 @@ "node": ">= 4" } }, + "packages/core/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/core/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/core/node_modules/mime": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.7.tgz", @@ -17742,12 +18281,54 @@ "node": ">=16" } }, + "packages/core/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/core/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "packages/core/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "packages/core/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -17755,12 +18336,126 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "packages/core/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/core/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "packages/core/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/core/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/core/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/core/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/core/node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, "packages/sdk-typescript": { "name": "@qwen-code/sdk", "version": "0.5.1", "license": "Apache-2.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.4" + "@modelcontextprotocol/sdk": "^1.25.1", + "zod": "^3.25.0" }, "devDependencies": { "@types/node": "^20.14.0", @@ -17771,8 +18466,7 @@ "esbuild": "^0.25.12", "eslint": "^8.57.0", "typescript": "^5.4.5", - "vitest": "^1.6.0", - "zod": "^3.23.8" + "vitest": "^1.6.0" }, "engines": { "node": ">=18.0.0" @@ -18277,6 +18971,70 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "packages/sdk-typescript/node_modules/@modelcontextprotocol/sdk": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", + "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "packages/sdk-typescript/node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/sdk-typescript/node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, "packages/sdk-typescript/node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -18595,6 +19353,19 @@ "url": "https://opencollective.com/vitest" } }, + "packages/sdk-typescript/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "packages/sdk-typescript/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -18628,6 +19399,30 @@ "node": "*" } }, + "packages/sdk-typescript/node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "packages/sdk-typescript/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -18670,6 +19465,45 @@ "node": "*" } }, + "packages/sdk-typescript/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/sdk-typescript/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "packages/sdk-typescript/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "packages/sdk-typescript/node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -18843,6 +19677,49 @@ "url": "https://opencollective.com/eslint" } }, + "packages/sdk-typescript/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "packages/sdk-typescript/node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -18856,6 +19733,27 @@ "node": "^10.12.0 || >=12.0.0" } }, + "packages/sdk-typescript/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "packages/sdk-typescript/node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -18871,6 +19769,15 @@ "node": "^10.12.0 || >=12.0.0" } }, + "packages/sdk-typescript/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "packages/sdk-typescript/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -18930,6 +19837,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/sdk-typescript/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/sdk-typescript/node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "packages/sdk-typescript/node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -18940,6 +19883,12 @@ "node": ">=8" } }, + "packages/sdk-typescript/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "packages/sdk-typescript/node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -18950,6 +19899,52 @@ "get-func-name": "^2.0.1" } }, + "packages/sdk-typescript/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/sdk-typescript/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/sdk-typescript/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/sdk-typescript/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "packages/sdk-typescript/node_modules/p-limit": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", @@ -18998,6 +19993,36 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "packages/sdk-typescript/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/sdk-typescript/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "packages/sdk-typescript/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -19005,6 +20030,51 @@ "dev": true, "license": "MIT" }, + "packages/sdk-typescript/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/sdk-typescript/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "packages/sdk-typescript/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -19102,6 +20172,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/sdk-typescript/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "packages/sdk-typescript/node_modules/vite-node": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", @@ -20201,7 +21285,7 @@ "version": "0.5.1", "license": "LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.15.1", + "@modelcontextprotocol/sdk": "^1.25.1", "cors": "^2.8.5", "express": "^5.1.0", "markdown-it": "^14.1.0", @@ -20236,6 +21320,54 @@ "vscode": "^1.99.0" } }, + "packages/vscode-ide-companion/node_modules/@modelcontextprotocol/sdk": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", + "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "packages/vscode-ide-companion/node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, "packages/vscode-ide-companion/node_modules/@types/react": { "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", @@ -20271,6 +21403,22 @@ "dev": true, "license": "MIT" }, + "packages/vscode-ide-companion/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "packages/vscode-ide-companion/node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -20349,6 +21497,12 @@ "node": ">= 0.8" } }, + "packages/vscode-ide-companion/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "packages/vscode-ide-companion/node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", diff --git a/package.json b/package.json index e5ed33bd..d88f52be 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,6 @@ "scripts": { "start": "cross-env node scripts/start.js", "debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js", - "auth:npm": "npx google-artifactregistry-auth", - "auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev", - "auth": "npm run auth:npm && npm run auth:docker", "generate": "node scripts/generate-git-commit-info.js", "build": "node scripts/build.js", "build-and-start": "npm run build && npm run start", @@ -95,7 +92,6 @@ "eslint-plugin-react-hooks": "^5.2.0", "glob": "^10.5.0", "globals": "^16.0.0", - "google-artifactregistry-auth": "^3.4.0", "husky": "^9.1.7", "json": "^11.0.0", "lint-staged": "^16.1.6", diff --git a/packages/cli/package.json b/packages/cli/package.json index 150c12e5..7eb084c4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,10 +36,10 @@ "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.1" }, "dependencies": { - "@google/genai": "1.16.0", + "@google/genai": "1.30.0", "@iarna/toml": "^2.2.5", "@qwen-code/qwen-code-core": "file:../core", - "@modelcontextprotocol/sdk": "^1.15.1", + "@modelcontextprotocol/sdk": "^1.25.1", "@types/update-notifier": "^6.0.8", "ansi-regex": "^6.2.2", "command-exists": "^1.2.9", diff --git a/packages/cli/src/config/auth.ts b/packages/cli/src/config/auth.ts index 83761e3e..7f87db45 100644 --- a/packages/cli/src/config/auth.ts +++ b/packages/cli/src/config/auth.ts @@ -26,5 +26,23 @@ export function validateAuthMethod(authMethod: string): string | null { return null; } + if (authMethod === AuthType.USE_GEMINI) { + const hasApiKey = process.env['GEMINI_API_KEY']; + if (!hasApiKey) { + return 'GEMINI_API_KEY environment variable not found. Please set it in your .env file or environment variables.'; + } + return null; + } + + if (authMethod === AuthType.USE_VERTEX_AI) { + const hasApiKey = process.env['GOOGLE_API_KEY']; + if (!hasApiKey) { + return 'GOOGLE_API_KEY environment variable not found. Please set it in your .env file or environment variables.'; + } + + process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true'; + return null; + } + return 'Invalid auth method selected.'; } diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index c3754daa..07c65db9 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -460,7 +460,12 @@ export async function parseArguments(settings: Settings): Promise { }) .option('auth-type', { type: 'string', - choices: [AuthType.USE_OPENAI, AuthType.QWEN_OAUTH], + choices: [ + AuthType.USE_OPENAI, + AuthType.QWEN_OAUTH, + AuthType.USE_GEMINI, + AuthType.USE_VERTEX_AI, + ], description: 'Authentication type', }) .deprecateOption( diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 4c1ce34c..279a7e2b 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -4,10 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { Config } from '@qwen-code/qwen-code-core'; +import type { Config , + AuthType} from '@qwen-code/qwen-code-core'; import { - AuthType, - getOauthClient, InputFormat, logUserPrompt, } from '@qwen-code/qwen-code-core'; @@ -399,15 +398,6 @@ export async function main() { initializationResult = await initializeApp(config, settings); } - if ( - settings.merged.security?.auth?.selectedType === - AuthType.LOGIN_WITH_GOOGLE && - config.isBrowserLaunchSuppressed() - ) { - // Do oauth before app renders to make copying the link possible. - await getOauthClient(settings.merged.security.auth.selectedType, config); - } - if (config.getExperimentalZedIntegration()) { return runAcpAgent(config, settings, extensions, argv); } diff --git a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts index 915fb721..ef665537 100644 --- a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts +++ b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts @@ -610,8 +610,6 @@ export abstract class BaseJsonOutputAdapter { const errorText = parseAndFormatApiError( event.value.error, this.config.getContentGeneratorConfig()?.authType, - undefined, - this.config.getModel(), ); this.appendText(state, errorText, null); break; diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 1614c304..1719a053 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -221,8 +221,6 @@ export async function runNonInteractive( const errorText = parseAndFormatApiError( event.value.error, config.getContentGeneratorConfig()?.authType, - undefined, - config.getModel(), ); process.stderr.write(`${errorText}\n`); } diff --git a/packages/cli/src/services/McpPromptLoader.test.ts b/packages/cli/src/services/McpPromptLoader.test.ts index c58c3daa..3e42fb82 100644 --- a/packages/cli/src/services/McpPromptLoader.test.ts +++ b/packages/cli/src/services/McpPromptLoader.test.ts @@ -28,7 +28,7 @@ const mockPrompt = { { name: 'trail', required: false, description: "The animal's trail." }, ], invoke: vi.fn().mockResolvedValue({ - messages: [{ content: { text: 'Hello, world!' } }], + messages: [{ content: { type: 'text', text: 'Hello, world!' } }], }), }; diff --git a/packages/cli/src/services/McpPromptLoader.ts b/packages/cli/src/services/McpPromptLoader.ts index 47858a13..36da96d6 100644 --- a/packages/cli/src/services/McpPromptLoader.ts +++ b/packages/cli/src/services/McpPromptLoader.ts @@ -123,7 +123,10 @@ export class McpPromptLoader implements ICommandLoader { }; } - if (!result.messages?.[0]?.content?.['text']) { + const firstMessage = result.messages?.[0]; + const content = firstMessage?.content; + + if (content?.type !== 'text') { return { type: 'message', messageType: 'error', @@ -134,7 +137,7 @@ export class McpPromptLoader implements ICommandLoader { return { type: 'submit_prompt', - content: JSON.stringify(result.messages[0].content.text), + content: JSON.stringify(content.text), }; } catch (error) { return { diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 0abb960c..60426f1d 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -23,7 +23,6 @@ import { } from '@qwen-code/qwen-code-core'; import type { LoadedSettings } from '../config/settings.js'; import type { InitializationResult } from '../core/initializer.js'; -import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js'; import { UIStateContext, type UIState } from './contexts/UIStateContext.js'; import { UIActionsContext, @@ -56,7 +55,6 @@ vi.mock('./App.js', () => ({ App: TestContextConsumer, })); -vi.mock('./hooks/useQuotaAndFallback.js'); vi.mock('./hooks/useHistoryManager.js'); vi.mock('./hooks/useThemeCommand.js'); vi.mock('./auth/useAuth.js'); @@ -122,7 +120,6 @@ describe('AppContainer State Management', () => { let mockInitResult: InitializationResult; // Create typed mocks for all hooks - const mockedUseQuotaAndFallback = useQuotaAndFallback as Mock; const mockedUseHistory = useHistory as Mock; const mockedUseThemeCommand = useThemeCommand as Mock; const mockedUseAuthCommand = useAuthCommand as Mock; @@ -164,10 +161,6 @@ describe('AppContainer State Management', () => { capturedUIActions = null!; // **Provide a default return value for EVERY mocked hook.** - mockedUseQuotaAndFallback.mockReturnValue({ - proQuotaRequest: null, - handleProQuotaChoice: vi.fn(), - }); mockedUseHistory.mockReturnValue({ history: [], addItem: vi.fn(), @@ -567,75 +560,6 @@ describe('AppContainer State Management', () => { }); }); - describe('Quota and Fallback Integration', () => { - it('passes a null proQuotaRequest to UIStateContext by default', () => { - // The default mock from beforeEach already sets proQuotaRequest to null - render( - , - ); - - // Assert that the context value is as expected - expect(capturedUIState.proQuotaRequest).toBeNull(); - }); - - it('passes a valid proQuotaRequest to UIStateContext when provided by the hook', () => { - // Arrange: Create a mock request object that a UI dialog would receive - const mockRequest = { - failedModel: 'gemini-pro', - fallbackModel: 'gemini-flash', - resolve: vi.fn(), - }; - mockedUseQuotaAndFallback.mockReturnValue({ - proQuotaRequest: mockRequest, - handleProQuotaChoice: vi.fn(), - }); - - // Act: Render the container - render( - , - ); - - // Assert: The mock request is correctly passed through the context - expect(capturedUIState.proQuotaRequest).toEqual(mockRequest); - }); - - it('passes the handleProQuotaChoice function to UIActionsContext', () => { - // Arrange: Create a mock handler function - const mockHandler = vi.fn(); - mockedUseQuotaAndFallback.mockReturnValue({ - proQuotaRequest: null, - handleProQuotaChoice: mockHandler, - }); - - // Act: Render the container - render( - , - ); - - // Assert: The action in the context is the mock handler we provided - expect(capturedUIActions.handleProQuotaChoice).toBe(mockHandler); - - // You can even verify that the plumbed function is callable - capturedUIActions.handleProQuotaChoice('auth'); - expect(mockHandler).toHaveBeenCalledWith('auth'); - }); - }); - describe('Terminal Title Update Feature', () => { beforeEach(() => { // Reset mock stdout for each test diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index e70c0446..7921b203 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -32,7 +32,6 @@ import { type Config, type IdeInfo, type IdeContext, - type UserTierId, DEFAULT_GEMINI_FLASH_MODEL, IdeClient, ideContextStore, @@ -48,7 +47,6 @@ import { useHistory } from './hooks/useHistoryManager.js'; import { useMemoryMonitor } from './hooks/useMemoryMonitor.js'; import { useThemeCommand } from './hooks/useThemeCommand.js'; import { useAuthCommand } from './auth/useAuth.js'; -import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js'; import { useEditorSettings } from './hooks/useEditorSettings.js'; import { useSettingsCommand } from './hooks/useSettingsCommand.js'; import { useModelCommand } from './hooks/useModelCommand.js'; @@ -192,8 +190,6 @@ export const AppContainer = (props: AppContainerProps) => { const [currentModel, setCurrentModel] = useState(getEffectiveModel()); - const [userTier] = useState(undefined); - const [isConfigInitialized, setConfigInitialized] = useState(false); const [userMessages, setUserMessages] = useState([]); @@ -367,14 +363,6 @@ export const AppContainer = (props: AppContainerProps) => { cancelAuthentication, } = useAuthCommand(settings, config, historyManager.addItem); - const { proQuotaRequest, handleProQuotaChoice } = useQuotaAndFallback({ - config, - historyManager, - userTier, - setAuthState, - setModelSwitchedFromQuotaError, - }); - useInitializationAuthError(initializationResult.authError, onAuthError); // Sync user tier from config when authentication changes @@ -752,8 +740,7 @@ export const AppContainer = (props: AppContainerProps) => { !initError && !isProcessing && (streamingState === StreamingState.Idle || - streamingState === StreamingState.Responding) && - !proQuotaRequest; + streamingState === StreamingState.Responding); const [controlsHeight, setControlsHeight] = useState(0); @@ -1206,7 +1193,6 @@ export const AppContainer = (props: AppContainerProps) => { isAuthenticating || isEditorDialogOpen || showIdeRestartPrompt || - !!proQuotaRequest || isSubagentCreateDialogOpen || isAgentsManagerDialogOpen || isApprovalModeDialogOpen || @@ -1277,8 +1263,6 @@ export const AppContainer = (props: AppContainerProps) => { showWorkspaceMigrationDialog, workspaceExtensions, currentModel, - userTier, - proQuotaRequest, contextFileNames, errorCount, availableTerminalHeight, @@ -1367,8 +1351,6 @@ export const AppContainer = (props: AppContainerProps) => { showAutoAcceptIndicator, showWorkspaceMigrationDialog, workspaceExtensions, - userTier, - proQuotaRequest, contextFileNames, errorCount, availableTerminalHeight, @@ -1430,7 +1412,6 @@ export const AppContainer = (props: AppContainerProps) => { handleClearScreen, onWorkspaceMigrationDialogOpen, onWorkspaceMigrationDialogClose, - handleProQuotaChoice, // Vision switch dialog handleVisionSwitchSelect, // Welcome back dialog @@ -1468,7 +1449,6 @@ export const AppContainer = (props: AppContainerProps) => { handleClearScreen, onWorkspaceMigrationDialogOpen, onWorkspaceMigrationDialogClose, - handleProQuotaChoice, handleVisionSwitchSelect, handleWelcomeBackSelection, handleWelcomeBackClose, diff --git a/packages/cli/src/ui/auth/AuthDialog.test.tsx b/packages/cli/src/ui/auth/AuthDialog.test.tsx index 34398941..0b99eed9 100644 --- a/packages/cli/src/ui/auth/AuthDialog.test.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.test.tsx @@ -168,7 +168,7 @@ describe('AuthDialog', () => { it('should not show the GEMINI_API_KEY message if QWEN_DEFAULT_AUTH_TYPE is set to something else', () => { process.env['GEMINI_API_KEY'] = 'foobar'; - process.env['QWEN_DEFAULT_AUTH_TYPE'] = AuthType.LOGIN_WITH_GOOGLE; + process.env['QWEN_DEFAULT_AUTH_TYPE'] = AuthType.USE_OPENAI; const settings: LoadedSettings = new LoadedSettings( { @@ -212,7 +212,7 @@ describe('AuthDialog', () => { it('should show the GEMINI_API_KEY message if QWEN_DEFAULT_AUTH_TYPE is set to use api key', () => { process.env['GEMINI_API_KEY'] = 'foobar'; - process.env['QWEN_DEFAULT_AUTH_TYPE'] = AuthType.USE_GEMINI; + process.env['QWEN_DEFAULT_AUTH_TYPE'] = AuthType.USE_OPENAI; const settings: LoadedSettings = new LoadedSettings( { @@ -504,12 +504,12 @@ describe('AuthDialog', () => { }, { settings: { - security: { auth: { selectedType: AuthType.LOGIN_WITH_GOOGLE } }, + security: { auth: { selectedType: AuthType.USE_OPENAI } }, ui: { customThemes: {} }, mcpServers: {}, }, originalSettings: { - security: { auth: { selectedType: AuthType.LOGIN_WITH_GOOGLE } }, + security: { auth: { selectedType: AuthType.USE_OPENAI } }, ui: { customThemes: {} }, mcpServers: {}, }, diff --git a/packages/cli/src/ui/auth/useAuth.ts b/packages/cli/src/ui/auth/useAuth.ts index d2369690..ba569fe1 100644 --- a/packages/cli/src/ui/auth/useAuth.ts +++ b/packages/cli/src/ui/auth/useAuth.ts @@ -225,16 +225,24 @@ export const useAuthCommand = ( const defaultAuthType = process.env['QWEN_DEFAULT_AUTH_TYPE']; if ( defaultAuthType && - ![AuthType.QWEN_OAUTH, AuthType.USE_OPENAI].includes( - defaultAuthType as AuthType, - ) + ![ + AuthType.QWEN_OAUTH, + AuthType.USE_OPENAI, + AuthType.USE_GEMINI, + AuthType.USE_VERTEX_AI, + ].includes(defaultAuthType as AuthType) ) { onAuthError( t( 'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}', { value: defaultAuthType, - validValues: [AuthType.QWEN_OAUTH, AuthType.USE_OPENAI].join(', '), + validValues: [ + AuthType.QWEN_OAUTH, + AuthType.USE_OPENAI, + AuthType.USE_GEMINI, + AuthType.USE_VERTEX_AI, + ].join(', '), }, ), ); diff --git a/packages/cli/src/ui/commands/ideCommand.test.ts b/packages/cli/src/ui/commands/ideCommand.test.ts index a2a4539d..1a5d493e 100644 --- a/packages/cli/src/ui/commands/ideCommand.test.ts +++ b/packages/cli/src/ui/commands/ideCommand.test.ts @@ -15,7 +15,6 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { const original = await importOriginal(); return { ...original, - getOauthClient: vi.fn(original.getOauthClient), getIdeInstaller: vi.fn(original.getIdeInstaller), IdeClient: { getInstance: vi.fn(), diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index c00c065e..c3e1a128 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -17,7 +17,6 @@ import { AuthDialog } from '../auth/AuthDialog.js'; import { OpenAIKeyPrompt } from './OpenAIKeyPrompt.js'; import { EditorSettingsDialog } from './EditorSettingsDialog.js'; import { WorkspaceMigrationDialog } from './WorkspaceMigrationDialog.js'; -import { ProQuotaDialog } from './ProQuotaDialog.js'; import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js'; import { ModelDialog } from './ModelDialog.js'; import { ApprovalModeDialog } from './ApprovalModeDialog.js'; @@ -87,15 +86,6 @@ export const DialogManager = ({ /> ); } - if (uiState.proQuotaRequest) { - return ( - - ); - } if (uiState.shouldShowIdePrompt) { return ( ({ - RadioButtonSelect: vi.fn(), -})); - -describe('ProQuotaDialog', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should render with correct title and options', () => { - const { lastFrame } = render( - {}} - />, - ); - - const output = lastFrame(); - expect(output).toContain('Pro quota limit reached for gemini-2.5-pro.'); - - // Check that RadioButtonSelect was called with the correct items - expect(RadioButtonSelect).toHaveBeenCalledWith( - expect.objectContaining({ - items: [ - { - label: 'Change auth (executes the /auth command)', - value: 'auth', - key: 'auth', - }, - { - label: `Continue with gemini-2.5-flash`, - value: 'continue', - key: 'continue', - }, - ], - }), - undefined, - ); - }); - - it('should call onChoice with "auth" when "Change auth" is selected', () => { - const mockOnChoice = vi.fn(); - render( - , - ); - - // Get the onSelect function passed to RadioButtonSelect - const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect; - - // Simulate the selection - onSelect('auth'); - - expect(mockOnChoice).toHaveBeenCalledWith('auth'); - }); - - it('should call onChoice with "continue" when "Continue with flash" is selected', () => { - const mockOnChoice = vi.fn(); - render( - , - ); - - // Get the onSelect function passed to RadioButtonSelect - const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect; - - // Simulate the selection - onSelect('continue'); - - expect(mockOnChoice).toHaveBeenCalledWith('continue'); - }); -}); diff --git a/packages/cli/src/ui/components/ProQuotaDialog.tsx b/packages/cli/src/ui/components/ProQuotaDialog.tsx deleted file mode 100644 index cc9bd5f8..00000000 --- a/packages/cli/src/ui/components/ProQuotaDialog.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Box, Text } from 'ink'; -import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; -import { theme } from '../semantic-colors.js'; -import { t } from '../../i18n/index.js'; - -interface ProQuotaDialogProps { - failedModel: string; - fallbackModel: string; - onChoice: (choice: 'auth' | 'continue') => void; -} - -export function ProQuotaDialog({ - failedModel, - fallbackModel, - onChoice, -}: ProQuotaDialogProps): React.JSX.Element { - const items = [ - { - label: t('Change auth (executes the /auth command)'), - value: 'auth' as const, - key: 'auth', - }, - { - label: t('Continue with {{model}}', { model: fallbackModel }), - value: 'continue' as const, - key: 'continue', - }, - ]; - - const handleSelect = (choice: 'auth' | 'continue') => { - onChoice(choice); - }; - - return ( - - - {t('Pro quota limit reached for {{model}}.', { model: failedModel })} - - - - - - ); -} diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index 2e396335..b9842c81 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -55,7 +55,6 @@ export interface UIActions { handleClearScreen: () => void; onWorkspaceMigrationDialogOpen: () => void; onWorkspaceMigrationDialogClose: () => void; - handleProQuotaChoice: (choice: 'auth' | 'continue') => void; // Vision switch dialog handleVisionSwitchSelect: (outcome: VisionSwitchOutcome) => void; // Welcome back dialog diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index d009d59e..806cf09b 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -22,21 +22,13 @@ import type { AuthType, IdeContext, ApprovalMode, - UserTierId, IdeInfo, - FallbackIntent, } from '@qwen-code/qwen-code-core'; import type { DOMElement } from 'ink'; import type { SessionStatsState } from '../contexts/SessionContext.js'; import type { ExtensionUpdateState } from '../state/extensions.js'; import type { UpdateObject } from '../utils/updateCheck.js'; -export interface ProQuotaDialogRequest { - failedModel: string; - fallbackModel: string; - resolve: (intent: FallbackIntent) => void; -} - import { type UseHistoryManagerReturn } from '../hooks/useHistoryManager.js'; import { type RestartReason } from '../hooks/useIdeTrustListener.js'; @@ -99,8 +91,6 @@ export interface UIState { // eslint-disable-next-line @typescript-eslint/no-explicit-any workspaceExtensions: any[]; // Extension[] // Quota-related state - userTier: UserTierId | undefined; - proQuotaRequest: ProQuotaDialogRequest | null; currentModel: string; contextFileNames: string[]; errorCount: number; diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index f82caa80..332b6afe 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -1323,7 +1323,7 @@ describe('useGeminiStream', () => { it('should call parseAndFormatApiError with the correct authType on stream initialization failure', async () => { // 1. Setup const mockError = new Error('Rate limit exceeded'); - const mockAuthType = AuthType.LOGIN_WITH_GOOGLE; + const mockAuthType = AuthType.USE_VERTEX_AI; mockParseAndFormatApiError.mockClear(); mockSendMessageStream.mockReturnValue( (async function* () { @@ -1374,9 +1374,6 @@ describe('useGeminiStream', () => { expect(mockParseAndFormatApiError).toHaveBeenCalledWith( 'Rate limit exceeded', mockAuthType, - undefined, - 'gemini-2.5-pro', - 'gemini-2.5-flash', ); }); }); @@ -2493,9 +2490,6 @@ describe('useGeminiStream', () => { expect(mockParseAndFormatApiError).toHaveBeenCalledWith( { message: 'Test error' }, expect.any(String), - undefined, - 'gemini-2.5-pro', - 'gemini-2.5-flash', ); }); }); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index b4df01b0..5c871ea6 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -26,7 +26,6 @@ import { GitService, UnauthorizedError, UserPromptEvent, - DEFAULT_GEMINI_FLASH_MODEL, logConversationFinishedEvent, ConversationFinishedEvent, ApprovalMode, @@ -600,9 +599,6 @@ export const useGeminiStream = ( text: parseAndFormatApiError( eventValue.error, config.getContentGeneratorConfig()?.authType, - undefined, - config.getModel(), - DEFAULT_GEMINI_FLASH_MODEL, ), }, userMessageTimestamp, @@ -654,6 +650,9 @@ export const useGeminiStream = ( 'Response stopped due to image safety violations.', [FinishReason.UNEXPECTED_TOOL_CALL]: 'Response stopped due to unexpected tool call.', + [FinishReason.IMAGE_PROHIBITED_CONTENT]: + 'Response stopped due to image prohibited content.', + [FinishReason.NO_IMAGE]: 'Response stopped due to no image.', }; const message = finishReasonMessages[finishReason]; @@ -987,9 +986,6 @@ export const useGeminiStream = ( text: parseAndFormatApiError( getErrorMessage(error) || 'Unknown error', config.getContentGeneratorConfig()?.authType, - undefined, - config.getModel(), - DEFAULT_GEMINI_FLASH_MODEL, ), }, userMessageTimestamp, diff --git a/packages/cli/src/ui/hooks/useQuotaAndFallback.test.ts b/packages/cli/src/ui/hooks/useQuotaAndFallback.test.ts deleted file mode 100644 index 1bd06895..00000000 --- a/packages/cli/src/ui/hooks/useQuotaAndFallback.test.ts +++ /dev/null @@ -1,391 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - vi, - describe, - it, - expect, - beforeEach, - afterEach, - type Mock, -} from 'vitest'; -import { act, renderHook } from '@testing-library/react'; -import { - type Config, - type FallbackModelHandler, - UserTierId, - AuthType, - isGenericQuotaExceededError, - isProQuotaExceededError, - makeFakeConfig, -} from '@qwen-code/qwen-code-core'; -import { useQuotaAndFallback } from './useQuotaAndFallback.js'; -import type { UseHistoryManagerReturn } from './useHistoryManager.js'; -import { AuthState, MessageType } from '../types.js'; - -// Mock the error checking functions from the core package to control test scenarios -vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { - const original = - await importOriginal(); - return { - ...original, - isGenericQuotaExceededError: vi.fn(), - isProQuotaExceededError: vi.fn(), - }; -}); - -// Use a type alias for SpyInstance as it's not directly exported -type SpyInstance = ReturnType; - -describe('useQuotaAndFallback', () => { - let mockConfig: Config; - let mockHistoryManager: UseHistoryManagerReturn; - let mockSetAuthState: Mock; - let mockSetModelSwitchedFromQuotaError: Mock; - let setFallbackHandlerSpy: SpyInstance; - - const mockedIsGenericQuotaExceededError = isGenericQuotaExceededError as Mock; - const mockedIsProQuotaExceededError = isProQuotaExceededError as Mock; - - beforeEach(() => { - mockConfig = makeFakeConfig(); - - // Spy on the method that requires the private field and mock its return. - // This is cleaner than modifying the config class for tests. - vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({ - model: 'test-model', - authType: AuthType.LOGIN_WITH_GOOGLE, - }); - - mockHistoryManager = { - addItem: vi.fn(), - history: [], - updateItem: vi.fn(), - clearItems: vi.fn(), - loadHistory: vi.fn(), - }; - mockSetAuthState = vi.fn(); - mockSetModelSwitchedFromQuotaError = vi.fn(); - - setFallbackHandlerSpy = vi.spyOn(mockConfig, 'setFallbackModelHandler'); - vi.spyOn(mockConfig, 'setQuotaErrorOccurred'); - - mockedIsGenericQuotaExceededError.mockReturnValue(false); - mockedIsProQuotaExceededError.mockReturnValue(false); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should register a fallback handler on initialization', () => { - renderHook(() => - useQuotaAndFallback({ - config: mockConfig, - historyManager: mockHistoryManager, - userTier: UserTierId.FREE, - setAuthState: mockSetAuthState, - setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError, - }), - ); - - expect(setFallbackHandlerSpy).toHaveBeenCalledTimes(1); - expect(setFallbackHandlerSpy.mock.calls[0][0]).toBeInstanceOf(Function); - }); - - describe('Fallback Handler Logic', () => { - // Helper function to render the hook and extract the registered handler - const getRegisteredHandler = ( - userTier: UserTierId = UserTierId.FREE, - ): FallbackModelHandler => { - renderHook( - (props) => - useQuotaAndFallback({ - config: mockConfig, - historyManager: mockHistoryManager, - userTier: props.userTier, - setAuthState: mockSetAuthState, - setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError, - }), - { initialProps: { userTier } }, - ); - return setFallbackHandlerSpy.mock.calls[0][0] as FallbackModelHandler; - }; - - it('should return null and take no action if already in fallback mode', async () => { - vi.spyOn(mockConfig, 'isInFallbackMode').mockReturnValue(true); - const handler = getRegisteredHandler(); - const result = await handler('gemini-pro', 'gemini-flash', new Error()); - - expect(result).toBeNull(); - expect(mockHistoryManager.addItem).not.toHaveBeenCalled(); - }); - - it('should return null and take no action if authType is not LOGIN_WITH_GOOGLE', async () => { - // Override the default mock from beforeEach for this specific test - vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({ - model: 'test-model', - authType: AuthType.USE_GEMINI, - }); - - const handler = getRegisteredHandler(); - const result = await handler('gemini-pro', 'gemini-flash', new Error()); - - expect(result).toBeNull(); - expect(mockHistoryManager.addItem).not.toHaveBeenCalled(); - }); - - describe('Automatic Fallback Scenarios', () => { - const testCases = [ - { - errorType: 'generic', - tier: UserTierId.FREE, - expectedMessageSnippets: [ - 'Automatically switching from model-A to model-B', - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ], - }, - { - errorType: 'generic', - tier: UserTierId.STANDARD, // Paid tier - expectedMessageSnippets: [ - 'Automatically switching from model-A to model-B', - 'switch to using a paid API key from AI Studio', - ], - }, - { - errorType: 'other', - tier: UserTierId.FREE, - expectedMessageSnippets: [ - 'Automatically switching from model-A to model-B for faster responses', - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ], - }, - { - errorType: 'other', - tier: UserTierId.LEGACY, // Paid tier - expectedMessageSnippets: [ - 'Automatically switching from model-A to model-B for faster responses', - 'switch to using a paid API key from AI Studio', - ], - }, - ]; - - for (const { errorType, tier, expectedMessageSnippets } of testCases) { - it(`should handle ${errorType} error for ${tier} tier correctly`, async () => { - mockedIsGenericQuotaExceededError.mockReturnValue( - errorType === 'generic', - ); - - const handler = getRegisteredHandler(tier); - const result = await handler( - 'model-A', - 'model-B', - new Error('quota exceeded'), - ); - - // Automatic fallbacks should return 'stop' - expect(result).toBe('stop'); - - expect(mockHistoryManager.addItem).toHaveBeenCalledWith( - expect.objectContaining({ type: MessageType.INFO }), - expect.any(Number), - ); - - const message = (mockHistoryManager.addItem as Mock).mock.calls[0][0] - .text; - for (const snippet of expectedMessageSnippets) { - expect(message).toContain(snippet); - } - - expect(mockSetModelSwitchedFromQuotaError).toHaveBeenCalledWith(true); - expect(mockConfig.setQuotaErrorOccurred).toHaveBeenCalledWith(true); - }); - } - }); - - describe('Interactive Fallback (Pro Quota Error)', () => { - beforeEach(() => { - mockedIsProQuotaExceededError.mockReturnValue(true); - }); - - it('should set an interactive request and wait for user choice', async () => { - const { result } = renderHook(() => - useQuotaAndFallback({ - config: mockConfig, - historyManager: mockHistoryManager, - userTier: UserTierId.FREE, - setAuthState: mockSetAuthState, - setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError, - }), - ); - - const handler = setFallbackHandlerSpy.mock - .calls[0][0] as FallbackModelHandler; - - // Call the handler but do not await it, to check the intermediate state - const promise = handler( - 'gemini-pro', - 'gemini-flash', - new Error('pro quota'), - ); - - await act(async () => {}); - - // The hook should now have a pending request for the UI to handle - expect(result.current.proQuotaRequest).not.toBeNull(); - expect(result.current.proQuotaRequest?.failedModel).toBe('gemini-pro'); - - // Simulate the user choosing to continue with the fallback model - act(() => { - result.current.handleProQuotaChoice('continue'); - }); - - // The original promise from the handler should now resolve - const intent = await promise; - expect(intent).toBe('retry'); - - // The pending request should be cleared from the state - expect(result.current.proQuotaRequest).toBeNull(); - }); - - it('should handle race conditions by stopping subsequent requests', async () => { - const { result } = renderHook(() => - useQuotaAndFallback({ - config: mockConfig, - historyManager: mockHistoryManager, - userTier: UserTierId.FREE, - setAuthState: mockSetAuthState, - setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError, - }), - ); - - const handler = setFallbackHandlerSpy.mock - .calls[0][0] as FallbackModelHandler; - - const promise1 = handler( - 'gemini-pro', - 'gemini-flash', - new Error('pro quota 1'), - ); - await act(async () => {}); - - const firstRequest = result.current.proQuotaRequest; - expect(firstRequest).not.toBeNull(); - - const result2 = await handler( - 'gemini-pro', - 'gemini-flash', - new Error('pro quota 2'), - ); - - // The lock should have stopped the second request - expect(result2).toBe('stop'); - expect(result.current.proQuotaRequest).toBe(firstRequest); - - act(() => { - result.current.handleProQuotaChoice('continue'); - }); - - const intent1 = await promise1; - expect(intent1).toBe('retry'); - expect(result.current.proQuotaRequest).toBeNull(); - }); - }); - }); - - describe('handleProQuotaChoice', () => { - beforeEach(() => { - mockedIsProQuotaExceededError.mockReturnValue(true); - }); - - it('should do nothing if there is no pending pro quota request', () => { - const { result } = renderHook(() => - useQuotaAndFallback({ - config: mockConfig, - historyManager: mockHistoryManager, - userTier: UserTierId.FREE, - setAuthState: mockSetAuthState, - setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError, - }), - ); - - act(() => { - result.current.handleProQuotaChoice('auth'); - }); - - expect(mockSetAuthState).not.toHaveBeenCalled(); - expect(mockHistoryManager.addItem).not.toHaveBeenCalled(); - }); - - it('should resolve intent to "auth" and trigger auth state update', async () => { - const { result } = renderHook(() => - useQuotaAndFallback({ - config: mockConfig, - historyManager: mockHistoryManager, - userTier: UserTierId.FREE, - setAuthState: mockSetAuthState, - setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError, - }), - ); - - const handler = setFallbackHandlerSpy.mock - .calls[0][0] as FallbackModelHandler; - const promise = handler( - 'gemini-pro', - 'gemini-flash', - new Error('pro quota'), - ); - await act(async () => {}); // Allow state to update - - act(() => { - result.current.handleProQuotaChoice('auth'); - }); - - const intent = await promise; - expect(intent).toBe('auth'); - expect(mockSetAuthState).toHaveBeenCalledWith(AuthState.Updating); - expect(result.current.proQuotaRequest).toBeNull(); - }); - - it('should resolve intent to "retry" and add info message on continue', async () => { - const { result } = renderHook(() => - useQuotaAndFallback({ - config: mockConfig, - historyManager: mockHistoryManager, - userTier: UserTierId.FREE, - setAuthState: mockSetAuthState, - setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError, - }), - ); - - const handler = setFallbackHandlerSpy.mock - .calls[0][0] as FallbackModelHandler; - // The first `addItem` call is for the initial quota error message - const promise = handler( - 'gemini-pro', - 'gemini-flash', - new Error('pro quota'), - ); - await act(async () => {}); // Allow state to update - - act(() => { - result.current.handleProQuotaChoice('continue'); - }); - - const intent = await promise; - expect(intent).toBe('retry'); - expect(result.current.proQuotaRequest).toBeNull(); - - // Check for the second "Switched to fallback model" message - expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(2); - const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[1][0]; - expect(lastCall.type).toBe(MessageType.INFO); - expect(lastCall.text).toContain('Switched to fallback model.'); - }); - }); -}); diff --git a/packages/cli/src/ui/hooks/useQuotaAndFallback.ts b/packages/cli/src/ui/hooks/useQuotaAndFallback.ts deleted file mode 100644 index 75319ae7..00000000 --- a/packages/cli/src/ui/hooks/useQuotaAndFallback.ts +++ /dev/null @@ -1,175 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - AuthType, - type Config, - type FallbackModelHandler, - type FallbackIntent, - isGenericQuotaExceededError, - isProQuotaExceededError, - UserTierId, -} from '@qwen-code/qwen-code-core'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { type UseHistoryManagerReturn } from './useHistoryManager.js'; -import { AuthState, MessageType } from '../types.js'; -import { type ProQuotaDialogRequest } from '../contexts/UIStateContext.js'; - -interface UseQuotaAndFallbackArgs { - config: Config; - historyManager: UseHistoryManagerReturn; - userTier: UserTierId | undefined; - setAuthState: (state: AuthState) => void; - setModelSwitchedFromQuotaError: (value: boolean) => void; -} - -export function useQuotaAndFallback({ - config, - historyManager, - userTier, - setAuthState, - setModelSwitchedFromQuotaError, -}: UseQuotaAndFallbackArgs) { - const [proQuotaRequest, setProQuotaRequest] = - useState(null); - const isDialogPending = useRef(false); - - // Set up Flash fallback handler - useEffect(() => { - const fallbackHandler: FallbackModelHandler = async ( - failedModel, - fallbackModel, - error, - ): Promise => { - if (config.isInFallbackMode()) { - return null; - } - - // Fallbacks are currently only handled for OAuth users. - const contentGeneratorConfig = config.getContentGeneratorConfig(); - if ( - !contentGeneratorConfig || - contentGeneratorConfig.authType !== AuthType.LOGIN_WITH_GOOGLE - ) { - return null; - } - - // Use actual user tier if available; otherwise, default to FREE tier behavior (safe default) - const isPaidTier = - userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD; - - let message: string; - - if (error && isProQuotaExceededError(error)) { - // Pro Quota specific messages (Interactive) - if (isPaidTier) { - message = `⚡ You have reached your daily ${failedModel} quota limit. -⚡ You can choose to authenticate with a paid API key or continue with the fallback model. -⚡ To continue accessing the ${failedModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; - } else { - message = `⚡ You have reached your daily ${failedModel} quota limit. -⚡ You can choose to authenticate with a paid API key or continue with the fallback model. -⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist -⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key -⚡ You can switch authentication methods by typing /auth`; - } - } else if (error && isGenericQuotaExceededError(error)) { - // Generic Quota (Automatic fallback) - const actionMessage = `⚡ You have reached your daily quota limit.\n⚡ Automatically switching from ${failedModel} to ${fallbackModel} for the remainder of this session.`; - - if (isPaidTier) { - message = `${actionMessage} -⚡ To continue accessing the ${failedModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; - } else { - message = `${actionMessage} -⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist -⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key -⚡ You can switch authentication methods by typing /auth`; - } - } else { - // Consecutive 429s or other errors (Automatic fallback) - const actionMessage = `⚡ Automatically switching from ${failedModel} to ${fallbackModel} for faster responses for the remainder of this session.`; - - if (isPaidTier) { - message = `${actionMessage} -⚡ Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${failedModel} quota limit -⚡ To continue accessing the ${failedModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; - } else { - message = `${actionMessage} -⚡ Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${failedModel} quota limit -⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist -⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key -⚡ You can switch authentication methods by typing /auth`; - } - } - - // Add message to UI history - historyManager.addItem( - { - type: MessageType.INFO, - text: message, - }, - Date.now(), - ); - - setModelSwitchedFromQuotaError(true); - config.setQuotaErrorOccurred(true); - - // Interactive Fallback for Pro quota - if (error && isProQuotaExceededError(error)) { - if (isDialogPending.current) { - return 'stop'; // A dialog is already active, so just stop this request. - } - isDialogPending.current = true; - - const intent: FallbackIntent = await new Promise( - (resolve) => { - setProQuotaRequest({ - failedModel, - fallbackModel, - resolve, - }); - }, - ); - - return intent; - } - - return 'stop'; - }; - - config.setFallbackModelHandler(fallbackHandler); - }, [config, historyManager, userTier, setModelSwitchedFromQuotaError]); - - const handleProQuotaChoice = useCallback( - (choice: 'auth' | 'continue') => { - if (!proQuotaRequest) return; - - const intent: FallbackIntent = choice === 'auth' ? 'auth' : 'retry'; - proQuotaRequest.resolve(intent); - setProQuotaRequest(null); - isDialogPending.current = false; // Reset the flag here - - if (choice === 'auth') { - setAuthState(AuthState.Updating); - } else { - historyManager.addItem( - { - type: MessageType.INFO, - text: 'Switched to fallback model. Tip: Press Ctrl+P (or Up Arrow) to recall your previous prompt and submit it again if you wish.', - }, - Date.now(), - ); - } - }, - [proQuotaRequest, setAuthState, historyManager], - ); - - return { - proQuotaRequest, - handleProQuotaChoice, - }; -} diff --git a/packages/cli/src/ui/hooks/useQwenAuth.test.ts b/packages/cli/src/ui/hooks/useQwenAuth.test.ts index 06644a00..43611afe 100644 --- a/packages/cli/src/ui/hooks/useQwenAuth.test.ts +++ b/packages/cli/src/ui/hooks/useQwenAuth.test.ts @@ -411,7 +411,7 @@ describe('useQwenAuth', () => { expect(geminiResult.current.qwenAuthState.authStatus).toBe('idle'); const { result: oauthResult } = renderHook(() => - useQwenAuth(AuthType.LOGIN_WITH_GOOGLE, true), + useQwenAuth(AuthType.USE_OPENAI, true), ); expect(oauthResult.current.qwenAuthState.authStatus).toBe('idle'); }); diff --git a/packages/cli/src/ui/hooks/useToolScheduler.test.ts b/packages/cli/src/ui/hooks/useToolScheduler.test.ts index cab0b5ee..d7b2b810 100644 --- a/packages/cli/src/ui/hooks/useToolScheduler.test.ts +++ b/packages/cli/src/ui/hooks/useToolScheduler.test.ts @@ -62,7 +62,7 @@ const mockConfig = { getAllowedTools: vi.fn(() => []), getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getUseSmartEdit: () => false, getUseModelRouter: () => false, diff --git a/packages/cli/src/validateNonInterActiveAuth.ts b/packages/cli/src/validateNonInterActiveAuth.ts index 1590c074..e89eaa0e 100644 --- a/packages/cli/src/validateNonInterActiveAuth.ts +++ b/packages/cli/src/validateNonInterActiveAuth.ts @@ -21,6 +21,13 @@ function getAuthTypeFromEnv(): AuthType | undefined { return AuthType.QWEN_OAUTH; } + if (process.env['GEMINI_API_KEY']) { + return AuthType.USE_GEMINI; + } + if (process.env['GOOGLE_API_KEY']) { + return AuthType.USE_VERTEX_AI; + } + return undefined; } diff --git a/packages/core/package.json b/packages/core/package.json index 77fdbb28..297c2765 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,8 +23,8 @@ "scripts/postinstall.js" ], "dependencies": { - "@google/genai": "1.16.0", - "@modelcontextprotocol/sdk": "^1.11.0", + "@google/genai": "1.30.0", + "@modelcontextprotocol/sdk": "^1.25.1", "@opentelemetry/api": "^1.9.0", "async-mutex": "^0.5.0", "@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0", @@ -34,7 +34,6 @@ "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", "@opentelemetry/instrumentation-http": "^0.203.0", - "@opentelemetry/resource-detector-gcp": "^0.40.0", "@opentelemetry/sdk-node": "^0.203.0", "@types/html-to-text": "^9.0.4", "@xterm/headless": "5.5.0", @@ -48,7 +47,7 @@ "fdir": "^6.4.6", "fzf": "^0.5.2", "glob": "^10.5.0", - "google-auth-library": "^9.11.0", + "google-auth-library": "^10.5.0", "html-to-text": "^9.0.5", "https-proxy-agent": "^7.0.6", "ignore": "^7.0.0", diff --git a/packages/core/src/code_assist/codeAssist.ts b/packages/core/src/code_assist/codeAssist.ts deleted file mode 100644 index c8ade92e..00000000 --- a/packages/core/src/code_assist/codeAssist.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { ContentGenerator } from '../core/contentGenerator.js'; -import { AuthType } from '../core/contentGenerator.js'; -import { getOauthClient } from './oauth2.js'; -import { setupUser } from './setup.js'; -import type { HttpOptions } from './server.js'; -import { CodeAssistServer } from './server.js'; -import type { Config } from '../config/config.js'; -import { LoggingContentGenerator } from '../core/loggingContentGenerator.js'; - -export async function createCodeAssistContentGenerator( - httpOptions: HttpOptions, - authType: AuthType, - config: Config, - sessionId?: string, -): Promise { - if ( - authType === AuthType.LOGIN_WITH_GOOGLE || - authType === AuthType.CLOUD_SHELL - ) { - const authClient = await getOauthClient(authType, config); - const userData = await setupUser(authClient); - return new CodeAssistServer( - authClient, - userData.projectId, - httpOptions, - sessionId, - userData.userTier, - ); - } - - throw new Error(`Unsupported authType: ${authType}`); -} - -export function getCodeAssistServer( - config: Config, -): CodeAssistServer | undefined { - let server = config.getContentGenerator(); - - // Unwrap LoggingContentGenerator if present - if (server instanceof LoggingContentGenerator) { - server = server.getWrapped(); - } - - if (!(server instanceof CodeAssistServer)) { - return undefined; - } - return server; -} diff --git a/packages/core/src/code_assist/converter.test.ts b/packages/core/src/code_assist/converter.test.ts deleted file mode 100644 index 501651f3..00000000 --- a/packages/core/src/code_assist/converter.test.ts +++ /dev/null @@ -1,456 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect } from 'vitest'; -import type { CaGenerateContentResponse } from './converter.js'; -import { - toGenerateContentRequest, - fromGenerateContentResponse, - toContents, -} from './converter.js'; -import type { - ContentListUnion, - GenerateContentParameters, -} from '@google/genai'; -import { - GenerateContentResponse, - FinishReason, - BlockedReason, - type Part, -} from '@google/genai'; - -describe('converter', () => { - describe('toCodeAssistRequest', () => { - it('should convert a simple request with project', () => { - const genaiReq: GenerateContentParameters = { - model: 'gemini-pro', - contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], - }; - const codeAssistReq = toGenerateContentRequest( - genaiReq, - 'my-prompt', - 'my-project', - 'my-session', - ); - expect(codeAssistReq).toEqual({ - model: 'gemini-pro', - project: 'my-project', - request: { - contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], - systemInstruction: undefined, - cachedContent: undefined, - tools: undefined, - toolConfig: undefined, - labels: undefined, - safetySettings: undefined, - generationConfig: undefined, - session_id: 'my-session', - }, - user_prompt_id: 'my-prompt', - }); - }); - - it('should convert a request without a project', () => { - const genaiReq: GenerateContentParameters = { - model: 'gemini-pro', - contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], - }; - const codeAssistReq = toGenerateContentRequest( - genaiReq, - 'my-prompt', - undefined, - 'my-session', - ); - expect(codeAssistReq).toEqual({ - model: 'gemini-pro', - project: undefined, - request: { - contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], - systemInstruction: undefined, - cachedContent: undefined, - tools: undefined, - toolConfig: undefined, - labels: undefined, - safetySettings: undefined, - generationConfig: undefined, - session_id: 'my-session', - }, - user_prompt_id: 'my-prompt', - }); - }); - - it('should convert a request with sessionId', () => { - const genaiReq: GenerateContentParameters = { - model: 'gemini-pro', - contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], - }; - const codeAssistReq = toGenerateContentRequest( - genaiReq, - 'my-prompt', - 'my-project', - 'session-123', - ); - expect(codeAssistReq).toEqual({ - model: 'gemini-pro', - project: 'my-project', - request: { - contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], - systemInstruction: undefined, - cachedContent: undefined, - tools: undefined, - toolConfig: undefined, - labels: undefined, - safetySettings: undefined, - generationConfig: undefined, - session_id: 'session-123', - }, - user_prompt_id: 'my-prompt', - }); - }); - - it('should handle string content', () => { - const genaiReq: GenerateContentParameters = { - model: 'gemini-pro', - contents: 'Hello', - }; - const codeAssistReq = toGenerateContentRequest( - genaiReq, - 'my-prompt', - 'my-project', - 'my-session', - ); - expect(codeAssistReq.request.contents).toEqual([ - { role: 'user', parts: [{ text: 'Hello' }] }, - ]); - }); - - it('should handle Part[] content', () => { - const genaiReq: GenerateContentParameters = { - model: 'gemini-pro', - contents: [{ text: 'Hello' }, { text: 'World' }], - }; - const codeAssistReq = toGenerateContentRequest( - genaiReq, - 'my-prompt', - 'my-project', - 'my-session', - ); - expect(codeAssistReq.request.contents).toEqual([ - { role: 'user', parts: [{ text: 'Hello' }] }, - { role: 'user', parts: [{ text: 'World' }] }, - ]); - }); - - it('should handle system instructions', () => { - const genaiReq: GenerateContentParameters = { - model: 'gemini-pro', - contents: 'Hello', - config: { - systemInstruction: 'You are a helpful assistant.', - }, - }; - const codeAssistReq = toGenerateContentRequest( - genaiReq, - 'my-prompt', - 'my-project', - 'my-session', - ); - expect(codeAssistReq.request.systemInstruction).toEqual({ - role: 'user', - parts: [{ text: 'You are a helpful assistant.' }], - }); - }); - - it('should handle generation config', () => { - const genaiReq: GenerateContentParameters = { - model: 'gemini-pro', - contents: 'Hello', - config: { - temperature: 0.8, - topK: 40, - }, - }; - const codeAssistReq = toGenerateContentRequest( - genaiReq, - 'my-prompt', - 'my-project', - 'my-session', - ); - expect(codeAssistReq.request.generationConfig).toEqual({ - temperature: 0.8, - topK: 40, - }); - }); - - it('should handle all generation config fields', () => { - const genaiReq: GenerateContentParameters = { - model: 'gemini-pro', - contents: 'Hello', - config: { - temperature: 0.1, - topP: 0.2, - topK: 3, - candidateCount: 4, - maxOutputTokens: 5, - stopSequences: ['a'], - responseLogprobs: true, - logprobs: 6, - presencePenalty: 0.7, - frequencyPenalty: 0.8, - seed: 9, - responseMimeType: 'application/json', - }, - }; - const codeAssistReq = toGenerateContentRequest( - genaiReq, - 'my-prompt', - 'my-project', - 'my-session', - ); - expect(codeAssistReq.request.generationConfig).toEqual({ - temperature: 0.1, - topP: 0.2, - topK: 3, - candidateCount: 4, - maxOutputTokens: 5, - stopSequences: ['a'], - responseLogprobs: true, - logprobs: 6, - presencePenalty: 0.7, - frequencyPenalty: 0.8, - seed: 9, - responseMimeType: 'application/json', - }); - }); - }); - - describe('fromCodeAssistResponse', () => { - it('should convert a simple response', () => { - const codeAssistRes: CaGenerateContentResponse = { - response: { - candidates: [ - { - index: 0, - content: { - role: 'model', - parts: [{ text: 'Hi there!' }], - }, - finishReason: FinishReason.STOP, - safetyRatings: [], - }, - ], - }, - }; - const genaiRes = fromGenerateContentResponse(codeAssistRes); - expect(genaiRes).toBeInstanceOf(GenerateContentResponse); - expect(genaiRes.candidates).toEqual(codeAssistRes.response.candidates); - }); - - it('should handle prompt feedback and usage metadata', () => { - const codeAssistRes: CaGenerateContentResponse = { - response: { - candidates: [], - promptFeedback: { - blockReason: BlockedReason.SAFETY, - safetyRatings: [], - }, - usageMetadata: { - promptTokenCount: 10, - candidatesTokenCount: 20, - totalTokenCount: 30, - }, - }, - }; - const genaiRes = fromGenerateContentResponse(codeAssistRes); - expect(genaiRes.promptFeedback).toEqual( - codeAssistRes.response.promptFeedback, - ); - expect(genaiRes.usageMetadata).toEqual( - codeAssistRes.response.usageMetadata, - ); - }); - - it('should handle automatic function calling history', () => { - const codeAssistRes: CaGenerateContentResponse = { - response: { - candidates: [], - automaticFunctionCallingHistory: [ - { - role: 'model', - parts: [ - { - functionCall: { - name: 'test_function', - args: { - foo: 'bar', - }, - }, - }, - ], - }, - ], - }, - }; - const genaiRes = fromGenerateContentResponse(codeAssistRes); - expect(genaiRes.automaticFunctionCallingHistory).toEqual( - codeAssistRes.response.automaticFunctionCallingHistory, - ); - }); - - it('should handle modelVersion', () => { - const codeAssistRes: CaGenerateContentResponse = { - response: { - candidates: [], - modelVersion: 'qwen3-coder-plus', - }, - }; - const genaiRes = fromGenerateContentResponse(codeAssistRes); - expect(genaiRes.modelVersion).toEqual('qwen3-coder-plus'); - }); - }); - - describe('toContents', () => { - it('should handle Content', () => { - const content: ContentListUnion = { - role: 'user', - parts: [{ text: 'hello' }], - }; - expect(toContents(content)).toEqual([ - { role: 'user', parts: [{ text: 'hello' }] }, - ]); - }); - - it('should handle array of Contents', () => { - const contents: ContentListUnion = [ - { role: 'user', parts: [{ text: 'hello' }] }, - { role: 'model', parts: [{ text: 'hi' }] }, - ]; - expect(toContents(contents)).toEqual([ - { role: 'user', parts: [{ text: 'hello' }] }, - { role: 'model', parts: [{ text: 'hi' }] }, - ]); - }); - - it('should handle Part', () => { - const part: ContentListUnion = { text: 'a part' }; - expect(toContents(part)).toEqual([ - { role: 'user', parts: [{ text: 'a part' }] }, - ]); - }); - - it('should handle array of Parts', () => { - const parts = [{ text: 'part 1' }, 'part 2']; - expect(toContents(parts)).toEqual([ - { role: 'user', parts: [{ text: 'part 1' }] }, - { role: 'user', parts: [{ text: 'part 2' }] }, - ]); - }); - - it('should handle string', () => { - const str: ContentListUnion = 'a string'; - expect(toContents(str)).toEqual([ - { role: 'user', parts: [{ text: 'a string' }] }, - ]); - }); - - it('should handle array of strings', () => { - const strings: ContentListUnion = ['string 1', 'string 2']; - expect(toContents(strings)).toEqual([ - { role: 'user', parts: [{ text: 'string 1' }] }, - { role: 'user', parts: [{ text: 'string 2' }] }, - ]); - }); - - it('should convert thought parts to text parts for API compatibility', () => { - const contentWithThought: ContentListUnion = { - role: 'model', - parts: [ - { text: 'regular text' }, - { thought: 'thinking about the problem' } as Part & { - thought: string; - }, - { text: 'more text' }, - ], - }; - expect(toContents(contentWithThought)).toEqual([ - { - role: 'model', - parts: [ - { text: 'regular text' }, - { text: '[Thought: thinking about the problem]' }, - { text: 'more text' }, - ], - }, - ]); - }); - - it('should combine text and thought for text parts with thoughts', () => { - const contentWithTextAndThought: ContentListUnion = { - role: 'model', - parts: [ - { - text: 'Here is my response', - thought: 'I need to be careful here', - } as Part & { thought: string }, - ], - }; - expect(toContents(contentWithTextAndThought)).toEqual([ - { - role: 'model', - parts: [ - { - text: 'Here is my response\n[Thought: I need to be careful here]', - }, - ], - }, - ]); - }); - - it('should preserve non-thought properties while removing thought', () => { - const contentWithComplexPart: ContentListUnion = { - role: 'model', - parts: [ - { - functionCall: { name: 'calculate', args: { x: 5, y: 10 } }, - thought: 'Performing calculation', - } as Part & { thought: string }, - ], - }; - expect(toContents(contentWithComplexPart)).toEqual([ - { - role: 'model', - parts: [ - { - functionCall: { name: 'calculate', args: { x: 5, y: 10 } }, - }, - ], - }, - ]); - }); - - it('should convert invalid text content to valid text part with thought', () => { - const contentWithInvalidText: ContentListUnion = { - role: 'model', - parts: [ - { - text: 123, // Invalid - should be string - thought: 'Processing number', - } as Part & { thought: string; text: number }, - ], - }; - expect(toContents(contentWithInvalidText)).toEqual([ - { - role: 'model', - parts: [ - { - text: '123\n[Thought: Processing number]', - }, - ], - }, - ]); - }); - }); -}); diff --git a/packages/core/src/code_assist/converter.ts b/packages/core/src/code_assist/converter.ts deleted file mode 100644 index 78e74313..00000000 --- a/packages/core/src/code_assist/converter.ts +++ /dev/null @@ -1,285 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { - Content, - ContentListUnion, - ContentUnion, - GenerateContentConfig, - GenerateContentParameters, - CountTokensParameters, - CountTokensResponse, - GenerationConfigRoutingConfig, - MediaResolution, - Candidate, - ModelSelectionConfig, - GenerateContentResponsePromptFeedback, - GenerateContentResponseUsageMetadata, - Part, - SafetySetting, - PartUnion, - SpeechConfigUnion, - ThinkingConfig, - ToolListUnion, - ToolConfig, -} from '@google/genai'; -import { GenerateContentResponse } from '@google/genai'; - -export interface CAGenerateContentRequest { - model: string; - project?: string; - user_prompt_id?: string; - request: VertexGenerateContentRequest; -} - -interface VertexGenerateContentRequest { - contents: Content[]; - systemInstruction?: Content; - cachedContent?: string; - tools?: ToolListUnion; - toolConfig?: ToolConfig; - labels?: Record; - safetySettings?: SafetySetting[]; - generationConfig?: VertexGenerationConfig; - session_id?: string; -} - -interface VertexGenerationConfig { - temperature?: number; - topP?: number; - topK?: number; - candidateCount?: number; - maxOutputTokens?: number; - stopSequences?: string[]; - responseLogprobs?: boolean; - logprobs?: number; - presencePenalty?: number; - frequencyPenalty?: number; - seed?: number; - responseMimeType?: string; - responseJsonSchema?: unknown; - responseSchema?: unknown; - routingConfig?: GenerationConfigRoutingConfig; - modelSelectionConfig?: ModelSelectionConfig; - responseModalities?: string[]; - mediaResolution?: MediaResolution; - speechConfig?: SpeechConfigUnion; - audioTimestamp?: boolean; - thinkingConfig?: ThinkingConfig; -} - -export interface CaGenerateContentResponse { - response: VertexGenerateContentResponse; -} - -interface VertexGenerateContentResponse { - candidates: Candidate[]; - automaticFunctionCallingHistory?: Content[]; - promptFeedback?: GenerateContentResponsePromptFeedback; - usageMetadata?: GenerateContentResponseUsageMetadata; - modelVersion?: string; -} - -export interface CaCountTokenRequest { - request: VertexCountTokenRequest; -} - -interface VertexCountTokenRequest { - model: string; - contents: Content[]; -} - -export interface CaCountTokenResponse { - totalTokens: number; -} - -export function toCountTokenRequest( - req: CountTokensParameters, -): CaCountTokenRequest { - return { - request: { - model: 'models/' + req.model, - contents: toContents(req.contents), - }, - }; -} - -export function fromCountTokenResponse( - res: CaCountTokenResponse, -): CountTokensResponse { - return { - totalTokens: res.totalTokens, - }; -} - -export function toGenerateContentRequest( - req: GenerateContentParameters, - userPromptId: string, - project?: string, - sessionId?: string, -): CAGenerateContentRequest { - return { - model: req.model, - project, - user_prompt_id: userPromptId, - request: toVertexGenerateContentRequest(req, sessionId), - }; -} - -export function fromGenerateContentResponse( - res: CaGenerateContentResponse, -): GenerateContentResponse { - const inres = res.response; - const out = new GenerateContentResponse(); - out.candidates = inres.candidates; - out.automaticFunctionCallingHistory = inres.automaticFunctionCallingHistory; - out.promptFeedback = inres.promptFeedback; - out.usageMetadata = inres.usageMetadata; - out.modelVersion = inres.modelVersion; - return out; -} - -function toVertexGenerateContentRequest( - req: GenerateContentParameters, - sessionId?: string, -): VertexGenerateContentRequest { - return { - contents: toContents(req.contents), - systemInstruction: maybeToContent(req.config?.systemInstruction), - cachedContent: req.config?.cachedContent, - tools: req.config?.tools, - toolConfig: req.config?.toolConfig, - labels: req.config?.labels, - safetySettings: req.config?.safetySettings, - generationConfig: toVertexGenerationConfig(req.config), - session_id: sessionId, - }; -} - -export function toContents(contents: ContentListUnion): Content[] { - if (Array.isArray(contents)) { - // it's a Content[] or a PartsUnion[] - return contents.map(toContent); - } - // it's a Content or a PartsUnion - return [toContent(contents)]; -} - -function maybeToContent(content?: ContentUnion): Content | undefined { - if (!content) { - return undefined; - } - return toContent(content); -} - -function toContent(content: ContentUnion): Content { - if (Array.isArray(content)) { - // it's a PartsUnion[] - return { - role: 'user', - parts: toParts(content), - }; - } - if (typeof content === 'string') { - // it's a string - return { - role: 'user', - parts: [{ text: content }], - }; - } - if ('parts' in content) { - // it's a Content - process parts to handle thought filtering - return { - ...content, - parts: content.parts - ? toParts(content.parts.filter((p) => p != null)) - : [], - }; - } - // it's a Part - return { - role: 'user', - parts: [toPart(content as Part)], - }; -} - -export function toParts(parts: PartUnion[]): Part[] { - return parts.map(toPart); -} - -function toPart(part: PartUnion): Part { - if (typeof part === 'string') { - // it's a string - return { text: part }; - } - - // Handle thought parts for CountToken API compatibility - // The CountToken API expects parts to have certain required "oneof" fields initialized, - // but thought parts don't conform to this schema and cause API failures - if ('thought' in part && part.thought) { - const thoughtText = `[Thought: ${part.thought}]`; - - const newPart = { ...part }; - delete (newPart as Record)['thought']; - - const hasApiContent = - 'functionCall' in newPart || - 'functionResponse' in newPart || - 'inlineData' in newPart || - 'fileData' in newPart; - - if (hasApiContent) { - // It's a functionCall or other non-text part. Just strip the thought. - return newPart; - } - - // If no other valid API content, this must be a text part. - // Combine existing text (if any) with the thought, preserving other properties. - const text = (newPart as { text?: unknown }).text; - const existingText = text ? String(text) : ''; - const combinedText = existingText - ? `${existingText}\n${thoughtText}` - : thoughtText; - - return { - ...newPart, - text: combinedText, - }; - } - - return part; -} - -function toVertexGenerationConfig( - config?: GenerateContentConfig, -): VertexGenerationConfig | undefined { - if (!config) { - return undefined; - } - return { - temperature: config.temperature, - topP: config.topP, - topK: config.topK, - candidateCount: config.candidateCount, - maxOutputTokens: config.maxOutputTokens, - stopSequences: config.stopSequences, - responseLogprobs: config.responseLogprobs, - logprobs: config.logprobs, - presencePenalty: config.presencePenalty, - frequencyPenalty: config.frequencyPenalty, - seed: config.seed, - responseMimeType: config.responseMimeType, - responseSchema: config.responseSchema, - responseJsonSchema: config.responseJsonSchema, - routingConfig: config.routingConfig, - modelSelectionConfig: config.modelSelectionConfig, - responseModalities: config.responseModalities, - mediaResolution: config.mediaResolution, - speechConfig: config.speechConfig, - audioTimestamp: config.audioTimestamp, - thinkingConfig: config.thinkingConfig, - }; -} diff --git a/packages/core/src/code_assist/oauth-credential-storage.test.ts b/packages/core/src/code_assist/oauth-credential-storage.test.ts deleted file mode 100644 index f044546e..00000000 --- a/packages/core/src/code_assist/oauth-credential-storage.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { type Credentials } from 'google-auth-library'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { OAuthCredentialStorage } from './oauth-credential-storage.js'; -import type { OAuthCredentials } from '../mcp/token-storage/types.js'; - -import * as path from 'node:path'; -import * as os from 'node:os'; -import { promises as fs } from 'node:fs'; - -// Mock external dependencies -const mockHybridTokenStorage = vi.hoisted(() => ({ - getCredentials: vi.fn(), - setCredentials: vi.fn(), - deleteCredentials: vi.fn(), -})); -vi.mock('../mcp/token-storage/hybrid-token-storage.js', () => ({ - HybridTokenStorage: vi.fn(() => mockHybridTokenStorage), -})); -vi.mock('node:fs', () => ({ - promises: { - readFile: vi.fn(), - rm: vi.fn(), - }, -})); -vi.mock('node:os'); -vi.mock('node:path'); - -describe('OAuthCredentialStorage', () => { - const mockCredentials: Credentials = { - access_token: 'mock_access_token', - refresh_token: 'mock_refresh_token', - expiry_date: Date.now() + 3600 * 1000, - token_type: 'Bearer', - scope: 'email profile', - }; - - const mockMcpCredentials: OAuthCredentials = { - serverName: 'main-account', - token: { - accessToken: 'mock_access_token', - refreshToken: 'mock_refresh_token', - tokenType: 'Bearer', - scope: 'email profile', - expiresAt: mockCredentials.expiry_date!, - }, - updatedAt: expect.any(Number), - }; - - const oldFilePath = '/mock/home/.qwen/oauth.json'; - - beforeEach(() => { - vi.spyOn(mockHybridTokenStorage, 'getCredentials').mockResolvedValue(null); - vi.spyOn(mockHybridTokenStorage, 'setCredentials').mockResolvedValue( - undefined, - ); - vi.spyOn(mockHybridTokenStorage, 'deleteCredentials').mockResolvedValue( - undefined, - ); - - vi.spyOn(fs, 'readFile').mockRejectedValue(new Error('File not found')); - vi.spyOn(fs, 'rm').mockResolvedValue(undefined); - - vi.spyOn(os, 'homedir').mockReturnValue('/mock/home'); - vi.spyOn(path, 'join').mockReturnValue(oldFilePath); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('loadCredentials', () => { - it('should load credentials from HybridTokenStorage if available', async () => { - vi.spyOn(mockHybridTokenStorage, 'getCredentials').mockResolvedValue( - mockMcpCredentials, - ); - - const result = await OAuthCredentialStorage.loadCredentials(); - - expect(mockHybridTokenStorage.getCredentials).toHaveBeenCalledWith( - 'main-account', - ); - expect(result).toEqual(mockCredentials); - }); - - it('should fallback to migrateFromFileStorage if no credentials in HybridTokenStorage', async () => { - vi.spyOn(mockHybridTokenStorage, 'getCredentials').mockResolvedValue( - null, - ); - vi.spyOn(fs, 'readFile').mockResolvedValue( - JSON.stringify(mockCredentials), - ); - - const result = await OAuthCredentialStorage.loadCredentials(); - - expect(mockHybridTokenStorage.getCredentials).toHaveBeenCalledWith( - 'main-account', - ); - expect(fs.readFile).toHaveBeenCalledWith(oldFilePath, 'utf-8'); - expect(mockHybridTokenStorage.setCredentials).toHaveBeenCalled(); // Verify credentials were saved - expect(fs.rm).toHaveBeenCalledWith(oldFilePath, { force: true }); // Verify old file was removed - expect(result).toEqual(mockCredentials); - }); - - it('should return null if no credentials found and no old file to migrate', async () => { - vi.spyOn(fs, 'readFile').mockRejectedValue({ - message: 'File not found', - code: 'ENOENT', - }); - - const result = await OAuthCredentialStorage.loadCredentials(); - - expect(result).toBeNull(); - }); - - it('should throw an error if loading fails', async () => { - vi.spyOn(mockHybridTokenStorage, 'getCredentials').mockRejectedValue( - new Error('Loading error'), - ); - - await expect(OAuthCredentialStorage.loadCredentials()).rejects.toThrow( - 'Failed to load OAuth credentials', - ); - }); - - it('should throw an error if read file fails', async () => { - vi.spyOn(mockHybridTokenStorage, 'getCredentials').mockResolvedValue( - null, - ); - vi.spyOn(fs, 'readFile').mockRejectedValue( - new Error('Permission denied'), - ); - - await expect(OAuthCredentialStorage.loadCredentials()).rejects.toThrow( - 'Failed to load OAuth credentials', - ); - }); - - it('should not throw error if migration file removal failed', async () => { - vi.spyOn(mockHybridTokenStorage, 'getCredentials').mockResolvedValue( - null, - ); - vi.spyOn(fs, 'readFile').mockResolvedValue( - JSON.stringify(mockCredentials), - ); - vi.spyOn(OAuthCredentialStorage, 'saveCredentials').mockResolvedValue( - undefined, - ); - vi.spyOn(fs, 'rm').mockRejectedValue(new Error('Deletion failed')); - - const result = await OAuthCredentialStorage.loadCredentials(); - - expect(result).toEqual(mockCredentials); - }); - }); - - describe('saveCredentials', () => { - it('should save credentials to HybridTokenStorage', async () => { - await OAuthCredentialStorage.saveCredentials(mockCredentials); - - expect(mockHybridTokenStorage.setCredentials).toHaveBeenCalledWith( - mockMcpCredentials, - ); - }); - - it('should throw an error if access_token is missing', async () => { - const invalidCredentials: Credentials = { - ...mockCredentials, - access_token: undefined, - }; - await expect( - OAuthCredentialStorage.saveCredentials(invalidCredentials), - ).rejects.toThrow( - 'Attempted to save credentials without an access token.', - ); - }); - }); - - describe('clearCredentials', () => { - it('should delete credentials from HybridTokenStorage', async () => { - await OAuthCredentialStorage.clearCredentials(); - - expect(mockHybridTokenStorage.deleteCredentials).toHaveBeenCalledWith( - 'main-account', - ); - }); - - it('should attempt to remove the old file-based storage', async () => { - await OAuthCredentialStorage.clearCredentials(); - - expect(fs.rm).toHaveBeenCalledWith(oldFilePath, { force: true }); - }); - - it('should not throw an error if deleting old file fails', async () => { - vi.spyOn(fs, 'rm').mockRejectedValue(new Error('File deletion failed')); - - await expect( - OAuthCredentialStorage.clearCredentials(), - ).resolves.toBeUndefined(); - }); - - it('should throw an error if clearing from HybridTokenStorage fails', async () => { - vi.spyOn(mockHybridTokenStorage, 'deleteCredentials').mockRejectedValue( - new Error('Deletion error'), - ); - - await expect(OAuthCredentialStorage.clearCredentials()).rejects.toThrow( - 'Failed to clear OAuth credentials', - ); - }); - }); -}); diff --git a/packages/core/src/code_assist/oauth-credential-storage.ts b/packages/core/src/code_assist/oauth-credential-storage.ts deleted file mode 100644 index 622fa1d8..00000000 --- a/packages/core/src/code_assist/oauth-credential-storage.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { type Credentials } from 'google-auth-library'; -import { HybridTokenStorage } from '../mcp/token-storage/hybrid-token-storage.js'; -import { OAUTH_FILE } from '../config/storage.js'; -import type { OAuthCredentials } from '../mcp/token-storage/types.js'; -import * as path from 'node:path'; -import * as os from 'node:os'; -import { promises as fs } from 'node:fs'; - -const QWEN_DIR = '.qwen'; -const KEYCHAIN_SERVICE_NAME = 'qwen-code-oauth'; -const MAIN_ACCOUNT_KEY = 'main-account'; - -export class OAuthCredentialStorage { - private static storage: HybridTokenStorage = new HybridTokenStorage( - KEYCHAIN_SERVICE_NAME, - ); - - /** - * Load cached OAuth credentials - */ - static async loadCredentials(): Promise { - try { - const credentials = await this.storage.getCredentials(MAIN_ACCOUNT_KEY); - - if (credentials?.token) { - const { accessToken, refreshToken, expiresAt, tokenType, scope } = - credentials.token; - // Convert from OAuthCredentials format to Google Credentials format - const googleCreds: Credentials = { - access_token: accessToken, - refresh_token: refreshToken || undefined, - token_type: tokenType || undefined, - scope: scope || undefined, - }; - - if (expiresAt) { - googleCreds.expiry_date = expiresAt; - } - - return googleCreds; - } - - // Fallback: Try to migrate from old file-based storage - return await this.migrateFromFileStorage(); - } catch (error: unknown) { - console.error(error); - throw new Error('Failed to load OAuth credentials'); - } - } - - /** - * Save OAuth credentials - */ - static async saveCredentials(credentials: Credentials): Promise { - if (!credentials.access_token) { - throw new Error('Attempted to save credentials without an access token.'); - } - - // Convert Google Credentials to OAuthCredentials format - const mcpCredentials: OAuthCredentials = { - serverName: MAIN_ACCOUNT_KEY, - token: { - accessToken: credentials.access_token, - refreshToken: credentials.refresh_token || undefined, - tokenType: credentials.token_type || 'Bearer', - scope: credentials.scope || undefined, - expiresAt: credentials.expiry_date || undefined, - }, - updatedAt: Date.now(), - }; - - await this.storage.setCredentials(mcpCredentials); - } - - /** - * Clear cached OAuth credentials - */ - static async clearCredentials(): Promise { - try { - await this.storage.deleteCredentials(MAIN_ACCOUNT_KEY); - - // Also try to remove the old file if it exists - const oldFilePath = path.join(os.homedir(), QWEN_DIR, OAUTH_FILE); - await fs.rm(oldFilePath, { force: true }).catch(() => {}); - } catch (error: unknown) { - console.error(error); - throw new Error('Failed to clear OAuth credentials'); - } - } - - /** - * Migrate credentials from old file-based storage to keychain - */ - private static async migrateFromFileStorage(): Promise { - const oldFilePath = path.join(os.homedir(), QWEN_DIR, OAUTH_FILE); - - let credsJson: string; - try { - credsJson = await fs.readFile(oldFilePath, 'utf-8'); - } catch (error: unknown) { - if ( - typeof error === 'object' && - error !== null && - 'code' in error && - error.code === 'ENOENT' - ) { - // File doesn't exist, so no migration. - return null; - } - // Other read errors should propagate. - throw error; - } - - const credentials = JSON.parse(credsJson) as Credentials; - - // Save to new storage - await this.saveCredentials(credentials); - - // Remove old file after successful migration - await fs.rm(oldFilePath, { force: true }).catch(() => {}); - - return credentials; - } -} diff --git a/packages/core/src/code_assist/oauth2.test.ts b/packages/core/src/code_assist/oauth2.test.ts deleted file mode 100644 index 56ae40ec..00000000 --- a/packages/core/src/code_assist/oauth2.test.ts +++ /dev/null @@ -1,1166 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { Credentials } from 'google-auth-library'; -import type { Mock } from 'vitest'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { - clearCachedCredentialFile, - clearOauthClientCache, - getOauthClient, - resetOauthClientForTesting, -} from './oauth2.js'; -import { UserAccountManager } from '../utils/userAccountManager.js'; -import { OAuth2Client, Compute } from 'google-auth-library'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import http from 'node:http'; -import open from 'open'; -import crypto from 'node:crypto'; -import * as os from 'node:os'; -import { AuthType } from '../core/contentGenerator.js'; -import type { Config } from '../config/config.js'; -import readline from 'node:readline'; -import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js'; -import { QWEN_DIR } from '../utils/paths.js'; - -vi.mock('os', async (importOriginal) => { - const os = await importOriginal(); - return { - ...os, - homedir: vi.fn(), - }; -}); - -vi.mock('google-auth-library'); -vi.mock('http'); -vi.mock('open'); -vi.mock('crypto'); -vi.mock('node:readline'); -vi.mock('../utils/browser.js', () => ({ - shouldAttemptBrowserLaunch: () => true, -})); - -vi.mock('./oauth-credential-storage.js', () => ({ - OAuthCredentialStorage: { - saveCredentials: vi.fn(), - loadCredentials: vi.fn(), - clearCredentials: vi.fn(), - }, -})); - -const mockConfig = { - getNoBrowser: () => false, - getProxy: () => 'http://test.proxy.com:8080', - isBrowserLaunchSuppressed: () => false, -} as unknown as Config; - -// Mock fetch globally -global.fetch = vi.fn(); - -describe('oauth2', () => { - describe('with encrypted flag false', () => { - let tempHomeDir: string; - - beforeEach(() => { - process.env[FORCE_ENCRYPTED_FILE_ENV_VAR] = 'false'; - tempHomeDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'qwen-code-test-home-'), - ); - (os.homedir as Mock).mockReturnValue(tempHomeDir); - }); - afterEach(() => { - fs.rmSync(tempHomeDir, { recursive: true, force: true }); - vi.clearAllMocks(); - resetOauthClientForTesting(); - vi.unstubAllEnvs(); - }); - - it('should perform a web login', async () => { - const mockAuthUrl = 'https://example.com/auth'; - const mockCode = 'test-code'; - const mockState = 'test-state'; - const mockTokens = { - access_token: 'test-access-token', - refresh_token: 'test-refresh-token', - }; - - const mockGenerateAuthUrl = vi.fn().mockReturnValue(mockAuthUrl); - const mockGetToken = vi.fn().mockResolvedValue({ tokens: mockTokens }); - const mockSetCredentials = vi.fn(); - const mockGetAccessToken = vi - .fn() - .mockResolvedValue({ token: 'mock-access-token' }); - const mockOAuth2Client = { - generateAuthUrl: mockGenerateAuthUrl, - getToken: mockGetToken, - setCredentials: mockSetCredentials, - getAccessToken: mockGetAccessToken, - credentials: mockTokens, - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never); - (open as Mock).mockImplementation(async () => ({ on: vi.fn() }) as never); - - // Mock the UserInfo API response - (global.fetch as Mock).mockResolvedValue({ - ok: true, - json: vi - .fn() - .mockResolvedValue({ email: 'test-google-account@gmail.com' }), - } as unknown as Response); - - let requestCallback!: http.RequestListener< - typeof http.IncomingMessage, - typeof http.ServerResponse - >; - - let serverListeningCallback: (value: unknown) => void; - const serverListeningPromise = new Promise( - (resolve) => (serverListeningCallback = resolve), - ); - - let capturedPort = 0; - const mockHttpServer = { - listen: vi.fn((port: number, _host: string, callback?: () => void) => { - capturedPort = port; - if (callback) { - callback(); - } - serverListeningCallback(undefined); - }), - close: vi.fn((callback?: () => void) => { - if (callback) { - callback(); - } - }), - on: vi.fn(), - address: () => ({ port: capturedPort }), - }; - (http.createServer as Mock).mockImplementation((cb) => { - requestCallback = cb as http.RequestListener< - typeof http.IncomingMessage, - typeof http.ServerResponse - >; - return mockHttpServer as unknown as http.Server; - }); - - const clientPromise = getOauthClient( - AuthType.LOGIN_WITH_GOOGLE, - mockConfig, - ); - - // wait for server to start listening. - await serverListeningPromise; - - const mockReq = { - url: `/oauth2callback?code=${mockCode}&state=${mockState}`, - } as http.IncomingMessage; - const mockRes = { - writeHead: vi.fn(), - end: vi.fn(), - } as unknown as http.ServerResponse; - - await requestCallback(mockReq, mockRes); - - const client = await clientPromise; - expect(client).toBe(mockOAuth2Client); - - expect(open).toHaveBeenCalledWith(mockAuthUrl); - expect(mockGetToken).toHaveBeenCalledWith({ - code: mockCode, - redirect_uri: `http://localhost:${capturedPort}/oauth2callback`, - }); - expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens); - - // Verify Google Account was cached - const googleAccountPath = path.join( - tempHomeDir, - QWEN_DIR, - 'google_accounts.json', - ); - expect(fs.existsSync(googleAccountPath)).toBe(true); - const cachedGoogleAccount = fs.readFileSync(googleAccountPath, 'utf-8'); - expect(JSON.parse(cachedGoogleAccount)).toEqual({ - active: 'test-google-account@gmail.com', - old: [], - }); - - // Verify the getCachedGoogleAccount function works - const userAccountManager = new UserAccountManager(); - expect(userAccountManager.getCachedGoogleAccount()).toBe( - 'test-google-account@gmail.com', - ); - }); - - it('should perform login with user code', async () => { - const mockConfigWithNoBrowser = { - getNoBrowser: () => true, - getProxy: () => 'http://test.proxy.com:8080', - isBrowserLaunchSuppressed: () => true, - } as unknown as Config; - - const mockCodeVerifier = { - codeChallenge: 'test-challenge', - codeVerifier: 'test-verifier', - }; - const mockAuthUrl = 'https://example.com/auth-user-code'; - const mockCode = 'test-user-code'; - const mockTokens = { - access_token: 'test-access-token-user-code', - refresh_token: 'test-refresh-token-user-code', - }; - - const mockGenerateAuthUrl = vi.fn().mockReturnValue(mockAuthUrl); - const mockGetToken = vi.fn().mockResolvedValue({ tokens: mockTokens }); - const mockSetCredentials = vi.fn(); - const mockGenerateCodeVerifierAsync = vi - .fn() - .mockResolvedValue(mockCodeVerifier); - - const mockOAuth2Client = { - generateAuthUrl: mockGenerateAuthUrl, - getToken: mockGetToken, - setCredentials: mockSetCredentials, - generateCodeVerifierAsync: mockGenerateCodeVerifierAsync, - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - const mockReadline = { - question: vi.fn((_query, callback) => callback(mockCode)), - close: vi.fn(), - }; - (readline.createInterface as Mock).mockReturnValue(mockReadline); - - const consoleLogSpy = vi - .spyOn(console, 'log') - .mockImplementation(() => {}); - - const client = await getOauthClient( - AuthType.LOGIN_WITH_GOOGLE, - mockConfigWithNoBrowser, - ); - - expect(client).toBe(mockOAuth2Client); - - // Verify the auth flow - expect(mockGenerateCodeVerifierAsync).toHaveBeenCalled(); - expect(mockGenerateAuthUrl).toHaveBeenCalled(); - expect(consoleLogSpy).toHaveBeenCalledWith( - expect.stringContaining(mockAuthUrl), - ); - expect(mockReadline.question).toHaveBeenCalledWith( - 'Enter the authorization code: ', - expect.any(Function), - ); - expect(mockGetToken).toHaveBeenCalledWith({ - code: mockCode, - codeVerifier: mockCodeVerifier.codeVerifier, - redirect_uri: 'https://codeassist.google.com/authcode', - }); - expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens); - - consoleLogSpy.mockRestore(); - }); - - describe('in Cloud Shell', () => { - const mockGetAccessToken = vi.fn(); - let mockComputeClient: Compute; - - beforeEach(() => { - mockGetAccessToken.mockResolvedValue({ token: 'test-access-token' }); - mockComputeClient = { - credentials: { refresh_token: 'test-refresh-token' }, - getAccessToken: mockGetAccessToken, - } as unknown as Compute; - - (Compute as unknown as Mock).mockImplementation( - () => mockComputeClient, - ); - }); - - it('should attempt to load cached credentials first', async () => { - const cachedCreds = { refresh_token: 'cached-token' }; - const credsPath = path.join(tempHomeDir, '.qwen', 'oauth_creds.json'); - await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); - await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds)); - - const mockClient = { - setCredentials: vi.fn(), - getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }), - getTokenInfo: vi.fn().mockResolvedValue({}), - on: vi.fn(), - }; - - // To mock the new OAuth2Client() inside the function - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockClient as unknown as OAuth2Client, - ); - - await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig); - - expect(mockClient.setCredentials).toHaveBeenCalledWith(cachedCreds); - expect(mockClient.getAccessToken).toHaveBeenCalled(); - expect(mockClient.getTokenInfo).toHaveBeenCalled(); - expect(Compute).not.toHaveBeenCalled(); // Should not fetch new client if cache is valid - }); - - it('should use Compute to get a client if no cached credentials exist', async () => { - await getOauthClient(AuthType.CLOUD_SHELL, mockConfig); - - expect(Compute).toHaveBeenCalledWith({}); - expect(mockGetAccessToken).toHaveBeenCalled(); - }); - - it('should not cache the credentials after fetching them via ADC', async () => { - const newCredentials = { refresh_token: 'new-adc-token' }; - mockComputeClient.credentials = newCredentials; - mockGetAccessToken.mockResolvedValue({ token: 'new-adc-token' }); - - await getOauthClient(AuthType.CLOUD_SHELL, mockConfig); - - const credsPath = path.join(tempHomeDir, '.qwen', 'oauth_creds.json'); - expect(fs.existsSync(credsPath)).toBe(false); - }); - - it('should return the Compute client on successful ADC authentication', async () => { - const client = await getOauthClient(AuthType.CLOUD_SHELL, mockConfig); - expect(client).toBe(mockComputeClient); - }); - - it('should throw an error if ADC fails', async () => { - const testError = new Error('ADC Failed'); - mockGetAccessToken.mockRejectedValue(testError); - - await expect( - getOauthClient(AuthType.CLOUD_SHELL, mockConfig), - ).rejects.toThrow( - 'Could not authenticate using Cloud Shell credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ADC Failed', - ); - }); - }); - - describe('credential loading order', () => { - it('should prioritize default cached credentials over GOOGLE_APPLICATION_CREDENTIALS', async () => { - // Setup default cached credentials - const defaultCreds = { refresh_token: 'default-cached-token' }; - const defaultCredsPath = path.join( - tempHomeDir, - '.qwen', - 'oauth_creds.json', - ); - await fs.promises.mkdir(path.dirname(defaultCredsPath), { - recursive: true, - }); - await fs.promises.writeFile( - defaultCredsPath, - JSON.stringify(defaultCreds), - ); - - // Setup credentials via environment variable - const envCreds = { refresh_token: 'env-var-token' }; - const envCredsPath = path.join(tempHomeDir, 'env_creds.json'); - await fs.promises.writeFile(envCredsPath, JSON.stringify(envCreds)); - vi.stubEnv('GOOGLE_APPLICATION_CREDENTIALS', envCredsPath); - - const mockClient = { - setCredentials: vi.fn(), - getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }), - getTokenInfo: vi.fn().mockResolvedValue({}), - on: vi.fn(), - }; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockClient as unknown as OAuth2Client, - ); - - await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig); - - // Assert the correct credentials were used - expect(mockClient.setCredentials).toHaveBeenCalledWith(defaultCreds); - expect(mockClient.setCredentials).not.toHaveBeenCalledWith(envCreds); - }); - - it('should fall back to GOOGLE_APPLICATION_CREDENTIALS if default cache is missing', async () => { - // Setup credentials via environment variable - const envCreds = { refresh_token: 'env-var-token' }; - const envCredsPath = path.join(tempHomeDir, 'env_creds.json'); - await fs.promises.writeFile(envCredsPath, JSON.stringify(envCreds)); - vi.stubEnv('GOOGLE_APPLICATION_CREDENTIALS', envCredsPath); - - const mockClient = { - setCredentials: vi.fn(), - getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }), - getTokenInfo: vi.fn().mockResolvedValue({}), - on: vi.fn(), - }; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockClient as unknown as OAuth2Client, - ); - - await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig); - - // Assert the correct credentials were used - expect(mockClient.setCredentials).toHaveBeenCalledWith(envCreds); - }); - }); - - describe('with GCP environment variables', () => { - it('should use GOOGLE_CLOUD_ACCESS_TOKEN when GOOGLE_GENAI_USE_GCA is true', async () => { - vi.stubEnv('GOOGLE_GENAI_USE_GCA', 'true'); - vi.stubEnv('GOOGLE_CLOUD_ACCESS_TOKEN', 'gcp-access-token'); - - const mockSetCredentials = vi.fn(); - const mockGetAccessToken = vi - .fn() - .mockResolvedValue({ token: 'gcp-access-token' }); - const mockOAuth2Client = { - setCredentials: mockSetCredentials, - getAccessToken: mockGetAccessToken, - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - // Mock the UserInfo API response for fetchAndCacheUserInfo - (global.fetch as Mock).mockResolvedValue({ - ok: true, - json: vi - .fn() - .mockResolvedValue({ email: 'test-gcp-account@gmail.com' }), - } as unknown as Response); - - const client = await getOauthClient( - AuthType.LOGIN_WITH_GOOGLE, - mockConfig, - ); - - expect(client).toBe(mockOAuth2Client); - expect(mockSetCredentials).toHaveBeenCalledWith({ - access_token: 'gcp-access-token', - }); - - // Verify fetchAndCacheUserInfo was effectively called - expect(mockGetAccessToken).toHaveBeenCalled(); - expect(global.fetch).toHaveBeenCalledWith( - 'https://www.googleapis.com/oauth2/v2/userinfo', - { - headers: { - Authorization: 'Bearer gcp-access-token', - }, - }, - ); - - // Verify Google Account was cached - const googleAccountPath = path.join( - tempHomeDir, - '.qwen', - 'google_accounts.json', - ); - const cachedContent = fs.readFileSync(googleAccountPath, 'utf-8'); - expect(JSON.parse(cachedContent)).toEqual({ - active: 'test-gcp-account@gmail.com', - old: [], - }); - }); - - it('should not use GCP token if GOOGLE_CLOUD_ACCESS_TOKEN is not set', async () => { - vi.stubEnv('GOOGLE_GENAI_USE_GCA', 'true'); - - const mockSetCredentials = vi.fn(); - const mockGetAccessToken = vi - .fn() - .mockResolvedValue({ token: 'cached-access-token' }); - const mockGetTokenInfo = vi.fn().mockResolvedValue({}); - const mockOAuth2Client = { - setCredentials: mockSetCredentials, - getAccessToken: mockGetAccessToken, - getTokenInfo: mockGetTokenInfo, - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - // Make it fall through to cached credentials path - const cachedCreds = { refresh_token: 'cached-token' }; - const credsPath = path.join(tempHomeDir, '.qwen', 'oauth_creds.json'); - await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); - await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds)); - - await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig); - - // It should be called with the cached credentials, not the GCP access token. - expect(mockSetCredentials).toHaveBeenCalledTimes(1); - expect(mockSetCredentials).toHaveBeenCalledWith(cachedCreds); - }); - - it('should not use GCP token if GOOGLE_GENAI_USE_GCA is not set', async () => { - vi.stubEnv('GOOGLE_CLOUD_ACCESS_TOKEN', 'gcp-access-token'); - - const mockSetCredentials = vi.fn(); - const mockGetAccessToken = vi - .fn() - .mockResolvedValue({ token: 'cached-access-token' }); - const mockGetTokenInfo = vi.fn().mockResolvedValue({}); - const mockOAuth2Client = { - setCredentials: mockSetCredentials, - getAccessToken: mockGetAccessToken, - getTokenInfo: mockGetTokenInfo, - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - // Make it fall through to cached credentials path - const cachedCreds = { refresh_token: 'cached-token' }; - const credsPath = path.join(tempHomeDir, '.qwen', 'oauth_creds.json'); - await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); - await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds)); - - await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig); - - // It should be called with the cached credentials, not the GCP access token. - expect(mockSetCredentials).toHaveBeenCalledTimes(1); - expect(mockSetCredentials).toHaveBeenCalledWith(cachedCreds); - }); - }); - - describe('error handling', () => { - it('should handle browser launch failure with FatalAuthenticationError', async () => { - const mockError = new Error('Browser launch failed'); - (open as Mock).mockRejectedValue(mockError); - - const mockOAuth2Client = { - generateAuthUrl: vi.fn().mockReturnValue('https://example.com/auth'), - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - await expect( - getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig), - ).rejects.toThrow('Failed to open browser: Browser launch failed'); - }); - - it('should handle authentication timeout with proper error message', async () => { - const mockAuthUrl = 'https://example.com/auth'; - const mockOAuth2Client = { - generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl), - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - (open as Mock).mockImplementation( - async () => ({ on: vi.fn() }) as never, - ); - - const mockHttpServer = { - listen: vi.fn(), - close: vi.fn(), - on: vi.fn(), - address: () => ({ port: 3000 }), - }; - (http.createServer as Mock).mockImplementation( - () => mockHttpServer as unknown as http.Server, - ); - - // Mock setTimeout to trigger timeout immediately - const originalSetTimeout = global.setTimeout; - global.setTimeout = vi.fn( - (callback) => (callback(), {} as unknown as NodeJS.Timeout), - ) as unknown as typeof setTimeout; - - await expect( - getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig), - ).rejects.toThrow( - 'Authentication timed out after 5 minutes. The browser tab may have gotten stuck in a loading state. Please try again or use NO_BROWSER=true for manual authentication.', - ); - - global.setTimeout = originalSetTimeout; - }); - - it('should handle OAuth callback errors with descriptive messages', async () => { - const mockAuthUrl = 'https://example.com/auth'; - const mockOAuth2Client = { - generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl), - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - (open as Mock).mockImplementation( - async () => ({ on: vi.fn() }) as never, - ); - - let requestCallback!: http.RequestListener; - let serverListeningCallback: (value: unknown) => void; - const serverListeningPromise = new Promise( - (resolve) => (serverListeningCallback = resolve), - ); - - const mockHttpServer = { - listen: vi.fn( - (_port: number, _host: string, callback?: () => void) => { - if (callback) callback(); - serverListeningCallback(undefined); - }, - ), - close: vi.fn(), - on: vi.fn(), - address: () => ({ port: 3000 }), - }; - (http.createServer as Mock).mockImplementation((cb) => { - requestCallback = cb; - return mockHttpServer as unknown as http.Server; - }); - - const clientPromise = getOauthClient( - AuthType.LOGIN_WITH_GOOGLE, - mockConfig, - ); - await serverListeningPromise; - - // Test OAuth error with description - const mockReq = { - url: '/oauth2callback?error=access_denied&error_description=User+denied+access', - } as http.IncomingMessage; - const mockRes = { - writeHead: vi.fn(), - end: vi.fn(), - } as unknown as http.ServerResponse; - - await expect(async () => { - await requestCallback(mockReq, mockRes); - await clientPromise; - }).rejects.toThrow( - 'Google OAuth error: access_denied. User denied access', - ); - }); - - it('should handle OAuth error without description', async () => { - const mockAuthUrl = 'https://example.com/auth'; - const mockOAuth2Client = { - generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl), - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - (open as Mock).mockImplementation( - async () => ({ on: vi.fn() }) as never, - ); - - let requestCallback!: http.RequestListener; - let serverListeningCallback: (value: unknown) => void; - const serverListeningPromise = new Promise( - (resolve) => (serverListeningCallback = resolve), - ); - - const mockHttpServer = { - listen: vi.fn( - (_port: number, _host: string, callback?: () => void) => { - if (callback) callback(); - serverListeningCallback(undefined); - }, - ), - close: vi.fn(), - on: vi.fn(), - address: () => ({ port: 3000 }), - }; - (http.createServer as Mock).mockImplementation((cb) => { - requestCallback = cb; - return mockHttpServer as unknown as http.Server; - }); - - const clientPromise = getOauthClient( - AuthType.LOGIN_WITH_GOOGLE, - mockConfig, - ); - await serverListeningPromise; - - // Test OAuth error without description - const mockReq = { - url: '/oauth2callback?error=server_error', - } as http.IncomingMessage; - const mockRes = { - writeHead: vi.fn(), - end: vi.fn(), - } as unknown as http.ServerResponse; - - await expect(async () => { - await requestCallback(mockReq, mockRes); - await clientPromise; - }).rejects.toThrow( - 'Google OAuth error: server_error. No additional details provided', - ); - }); - - it('should handle token exchange failure with descriptive error', async () => { - const mockAuthUrl = 'https://example.com/auth'; - const mockCode = 'test-code'; - const mockState = 'test-state'; - - const mockOAuth2Client = { - generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl), - getToken: vi - .fn() - .mockRejectedValue(new Error('Token exchange failed')), - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never); - (open as Mock).mockImplementation( - async () => ({ on: vi.fn() }) as never, - ); - - let requestCallback!: http.RequestListener; - let serverListeningCallback: (value: unknown) => void; - const serverListeningPromise = new Promise( - (resolve) => (serverListeningCallback = resolve), - ); - - const mockHttpServer = { - listen: vi.fn( - (_port: number, _host: string, callback?: () => void) => { - if (callback) callback(); - serverListeningCallback(undefined); - }, - ), - close: vi.fn(), - on: vi.fn(), - address: () => ({ port: 3000 }), - }; - (http.createServer as Mock).mockImplementation((cb) => { - requestCallback = cb; - return mockHttpServer as unknown as http.Server; - }); - - const clientPromise = getOauthClient( - AuthType.LOGIN_WITH_GOOGLE, - mockConfig, - ); - await serverListeningPromise; - - const mockReq = { - url: `/oauth2callback?code=${mockCode}&state=${mockState}`, - } as http.IncomingMessage; - const mockRes = { - writeHead: vi.fn(), - end: vi.fn(), - } as unknown as http.ServerResponse; - - await expect(async () => { - await requestCallback(mockReq, mockRes); - await clientPromise; - }).rejects.toThrow( - 'Failed to exchange authorization code for tokens: Token exchange failed', - ); - }); - - it('should handle fetchAndCacheUserInfo failure gracefully', async () => { - const mockAuthUrl = 'https://example.com/auth'; - const mockCode = 'test-code'; - const mockState = 'test-state'; - const mockTokens = { - access_token: 'test-access-token', - refresh_token: 'test-refresh-token', - }; - - const mockOAuth2Client = { - generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl), - getToken: vi.fn().mockResolvedValue({ tokens: mockTokens }), - setCredentials: vi.fn(), - getAccessToken: vi - .fn() - .mockResolvedValue({ token: 'test-access-token' }), - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never); - (open as Mock).mockImplementation( - async () => ({ on: vi.fn() }) as never, - ); - - // Mock fetch to fail - (global.fetch as Mock).mockResolvedValue({ - ok: false, - status: 500, - statusText: 'Internal Server Error', - } as unknown as Response); - - const consoleErrorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - - let requestCallback!: http.RequestListener; - let serverListeningCallback: (value: unknown) => void; - const serverListeningPromise = new Promise( - (resolve) => (serverListeningCallback = resolve), - ); - - const mockHttpServer = { - listen: vi.fn( - (_port: number, _host: string, callback?: () => void) => { - if (callback) callback(); - serverListeningCallback(undefined); - }, - ), - close: vi.fn(), - on: vi.fn(), - address: () => ({ port: 3000 }), - }; - (http.createServer as Mock).mockImplementation((cb) => { - requestCallback = cb; - return mockHttpServer as unknown as http.Server; - }); - - const clientPromise = getOauthClient( - AuthType.LOGIN_WITH_GOOGLE, - mockConfig, - ); - await serverListeningPromise; - - const mockReq = { - url: `/oauth2callback?code=${mockCode}&state=${mockState}`, - } as http.IncomingMessage; - const mockRes = { - writeHead: vi.fn(), - end: vi.fn(), - } as unknown as http.ServerResponse; - - await requestCallback(mockReq, mockRes); - const client = await clientPromise; - - // Authentication should succeed even if fetchAndCacheUserInfo fails - expect(client).toBe(mockOAuth2Client); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Failed to fetch user info:', - 500, - 'Internal Server Error', - ); - - consoleErrorSpy.mockRestore(); - }); - - it('should handle user code authentication failure with descriptive error', async () => { - const mockConfigWithNoBrowser = { - getNoBrowser: () => true, - getProxy: () => 'http://test.proxy.com:8080', - isBrowserLaunchSuppressed: () => true, - } as unknown as Config; - - const mockOAuth2Client = { - generateCodeVerifierAsync: vi.fn().mockResolvedValue({ - codeChallenge: 'test-challenge', - codeVerifier: 'test-verifier', - }), - generateAuthUrl: vi.fn().mockReturnValue('https://example.com/auth'), - getToken: vi - .fn() - .mockRejectedValue(new Error('Invalid authorization code')), - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - const mockReadline = { - question: vi.fn((_query, callback) => callback('invalid-code')), - close: vi.fn(), - }; - (readline.createInterface as Mock).mockReturnValue(mockReadline); - - const consoleLogSpy = vi - .spyOn(console, 'log') - .mockImplementation(() => {}); - const consoleErrorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - - await expect( - getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfigWithNoBrowser), - ).rejects.toThrow('Failed to authenticate with user code.'); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Failed to authenticate with authorization code:', - 'Invalid authorization code', - ); - - consoleLogSpy.mockRestore(); - consoleErrorSpy.mockRestore(); - }); - }); - - describe('clearCachedCredentialFile', () => { - it('should clear cached credentials and Google account', async () => { - const cachedCreds = { refresh_token: 'test-token' }; - const credsPath = path.join(tempHomeDir, '.qwen', 'oauth_creds.json'); - await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); - await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds)); - - const googleAccountPath = path.join( - tempHomeDir, - '.qwen', - 'google_accounts.json', - ); - const accountData = { active: 'test@example.com', old: [] }; - await fs.promises.writeFile( - googleAccountPath, - JSON.stringify(accountData), - ); - const userAccountManager = new UserAccountManager(); - - expect(fs.existsSync(credsPath)).toBe(true); - expect(fs.existsSync(googleAccountPath)).toBe(true); - expect(userAccountManager.getCachedGoogleAccount()).toBe( - 'test@example.com', - ); - - await clearCachedCredentialFile(); - expect(fs.existsSync(credsPath)).toBe(false); - expect(userAccountManager.getCachedGoogleAccount()).toBeNull(); - const updatedAccountData = JSON.parse( - fs.readFileSync(googleAccountPath, 'utf-8'), - ); - expect(updatedAccountData.active).toBeNull(); - expect(updatedAccountData.old).toContain('test@example.com'); - }); - - it('should clear the in-memory OAuth client cache', async () => { - const mockSetCredentials = vi.fn(); - const mockGetAccessToken = vi - .fn() - .mockResolvedValue({ token: 'test-token' }); - const mockGetTokenInfo = vi.fn().mockResolvedValue({}); - const mockOAuth2Client = { - setCredentials: mockSetCredentials, - getAccessToken: mockGetAccessToken, - getTokenInfo: mockGetTokenInfo, - on: vi.fn(), - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - // Pre-populate credentials to make getOauthClient resolve quickly - const credsPath = path.join(tempHomeDir, '.qwen', 'oauth_creds.json'); - await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); - await fs.promises.writeFile( - credsPath, - JSON.stringify({ refresh_token: 'token' }), - ); - - // First call, should create a client - await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig); - expect(OAuth2Client).toHaveBeenCalledTimes(1); - - // Second call, should use cached client - await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig); - expect(OAuth2Client).toHaveBeenCalledTimes(1); - - clearOauthClientCache(); - - // Third call, after clearing cache, should create a new client - await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig); - expect(OAuth2Client).toHaveBeenCalledTimes(2); - }); - }); - }); - - describe('with encrypted flag true', () => { - let tempHomeDir: string; - beforeEach(() => { - process.env[FORCE_ENCRYPTED_FILE_ENV_VAR] = 'true'; - tempHomeDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'qwen-code-test-home-'), - ); - (os.homedir as Mock).mockReturnValue(tempHomeDir); - }); - - afterEach(() => { - fs.rmSync(tempHomeDir, { recursive: true, force: true }); - vi.clearAllMocks(); - resetOauthClientForTesting(); - vi.unstubAllEnvs(); - }); - - it('should save credentials using OAuthCredentialStorage during web login', async () => { - const { OAuthCredentialStorage } = await import( - './oauth-credential-storage.js' - ); - const mockAuthUrl = 'https://example.com/auth'; - const mockCode = 'test-code'; - const mockState = 'test-state'; - const mockTokens = { - access_token: 'test-access-token', - refresh_token: 'test-refresh-token', - }; - - let onTokensCallback: (tokens: Credentials) => void = () => {}; - const mockOn = vi.fn((event, callback) => { - if (event === 'tokens') { - onTokensCallback = callback; - } - }); - - const mockGetToken = vi.fn().mockImplementation(async () => { - onTokensCallback(mockTokens); - return { tokens: mockTokens }; - }); - - const mockOAuth2Client = { - generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl), - getToken: mockGetToken, - setCredentials: vi.fn(), - getAccessToken: vi - .fn() - .mockResolvedValue({ token: 'mock-access-token' }), - on: mockOn, - credentials: mockTokens, - } as unknown as OAuth2Client; - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockOAuth2Client, - ); - - vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never); - (open as Mock).mockImplementation(async () => ({ on: vi.fn() }) as never); - - (global.fetch as Mock).mockResolvedValue({ - ok: true, - json: vi - .fn() - .mockResolvedValue({ email: 'test-google-account@gmail.com' }), - } as unknown as Response); - - let requestCallback!: http.RequestListener; - let serverListeningCallback: (value: unknown) => void; - const serverListeningPromise = new Promise( - (resolve) => (serverListeningCallback = resolve), - ); - - let capturedPort = 0; - const mockHttpServer = { - listen: vi.fn((port: number, _host: string, callback?: () => void) => { - capturedPort = port; - if (callback) { - callback(); - } - serverListeningCallback(undefined); - }), - close: vi.fn((callback?: () => void) => { - if (callback) { - callback(); - } - }), - on: vi.fn(), - address: () => ({ port: capturedPort }), - }; - (http.createServer as Mock).mockImplementation((cb) => { - requestCallback = cb as http.RequestListener; - return mockHttpServer as unknown as http.Server; - }); - - const clientPromise = getOauthClient( - AuthType.LOGIN_WITH_GOOGLE, - mockConfig, - ); - - await serverListeningPromise; - - const mockReq = { - url: `/oauth2callback?code=${mockCode}&state=${mockState}`, - } as http.IncomingMessage; - const mockRes = { - writeHead: vi.fn(), - end: vi.fn(), - } as unknown as http.ServerResponse; - - requestCallback(mockReq, mockRes); - - await clientPromise; - - expect( - OAuthCredentialStorage.saveCredentials as Mock, - ).toHaveBeenCalledWith(mockTokens); - const credsPath = path.join(tempHomeDir, '.qwen', 'oauth_creds.json'); - expect(fs.existsSync(credsPath)).toBe(false); - }); - - it('should load credentials using OAuthCredentialStorage and not from file', async () => { - const { OAuthCredentialStorage } = await import( - './oauth-credential-storage.js' - ); - const cachedCreds = { refresh_token: 'cached-encrypted-token' }; - (OAuthCredentialStorage.loadCredentials as Mock).mockResolvedValue( - cachedCreds, - ); - - // Create a dummy unencrypted credential file. - // If the logic is correct, this file should be ignored. - const unencryptedCreds = { refresh_token: 'unencrypted-token' }; - const credsPath = path.join(tempHomeDir, '.qwen', 'oauth_creds.json'); - await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); - await fs.promises.writeFile(credsPath, JSON.stringify(unencryptedCreds)); - - const mockClient = { - setCredentials: vi.fn(), - getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }), - getTokenInfo: vi.fn().mockResolvedValue({}), - on: vi.fn(), - }; - - (OAuth2Client as unknown as Mock).mockImplementation( - () => mockClient as unknown as OAuth2Client, - ); - - await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig); - - expect(OAuthCredentialStorage.loadCredentials as Mock).toHaveBeenCalled(); - expect(mockClient.setCredentials).toHaveBeenCalledWith(cachedCreds); - expect(mockClient.setCredentials).not.toHaveBeenCalledWith( - unencryptedCreds, - ); - }); - - it('should clear credentials using OAuthCredentialStorage', async () => { - const { OAuthCredentialStorage } = await import( - './oauth-credential-storage.js' - ); - - // Create a dummy unencrypted credential file. It should be deleted as part of cleanup. - const credsPath = path.join(tempHomeDir, '.qwen', 'oauth_creds.json'); - await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); - await fs.promises.writeFile(credsPath, '{}'); - - await clearCachedCredentialFile(); - - expect( - OAuthCredentialStorage.clearCredentials as Mock, - ).toHaveBeenCalled(); - expect(fs.existsSync(credsPath)).toBe(false); // The unencrypted file should be cleaned up - }); - }); -}); diff --git a/packages/core/src/code_assist/oauth2.ts b/packages/core/src/code_assist/oauth2.ts deleted file mode 100644 index b86148e9..00000000 --- a/packages/core/src/code_assist/oauth2.ts +++ /dev/null @@ -1,563 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { Credentials } from 'google-auth-library'; -import { - CodeChallengeMethod, - Compute, - OAuth2Client, -} from 'google-auth-library'; -import crypto from 'node:crypto'; -import { promises as fs } from 'node:fs'; -import * as http from 'node:http'; -import * as net from 'node:net'; -import path from 'node:path'; -import readline from 'node:readline'; -import url from 'node:url'; -import open from 'open'; -import type { Config } from '../config/config.js'; -import { Storage } from '../config/storage.js'; -import { AuthType } from '../core/contentGenerator.js'; -import { FatalAuthenticationError, getErrorMessage } from '../utils/errors.js'; -import { UserAccountManager } from '../utils/userAccountManager.js'; -import { OAuthCredentialStorage } from './oauth-credential-storage.js'; -import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js'; - -const userAccountManager = new UserAccountManager(); - -// OAuth Client ID used to initiate OAuth2Client class. -const OAUTH_CLIENT_ID = - '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com'; - -// OAuth Secret value used to initiate OAuth2Client class. -// Note: It's ok to save this in git because this is an installed application -// as described here: https://developers.google.com/identity/protocols/oauth2#installed -// "The process results in a client ID and, in some cases, a client secret, -// which you embed in the source code of your application. (In this context, -// the client secret is obviously not treated as a secret.)" -const OAUTH_CLIENT_SECRET = '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'; - -// OAuth Scopes for Cloud Code authorization. -const OAUTH_SCOPE = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile', -]; - -const HTTP_REDIRECT = 301; -const SIGN_IN_SUCCESS_URL = - 'https://developers.google.com/gemini-code-assist/auth_success_gemini'; -const SIGN_IN_FAILURE_URL = - 'https://developers.google.com/gemini-code-assist/auth_failure_gemini'; - -/** - * An Authentication URL for updating the credentials of a Oauth2Client - * as well as a promise that will resolve when the credentials have - * been refreshed (or which throws error when refreshing credentials failed). - */ -export interface OauthWebLogin { - authUrl: string; - loginCompletePromise: Promise; -} - -const oauthClientPromises = new Map>(); - -function getUseEncryptedStorageFlag() { - return process.env[FORCE_ENCRYPTED_FILE_ENV_VAR] === 'true'; -} - -async function initOauthClient( - authType: AuthType, - config: Config, -): Promise { - const client = new OAuth2Client({ - clientId: OAUTH_CLIENT_ID, - clientSecret: OAUTH_CLIENT_SECRET, - transporterOptions: { - proxy: config.getProxy(), - }, - }); - const useEncryptedStorage = getUseEncryptedStorageFlag(); - - if ( - process.env['GOOGLE_GENAI_USE_GCA'] && - process.env['GOOGLE_CLOUD_ACCESS_TOKEN'] - ) { - client.setCredentials({ - access_token: process.env['GOOGLE_CLOUD_ACCESS_TOKEN'], - }); - await fetchAndCacheUserInfo(client); - return client; - } - - client.on('tokens', async (tokens: Credentials) => { - if (useEncryptedStorage) { - await OAuthCredentialStorage.saveCredentials(tokens); - } else { - await cacheCredentials(tokens); - } - }); - - // If there are cached creds on disk, they always take precedence - if (await loadCachedCredentials(client)) { - // Found valid cached credentials. - // Check if we need to retrieve Google Account ID or Email - if (!userAccountManager.getCachedGoogleAccount()) { - try { - await fetchAndCacheUserInfo(client); - } catch (error) { - // Non-fatal, continue with existing auth. - console.warn('Failed to fetch user info:', getErrorMessage(error)); - } - } - console.log('Loaded cached credentials.'); - return client; - } - - // In Google Cloud Shell, we can use Application Default Credentials (ADC) - // provided via its metadata server to authenticate non-interactively using - // the identity of the user logged into Cloud Shell. - if (authType === AuthType.CLOUD_SHELL) { - try { - console.log("Attempting to authenticate via Cloud Shell VM's ADC."); - const computeClient = new Compute({ - // We can leave this empty, since the metadata server will provide - // the service account email. - }); - await computeClient.getAccessToken(); - console.log('Authentication successful.'); - - // Do not cache creds in this case; note that Compute client will handle its own refresh - return computeClient; - } catch (e) { - throw new Error( - `Could not authenticate using Cloud Shell credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ${getErrorMessage( - e, - )}`, - ); - } - } - - if (config.isBrowserLaunchSuppressed()) { - let success = false; - const maxRetries = 2; - for (let i = 0; !success && i < maxRetries; i++) { - success = await authWithUserCode(client); - if (!success) { - console.error( - '\nFailed to authenticate with user code.', - i === maxRetries - 1 ? '' : 'Retrying...\n', - ); - } - } - if (!success) { - throw new FatalAuthenticationError( - 'Failed to authenticate with user code.', - ); - } - } else { - const webLogin = await authWithWeb(client); - - console.log( - `\n\nCode Assist login required.\n` + - `Attempting to open authentication page in your browser.\n` + - `Otherwise navigate to:\n\n${webLogin.authUrl}\n\n`, - ); - try { - // Attempt to open the authentication URL in the default browser. - // We do not use the `wait` option here because the main script's execution - // is already paused by `loginCompletePromise`, which awaits the server callback. - const childProcess = await open(webLogin.authUrl); - - // IMPORTANT: Attach an error handler to the returned child process. - // Without this, if `open` fails to spawn a process (e.g., `xdg-open` is not found - // in a minimal Docker container), it will emit an unhandled 'error' event, - // causing the entire Node.js process to crash. - childProcess.on('error', (error) => { - console.error( - 'Failed to open browser automatically. Please try running again with NO_BROWSER=true set.', - ); - console.error('Browser error details:', getErrorMessage(error)); - }); - } catch (err) { - console.error( - 'An unexpected error occurred while trying to open the browser:', - getErrorMessage(err), - '\nThis might be due to browser compatibility issues or system configuration.', - '\nPlease try running again with NO_BROWSER=true set for manual authentication.', - ); - throw new FatalAuthenticationError( - `Failed to open browser: ${getErrorMessage(err)}`, - ); - } - console.log('Waiting for authentication...'); - - // Add timeout to prevent infinite waiting when browser tab gets stuck - const authTimeout = 5 * 60 * 1000; // 5 minutes timeout - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - reject( - new FatalAuthenticationError( - 'Authentication timed out after 5 minutes. The browser tab may have gotten stuck in a loading state. ' + - 'Please try again or use NO_BROWSER=true for manual authentication.', - ), - ); - }, authTimeout); - }); - - await Promise.race([webLogin.loginCompletePromise, timeoutPromise]); - } - - return client; -} - -export async function getOauthClient( - authType: AuthType, - config: Config, -): Promise { - if (!oauthClientPromises.has(authType)) { - oauthClientPromises.set(authType, initOauthClient(authType, config)); - } - return oauthClientPromises.get(authType)!; -} - -async function authWithUserCode(client: OAuth2Client): Promise { - const redirectUri = 'https://codeassist.google.com/authcode'; - const codeVerifier = await client.generateCodeVerifierAsync(); - const state = crypto.randomBytes(32).toString('hex'); - const authUrl: string = client.generateAuthUrl({ - redirect_uri: redirectUri, - access_type: 'offline', - scope: OAUTH_SCOPE, - code_challenge_method: CodeChallengeMethod.S256, - code_challenge: codeVerifier.codeChallenge, - state, - }); - console.log('Please visit the following URL to authorize the application:'); - console.log(''); - console.log(authUrl); - console.log(''); - - const code = await new Promise((resolve) => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - rl.question('Enter the authorization code: ', (code) => { - rl.close(); - resolve(code.trim()); - }); - }); - - if (!code) { - console.error('Authorization code is required.'); - return false; - } - - try { - const { tokens } = await client.getToken({ - code, - codeVerifier: codeVerifier.codeVerifier, - redirect_uri: redirectUri, - }); - client.setCredentials(tokens); - } catch (error) { - console.error( - 'Failed to authenticate with authorization code:', - getErrorMessage(error), - ); - return false; - } - return true; -} - -async function authWithWeb(client: OAuth2Client): Promise { - const port = await getAvailablePort(); - // The hostname used for the HTTP server binding (e.g., '0.0.0.0' in Docker). - const host = process.env['OAUTH_CALLBACK_HOST'] || 'localhost'; - // The `redirectUri` sent to Google's authorization server MUST use a loopback IP literal - // (i.e., 'localhost' or '127.0.0.1'). This is a strict security policy for credentials of - // type 'Desktop app' or 'Web application' (when using loopback flow) to mitigate - // authorization code interception attacks. - const redirectUri = `http://localhost:${port}/oauth2callback`; - const state = crypto.randomBytes(32).toString('hex'); - const authUrl = client.generateAuthUrl({ - redirect_uri: redirectUri, - access_type: 'offline', - scope: OAUTH_SCOPE, - state, - }); - - const loginCompletePromise = new Promise((resolve, reject) => { - const server = http.createServer(async (req, res) => { - try { - if (req.url!.indexOf('/oauth2callback') === -1) { - res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL }); - res.end(); - reject( - new FatalAuthenticationError( - 'OAuth callback not received. Unexpected request: ' + req.url, - ), - ); - } - // acquire the code from the querystring, and close the web server. - const qs = new url.URL(req.url!, 'http://localhost:3000').searchParams; - if (qs.get('error')) { - res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL }); - res.end(); - - const errorCode = qs.get('error'); - const errorDescription = - qs.get('error_description') || 'No additional details provided'; - reject( - new FatalAuthenticationError( - `Google OAuth error: ${errorCode}. ${errorDescription}`, - ), - ); - } else if (qs.get('state') !== state) { - res.end('State mismatch. Possible CSRF attack'); - - reject( - new FatalAuthenticationError( - 'OAuth state mismatch. Possible CSRF attack or browser session issue.', - ), - ); - } else if (qs.get('code')) { - try { - const { tokens } = await client.getToken({ - code: qs.get('code')!, - redirect_uri: redirectUri, - }); - client.setCredentials(tokens); - - // Retrieve and cache Google Account ID during authentication - try { - await fetchAndCacheUserInfo(client); - } catch (error) { - console.warn( - 'Failed to retrieve Google Account ID during authentication:', - getErrorMessage(error), - ); - // Don't fail the auth flow if Google Account ID retrieval fails - } - - res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_SUCCESS_URL }); - res.end(); - resolve(); - } catch (error) { - res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL }); - res.end(); - reject( - new FatalAuthenticationError( - `Failed to exchange authorization code for tokens: ${getErrorMessage(error)}`, - ), - ); - } - } else { - reject( - new FatalAuthenticationError( - 'No authorization code received from Google OAuth. Please try authenticating again.', - ), - ); - } - } catch (e) { - // Provide more specific error message for unexpected errors during OAuth flow - if (e instanceof FatalAuthenticationError) { - reject(e); - } else { - reject( - new FatalAuthenticationError( - `Unexpected error during OAuth authentication: ${getErrorMessage(e)}`, - ), - ); - } - } finally { - server.close(); - } - }); - - server.listen(port, host, () => { - // Server started successfully - }); - - server.on('error', (err) => { - reject( - new FatalAuthenticationError( - `OAuth callback server error: ${getErrorMessage(err)}`, - ), - ); - }); - }); - - return { - authUrl, - loginCompletePromise, - }; -} - -export function getAvailablePort(): Promise { - return new Promise((resolve, reject) => { - let port = 0; - try { - const portStr = process.env['OAUTH_CALLBACK_PORT']; - if (portStr) { - port = parseInt(portStr, 10); - if (isNaN(port) || port <= 0 || port > 65535) { - return reject( - new Error(`Invalid value for OAUTH_CALLBACK_PORT: "${portStr}"`), - ); - } - return resolve(port); - } - const server = net.createServer(); - server.listen(0, () => { - const address = server.address()! as net.AddressInfo; - port = address.port; - }); - server.on('listening', () => { - server.close(); - server.unref(); - }); - server.on('error', (e) => reject(e)); - server.on('close', () => resolve(port)); - } catch (e) { - reject(e); - } - }); -} - -async function loadCachedCredentials(client: OAuth2Client): Promise { - const useEncryptedStorage = getUseEncryptedStorageFlag(); - if (useEncryptedStorage) { - const credentials = await OAuthCredentialStorage.loadCredentials(); - if (credentials) { - client.setCredentials(credentials); - return true; - } - return false; - } - - const pathsToTry = [ - Storage.getOAuthCredsPath(), - process.env['GOOGLE_APPLICATION_CREDENTIALS'], - ].filter((p): p is string => !!p); - - for (const keyFile of pathsToTry) { - try { - const creds = await fs.readFile(keyFile, 'utf-8'); - client.setCredentials(JSON.parse(creds)); - - // This will verify locally that the credentials look good. - const { token } = await client.getAccessToken(); - if (!token) { - continue; - } - - // This will check with the server to see if it hasn't been revoked. - await client.getTokenInfo(token); - - return true; - } catch (error) { - // Log specific error for debugging, but continue trying other paths - console.debug( - `Failed to load credentials from ${keyFile}:`, - getErrorMessage(error), - ); - } - } - - return false; -} - -async function cacheCredentials(credentials: Credentials) { - const filePath = Storage.getOAuthCredsPath(); - await fs.mkdir(path.dirname(filePath), { recursive: true }); - - const credString = JSON.stringify(credentials, null, 2); - await fs.writeFile(filePath, credString, { mode: 0o600 }); - try { - await fs.chmod(filePath, 0o600); - } catch { - /* empty */ - } -} - -export function clearOauthClientCache() { - oauthClientPromises.clear(); -} - -export async function clearCachedCredentialFile() { - try { - const useEncryptedStorage = getUseEncryptedStorageFlag(); - if (useEncryptedStorage) { - await OAuthCredentialStorage.clearCredentials(); - } else { - await fs.rm(Storage.getOAuthCredsPath(), { force: true }); - } - // Clear the Google Account ID cache when credentials are cleared - await userAccountManager.clearCachedGoogleAccount(); - // Clear the in-memory OAuth client cache to force re-authentication - clearOauthClientCache(); - - /** - * Also clear Qwen SharedTokenManager cache and credentials file to prevent stale credentials - * when switching between auth types - * TODO: We do not depend on code_assist, we'll have to build an independent auth-cleaning procedure. - */ - try { - const { SharedTokenManager } = await import( - '../qwen/sharedTokenManager.js' - ); - const { clearQwenCredentials } = await import('../qwen/qwenOAuth2.js'); - - const sharedManager = SharedTokenManager.getInstance(); - sharedManager.clearCache(); - - await clearQwenCredentials(); - } catch (qwenError) { - console.debug('Could not clear Qwen credentials:', qwenError); - } - } catch (e) { - console.error('Failed to clear cached credentials:', e); - } -} - -async function fetchAndCacheUserInfo(client: OAuth2Client): Promise { - try { - const { token } = await client.getAccessToken(); - if (!token) { - return; - } - - const response = await fetch( - 'https://www.googleapis.com/oauth2/v2/userinfo', - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ); - - if (!response.ok) { - console.error( - 'Failed to fetch user info:', - response.status, - response.statusText, - ); - return; - } - - const userInfo = await response.json(); - await userAccountManager.cacheGoogleAccount(userInfo.email); - } catch (error) { - console.error('Error retrieving user info:', error); - } -} - -// Helper to ensure test isolation -export function resetOauthClientForTesting() { - oauthClientPromises.clear(); -} diff --git a/packages/core/src/code_assist/server.test.ts b/packages/core/src/code_assist/server.test.ts deleted file mode 100644 index 967493ab..00000000 --- a/packages/core/src/code_assist/server.test.ts +++ /dev/null @@ -1,255 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { beforeEach, describe, it, expect, vi } from 'vitest'; -import { CodeAssistServer } from './server.js'; -import { OAuth2Client } from 'google-auth-library'; -import { UserTierId } from './types.js'; - -vi.mock('google-auth-library'); - -describe('CodeAssistServer', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('should be able to be constructed', () => { - const auth = new OAuth2Client(); - const server = new CodeAssistServer( - auth, - 'test-project', - {}, - 'test-session', - UserTierId.FREE, - ); - expect(server).toBeInstanceOf(CodeAssistServer); - }); - - it('should call the generateContent endpoint', async () => { - const client = new OAuth2Client(); - const server = new CodeAssistServer( - client, - 'test-project', - {}, - 'test-session', - UserTierId.FREE, - ); - const mockResponse = { - response: { - candidates: [ - { - index: 0, - content: { - role: 'model', - parts: [{ text: 'response' }], - }, - finishReason: 'STOP', - safetyRatings: [], - }, - ], - }, - }; - vi.spyOn(server, 'requestPost').mockResolvedValue(mockResponse); - - const response = await server.generateContent( - { - model: 'test-model', - contents: [{ role: 'user', parts: [{ text: 'request' }] }], - }, - 'user-prompt-id', - ); - - expect(server.requestPost).toHaveBeenCalledWith( - 'generateContent', - expect.any(Object), - undefined, - ); - expect(response.candidates?.[0]?.content?.parts?.[0]?.text).toBe( - 'response', - ); - }); - - it('should call the generateContentStream endpoint', async () => { - const client = new OAuth2Client(); - const server = new CodeAssistServer( - client, - 'test-project', - {}, - 'test-session', - UserTierId.FREE, - ); - const mockResponse = (async function* () { - yield { - response: { - candidates: [ - { - index: 0, - content: { - role: 'model', - parts: [{ text: 'response' }], - }, - finishReason: 'STOP', - safetyRatings: [], - }, - ], - }, - }; - })(); - vi.spyOn(server, 'requestStreamingPost').mockResolvedValue(mockResponse); - - const stream = await server.generateContentStream( - { - model: 'test-model', - contents: [{ role: 'user', parts: [{ text: 'request' }] }], - }, - 'user-prompt-id', - ); - - for await (const res of stream) { - expect(server.requestStreamingPost).toHaveBeenCalledWith( - 'streamGenerateContent', - expect.any(Object), - undefined, - ); - expect(res.candidates?.[0]?.content?.parts?.[0]?.text).toBe('response'); - } - }); - - it('should call the onboardUser endpoint', async () => { - const client = new OAuth2Client(); - const server = new CodeAssistServer( - client, - 'test-project', - {}, - 'test-session', - UserTierId.FREE, - ); - const mockResponse = { - name: 'operations/123', - done: true, - }; - vi.spyOn(server, 'requestPost').mockResolvedValue(mockResponse); - - const response = await server.onboardUser({ - tierId: 'test-tier', - cloudaicompanionProject: 'test-project', - metadata: {}, - }); - - expect(server.requestPost).toHaveBeenCalledWith( - 'onboardUser', - expect.any(Object), - ); - expect(response.name).toBe('operations/123'); - }); - - it('should call the loadCodeAssist endpoint', async () => { - const client = new OAuth2Client(); - const server = new CodeAssistServer( - client, - 'test-project', - {}, - 'test-session', - UserTierId.FREE, - ); - const mockResponse = { - currentTier: { - id: UserTierId.FREE, - name: 'Free', - description: 'free tier', - }, - allowedTiers: [], - ineligibleTiers: [], - cloudaicompanionProject: 'projects/test', - }; - vi.spyOn(server, 'requestPost').mockResolvedValue(mockResponse); - - const response = await server.loadCodeAssist({ - metadata: {}, - }); - - expect(server.requestPost).toHaveBeenCalledWith( - 'loadCodeAssist', - expect.any(Object), - ); - expect(response).toEqual(mockResponse); - }); - - it('should return 0 for countTokens', async () => { - const client = new OAuth2Client(); - const server = new CodeAssistServer( - client, - 'test-project', - {}, - 'test-session', - UserTierId.FREE, - ); - const mockResponse = { - totalTokens: 100, - }; - vi.spyOn(server, 'requestPost').mockResolvedValue(mockResponse); - - const response = await server.countTokens({ - model: 'test-model', - contents: [{ role: 'user', parts: [{ text: 'request' }] }], - }); - expect(response.totalTokens).toBe(100); - }); - - it('should throw an error for embedContent', async () => { - const client = new OAuth2Client(); - const server = new CodeAssistServer( - client, - 'test-project', - {}, - 'test-session', - UserTierId.FREE, - ); - await expect( - server.embedContent({ - model: 'test-model', - contents: [{ role: 'user', parts: [{ text: 'request' }] }], - }), - ).rejects.toThrow(); - }); - - it('should handle VPC-SC errors when calling loadCodeAssist', async () => { - const client = new OAuth2Client(); - const server = new CodeAssistServer( - client, - 'test-project', - {}, - 'test-session', - UserTierId.FREE, - ); - const mockVpcScError = { - response: { - data: { - error: { - details: [ - { - reason: 'SECURITY_POLICY_VIOLATED', - }, - ], - }, - }, - }, - }; - vi.spyOn(server, 'requestPost').mockRejectedValue(mockVpcScError); - - const response = await server.loadCodeAssist({ - metadata: {}, - }); - - expect(server.requestPost).toHaveBeenCalledWith( - 'loadCodeAssist', - expect.any(Object), - ); - expect(response).toEqual({ - currentTier: { id: UserTierId.STANDARD }, - }); - }); -}); diff --git a/packages/core/src/code_assist/server.ts b/packages/core/src/code_assist/server.ts deleted file mode 100644 index cd4d0e8a..00000000 --- a/packages/core/src/code_assist/server.ts +++ /dev/null @@ -1,253 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { OAuth2Client } from 'google-auth-library'; -import type { - CodeAssistGlobalUserSettingResponse, - GoogleRpcResponse, - LoadCodeAssistRequest, - LoadCodeAssistResponse, - LongRunningOperationResponse, - OnboardUserRequest, - SetCodeAssistGlobalUserSettingRequest, -} from './types.js'; -import type { - CountTokensParameters, - CountTokensResponse, - EmbedContentParameters, - EmbedContentResponse, - GenerateContentParameters, - GenerateContentResponse, -} from '@google/genai'; -import * as readline from 'node:readline'; -import type { ContentGenerator } from '../core/contentGenerator.js'; -import { UserTierId } from './types.js'; -import type { - CaCountTokenResponse, - CaGenerateContentResponse, -} from './converter.js'; -import { - fromCountTokenResponse, - fromGenerateContentResponse, - toCountTokenRequest, - toGenerateContentRequest, -} from './converter.js'; - -/** HTTP options to be used in each of the requests. */ -export interface HttpOptions { - /** Additional HTTP headers to be sent with the request. */ - headers?: Record; -} - -export const CODE_ASSIST_ENDPOINT = 'https://localhost:0'; // Disable Google Code Assist API Request -export const CODE_ASSIST_API_VERSION = 'v1internal'; - -export class CodeAssistServer implements ContentGenerator { - constructor( - readonly client: OAuth2Client, - readonly projectId?: string, - readonly httpOptions: HttpOptions = {}, - readonly sessionId?: string, - readonly userTier?: UserTierId, - ) {} - - async generateContentStream( - req: GenerateContentParameters, - userPromptId: string, - ): Promise> { - const resps = await this.requestStreamingPost( - 'streamGenerateContent', - toGenerateContentRequest( - req, - userPromptId, - this.projectId, - this.sessionId, - ), - req.config?.abortSignal, - ); - return (async function* (): AsyncGenerator { - for await (const resp of resps) { - yield fromGenerateContentResponse(resp); - } - })(); - } - - async generateContent( - req: GenerateContentParameters, - userPromptId: string, - ): Promise { - const resp = await this.requestPost( - 'generateContent', - toGenerateContentRequest( - req, - userPromptId, - this.projectId, - this.sessionId, - ), - req.config?.abortSignal, - ); - return fromGenerateContentResponse(resp); - } - - async onboardUser( - req: OnboardUserRequest, - ): Promise { - return await this.requestPost( - 'onboardUser', - req, - ); - } - - async loadCodeAssist( - req: LoadCodeAssistRequest, - ): Promise { - try { - return await this.requestPost( - 'loadCodeAssist', - req, - ); - } catch (e) { - if (isVpcScAffectedUser(e)) { - return { - currentTier: { id: UserTierId.STANDARD }, - }; - } else { - throw e; - } - } - } - - async getCodeAssistGlobalUserSetting(): Promise { - return await this.requestGet( - 'getCodeAssistGlobalUserSetting', - ); - } - - async setCodeAssistGlobalUserSetting( - req: SetCodeAssistGlobalUserSettingRequest, - ): Promise { - return await this.requestPost( - 'setCodeAssistGlobalUserSetting', - req, - ); - } - - async countTokens(req: CountTokensParameters): Promise { - const resp = await this.requestPost( - 'countTokens', - toCountTokenRequest(req), - ); - return fromCountTokenResponse(resp); - } - - async embedContent( - _req: EmbedContentParameters, - ): Promise { - throw Error(); - } - - async requestPost( - method: string, - req: object, - signal?: AbortSignal, - ): Promise { - const res = await this.client.request({ - url: this.getMethodUrl(method), - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...this.httpOptions.headers, - }, - responseType: 'json', - body: JSON.stringify(req), - signal, - }); - return res.data as T; - } - - async requestGet(method: string, signal?: AbortSignal): Promise { - const res = await this.client.request({ - url: this.getMethodUrl(method), - method: 'GET', - headers: { - 'Content-Type': 'application/json', - ...this.httpOptions.headers, - }, - responseType: 'json', - signal, - }); - return res.data as T; - } - - async requestStreamingPost( - method: string, - req: object, - signal?: AbortSignal, - ): Promise> { - const res = await this.client.request({ - url: this.getMethodUrl(method), - method: 'POST', - params: { - alt: 'sse', - }, - headers: { - 'Content-Type': 'application/json', - ...this.httpOptions.headers, - }, - responseType: 'stream', - body: JSON.stringify(req), - signal, - }); - - return (async function* (): AsyncGenerator { - const rl = readline.createInterface({ - input: res.data as NodeJS.ReadableStream, - crlfDelay: Infinity, // Recognizes '\r\n' and '\n' as line breaks - }); - - let bufferedLines: string[] = []; - for await (const line of rl) { - // blank lines are used to separate JSON objects in the stream - if (line === '') { - if (bufferedLines.length === 0) { - continue; // no data to yield - } - yield JSON.parse(bufferedLines.join('\n')) as T; - bufferedLines = []; // Reset the buffer after yielding - } else if (line.startsWith('data: ')) { - bufferedLines.push(line.slice(6).trim()); - } else { - throw new Error(`Unexpected line format in response: ${line}`); - } - } - })(); - } - - getMethodUrl(method: string): string { - const endpoint = - process.env['CODE_ASSIST_ENDPOINT'] ?? CODE_ASSIST_ENDPOINT; - return `${endpoint}/${CODE_ASSIST_API_VERSION}:${method}`; - } -} - -function isVpcScAffectedUser(error: unknown): boolean { - if (error && typeof error === 'object' && 'response' in error) { - const gaxiosError = error as { - response?: { - data?: unknown; - }; - }; - const response = gaxiosError.response?.data as - | GoogleRpcResponse - | undefined; - if (Array.isArray(response?.error?.details)) { - return response.error.details.some( - (detail) => detail.reason === 'SECURITY_POLICY_VIOLATED', - ); - } - } - return false; -} diff --git a/packages/core/src/code_assist/setup.test.ts b/packages/core/src/code_assist/setup.test.ts deleted file mode 100644 index 54ad7f24..00000000 --- a/packages/core/src/code_assist/setup.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { setupUser, ProjectIdRequiredError } from './setup.js'; -import { CodeAssistServer } from '../code_assist/server.js'; -import type { OAuth2Client } from 'google-auth-library'; -import type { GeminiUserTier } from './types.js'; -import { UserTierId } from './types.js'; - -vi.mock('../code_assist/server.js'); - -const mockPaidTier: GeminiUserTier = { - id: UserTierId.STANDARD, - name: 'paid', - description: 'Paid tier', - isDefault: true, -}; - -const mockFreeTier: GeminiUserTier = { - id: UserTierId.FREE, - name: 'free', - description: 'Free tier', - isDefault: true, -}; - -describe('setupUser for existing user', () => { - let mockLoad: ReturnType; - let mockOnboardUser: ReturnType; - - beforeEach(() => { - vi.resetAllMocks(); - mockLoad = vi.fn(); - mockOnboardUser = vi.fn().mockResolvedValue({ - done: true, - response: { - cloudaicompanionProject: { - id: 'server-project', - }, - }, - }); - vi.mocked(CodeAssistServer).mockImplementation( - () => - ({ - loadCodeAssist: mockLoad, - onboardUser: mockOnboardUser, - }) as unknown as CodeAssistServer, - ); - }); - - afterEach(() => { - vi.unstubAllEnvs(); - }); - - it('should use GOOGLE_CLOUD_PROJECT when set and project from server is undefined', async () => { - vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project'); - mockLoad.mockResolvedValue({ - currentTier: mockPaidTier, - }); - await setupUser({} as OAuth2Client); - expect(CodeAssistServer).toHaveBeenCalledWith( - {}, - 'test-project', - {}, - '', - undefined, - ); - }); - - it('should ignore GOOGLE_CLOUD_PROJECT when project from server is set', async () => { - vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project'); - mockLoad.mockResolvedValue({ - cloudaicompanionProject: 'server-project', - currentTier: mockPaidTier, - }); - const projectId = await setupUser({} as OAuth2Client); - expect(CodeAssistServer).toHaveBeenCalledWith( - {}, - 'test-project', - {}, - '', - undefined, - ); - expect(projectId).toEqual({ - projectId: 'server-project', - userTier: 'standard-tier', - }); - }); - - it('should throw ProjectIdRequiredError when no project ID is available', async () => { - vi.stubEnv('GOOGLE_CLOUD_PROJECT', ''); - // And the server itself requires a project ID internally - vi.mocked(CodeAssistServer).mockImplementation(() => { - throw new ProjectIdRequiredError(); - }); - - await expect(setupUser({} as OAuth2Client)).rejects.toThrow( - ProjectIdRequiredError, - ); - }); -}); - -describe('setupUser for new user', () => { - let mockLoad: ReturnType; - let mockOnboardUser: ReturnType; - - beforeEach(() => { - vi.resetAllMocks(); - mockLoad = vi.fn(); - mockOnboardUser = vi.fn().mockResolvedValue({ - done: true, - response: { - cloudaicompanionProject: { - id: 'server-project', - }, - }, - }); - vi.mocked(CodeAssistServer).mockImplementation( - () => - ({ - loadCodeAssist: mockLoad, - onboardUser: mockOnboardUser, - }) as unknown as CodeAssistServer, - ); - }); - - afterEach(() => { - vi.unstubAllEnvs(); - }); - - it('should use GOOGLE_CLOUD_PROJECT when set and onboard a new paid user', async () => { - vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project'); - mockLoad.mockResolvedValue({ - allowedTiers: [mockPaidTier], - }); - const userData = await setupUser({} as OAuth2Client); - expect(CodeAssistServer).toHaveBeenCalledWith( - {}, - 'test-project', - {}, - '', - undefined, - ); - expect(mockLoad).toHaveBeenCalled(); - expect(mockOnboardUser).toHaveBeenCalledWith({ - tierId: 'standard-tier', - cloudaicompanionProject: 'test-project', - metadata: { - ideType: 'IDE_UNSPECIFIED', - platform: 'PLATFORM_UNSPECIFIED', - pluginType: 'GEMINI', - duetProject: 'test-project', - }, - }); - expect(userData).toEqual({ - projectId: 'server-project', - userTier: 'standard-tier', - }); - }); - - it('should onboard a new free user when GOOGLE_CLOUD_PROJECT is not set', async () => { - vi.stubEnv('GOOGLE_CLOUD_PROJECT', ''); - mockLoad.mockResolvedValue({ - allowedTiers: [mockFreeTier], - }); - const userData = await setupUser({} as OAuth2Client); - expect(CodeAssistServer).toHaveBeenCalledWith( - {}, - undefined, - {}, - '', - undefined, - ); - expect(mockLoad).toHaveBeenCalled(); - expect(mockOnboardUser).toHaveBeenCalledWith({ - tierId: 'free-tier', - cloudaicompanionProject: undefined, - metadata: { - ideType: 'IDE_UNSPECIFIED', - platform: 'PLATFORM_UNSPECIFIED', - pluginType: 'GEMINI', - }, - }); - expect(userData).toEqual({ - projectId: 'server-project', - userTier: 'free-tier', - }); - }); - - it('should use GOOGLE_CLOUD_PROJECT when onboard response has no project ID', async () => { - vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project'); - mockLoad.mockResolvedValue({ - allowedTiers: [mockPaidTier], - }); - mockOnboardUser.mockResolvedValue({ - done: true, - response: { - cloudaicompanionProject: undefined, - }, - }); - const userData = await setupUser({} as OAuth2Client); - expect(userData).toEqual({ - projectId: 'test-project', - userTier: 'standard-tier', - }); - }); - - it('should throw ProjectIdRequiredError when no project ID is available', async () => { - vi.stubEnv('GOOGLE_CLOUD_PROJECT', ''); - mockLoad.mockResolvedValue({ - allowedTiers: [mockPaidTier], - }); - mockOnboardUser.mockResolvedValue({ - done: true, - response: {}, - }); - await expect(setupUser({} as OAuth2Client)).rejects.toThrow( - ProjectIdRequiredError, - ); - }); -}); diff --git a/packages/core/src/code_assist/setup.ts b/packages/core/src/code_assist/setup.ts deleted file mode 100644 index 43e9fb27..00000000 --- a/packages/core/src/code_assist/setup.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { - ClientMetadata, - GeminiUserTier, - LoadCodeAssistResponse, - OnboardUserRequest, -} from './types.js'; -import { UserTierId } from './types.js'; -import { CodeAssistServer } from './server.js'; -import type { OAuth2Client } from 'google-auth-library'; - -export class ProjectIdRequiredError extends Error { - constructor() { - super( - 'This account requires setting the GOOGLE_CLOUD_PROJECT env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca', - ); - } -} - -export interface UserData { - projectId: string; - userTier: UserTierId; -} - -/** - * - * @param projectId the user's project id, if any - * @returns the user's actual project id - */ -export async function setupUser(client: OAuth2Client): Promise { - const projectId = process.env['GOOGLE_CLOUD_PROJECT'] || undefined; - const caServer = new CodeAssistServer(client, projectId, {}, '', undefined); - const coreClientMetadata: ClientMetadata = { - ideType: 'IDE_UNSPECIFIED', - platform: 'PLATFORM_UNSPECIFIED', - pluginType: 'GEMINI', - }; - - const loadRes = await caServer.loadCodeAssist({ - cloudaicompanionProject: projectId, - metadata: { - ...coreClientMetadata, - duetProject: projectId, - }, - }); - - if (loadRes.currentTier) { - if (!loadRes.cloudaicompanionProject) { - if (projectId) { - return { - projectId, - userTier: loadRes.currentTier.id, - }; - } - throw new ProjectIdRequiredError(); - } - return { - projectId: loadRes.cloudaicompanionProject, - userTier: loadRes.currentTier.id, - }; - } - - const tier = getOnboardTier(loadRes); - - let onboardReq: OnboardUserRequest; - if (tier.id === UserTierId.FREE) { - // The free tier uses a managed google cloud project. Setting a project in the `onboardUser` request causes a `Precondition Failed` error. - onboardReq = { - tierId: tier.id, - cloudaicompanionProject: undefined, - metadata: coreClientMetadata, - }; - } else { - onboardReq = { - tierId: tier.id, - cloudaicompanionProject: projectId, - metadata: { - ...coreClientMetadata, - duetProject: projectId, - }, - }; - } - - // Poll onboardUser until long running operation is complete. - let lroRes = await caServer.onboardUser(onboardReq); - while (!lroRes.done) { - await new Promise((f) => setTimeout(f, 5000)); - lroRes = await caServer.onboardUser(onboardReq); - } - - if (!lroRes.response?.cloudaicompanionProject?.id) { - if (projectId) { - return { - projectId, - userTier: tier.id, - }; - } - throw new ProjectIdRequiredError(); - } - - return { - projectId: lroRes.response.cloudaicompanionProject.id, - userTier: tier.id, - }; -} - -function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier { - for (const tier of res.allowedTiers || []) { - if (tier.isDefault) { - return tier; - } - } - return { - name: '', - description: '', - id: UserTierId.LEGACY, - userDefinedCloudaicompanionProject: true, - }; -} diff --git a/packages/core/src/code_assist/types.ts b/packages/core/src/code_assist/types.ts deleted file mode 100644 index b79094bb..00000000 --- a/packages/core/src/code_assist/types.ts +++ /dev/null @@ -1,201 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -export interface ClientMetadata { - ideType?: ClientMetadataIdeType; - ideVersion?: string; - pluginVersion?: string; - platform?: ClientMetadataPlatform; - updateChannel?: string; - duetProject?: string; - pluginType?: ClientMetadataPluginType; - ideName?: string; -} - -export type ClientMetadataIdeType = - | 'IDE_UNSPECIFIED' - | 'VSCODE' - | 'INTELLIJ' - | 'VSCODE_CLOUD_WORKSTATION' - | 'INTELLIJ_CLOUD_WORKSTATION' - | 'CLOUD_SHELL'; -export type ClientMetadataPlatform = - | 'PLATFORM_UNSPECIFIED' - | 'DARWIN_AMD64' - | 'DARWIN_ARM64' - | 'LINUX_AMD64' - | 'LINUX_ARM64' - | 'WINDOWS_AMD64'; -export type ClientMetadataPluginType = - | 'PLUGIN_UNSPECIFIED' - | 'CLOUD_CODE' - | 'GEMINI' - | 'AIPLUGIN_INTELLIJ' - | 'AIPLUGIN_STUDIO'; - -export interface LoadCodeAssistRequest { - cloudaicompanionProject?: string; - metadata: ClientMetadata; -} - -/** - * Represents LoadCodeAssistResponse proto json field - * http://google3/google/internal/cloud/code/v1internal/cloudcode.proto;l=224 - */ -export interface LoadCodeAssistResponse { - currentTier?: GeminiUserTier | null; - allowedTiers?: GeminiUserTier[] | null; - ineligibleTiers?: IneligibleTier[] | null; - cloudaicompanionProject?: string | null; -} - -/** - * GeminiUserTier reflects the structure received from the CodeAssist when calling LoadCodeAssist. - */ -export interface GeminiUserTier { - id: UserTierId; - name?: string; - description?: string; - // This value is used to declare whether a given tier requires the user to configure the project setting on the IDE settings or not. - userDefinedCloudaicompanionProject?: boolean | null; - isDefault?: boolean; - privacyNotice?: PrivacyNotice; - hasAcceptedTos?: boolean; - hasOnboardedPreviously?: boolean; -} - -/** - * Includes information specifying the reasons for a user's ineligibility for a specific tier. - * @param reasonCode mnemonic code representing the reason for in-eligibility. - * @param reasonMessage message to display to the user. - * @param tierId id of the tier. - * @param tierName name of the tier. - */ -export interface IneligibleTier { - reasonCode: IneligibleTierReasonCode; - reasonMessage: string; - tierId: UserTierId; - tierName: string; -} - -/** - * List of predefined reason codes when a tier is blocked from a specific tier. - * https://source.corp.google.com/piper///depot/google3/google/internal/cloud/code/v1internal/cloudcode.proto;l=378 - */ -export enum IneligibleTierReasonCode { - // go/keep-sorted start - DASHER_USER = 'DASHER_USER', - INELIGIBLE_ACCOUNT = 'INELIGIBLE_ACCOUNT', - NON_USER_ACCOUNT = 'NON_USER_ACCOUNT', - RESTRICTED_AGE = 'RESTRICTED_AGE', - RESTRICTED_NETWORK = 'RESTRICTED_NETWORK', - UNKNOWN = 'UNKNOWN', - UNKNOWN_LOCATION = 'UNKNOWN_LOCATION', - UNSUPPORTED_LOCATION = 'UNSUPPORTED_LOCATION', - // go/keep-sorted end -} -/** - * UserTierId represents IDs returned from the Cloud Code Private API representing a user's tier - * - * //depot/google3/cloud/developer_experience/cloudcode/pa/service/usertier.go;l=16 - */ -export enum UserTierId { - FREE = 'free-tier', - LEGACY = 'legacy-tier', - STANDARD = 'standard-tier', -} - -/** - * PrivacyNotice reflects the structure received from the CodeAssist in regards to a tier - * privacy notice. - */ -export interface PrivacyNotice { - showNotice: boolean; - noticeText?: string; -} - -/** - * Proto signature of OnboardUserRequest as payload to OnboardUser call - */ -export interface OnboardUserRequest { - tierId: string | undefined; - cloudaicompanionProject: string | undefined; - metadata: ClientMetadata | undefined; -} - -/** - * Represents LongRunningOperation proto - * http://google3/google/longrunning/operations.proto;rcl=698857719;l=107 - */ -export interface LongRunningOperationResponse { - name: string; - done?: boolean; - response?: OnboardUserResponse; -} - -/** - * Represents OnboardUserResponse proto - * http://google3/google/internal/cloud/code/v1internal/cloudcode.proto;l=215 - */ -export interface OnboardUserResponse { - // tslint:disable-next-line:enforce-name-casing This is the name of the field in the proto. - cloudaicompanionProject?: { - id: string; - name: string; - }; -} - -/** - * Status code of user license status - * it does not strictly correspond to the proto - * Error value is an additional value assigned to error responses from OnboardUser - */ -export enum OnboardUserStatusCode { - Default = 'DEFAULT', - Notice = 'NOTICE', - Warning = 'WARNING', - Error = 'ERROR', -} - -/** - * Status of user onboarded to gemini - */ -export interface OnboardUserStatus { - statusCode: OnboardUserStatusCode; - displayMessage: string; - helpLink: HelpLinkUrl | undefined; -} - -export interface HelpLinkUrl { - description: string; - url: string; -} - -export interface SetCodeAssistGlobalUserSettingRequest { - cloudaicompanionProject?: string; - freeTierDataCollectionOptin: boolean; -} - -export interface CodeAssistGlobalUserSettingResponse { - cloudaicompanionProject?: string; - freeTierDataCollectionOptin: boolean; -} - -/** - * Relevant fields that can be returned from a Google RPC response - */ -export interface GoogleRpcResponse { - error?: { - details?: GoogleRpcErrorInfo[]; - }; -} - -/** - * Relevant fields that can be returned in the details of an error returned from GoogleRPCs - */ -interface GoogleRpcErrorInfo { - reason?: string; -} diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 6aa49306..4e241ca6 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -283,23 +283,6 @@ describe('Server Config (config.ts)', () => { expect(config.isInFallbackMode()).toBe(false); }); - it('should strip thoughts when switching from GenAI to Vertex', async () => { - const config = new Config(baseParams); - - vi.mocked(createContentGeneratorConfig).mockImplementation( - (_: Config, authType: AuthType | undefined) => - ({ authType }) as unknown as ContentGeneratorConfig, - ); - - await config.refreshAuth(AuthType.USE_GEMINI); - - await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE); - - expect( - config.getGeminiClient().stripThoughtsFromHistory, - ).toHaveBeenCalledWith(); - }); - it('should not strip thoughts when switching from Vertex to GenAI', async () => { const config = new Config(baseParams); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 1cb79905..b59c4017 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -16,7 +16,8 @@ import { ProxyAgent, setGlobalDispatcher } from 'undici'; import type { ContentGenerator, ContentGeneratorConfig, -} from '../core/contentGenerator.js'; + + AuthType} from '../core/contentGenerator.js'; import type { FallbackModelHandler } from '../fallback/types.js'; import type { MCPOAuthConfig } from '../mcp/oauth-provider.js'; import type { ShellExecutionConfig } from '../services/shellExecutionService.js'; @@ -26,7 +27,6 @@ import type { AnyToolInvocation } from '../tools/tools.js'; import { BaseLlmClient } from '../core/baseLlmClient.js'; import { GeminiClient } from '../core/client.js'; import { - AuthType, createContentGenerator, createContentGeneratorConfig, } from '../core/contentGenerator.js'; @@ -684,16 +684,6 @@ export class Config { } async refreshAuth(authMethod: AuthType, isInitialAuth?: boolean) { - // Vertex and Genai have incompatible encryption and sending history with - // throughtSignature from Genai to Vertex will fail, we need to strip them - if ( - this.contentGeneratorConfig?.authType === AuthType.USE_GEMINI && - authMethod === AuthType.LOGIN_WITH_GOOGLE - ) { - // Restore the conversation history to the new client - this.geminiClient.stripThoughtsFromHistory(); - } - const newContentGeneratorConfig = createContentGeneratorConfig( this, authMethod, diff --git a/packages/core/src/config/flashFallback.test.ts b/packages/core/src/config/flashFallback.test.ts index 2de20b2b..2675ea84 100644 --- a/packages/core/src/config/flashFallback.test.ts +++ b/packages/core/src/config/flashFallback.test.ts @@ -31,7 +31,7 @@ describe('Flash Model Fallback Configuration', () => { config as unknown as { contentGeneratorConfig: unknown } ).contentGeneratorConfig = { model: DEFAULT_GEMINI_MODEL, - authType: 'oauth-personal', + authType: 'gemini-api-key', }; }); diff --git a/packages/core/src/core/__tests__/openaiTimeoutHandling.test.ts b/packages/core/src/core/__tests__/openaiTimeoutHandling.test.ts index 07d3b930..62e3e39e 100644 --- a/packages/core/src/core/__tests__/openaiTimeoutHandling.test.ts +++ b/packages/core/src/core/__tests__/openaiTimeoutHandling.test.ts @@ -73,6 +73,7 @@ describe('OpenAIContentGenerator Timeout Handling', () => { }), buildClient: vi.fn().mockReturnValue(mockOpenAIClient), buildRequest: vi.fn().mockImplementation((req) => req), + getDefaultGenerationConfig: vi.fn().mockReturnValue({}), }; // Create generator instance @@ -299,6 +300,7 @@ describe('OpenAIContentGenerator Timeout Handling', () => { }), buildClient: vi.fn().mockReturnValue(mockOpenAIClient), buildRequest: vi.fn().mockImplementation((req) => req), + getDefaultGenerationConfig: vi.fn().mockReturnValue({}), }; new OpenAIContentGenerator( @@ -333,6 +335,7 @@ describe('OpenAIContentGenerator Timeout Handling', () => { }), buildClient: vi.fn().mockReturnValue(mockOpenAIClient), buildRequest: vi.fn().mockImplementation((req) => req), + getDefaultGenerationConfig: vi.fn().mockReturnValue({}), }; new OpenAIContentGenerator( diff --git a/packages/core/src/core/baseLlmClient.test.ts b/packages/core/src/core/baseLlmClient.test.ts index 999a6903..3f2b71d1 100644 --- a/packages/core/src/core/baseLlmClient.test.ts +++ b/packages/core/src/core/baseLlmClient.test.ts @@ -146,13 +146,11 @@ describe('BaseLlmClient', () => { // Validate the parameters passed to the underlying generator expect(mockGenerateContent).toHaveBeenCalledTimes(1); expect(mockGenerateContent).toHaveBeenCalledWith( - { + expect.objectContaining({ model: 'test-model', contents: defaultOptions.contents, - config: { + config: expect.objectContaining({ abortSignal: defaultOptions.abortSignal, - temperature: 0, - topP: 1, tools: [ { functionDeclarations: [ @@ -164,9 +162,8 @@ describe('BaseLlmClient', () => { ], }, ], - // Crucial: systemInstruction should NOT be in the config object if not provided - }, - }, + }), + }), 'test-prompt-id', ); }); @@ -189,7 +186,6 @@ describe('BaseLlmClient', () => { expect.objectContaining({ config: expect.objectContaining({ temperature: 0.8, - topP: 1, // Default should remain if not overridden topK: 10, tools: expect.any(Array), }), diff --git a/packages/core/src/core/baseLlmClient.ts b/packages/core/src/core/baseLlmClient.ts index b8ce2a68..e97ce892 100644 --- a/packages/core/src/core/baseLlmClient.ts +++ b/packages/core/src/core/baseLlmClient.ts @@ -64,12 +64,6 @@ export interface GenerateJsonOptions { * A client dedicated to stateless, utility-focused LLM calls. */ export class BaseLlmClient { - // Default configuration for utility tasks - private readonly defaultUtilityConfig: GenerateContentConfig = { - temperature: 0, - topP: 1, - }; - constructor( private readonly contentGenerator: ContentGenerator, private readonly config: Config, @@ -90,7 +84,6 @@ export class BaseLlmClient { const requestConfig: GenerateContentConfig = { abortSignal, - ...this.defaultUtilityConfig, ...options.config, ...(systemInstruction && { systemInstruction }), }; diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index 8adaf4f6..f069ce4d 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -15,11 +15,7 @@ import { } from 'vitest'; import type { Content, GenerateContentResponse, Part } from '@google/genai'; -import { - isThinkingDefault, - isThinkingSupported, - GeminiClient, -} from './client.js'; +import { GeminiClient } from './client.js'; import { findCompressSplitPoint } from '../services/chatCompressionService.js'; import { AuthType, @@ -247,40 +243,6 @@ describe('findCompressSplitPoint', () => { }); }); -describe('isThinkingSupported', () => { - it('should return true for gemini-2.5', () => { - expect(isThinkingSupported('gemini-2.5')).toBe(true); - }); - - it('should return true for gemini-2.5-pro', () => { - expect(isThinkingSupported('gemini-2.5-pro')).toBe(true); - }); - - it('should return false for other models', () => { - expect(isThinkingSupported('gemini-1.5-flash')).toBe(false); - expect(isThinkingSupported('some-other-model')).toBe(false); - }); -}); - -describe('isThinkingDefault', () => { - it('should return false for gemini-2.5-flash-lite', () => { - expect(isThinkingDefault('gemini-2.5-flash-lite')).toBe(false); - }); - - it('should return true for gemini-2.5', () => { - expect(isThinkingDefault('gemini-2.5')).toBe(true); - }); - - it('should return true for gemini-2.5-pro', () => { - expect(isThinkingDefault('gemini-2.5-pro')).toBe(true); - }); - - it('should return false for other models', () => { - expect(isThinkingDefault('gemini-1.5-flash')).toBe(false); - expect(isThinkingDefault('some-other-model')).toBe(false); - }); -}); - describe('Gemini Client (client.ts)', () => { let mockContentGenerator: ContentGenerator; let mockConfig: Config; @@ -2304,16 +2266,15 @@ ${JSON.stringify( ); expect(mockContentGenerator.generateContent).toHaveBeenCalledWith( - { + expect.objectContaining({ model: DEFAULT_GEMINI_FLASH_MODEL, - config: { + config: expect.objectContaining({ abortSignal, systemInstruction: getCoreSystemPrompt(''), temperature: 0.5, - topP: 1, - }, + }), contents, - }, + }), 'test-session-id', ); }); diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 6e3be209..6c62478d 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -15,11 +15,7 @@ import type { // Config import { ApprovalMode, type Config } from '../config/config.js'; -import { - DEFAULT_GEMINI_FLASH_MODEL, - DEFAULT_GEMINI_MODEL_AUTO, - DEFAULT_THINKING_MODE, -} from '../config/models.js'; +import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; // Core modules import type { ContentGenerator } from './contentGenerator.js'; @@ -78,25 +74,10 @@ import { type File, type IdeContext } from '../ide/types.js'; // Fallback handling import { handleFallback } from '../fallback/handler.js'; -export function isThinkingSupported(model: string) { - return model.startsWith('gemini-2.5') || model === DEFAULT_GEMINI_MODEL_AUTO; -} - -export function isThinkingDefault(model: string) { - if (model.startsWith('gemini-2.5-flash-lite')) { - return false; - } - return model.startsWith('gemini-2.5') || model === DEFAULT_GEMINI_MODEL_AUTO; -} - const MAX_TURNS = 100; export class GeminiClient { private chat?: GeminiChat; - private readonly generateContentConfig: GenerateContentConfig = { - temperature: 0, - topP: 1, - }; private sessionTurnCount = 0; private readonly loopDetector: LoopDetectionService; @@ -208,20 +189,10 @@ export class GeminiClient { const model = this.config.getModel(); const systemInstruction = getCoreSystemPrompt(userMemory, model); - const config: GenerateContentConfig = { ...this.generateContentConfig }; - - if (isThinkingSupported(model)) { - config.thinkingConfig = { - includeThoughts: true, - thinkingBudget: DEFAULT_THINKING_MODE, - }; - } - return new GeminiChat( this.config, { systemInstruction, - ...config, tools, }, history, @@ -618,11 +589,6 @@ export class GeminiClient { ): Promise { let currentAttemptModel: string = model; - const configToUse: GenerateContentConfig = { - ...this.generateContentConfig, - ...generationConfig, - }; - try { const userMemory = this.config.getUserMemory(); const finalSystemInstruction = generationConfig.systemInstruction @@ -631,7 +597,7 @@ export class GeminiClient { const requestConfig: GenerateContentConfig = { abortSignal, - ...configToUse, + ...generationConfig, systemInstruction: finalSystemInstruction, }; @@ -672,7 +638,7 @@ export class GeminiClient { `Error generating content via API with model ${currentAttemptModel}.`, { requestContents: contents, - requestConfig: configToUse, + requestConfig: generationConfig, }, 'generateContent-api', ); diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index 729481c0..e70b4d13 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -5,42 +5,19 @@ */ import { describe, it, expect, vi } from 'vitest'; -import type { ContentGenerator } from './contentGenerator.js'; import { createContentGenerator, AuthType } from './contentGenerator.js'; -import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js'; import { GoogleGenAI } from '@google/genai'; import type { Config } from '../config/config.js'; -import { LoggingContentGenerator } from './loggingContentGenerator.js'; +import { LoggingContentGenerator } from './geminiContentGenerator/loggingContentGenerator.js'; -vi.mock('../code_assist/codeAssist.js'); vi.mock('@google/genai'); -const mockConfig = { - getCliVersion: vi.fn().mockReturnValue('1.0.0'), -} as unknown as Config; - describe('createContentGenerator', () => { - it('should create a CodeAssistContentGenerator', async () => { - const mockGenerator = {} as unknown as ContentGenerator; - vi.mocked(createCodeAssistContentGenerator).mockResolvedValue( - mockGenerator as never, - ); - const generator = await createContentGenerator( - { - model: 'test-model', - authType: AuthType.LOGIN_WITH_GOOGLE, - }, - mockConfig, - ); - expect(createCodeAssistContentGenerator).toHaveBeenCalled(); - expect(generator).toEqual( - new LoggingContentGenerator(mockGenerator, mockConfig), - ); - }); - - it('should create a GoogleGenAI content generator', async () => { + it('should create a Gemini content generator', async () => { const mockConfig = { getUsageStatisticsEnabled: () => true, + getContentGeneratorConfig: () => ({}), + getCliVersion: () => '1.0.0', } as unknown as Config; const mockGenerator = { @@ -65,17 +42,17 @@ describe('createContentGenerator', () => { }, }, }); - expect(generator).toEqual( - new LoggingContentGenerator( - (mockGenerator as GoogleGenAI).models, - mockConfig, - ), - ); + // We expect it to be a LoggingContentGenerator wrapping a GeminiContentGenerator + expect(generator).toBeInstanceOf(LoggingContentGenerator); + const wrapped = (generator as LoggingContentGenerator).getWrapped(); + expect(wrapped).toBeDefined(); }); - it('should create a GoogleGenAI content generator with client install id logging disabled', async () => { + it('should create a Gemini content generator with client install id logging disabled', async () => { const mockConfig = { getUsageStatisticsEnabled: () => false, + getContentGeneratorConfig: () => ({}), + getCliVersion: () => '1.0.0', } as unknown as Config; const mockGenerator = { models: {}, @@ -98,11 +75,6 @@ describe('createContentGenerator', () => { }, }, }); - expect(generator).toEqual( - new LoggingContentGenerator( - (mockGenerator as GoogleGenAI).models, - mockConfig, - ), - ); + expect(generator).toBeInstanceOf(LoggingContentGenerator); }); }); diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 75614ae6..52886467 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -12,15 +12,9 @@ import type { GenerateContentParameters, GenerateContentResponse, } from '@google/genai'; -import { GoogleGenAI } from '@google/genai'; -import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js'; import { DEFAULT_QWEN_MODEL } from '../config/models.js'; import type { Config } from '../config/config.js'; -import type { UserTierId } from '../code_assist/types.js'; -import { InstallationManager } from '../utils/installationManager.js'; -import { LoggingContentGenerator } from './loggingContentGenerator.js'; - /** * Interface abstracting the core functionalities for generating content and counting tokens. */ @@ -38,15 +32,11 @@ export interface ContentGenerator { countTokens(request: CountTokensParameters): Promise; embedContent(request: EmbedContentParameters): Promise; - - userTier?: UserTierId; } export enum AuthType { - LOGIN_WITH_GOOGLE = 'oauth-personal', USE_GEMINI = 'gemini-api-key', USE_VERTEX_AI = 'vertex-ai', - CLOUD_SHELL = 'cloud-shell', USE_OPENAI = 'openai', QWEN_OAUTH = 'qwen-oauth', } @@ -59,12 +49,9 @@ export type ContentGeneratorConfig = { authType?: AuthType | undefined; enableOpenAILogging?: boolean; openAILoggingDir?: string; - // Timeout configuration in milliseconds - timeout?: number; - // Maximum retries for failed requests - maxRetries?: number; - // Disable cache control for DashScope providers - disableCacheControl?: boolean; + timeout?: number; // Timeout configuration in milliseconds + maxRetries?: number; // Maximum retries for failed requests + disableCacheControl?: boolean; // Disable cache control for DashScope providers samplingParams?: { top_p?: number; top_k?: number; @@ -74,6 +61,9 @@ export type ContentGeneratorConfig = { temperature?: number; max_tokens?: number; }; + reasoning?: { + effort?: 'low' | 'medium' | 'high'; + }; proxy?: string | undefined; userAgent?: string; // Schema compliance mode for tool definitions @@ -123,48 +113,14 @@ export async function createContentGenerator( gcConfig: Config, isInitialAuth?: boolean, ): Promise { - const version = process.env['CLI_VERSION'] || process.version; - const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; - const baseHeaders: Record = { - 'User-Agent': userAgent, - }; - - if ( - config.authType === AuthType.LOGIN_WITH_GOOGLE || - config.authType === AuthType.CLOUD_SHELL - ) { - const httpOptions = { headers: baseHeaders }; - return new LoggingContentGenerator( - await createCodeAssistContentGenerator( - httpOptions, - config.authType, - gcConfig, - ), - gcConfig, - ); - } - if ( config.authType === AuthType.USE_GEMINI || config.authType === AuthType.USE_VERTEX_AI ) { - let headers: Record = { ...baseHeaders }; - if (gcConfig?.getUsageStatisticsEnabled()) { - const installationManager = new InstallationManager(); - const installationId = installationManager.getInstallationId(); - headers = { - ...headers, - 'x-gemini-api-privileged-user-id': `${installationId}`, - }; - } - const httpOptions = { headers }; - - const googleGenAI = new GoogleGenAI({ - apiKey: config.apiKey === '' ? undefined : config.apiKey, - vertexai: config.vertexai, - httpOptions, - }); - return new LoggingContentGenerator(googleGenAI.models, gcConfig); + const { createGeminiContentGenerator } = await import( + './geminiContentGenerator/index.js' + ); + return createGeminiContentGenerator(config, gcConfig); } if (config.authType === AuthType.USE_OPENAI) { diff --git a/packages/core/src/core/coreToolScheduler.test.ts b/packages/core/src/core/coreToolScheduler.test.ts index 26a1b29c..bd970b9d 100644 --- a/packages/core/src/core/coreToolScheduler.test.ts +++ b/packages/core/src/core/coreToolScheduler.test.ts @@ -240,7 +240,7 @@ describe('CoreToolScheduler', () => { getAllowedTools: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, @@ -318,7 +318,7 @@ describe('CoreToolScheduler', () => { getAllowedTools: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, @@ -497,7 +497,7 @@ describe('CoreToolScheduler', () => { getExcludeTools: () => ['write_file', 'edit', 'run_shell_command'], getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, @@ -584,7 +584,7 @@ describe('CoreToolScheduler', () => { getExcludeTools: () => ['write_file', 'edit'], // Different excluded tools getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, @@ -674,7 +674,7 @@ describe('CoreToolScheduler with payload', () => { getAllowedTools: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, @@ -1001,7 +1001,7 @@ describe('CoreToolScheduler edit cancellation', () => { getAllowedTools: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, @@ -1108,7 +1108,7 @@ describe('CoreToolScheduler YOLO mode', () => { getAllowedTools: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, @@ -1258,7 +1258,7 @@ describe('CoreToolScheduler cancellation during executing with live output', () getApprovalMode: () => ApprovalMode.DEFAULT, getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getToolRegistry: () => mockToolRegistry, getShellExecutionConfig: () => ({ @@ -1350,7 +1350,7 @@ describe('CoreToolScheduler request queueing', () => { getAllowedTools: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, @@ -1482,7 +1482,7 @@ describe('CoreToolScheduler request queueing', () => { getToolRegistry: () => toolRegistry, getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 80, @@ -1586,7 +1586,7 @@ describe('CoreToolScheduler request queueing', () => { getAllowedTools: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, @@ -1854,7 +1854,7 @@ describe('CoreToolScheduler Sequential Execution', () => { getAllowedTools: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, @@ -1975,7 +1975,7 @@ describe('CoreToolScheduler Sequential Execution', () => { getAllowedTools: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, diff --git a/packages/core/src/core/geminiChat.test.ts b/packages/core/src/core/geminiChat.test.ts index 5aaa814f..0b8176ff 100644 --- a/packages/core/src/core/geminiChat.test.ts +++ b/packages/core/src/core/geminiChat.test.ts @@ -111,7 +111,7 @@ describe('GeminiChat', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getContentGeneratorConfig: vi.fn().mockReturnValue({ - authType: 'oauth-personal', // Ensure this is set for fallback tests + authType: 'gemini-api-key', // Ensure this is set for fallback tests model: 'test-model', }), getModel: vi.fn().mockReturnValue('gemini-pro'), @@ -1382,7 +1382,7 @@ describe('GeminiChat', () => { }); it('should call handleFallback with the specific failed model and retry if handler returns true', async () => { - const authType = AuthType.LOGIN_WITH_GOOGLE; + const authType = AuthType.USE_GEMINI; vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({ model: 'test-model', authType, diff --git a/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.test.ts b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.test.ts new file mode 100644 index 00000000..62c50ec6 --- /dev/null +++ b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.test.ts @@ -0,0 +1,173 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { GeminiContentGenerator } from './geminiContentGenerator.js'; +import { GoogleGenAI } from '@google/genai'; + +vi.mock('@google/genai', () => { + const mockGenerateContent = vi.fn(); + const mockGenerateContentStream = vi.fn(); + const mockCountTokens = vi.fn(); + const mockEmbedContent = vi.fn(); + + return { + GoogleGenAI: vi.fn().mockImplementation(() => ({ + models: { + generateContent: mockGenerateContent, + generateContentStream: mockGenerateContentStream, + countTokens: mockCountTokens, + embedContent: mockEmbedContent, + }, + })), + }; +}); + +describe('GeminiContentGenerator', () => { + let generator: GeminiContentGenerator; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockGoogleGenAI: any; + + beforeEach(() => { + vi.clearAllMocks(); + generator = new GeminiContentGenerator({ + apiKey: 'test-api-key', + }); + mockGoogleGenAI = vi.mocked(GoogleGenAI).mock.results[0].value; + }); + + it('should call generateContent on the underlying model', async () => { + const request = { model: 'gemini-1.5-flash', contents: [] }; + const expectedResponse = { responseId: 'test-id' }; + mockGoogleGenAI.models.generateContent.mockResolvedValue(expectedResponse); + + const response = await generator.generateContent(request, 'prompt-id'); + + expect(mockGoogleGenAI.models.generateContent).toHaveBeenCalledWith( + expect.objectContaining({ + ...request, + config: expect.objectContaining({ + temperature: 1, + topP: 0.95, + thinkingConfig: { + includeThoughts: true, + thinkingLevel: 'HIGH', + }, + }), + }), + ); + expect(response).toBe(expectedResponse); + }); + + it('should call generateContentStream on the underlying model', async () => { + const request = { model: 'gemini-1.5-flash', contents: [] }; + const mockStream = (async function* () { + yield { responseId: '1' }; + })(); + mockGoogleGenAI.models.generateContentStream.mockResolvedValue(mockStream); + + const stream = await generator.generateContentStream(request, 'prompt-id'); + + expect(mockGoogleGenAI.models.generateContentStream).toHaveBeenCalledWith( + expect.objectContaining({ + ...request, + config: expect.objectContaining({ + temperature: 1, + topP: 0.95, + thinkingConfig: { + includeThoughts: true, + thinkingLevel: 'HIGH', + }, + }), + }), + ); + expect(stream).toBe(mockStream); + }); + + it('should call countTokens on the underlying model', async () => { + const request = { model: 'gemini-1.5-flash', contents: [] }; + const expectedResponse = { totalTokens: 10 }; + mockGoogleGenAI.models.countTokens.mockResolvedValue(expectedResponse); + + const response = await generator.countTokens(request); + + expect(mockGoogleGenAI.models.countTokens).toHaveBeenCalledWith(request); + expect(response).toBe(expectedResponse); + }); + + it('should call embedContent on the underlying model', async () => { + const request = { model: 'embedding-model', contents: [] }; + const expectedResponse = { embeddings: [] }; + mockGoogleGenAI.models.embedContent.mockResolvedValue(expectedResponse); + + const response = await generator.embedContent(request); + + expect(mockGoogleGenAI.models.embedContent).toHaveBeenCalledWith(request); + expect(response).toBe(expectedResponse); + }); + + it('should prioritize contentGeneratorConfig samplingParams over request config', async () => { + const generatorWithParams = new GeminiContentGenerator({ apiKey: 'test' }, { + model: 'gemini-1.5-flash', + samplingParams: { + temperature: 0.1, + top_p: 0.2, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const request = { + model: 'gemini-1.5-flash', + contents: [], + config: { + temperature: 0.9, + topP: 0.9, + }, + }; + + await generatorWithParams.generateContent(request, 'prompt-id'); + + expect(mockGoogleGenAI.models.generateContent).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + temperature: 0.1, + topP: 0.2, + }), + }), + ); + }); + + it('should map reasoning effort to thinkingConfig', async () => { + const generatorWithReasoning = new GeminiContentGenerator( + { apiKey: 'test' }, + { + model: 'gemini-2.5-pro', + reasoning: { + effort: 'high', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ); + + const request = { + model: 'gemini-2.5-pro', + contents: [], + }; + + await generatorWithReasoning.generateContent(request, 'prompt-id'); + + expect(mockGoogleGenAI.models.generateContent).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + thinkingConfig: { + includeThoughts: true, + thinkingLevel: 'HIGH', + }, + }), + }), + ); + }); +}); diff --git a/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts new file mode 100644 index 00000000..eca580c8 --- /dev/null +++ b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts @@ -0,0 +1,140 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + CountTokensParameters, + CountTokensResponse, + EmbedContentParameters, + EmbedContentResponse, + GenerateContentParameters, + GenerateContentResponse, + GenerateContentConfig, + ThinkingLevel, +} from '@google/genai'; +import { GoogleGenAI } from '@google/genai'; +import type { + ContentGenerator, + ContentGeneratorConfig, +} from '../contentGenerator.js'; + +/** + * A wrapper for GoogleGenAI that implements the ContentGenerator interface. + */ +export class GeminiContentGenerator implements ContentGenerator { + private readonly googleGenAI: GoogleGenAI; + private readonly contentGeneratorConfig?: ContentGeneratorConfig; + + constructor( + options: { + apiKey?: string; + vertexai?: boolean; + httpOptions?: { headers: Record }; + }, + contentGeneratorConfig?: ContentGeneratorConfig, + ) { + this.googleGenAI = new GoogleGenAI(options); + this.contentGeneratorConfig = contentGeneratorConfig; + } + + private buildSamplingParameters( + request: GenerateContentParameters, + ): GenerateContentConfig { + const configSamplingParams = this.contentGeneratorConfig?.samplingParams; + const requestConfig = request.config || {}; + + // Helper function to get parameter value with priority: config > request > default + const getParameterValue = ( + configValue: T | undefined, + requestKey: keyof GenerateContentConfig, + defaultValue?: T, + ): T | undefined => { + const requestValue = requestConfig[requestKey] as T | undefined; + + if (configValue !== undefined) return configValue; + if (requestValue !== undefined) return requestValue; + return defaultValue; + }; + + return { + ...requestConfig, + temperature: getParameterValue( + configSamplingParams?.temperature, + 'temperature', + 1, + ), + topP: getParameterValue( + configSamplingParams?.top_p, + 'topP', + 0.95, + ), + topK: getParameterValue(configSamplingParams?.top_k, 'topK', 64), + maxOutputTokens: getParameterValue( + configSamplingParams?.max_tokens, + 'maxOutputTokens', + ), + presencePenalty: getParameterValue( + configSamplingParams?.presence_penalty, + 'presencePenalty', + ), + frequencyPenalty: getParameterValue( + configSamplingParams?.frequency_penalty, + 'frequencyPenalty', + ), + thinkingConfig: getParameterValue( + this.contentGeneratorConfig?.reasoning + ? { + includeThoughts: true, + thinkingLevel: (this.contentGeneratorConfig.reasoning.effort === + 'low' + ? 'LOW' + : this.contentGeneratorConfig.reasoning.effort === 'high' + ? 'HIGH' + : 'THINKING_LEVEL_UNSPECIFIED') as ThinkingLevel, + } + : undefined, + 'thinkingConfig', + { + includeThoughts: true, + thinkingLevel: 'HIGH' as ThinkingLevel, + }, + ), + }; + } + + async generateContent( + request: GenerateContentParameters, + _userPromptId: string, + ): Promise { + const finalRequest = { + ...request, + config: this.buildSamplingParameters(request), + }; + return this.googleGenAI.models.generateContent(finalRequest); + } + + async generateContentStream( + request: GenerateContentParameters, + _userPromptId: string, + ): Promise> { + const finalRequest = { + ...request, + config: this.buildSamplingParameters(request), + }; + return this.googleGenAI.models.generateContentStream(finalRequest); + } + + async countTokens( + request: CountTokensParameters, + ): Promise { + return this.googleGenAI.models.countTokens(request); + } + + async embedContent( + request: EmbedContentParameters, + ): Promise { + return this.googleGenAI.models.embedContent(request); + } +} diff --git a/packages/core/src/core/geminiContentGenerator/index.test.ts b/packages/core/src/core/geminiContentGenerator/index.test.ts new file mode 100644 index 00000000..ac3f9f62 --- /dev/null +++ b/packages/core/src/core/geminiContentGenerator/index.test.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { createGeminiContentGenerator } from './index.js'; +import { GeminiContentGenerator } from './geminiContentGenerator.js'; +import { LoggingContentGenerator } from './loggingContentGenerator.js'; +import type { Config } from '../../config/config.js'; +import { AuthType } from '../contentGenerator.js'; + +vi.mock('./geminiContentGenerator.js', () => ({ + GeminiContentGenerator: vi.fn().mockImplementation(() => ({})), +})); + +vi.mock('./loggingContentGenerator.js', () => ({ + LoggingContentGenerator: vi.fn().mockImplementation((wrapped) => wrapped), +})); + +describe('createGeminiContentGenerator', () => { + let mockConfig: Config; + + beforeEach(() => { + vi.clearAllMocks(); + mockConfig = { + getUsageStatisticsEnabled: vi.fn().mockReturnValue(false), + getContentGeneratorConfig: vi.fn().mockReturnValue({}), + getCliVersion: vi.fn().mockReturnValue('1.0.0'), + } as unknown as Config; + }); + + it('should create a GeminiContentGenerator wrapped in LoggingContentGenerator', () => { + const config = { + model: 'gemini-1.5-flash', + apiKey: 'test-key', + authType: AuthType.USE_GEMINI, + }; + + const generator = createGeminiContentGenerator(config, mockConfig); + + expect(GeminiContentGenerator).toHaveBeenCalled(); + expect(LoggingContentGenerator).toHaveBeenCalled(); + expect(generator).toBeDefined(); + }); +}); diff --git a/packages/core/src/core/geminiContentGenerator/index.ts b/packages/core/src/core/geminiContentGenerator/index.ts new file mode 100644 index 00000000..60e74cbf --- /dev/null +++ b/packages/core/src/core/geminiContentGenerator/index.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { GeminiContentGenerator } from './geminiContentGenerator.js'; +import type { + ContentGenerator, + ContentGeneratorConfig, +} from '../contentGenerator.js'; +import type { Config } from '../../config/config.js'; +import { InstallationManager } from '../../utils/installationManager.js'; +import { LoggingContentGenerator } from './loggingContentGenerator.js'; + +export { GeminiContentGenerator } from './geminiContentGenerator.js'; +export { LoggingContentGenerator } from './loggingContentGenerator.js'; + +/** + * Create a Gemini content generator. + */ +export function createGeminiContentGenerator( + config: ContentGeneratorConfig, + gcConfig: Config, +): ContentGenerator { + const version = process.env['CLI_VERSION'] || process.version; + const userAgent = + config.userAgent || + `QwenCode/${version} (${process.platform}; ${process.arch})`; + const baseHeaders: Record = { + 'User-Agent': userAgent, + }; + + let headers: Record = { ...baseHeaders }; + if (gcConfig?.getUsageStatisticsEnabled()) { + const installationManager = new InstallationManager(); + const installationId = installationManager.getInstallationId(); + headers = { + ...headers, + 'x-gemini-api-privileged-user-id': `${installationId}`, + }; + } + const httpOptions = { headers }; + + const geminiContentGenerator = new GeminiContentGenerator( + { + apiKey: config.apiKey === '' ? undefined : config.apiKey, + vertexai: config.vertexai, + httpOptions, + }, + config, + ); + + return new LoggingContentGenerator(geminiContentGenerator, gcConfig); +} diff --git a/packages/core/src/core/loggingContentGenerator.ts b/packages/core/src/core/geminiContentGenerator/loggingContentGenerator.ts similarity index 63% rename from packages/core/src/core/loggingContentGenerator.ts rename to packages/core/src/core/geminiContentGenerator/loggingContentGenerator.ts index 151ac4ff..5bffff8c 100644 --- a/packages/core/src/core/loggingContentGenerator.ts +++ b/packages/core/src/core/geminiContentGenerator/loggingContentGenerator.ts @@ -13,21 +13,24 @@ import type { GenerateContentParameters, GenerateContentResponseUsageMetadata, GenerateContentResponse, + ContentListUnion, + ContentUnion, + Part, + PartUnion, } from '@google/genai'; import { ApiRequestEvent, ApiResponseEvent, ApiErrorEvent, -} from '../telemetry/types.js'; -import type { Config } from '../config/config.js'; +} from '../../telemetry/types.js'; +import type { Config } from '../../config/config.js'; import { logApiError, logApiRequest, logApiResponse, -} from '../telemetry/loggers.js'; -import type { ContentGenerator } from './contentGenerator.js'; -import { toContents } from '../code_assist/converter.js'; -import { isStructuredError } from '../utils/quotaErrorDetection.js'; +} from '../../telemetry/loggers.js'; +import type { ContentGenerator } from '../contentGenerator.js'; +import { isStructuredError } from '../../utils/quotaErrorDetection.js'; interface StructuredError { status: number; @@ -112,7 +115,7 @@ export class LoggingContentGenerator implements ContentGenerator { userPromptId: string, ): Promise { const startTime = Date.now(); - this.logApiRequest(toContents(req.contents), req.model, userPromptId); + this.logApiRequest(this.toContents(req.contents), req.model, userPromptId); try { const response = await this.wrapped.generateContent(req, userPromptId); const durationMs = Date.now() - startTime; @@ -137,7 +140,7 @@ export class LoggingContentGenerator implements ContentGenerator { userPromptId: string, ): Promise> { const startTime = Date.now(); - this.logApiRequest(toContents(req.contents), req.model, userPromptId); + this.logApiRequest(this.toContents(req.contents), req.model, userPromptId); let stream: AsyncGenerator; try { @@ -205,4 +208,91 @@ export class LoggingContentGenerator implements ContentGenerator { ): Promise { return this.wrapped.embedContent(req); } + + private toContents(contents: ContentListUnion): Content[] { + if (Array.isArray(contents)) { + // it's a Content[] or a PartsUnion[] + return contents.map((c) => this.toContent(c)); + } + // it's a Content or a PartsUnion + return [this.toContent(contents)]; + } + + private toContent(content: ContentUnion): Content { + if (Array.isArray(content)) { + // it's a PartsUnion[] + return { + role: 'user', + parts: this.toParts(content), + }; + } + if (typeof content === 'string') { + // it's a string + return { + role: 'user', + parts: [{ text: content }], + }; + } + if ('parts' in content) { + // it's a Content - process parts to handle thought filtering + return { + ...content, + parts: content.parts + ? this.toParts(content.parts.filter((p) => p != null)) + : [], + }; + } + // it's a Part + return { + role: 'user', + parts: [this.toPart(content as Part)], + }; + } + + private toParts(parts: PartUnion[]): Part[] { + return parts.map((p) => this.toPart(p)); + } + + private toPart(part: PartUnion): Part { + if (typeof part === 'string') { + // it's a string + return { text: part }; + } + + // Handle thought parts for CountToken API compatibility + // The CountToken API expects parts to have certain required "oneof" fields initialized, + // but thought parts don't conform to this schema and cause API failures + if ('thought' in part && part.thought) { + const thoughtText = `[Thought: ${part.thought}]`; + + const newPart = { ...part }; + delete (newPart as Record)['thought']; + + const hasApiContent = + 'functionCall' in newPart || + 'functionResponse' in newPart || + 'inlineData' in newPart || + 'fileData' in newPart; + + if (hasApiContent) { + // It's a functionCall or other non-text part. Just strip the thought. + return newPart; + } + + // If no other valid API content, this must be a text part. + // Combine existing text (if any) with the thought, preserving other properties. + const text = (newPart as { text?: unknown }).text; + const existingText = text ? String(text) : ''; + const combinedText = existingText + ? `${existingText}\n${thoughtText}` + : thoughtText; + + return { + ...newPart, + text: combinedText, + }; + } + + return part; + } } diff --git a/packages/core/src/core/nonInteractiveToolExecutor.test.ts b/packages/core/src/core/nonInteractiveToolExecutor.test.ts index e3b8d175..5296310f 100644 --- a/packages/core/src/core/nonInteractiveToolExecutor.test.ts +++ b/packages/core/src/core/nonInteractiveToolExecutor.test.ts @@ -47,7 +47,7 @@ describe('executeToolCall', () => { getDebugMode: () => false, getContentGeneratorConfig: () => ({ model: 'test-model', - authType: 'oauth-personal', + authType: 'gemini-api-key', }), getShellExecutionConfig: () => ({ terminalWidth: 90, diff --git a/packages/core/src/core/openaiContentGenerator/openaiContentGenerator.test.ts b/packages/core/src/core/openaiContentGenerator/openaiContentGenerator.test.ts index 3d1a516c..26a0dde0 100644 --- a/packages/core/src/core/openaiContentGenerator/openaiContentGenerator.test.ts +++ b/packages/core/src/core/openaiContentGenerator/openaiContentGenerator.test.ts @@ -99,6 +99,7 @@ describe('OpenAIContentGenerator (Refactored)', () => { }, } as unknown as OpenAI), buildRequest: vi.fn().mockImplementation((req) => req), + getDefaultGenerationConfig: vi.fn().mockReturnValue({}), }; generator = new OpenAIContentGenerator( @@ -211,6 +212,7 @@ describe('OpenAIContentGenerator (Refactored)', () => { }, } as unknown as OpenAI), buildRequest: vi.fn().mockImplementation((req) => req), + getDefaultGenerationConfig: vi.fn().mockReturnValue({}), }; const testGenerator = new TestGenerator( @@ -277,6 +279,7 @@ describe('OpenAIContentGenerator (Refactored)', () => { }, } as unknown as OpenAI), buildRequest: vi.fn().mockImplementation((req) => req), + getDefaultGenerationConfig: vi.fn().mockReturnValue({}), }; const testGenerator = new TestGenerator( diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts index 6ffb1623..bccd489e 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts @@ -60,6 +60,7 @@ describe('ContentGenerationPipeline', () => { buildClient: vi.fn().mockReturnValue(mockClient), buildRequest: vi.fn().mockImplementation((req) => req), buildHeaders: vi.fn().mockReturnValue({}), + getDefaultGenerationConfig: vi.fn().mockReturnValue({}), }; // Mock telemetry service diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.ts b/packages/core/src/core/openaiContentGenerator/pipeline.ts index 9b40d903..8ebc4bfc 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.ts @@ -283,16 +283,22 @@ export class ContentGenerationPipeline { private buildSamplingParameters( request: GenerateContentParameters, ): Record { + const defaultSamplingParams = + this.config.provider.getDefaultGenerationConfig(); const configSamplingParams = this.contentGeneratorConfig.samplingParams; // Helper function to get parameter value with priority: config > request > default const getParameterValue = ( configKey: keyof NonNullable, - requestKey: keyof NonNullable, - defaultValue?: T, + requestKey?: keyof NonNullable, ): T | undefined => { const configValue = configSamplingParams?.[configKey] as T | undefined; - const requestValue = request.config?.[requestKey] as T | undefined; + const requestValue = requestKey + ? (request.config?.[requestKey] as T | undefined) + : undefined; + const defaultValue = requestKey + ? (defaultSamplingParams[requestKey] as T) + : undefined; if (configValue !== undefined) return configValue; if (requestValue !== undefined) return requestValue; @@ -304,12 +310,8 @@ export class ContentGenerationPipeline { key: string, configKey: keyof NonNullable, requestKey?: keyof NonNullable, - defaultValue?: T, - ): Record | Record => { - const value = requestKey - ? getParameterValue(configKey, requestKey, defaultValue) - : ((configSamplingParams?.[configKey] as T | undefined) ?? - defaultValue); + ): Record => { + const value = getParameterValue(configKey, requestKey); return value !== undefined ? { [key]: value } : {}; }; @@ -323,10 +325,18 @@ export class ContentGenerationPipeline { ...addParameterIfDefined('max_tokens', 'max_tokens', 'maxOutputTokens'), // Config-only parameters (no request fallback) - ...addParameterIfDefined('top_k', 'top_k'), + ...addParameterIfDefined('top_k', 'top_k', 'topK'), ...addParameterIfDefined('repetition_penalty', 'repetition_penalty'), - ...addParameterIfDefined('presence_penalty', 'presence_penalty'), - ...addParameterIfDefined('frequency_penalty', 'frequency_penalty'), + ...addParameterIfDefined( + 'presence_penalty', + 'presence_penalty', + 'presencePenalty', + ), + ...addParameterIfDefined( + 'frequency_penalty', + 'frequency_penalty', + 'frequencyPenalty', + ), }; return params; diff --git a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts index 4a5b7748..4c3d7dfd 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts @@ -1,4 +1,5 @@ import OpenAI from 'openai'; +import type { GenerateContentConfig } from '@google/genai'; import type { Config } from '../../../config/config.js'; import type { ContentGeneratorConfig } from '../../contentGenerator.js'; import { AuthType } from '../../contentGenerator.js'; @@ -141,6 +142,14 @@ export class DashScopeOpenAICompatibleProvider }; } + getDefaultGenerationConfig(): GenerateContentConfig { + return { + temperature: 0.7, + topP: 0.8, + topK: 20, + }; + } + /** * Add cache control flag to specified message(s) for DashScope providers */ diff --git a/packages/core/src/core/openaiContentGenerator/provider/deepseek.ts b/packages/core/src/core/openaiContentGenerator/provider/deepseek.ts index 2dd974b7..9b5fd747 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/deepseek.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/deepseek.ts @@ -8,6 +8,7 @@ import type OpenAI from 'openai'; import type { Config } from '../../../config/config.js'; import type { ContentGeneratorConfig } from '../../contentGenerator.js'; import { DefaultOpenAICompatibleProvider } from './default.js'; +import type { GenerateContentConfig } from '@google/genai'; export class DeepSeekOpenAICompatibleProvider extends DefaultOpenAICompatibleProvider { constructor( @@ -76,4 +77,10 @@ export class DeepSeekOpenAICompatibleProvider extends DefaultOpenAICompatiblePro messages, }; } + + override getDefaultGenerationConfig(): GenerateContentConfig { + return { + temperature: 0, + }; + } } diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.ts b/packages/core/src/core/openaiContentGenerator/provider/default.ts index 1e87aff4..549b48ae 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/default.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/default.ts @@ -1,4 +1,5 @@ import OpenAI from 'openai'; +import type { GenerateContentConfig } from '@google/genai'; import type { Config } from '../../../config/config.js'; import type { ContentGeneratorConfig } from '../../contentGenerator.js'; import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js'; @@ -55,4 +56,11 @@ export class DefaultOpenAICompatibleProvider ...request, // Preserve all original parameters including sampling params }; } + + getDefaultGenerationConfig(): GenerateContentConfig { + return { + temperature: 1, + topP: 0.95, + }; + } } diff --git a/packages/core/src/core/openaiContentGenerator/provider/types.ts b/packages/core/src/core/openaiContentGenerator/provider/types.ts index 362ec69a..6998cb5b 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/types.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/types.ts @@ -1,3 +1,4 @@ +import type { GenerateContentConfig } from '@google/genai'; import type OpenAI from 'openai'; // Extended types to support cache_control for DashScope @@ -22,6 +23,7 @@ export interface OpenAICompatibleProvider { request: OpenAI.Chat.ChatCompletionCreateParams, userPromptId: string, ): OpenAI.Chat.ChatCompletionCreateParams; + getDefaultGenerationConfig(): GenerateContentConfig; } export type DashScopeRequestMetadata = { diff --git a/packages/core/src/fallback/handler.test.ts b/packages/core/src/fallback/handler.test.ts index 77c93756..f0021afd 100644 --- a/packages/core/src/fallback/handler.test.ts +++ b/packages/core/src/fallback/handler.test.ts @@ -4,36 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - describe, - it, - expect, - vi, - beforeEach, - type Mock, - type MockInstance, - afterEach, -} from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { handleFallback } from './handler.js'; import type { Config } from '../config/config.js'; import { AuthType } from '../core/contentGenerator.js'; -import { - DEFAULT_GEMINI_FLASH_MODEL, - DEFAULT_GEMINI_MODEL, -} from '../config/models.js'; -import { logFlashFallback } from '../telemetry/index.js'; -import type { FallbackModelHandler } from './types.js'; - -// Mock the telemetry logger and event class -vi.mock('../telemetry/index.js', () => ({ - logFlashFallback: vi.fn(), - FlashFallbackEvent: class {}, -})); - -const MOCK_PRO_MODEL = DEFAULT_GEMINI_MODEL; -const FALLBACK_MODEL = DEFAULT_GEMINI_FLASH_MODEL; -const AUTH_OAUTH = AuthType.LOGIN_WITH_GOOGLE; -const AUTH_API_KEY = AuthType.USE_GEMINI; const createMockConfig = (overrides: Partial = {}): Config => ({ @@ -45,174 +19,28 @@ const createMockConfig = (overrides: Partial = {}): Config => describe('handleFallback', () => { let mockConfig: Config; - let mockHandler: Mock; - let consoleErrorSpy: MockInstance; beforeEach(() => { vi.clearAllMocks(); - mockHandler = vi.fn(); - // Default setup: OAuth user, Pro model failed, handler injected - mockConfig = createMockConfig({ - fallbackModelHandler: mockHandler, - }); - consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + mockConfig = createMockConfig(); }); - afterEach(() => { - consoleErrorSpy.mockRestore(); - }); - - it('should return null immediately if authType is not OAuth', async () => { + it('should return null for unknown auth types', async () => { const result = await handleFallback( mockConfig, - MOCK_PRO_MODEL, - AUTH_API_KEY, + 'test-model', + 'unknown-auth', ); expect(result).toBeNull(); - expect(mockHandler).not.toHaveBeenCalled(); - expect(mockConfig.setFallbackMode).not.toHaveBeenCalled(); }); - it('should return null if the failed model is already the fallback model', async () => { + it('should handle Qwen OAuth error', async () => { const result = await handleFallback( mockConfig, - FALLBACK_MODEL, // Failed model is Flash - AUTH_OAUTH, + 'test-model', + AuthType.QWEN_OAUTH, + new Error('unauthorized'), ); expect(result).toBeNull(); - expect(mockHandler).not.toHaveBeenCalled(); - }); - - it('should return null if no fallbackHandler is injected in config', async () => { - const configWithoutHandler = createMockConfig({ - fallbackModelHandler: undefined, - }); - const result = await handleFallback( - configWithoutHandler, - MOCK_PRO_MODEL, - AUTH_OAUTH, - ); - expect(result).toBeNull(); - }); - - describe('when handler returns "retry"', () => { - it('should activate fallback mode, log telemetry, and return true', async () => { - mockHandler.mockResolvedValue('retry'); - - const result = await handleFallback( - mockConfig, - MOCK_PRO_MODEL, - AUTH_OAUTH, - ); - - expect(result).toBe(true); - expect(mockConfig.setFallbackMode).toHaveBeenCalledWith(true); - expect(logFlashFallback).toHaveBeenCalled(); - }); - }); - - describe('when handler returns "stop"', () => { - it('should activate fallback mode, log telemetry, and return false', async () => { - mockHandler.mockResolvedValue('stop'); - - const result = await handleFallback( - mockConfig, - MOCK_PRO_MODEL, - AUTH_OAUTH, - ); - - expect(result).toBe(false); - expect(mockConfig.setFallbackMode).toHaveBeenCalledWith(true); - expect(logFlashFallback).toHaveBeenCalled(); - }); - }); - - describe('when handler returns "auth"', () => { - it('should NOT activate fallback mode and return false', async () => { - mockHandler.mockResolvedValue('auth'); - - const result = await handleFallback( - mockConfig, - MOCK_PRO_MODEL, - AUTH_OAUTH, - ); - - expect(result).toBe(false); - expect(mockConfig.setFallbackMode).not.toHaveBeenCalled(); - expect(logFlashFallback).not.toHaveBeenCalled(); - }); - }); - - describe('when handler returns an unexpected value', () => { - it('should log an error and return null', async () => { - mockHandler.mockResolvedValue(null); - - const result = await handleFallback( - mockConfig, - MOCK_PRO_MODEL, - AUTH_OAUTH, - ); - - expect(result).toBeNull(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Fallback UI handler failed:', - new Error( - 'Unexpected fallback intent received from fallbackModelHandler: "null"', - ), - ); - expect(mockConfig.setFallbackMode).not.toHaveBeenCalled(); - }); - }); - - it('should pass the correct context (failedModel, fallbackModel, error) to the handler', async () => { - const mockError = new Error('Quota Exceeded'); - mockHandler.mockResolvedValue('retry'); - - await handleFallback(mockConfig, MOCK_PRO_MODEL, AUTH_OAUTH, mockError); - - expect(mockHandler).toHaveBeenCalledWith( - MOCK_PRO_MODEL, - FALLBACK_MODEL, - mockError, - ); - }); - - it('should not call setFallbackMode or log telemetry if already in fallback mode', async () => { - // Setup config where fallback mode is already active - const activeFallbackConfig = createMockConfig({ - fallbackModelHandler: mockHandler, - isInFallbackMode: vi.fn(() => true), // Already active - setFallbackMode: vi.fn(), - }); - - mockHandler.mockResolvedValue('retry'); - - const result = await handleFallback( - activeFallbackConfig, - MOCK_PRO_MODEL, - AUTH_OAUTH, - ); - - // Should still return true to allow the retry (which will use the active fallback mode) - expect(result).toBe(true); - // Should still consult the handler - expect(mockHandler).toHaveBeenCalled(); - // But should not mutate state or log telemetry again - expect(activeFallbackConfig.setFallbackMode).not.toHaveBeenCalled(); - expect(logFlashFallback).not.toHaveBeenCalled(); - }); - - it('should catch errors from the handler, log an error, and return null', async () => { - const handlerError = new Error('UI interaction failed'); - mockHandler.mockRejectedValue(handlerError); - - const result = await handleFallback(mockConfig, MOCK_PRO_MODEL, AUTH_OAUTH); - - expect(result).toBeNull(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Fallback UI handler failed:', - handlerError, - ); - expect(mockConfig.setFallbackMode).not.toHaveBeenCalled(); }); }); diff --git a/packages/core/src/fallback/handler.ts b/packages/core/src/fallback/handler.ts index 0a8f499e..375ce252 100644 --- a/packages/core/src/fallback/handler.ts +++ b/packages/core/src/fallback/handler.ts @@ -6,8 +6,6 @@ import type { Config } from '../config/config.js'; import { AuthType } from '../core/contentGenerator.js'; -import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; -import { logFlashFallback, FlashFallbackEvent } from '../telemetry/index.js'; export async function handleFallback( config: Config, @@ -20,48 +18,7 @@ export async function handleFallback( return handleQwenOAuthError(error); } - // Applicability Checks - if (authType !== AuthType.LOGIN_WITH_GOOGLE) return null; - - const fallbackModel = DEFAULT_GEMINI_FLASH_MODEL; - - if (failedModel === fallbackModel) return null; - - // Consult UI Handler for Intent - const fallbackModelHandler = config.fallbackModelHandler; - if (typeof fallbackModelHandler !== 'function') return null; - - try { - // Pass the specific failed model to the UI handler. - const intent = await fallbackModelHandler( - failedModel, - fallbackModel, - error, - ); - - // Process Intent and Update State - switch (intent) { - case 'retry': - // Activate fallback mode. The NEXT retry attempt will pick this up. - activateFallbackMode(config, authType); - return true; // Signal retryWithBackoff to continue. - - case 'stop': - activateFallbackMode(config, authType); - return false; - - case 'auth': - return false; - - default: - throw new Error( - `Unexpected fallback intent received from fallbackModelHandler: "${intent}"`, - ); - } - } catch (handlerError) { - console.error('Fallback UI handler failed:', handlerError); - return null; - } + return null; } /** @@ -118,12 +75,3 @@ async function handleQwenOAuthError(error?: unknown): Promise { // For other errors, don't handle them specially return null; } - -function activateFallbackMode(config: Config, authType: string | undefined) { - if (!config.isInFallbackMode()) { - config.setFallbackMode(true); - if (authType) { - logFlashFallback(config, new FlashFallbackEvent(authType)); - } - } -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 738aca57..f8e09686 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -12,7 +12,6 @@ export * from './output/json-formatter.js'; // Export Core Logic export * from './core/client.js'; export * from './core/contentGenerator.js'; -export * from './core/loggingContentGenerator.js'; export * from './core/geminiChat.js'; export * from './core/logger.js'; export * from './core/prompts.js'; @@ -24,11 +23,7 @@ export * from './core/nonInteractiveToolExecutor.js'; export * from './fallback/types.js'; -export * from './code_assist/codeAssist.js'; -export * from './code_assist/oauth2.js'; export * from './qwen/qwenOAuth2.js'; -export * from './code_assist/server.js'; -export * from './code_assist/types.js'; // Export utilities export * from './utils/paths.js'; diff --git a/packages/core/src/qwen/qwenOAuth2.ts b/packages/core/src/qwen/qwenOAuth2.ts index 77c5345a..1435c782 100644 --- a/packages/core/src/qwen/qwenOAuth2.ts +++ b/packages/core/src/qwen/qwenOAuth2.ts @@ -907,3 +907,5 @@ export async function clearQwenCredentials(): Promise { function getQwenCachedCredentialPath(): string { return path.join(os.homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME); } + +export const clearCachedCredentialFile = clearQwenCredentials; diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts index 3ece605f..4dd03720 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts @@ -30,7 +30,6 @@ import { ToolCallEvent, } from '../types.js'; import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js'; -import { UserAccountManager } from '../../utils/userAccountManager.js'; import { InstallationManager } from '../../utils/installationManager.js'; import { safeJsonStringify } from '../../utils/safeJsonStringify.js'; @@ -90,10 +89,8 @@ expect.extend({ }, }); -vi.mock('../../utils/userAccountManager.js'); vi.mock('../../utils/installationManager.js'); -const mockUserAccount = vi.mocked(UserAccountManager.prototype); const mockInstallMgr = vi.mocked(InstallationManager.prototype); // TODO(richieforeman): Consider moving this to test setup globally. @@ -128,11 +125,7 @@ describe('ClearcutLogger', () => { vi.unstubAllEnvs(); }); - function setup({ - config = {} as Partial, - lifetimeGoogleAccounts = 1, - cachedGoogleAccount = 'test@google.com', - } = {}) { + function setup({ config = {} as Partial } = {}) { server.resetHandlers( http.post(CLEARCUT_URL, () => HttpResponse.text(EXAMPLE_RESPONSE)), ); @@ -146,10 +139,6 @@ describe('ClearcutLogger', () => { }); ClearcutLogger.clearInstance(); - mockUserAccount.getCachedGoogleAccount.mockReturnValue(cachedGoogleAccount); - mockUserAccount.getLifetimeGoogleAccounts.mockReturnValue( - lifetimeGoogleAccounts, - ); mockInstallMgr.getInstallationId = vi .fn() .mockReturnValue('test-installation-id'); @@ -195,19 +184,6 @@ describe('ClearcutLogger', () => { }); describe('createLogEvent', () => { - it('logs the total number of google accounts', () => { - const { logger } = setup({ - lifetimeGoogleAccounts: 9001, - }); - - const event = logger?.createLogEvent(EventNames.API_ERROR, []); - - expect(event?.event_metadata[0]).toContainEqual({ - gemini_cli_key: EventMetadataKey.GEMINI_CLI_GOOGLE_ACCOUNTS_COUNT, - value: '9001', - }); - }); - it('logs the current surface from a github action', () => { const { logger } = setup({}); @@ -251,7 +227,6 @@ describe('ClearcutLogger', () => { // Define expected values const session_id = 'test-session-id'; const auth_type = AuthType.USE_GEMINI; - const google_accounts = 123; const surface = 'ide-1234'; const cli_version = CLI_VERSION; const git_commit_hash = GIT_COMMIT_INFO; @@ -260,7 +235,6 @@ describe('ClearcutLogger', () => { // Setup logger with expected values const { logger, loggerConfig } = setup({ - lifetimeGoogleAccounts: google_accounts, config: {}, }); vi.spyOn(loggerConfig, 'getContentGeneratorConfig').mockReturnValue({ @@ -283,10 +257,6 @@ describe('ClearcutLogger', () => { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AUTH_TYPE, value: JSON.stringify(auth_type), }, - { - gemini_cli_key: EventMetadataKey.GEMINI_CLI_GOOGLE_ACCOUNTS_COUNT, - value: `${google_accounts}`, - }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE, value: surface, @@ -404,10 +374,14 @@ describe('ClearcutLogger', () => { vi.stubEnv(key, value); } const event = logger?.createLogEvent(EventNames.API_ERROR, []); - expect(event?.event_metadata[0][3]).toEqual({ - gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE, - value: expectedValue, - }); + expect(event?.event_metadata[0]).toEqual( + expect.arrayContaining([ + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE, + value: expectedValue, + }, + ]), + ); }, ); }); diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 7ca1f670..5c75d0c2 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -34,7 +34,6 @@ import type { import { EventMetadataKey } from './event-metadata-key.js'; import type { Config } from '../../config/config.js'; import { InstallationManager } from '../../utils/installationManager.js'; -import { UserAccountManager } from '../../utils/userAccountManager.js'; import { safeJsonStringify } from '../../utils/safeJsonStringify.js'; import { FixedDeque } from 'mnemonist'; import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js'; @@ -157,7 +156,6 @@ export class ClearcutLogger { private sessionData: EventValue[] = []; private promptId: string = ''; private readonly installationManager: InstallationManager; - private readonly userAccountManager: UserAccountManager; /** * Queue of pending events that need to be flushed to the server. New events @@ -186,7 +184,6 @@ export class ClearcutLogger { this.events = new FixedDeque(Array, MAX_EVENTS); this.promptId = config?.getSessionId() ?? ''; this.installationManager = new InstallationManager(); - this.userAccountManager = new UserAccountManager(); } static getInstance(config?: Config): ClearcutLogger | undefined { @@ -233,14 +230,11 @@ export class ClearcutLogger { } createLogEvent(eventName: EventNames, data: EventValue[] = []): LogEvent { - const email = this.userAccountManager.getCachedGoogleAccount(); - if (eventName !== EventNames.START_SESSION) { data.push(...this.sessionData); } - const totalAccounts = this.userAccountManager.getLifetimeGoogleAccounts(); - data = this.addDefaultFields(data, totalAccounts); + data = this.addDefaultFields(data); const logEvent: LogEvent = { console_type: 'GEMINI_CLI', @@ -249,12 +243,7 @@ export class ClearcutLogger { event_metadata: [data], }; - // Should log either email or install ID, not both. See go/cloudmill-1p-oss-instrumentation#define-sessionable-id - if (email) { - logEvent.client_email = email; - } else { - logEvent.client_install_id = this.installationManager.getInstallationId(); - } + logEvent.client_install_id = this.installationManager.getInstallationId(); return logEvent; } @@ -1018,7 +1007,7 @@ export class ClearcutLogger { * Adds default fields to data, and returns a new data array. This fields * should exist on all log events. */ - addDefaultFields(data: EventValue[], totalAccounts: number): EventValue[] { + addDefaultFields(data: EventValue[]): EventValue[] { const surface = determineSurface(); const defaultLogMetadata: EventValue[] = [ @@ -1032,10 +1021,6 @@ export class ClearcutLogger { this.config?.getContentGeneratorConfig()?.authType, ), }, - { - gemini_cli_key: EventMetadataKey.GEMINI_CLI_GOOGLE_ACCOUNTS_COUNT, - value: `${totalAccounts}`, - }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE, value: surface, diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index 1167cc6a..ab226068 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -83,7 +83,6 @@ import type { } from '@google/genai'; import { DiscoveredMCPTool } from '../tools/mcp-tool.js'; import * as uiTelemetry from './uiTelemetry.js'; -import { UserAccountManager } from '../utils/userAccountManager.js'; import { makeFakeConfig } from '../test-utils/config.js'; describe('loggers', () => { @@ -101,10 +100,6 @@ describe('loggers', () => { vi.spyOn(uiTelemetry.uiTelemetryService, 'addEvent').mockImplementation( mockUiEvent.addEvent, ); - vi.spyOn( - UserAccountManager.prototype, - 'getCachedGoogleAccount', - ).mockReturnValue('test-user@example.com'); vi.useFakeTimers(); vi.setSystemTime(new Date('2025-01-01T00:00:00.000Z')); }); @@ -188,7 +183,6 @@ describe('loggers', () => { body: 'CLI configuration loaded.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_CLI_CONFIG, 'event.timestamp': '2025-01-01T00:00:00.000Z', model: 'test-model', @@ -233,7 +227,6 @@ describe('loggers', () => { body: 'User prompt. Length: 11.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_USER_PROMPT, 'event.timestamp': '2025-01-01T00:00:00.000Z', prompt_length: 11, @@ -255,7 +248,7 @@ describe('loggers', () => { const event = new UserPromptEvent( 11, 'prompt-id-9', - AuthType.CLOUD_SHELL, + AuthType.USE_GEMINI, 'test-prompt', ); @@ -265,12 +258,11 @@ describe('loggers', () => { body: 'User prompt. Length: 11.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_USER_PROMPT, 'event.timestamp': '2025-01-01T00:00:00.000Z', prompt_length: 11, prompt_id: 'prompt-id-9', - auth_type: 'cloud-shell', + auth_type: 'gemini-api-key', }, }); }); @@ -313,7 +305,7 @@ describe('loggers', () => { 'test-model', 100, 'prompt-id-1', - AuthType.LOGIN_WITH_GOOGLE, + AuthType.USE_GEMINI, usageData, 'test-response', ); @@ -324,7 +316,6 @@ describe('loggers', () => { body: 'API response from test-model. Status: 200. Duration: 100ms.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_API_RESPONSE, 'event.timestamp': '2025-01-01T00:00:00.000Z', [SemanticAttributes.HTTP_STATUS_CODE]: 200, @@ -340,7 +331,7 @@ describe('loggers', () => { total_token_count: 0, response_text: 'test-response', prompt_id: 'prompt-id-1', - auth_type: 'oauth-personal', + auth_type: 'gemini-api-key', }, }); @@ -386,7 +377,6 @@ describe('loggers', () => { body: 'API request to test-model.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_API_REQUEST, 'event.timestamp': '2025-01-01T00:00:00.000Z', model: 'test-model', @@ -405,7 +395,6 @@ describe('loggers', () => { body: 'API request to test-model.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_API_REQUEST, 'event.timestamp': '2025-01-01T00:00:00.000Z', model: 'test-model', @@ -430,7 +419,6 @@ describe('loggers', () => { body: 'Switching to flash as Fallback.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_FLASH_FALLBACK, 'event.timestamp': '2025-01-01T00:00:00.000Z', auth_type: 'vertex-ai', @@ -465,7 +453,6 @@ describe('loggers', () => { expect(emittedEvent.attributes).toEqual( expect.objectContaining({ 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_RIPGREP_FALLBACK, error: 'ripgrep is not available', }), @@ -484,7 +471,6 @@ describe('loggers', () => { expect(emittedEvent.attributes).toEqual( expect.objectContaining({ 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_RIPGREP_FALLBACK, error: 'rg not found', }), @@ -598,7 +584,6 @@ describe('loggers', () => { body: 'Tool call: test-function. Decision: accept. Success: true. Duration: 100ms.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_TOOL_CALL, 'event.timestamp': '2025-01-01T00:00:00.000Z', function_name: 'test-function', @@ -682,7 +667,6 @@ describe('loggers', () => { body: 'Tool call: test-function. Decision: reject. Success: false. Duration: 100ms.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_TOOL_CALL, 'event.timestamp': '2025-01-01T00:00:00.000Z', function_name: 'test-function', @@ -759,7 +743,6 @@ describe('loggers', () => { body: 'Tool call: test-function. Decision: modify. Success: true. Duration: 100ms.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_TOOL_CALL, 'event.timestamp': '2025-01-01T00:00:00.000Z', function_name: 'test-function', @@ -835,7 +818,6 @@ describe('loggers', () => { body: 'Tool call: test-function. Success: true. Duration: 100ms.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_TOOL_CALL, 'event.timestamp': '2025-01-01T00:00:00.000Z', function_name: 'test-function', @@ -910,7 +892,6 @@ describe('loggers', () => { body: 'Tool call: test-function. Success: false. Duration: 100ms.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_TOOL_CALL, 'event.timestamp': '2025-01-01T00:00:00.000Z', function_name: 'test-function', @@ -999,7 +980,6 @@ describe('loggers', () => { body: 'Tool call: mock_mcp_tool. Success: true. Duration: 100ms.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_TOOL_CALL, 'event.timestamp': '2025-01-01T00:00:00.000Z', function_name: 'mock_mcp_tool', @@ -1047,7 +1027,6 @@ describe('loggers', () => { body: 'Malformed JSON response from test-model.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_MALFORMED_JSON_RESPONSE, 'event.timestamp': '2025-01-01T00:00:00.000Z', model: 'test-model', @@ -1091,7 +1070,6 @@ describe('loggers', () => { body: 'File operation: read. Lines: 10.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_FILE_OPERATION, 'event.timestamp': '2025-01-01T00:00:00.000Z', tool_name: 'test-tool', @@ -1137,7 +1115,6 @@ describe('loggers', () => { body: 'Tool output truncated for test-tool.', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': 'tool_output_truncated', 'event.timestamp': '2025-01-01T00:00:00.000Z', eventName: 'tool_output_truncated', @@ -1184,7 +1161,6 @@ describe('loggers', () => { body: 'Installed extension vscode', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_EXTENSION_INSTALL, 'event.timestamp': '2025-01-01T00:00:00.000Z', extension_name: 'vscode', @@ -1223,7 +1199,6 @@ describe('loggers', () => { body: 'Uninstalled extension vscode', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_EXTENSION_UNINSTALL, 'event.timestamp': '2025-01-01T00:00:00.000Z', extension_name: 'vscode', @@ -1260,7 +1235,6 @@ describe('loggers', () => { body: 'Enabled extension vscode', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_EXTENSION_ENABLE, 'event.timestamp': '2025-01-01T00:00:00.000Z', extension_name: 'vscode', @@ -1297,7 +1271,6 @@ describe('loggers', () => { body: 'Disabled extension vscode', attributes: { 'session.id': 'test-session-id', - 'user.email': 'test-user@example.com', 'event.name': EVENT_EXTENSION_DISABLE, 'event.timestamp': '2025-01-01T00:00:00.000Z', extension_name: 'vscode', diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index b7039a55..127f0dd9 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -9,7 +9,6 @@ import { logs } from '@opentelemetry/api-logs'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import type { Config } from '../config/config.js'; import { safeJsonStringify } from '../utils/safeJsonStringify.js'; -import { UserAccountManager } from '../utils/userAccountManager.js'; import { EVENT_API_ERROR, EVENT_API_CANCEL, @@ -93,11 +92,8 @@ const shouldLogUserPrompts = (config: Config): boolean => config.getTelemetryLogPromptsEnabled(); function getCommonAttributes(config: Config): LogAttributes { - const userAccountManager = new UserAccountManager(); - const email = userAccountManager.getCachedGoogleAccount(); return { 'session.id': config.getSessionId(), - ...(email && { 'user.email': email }), }; } diff --git a/packages/core/src/tools/mcp-client.test.ts b/packages/core/src/tools/mcp-client.test.ts index a8a94484..fd9f9d50 100644 --- a/packages/core/src/tools/mcp-client.test.ts +++ b/packages/core/src/tools/mcp-client.test.ts @@ -217,9 +217,9 @@ describe('mcp-client', () => { false, ); - expect(transport).toEqual( - new StreamableHTTPClientTransport(new URL('http://test-server'), {}), - ); + expect(transport).toBeInstanceOf(StreamableHTTPClientTransport); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((transport as any)._url).toEqual(new URL('http://test-server')); }); it('with headers', async () => { @@ -232,13 +232,13 @@ describe('mcp-client', () => { false, ); - expect(transport).toEqual( - new StreamableHTTPClientTransport(new URL('http://test-server'), { - requestInit: { - headers: { Authorization: 'derp' }, - }, - }), - ); + expect(transport).toBeInstanceOf(StreamableHTTPClientTransport); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((transport as any)._url).toEqual(new URL('http://test-server')); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((transport as any)._requestInit?.headers).toEqual({ + Authorization: 'derp', + }); }); }); @@ -251,9 +251,9 @@ describe('mcp-client', () => { }, false, ); - expect(transport).toEqual( - new SSEClientTransport(new URL('http://test-server'), {}), - ); + expect(transport).toBeInstanceOf(SSEClientTransport); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((transport as any)._url).toEqual(new URL('http://test-server')); }); it('with headers', async () => { @@ -266,13 +266,13 @@ describe('mcp-client', () => { false, ); - expect(transport).toEqual( - new SSEClientTransport(new URL('http://test-server'), { - requestInit: { - headers: { Authorization: 'derp' }, - }, - }), - ); + expect(transport).toBeInstanceOf(SSEClientTransport); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((transport as any)._url).toEqual(new URL('http://test-server')); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((transport as any)._requestInit?.headers).toEqual({ + Authorization: 'derp', + }); }); }); diff --git a/packages/core/src/utils/errorParsing.test.ts b/packages/core/src/utils/errorParsing.test.ts index 9c71f4d8..bda1f86f 100644 --- a/packages/core/src/utils/errorParsing.test.ts +++ b/packages/core/src/utils/errorParsing.test.ts @@ -6,9 +6,6 @@ import { describe, it, expect } from 'vitest'; import { parseAndFormatApiError } from './errorParsing.js'; -import { isProQuotaExceededError } from './quotaErrorDetection.js'; -import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; -import { UserTierId } from '../code_assist/types.js'; import { AuthType } from '../core/contentGenerator.js'; import type { StructuredError } from '../core/turn.js'; @@ -27,32 +24,10 @@ describe('parseAndFormatApiError', () => { it('should format a 429 API error with the default message', () => { const errorMessage = 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - undefined, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); + const result = parseAndFormatApiError(errorMessage, undefined); expect(result).toContain('[API Error: Rate limit exceeded'); expect(result).toContain( - 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', - ); - }); - - it('should format a 429 API error with the personal message', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain('[API Error: Rate limit exceeded'); - expect(result).toContain( - 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', + 'Possible quota limitations in place or slow response times detected. Please wait and try again later.', ); }); @@ -132,230 +107,4 @@ describe('parseAndFormatApiError', () => { const expected = '[API Error: An unknown error occurred.]'; expect(parseAndFormatApiError(error)).toBe(expected); }); - - it('should format a 429 API error with Pro quota exceeded message for Google auth (Free tier)', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", - ); - expect(result).toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - expect(result).toContain('upgrade to get higher limits'); - }); - - it('should format a regular 429 API error with standard message for Google auth', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain('[API Error: Rate limit exceeded'); - expect(result).toContain( - 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', - ); - expect(result).not.toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - }); - - it('should format a 429 API error with generic quota exceeded message for Google auth', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'GenerationRequests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'GenerationRequests'", - ); - expect(result).toContain('You have reached your daily quota limit'); - expect(result).not.toContain( - 'You have reached your daily Gemini 2.5 Pro quota limit', - ); - }); - - it('should prioritize Pro quota message over generic quota message for Google auth', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", - ); - expect(result).toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - expect(result).not.toContain('You have reached your daily quota limit'); - }); - - it('should format a 429 API error with Pro quota exceeded message for Google auth (Standard tier)', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - UserTierId.STANDARD, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", - ); - expect(result).toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - expect(result).toContain( - 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', - ); - expect(result).not.toContain('upgrade to get higher limits'); - }); - - it('should format a 429 API error with Pro quota exceeded message for Google auth (Legacy tier)', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - UserTierId.LEGACY, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", - ); - expect(result).toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - expect(result).toContain( - 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', - ); - expect(result).not.toContain('upgrade to get higher limits'); - }); - - it('should handle different Gemini 2.5 version strings in Pro quota exceeded errors', () => { - const errorMessage25 = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const errorMessagePreview = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5-preview Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - - const result25 = parseAndFormatApiError( - errorMessage25, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - const resultPreview = parseAndFormatApiError( - errorMessagePreview, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-preview-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - - expect(result25).toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - expect(resultPreview).toContain( - 'You have reached your daily gemini-2.5-preview-pro quota limit', - ); - expect(result25).toContain('upgrade to get higher limits'); - expect(resultPreview).toContain('upgrade to get higher limits'); - }); - - it('should not match non-Pro models with similar version strings', () => { - // Test that Flash models with similar version strings don't match - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'Gemini 2.5 Flash Requests' and limit", - ), - ).toBe(false); - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'Gemini 2.5-preview Flash Requests' and limit", - ), - ).toBe(false); - - // Test other model types - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'Gemini 2.5 Ultra Requests' and limit", - ), - ).toBe(false); - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'Gemini 2.5 Standard Requests' and limit", - ), - ).toBe(false); - - // Test generic quota messages - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'GenerationRequests' and limit", - ), - ).toBe(false); - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'EmbeddingRequests' and limit", - ), - ).toBe(false); - }); - - it('should format a generic quota exceeded message for Google auth (Standard tier)', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'GenerationRequests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - UserTierId.STANDARD, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'GenerationRequests'", - ); - expect(result).toContain('You have reached your daily quota limit'); - expect(result).toContain( - 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', - ); - expect(result).not.toContain('upgrade to get higher limits'); - }); - - it('should format a regular 429 API error with standard message for Google auth (Standard tier)', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - UserTierId.STANDARD, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain('[API Error: Rate limit exceeded'); - expect(result).toContain( - 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', - ); - expect(result).not.toContain('upgrade to get higher limits'); - }); }); diff --git a/packages/core/src/utils/errorParsing.ts b/packages/core/src/utils/errorParsing.ts index ecfc2375..ef1c009b 100644 --- a/packages/core/src/utils/errorParsing.ts +++ b/packages/core/src/utils/errorParsing.ts @@ -4,120 +4,36 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - isProQuotaExceededError, - isGenericQuotaExceededError, - isApiError, - isStructuredError, -} from './quotaErrorDetection.js'; -import { - DEFAULT_GEMINI_MODEL, - DEFAULT_GEMINI_FLASH_MODEL, -} from '../config/models.js'; -import { UserTierId } from '../code_assist/types.js'; +import { isApiError, isStructuredError } from './quotaErrorDetection.js'; import { AuthType } from '../core/contentGenerator.js'; // Free Tier message functions -const getRateLimitErrorMessageGoogleFree = ( - fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, -) => - `\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`; - -const getRateLimitErrorMessageGoogleProQuotaFree = ( - currentModel: string = DEFAULT_GEMINI_MODEL, - fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, -) => - `\nYou have reached your daily ${currentModel} quota limit. You will be switched to the ${fallbackModel} model for the rest of this session. To increase your limits, upgrade to get higher limits at https://goo.gle/set-up-gemini-code-assist, or use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; - -const getRateLimitErrorMessageGoogleGenericQuotaFree = () => - `\nYou have reached your daily quota limit. To increase your limits, upgrade to get higher limits at https://goo.gle/set-up-gemini-code-assist, or use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; - -// Legacy/Standard Tier message functions -const getRateLimitErrorMessageGooglePaid = ( - fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, -) => - `\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session. We appreciate you for choosing Gemini Code Assist and the Gemini CLI.`; - -const getRateLimitErrorMessageGoogleProQuotaPaid = ( - currentModel: string = DEFAULT_GEMINI_MODEL, - fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, -) => - `\nYou have reached your daily ${currentModel} quota limit. You will be switched to the ${fallbackModel} model for the rest of this session. We appreciate you for choosing Gemini Code Assist and the Gemini CLI. To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; - -const getRateLimitErrorMessageGoogleGenericQuotaPaid = ( - currentModel: string = DEFAULT_GEMINI_MODEL, -) => - `\nYou have reached your daily quota limit. We appreciate you for choosing Gemini Code Assist and the Gemini CLI. To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; const RATE_LIMIT_ERROR_MESSAGE_USE_GEMINI = '\nPlease wait and try again later. To increase your limits, request a quota increase through AI Studio, or switch to another /auth method'; const RATE_LIMIT_ERROR_MESSAGE_VERTEX = '\nPlease wait and try again later. To increase your limits, request a quota increase through Vertex, or switch to another /auth method'; -const getRateLimitErrorMessageDefault = ( - fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, -) => - `\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`; +const RATE_LIMIT_ERROR_MESSAGE_DEFAULT = + '\nPossible quota limitations in place or slow response times detected. Please wait and try again later.'; -function getRateLimitMessage( - authType?: AuthType, - error?: unknown, - userTier?: UserTierId, - currentModel?: string, - fallbackModel?: string, -): string { +function getRateLimitMessage(authType?: AuthType): string { switch (authType) { - case AuthType.LOGIN_WITH_GOOGLE: { - // Determine if user is on a paid tier (Legacy or Standard) - default to FREE if not specified - const isPaidTier = - userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD; - - if (isProQuotaExceededError(error)) { - return isPaidTier - ? getRateLimitErrorMessageGoogleProQuotaPaid( - currentModel || DEFAULT_GEMINI_MODEL, - fallbackModel, - ) - : getRateLimitErrorMessageGoogleProQuotaFree( - currentModel || DEFAULT_GEMINI_MODEL, - fallbackModel, - ); - } else if (isGenericQuotaExceededError(error)) { - return isPaidTier - ? getRateLimitErrorMessageGoogleGenericQuotaPaid( - currentModel || DEFAULT_GEMINI_MODEL, - ) - : getRateLimitErrorMessageGoogleGenericQuotaFree(); - } else { - return isPaidTier - ? getRateLimitErrorMessageGooglePaid(fallbackModel) - : getRateLimitErrorMessageGoogleFree(fallbackModel); - } - } case AuthType.USE_GEMINI: return RATE_LIMIT_ERROR_MESSAGE_USE_GEMINI; case AuthType.USE_VERTEX_AI: return RATE_LIMIT_ERROR_MESSAGE_VERTEX; default: - return getRateLimitErrorMessageDefault(fallbackModel); + return RATE_LIMIT_ERROR_MESSAGE_DEFAULT; } } export function parseAndFormatApiError( error: unknown, authType?: AuthType, - userTier?: UserTierId, - currentModel?: string, - fallbackModel?: string, ): string { if (isStructuredError(error)) { let text = `[API Error: ${error.message}]`; if (error.status === 429) { - text += getRateLimitMessage( - authType, - error, - userTier, - currentModel, - fallbackModel, - ); + text += getRateLimitMessage(authType); } return text; } @@ -146,13 +62,7 @@ export function parseAndFormatApiError( } let text = `[API Error: ${finalMessage} (Status: ${parsedError.error.status})]`; if (parsedError.error.code === 429) { - text += getRateLimitMessage( - authType, - parsedError, - userTier, - currentModel, - fallbackModel, - ); + text += getRateLimitMessage(authType); } return text; } diff --git a/packages/core/src/utils/flashFallback.test.ts b/packages/core/src/utils/flashFallback.test.ts index 7f21fe01..184cb203 100644 --- a/packages/core/src/utils/flashFallback.test.ts +++ b/packages/core/src/utils/flashFallback.test.ts @@ -11,12 +11,9 @@ import { setSimulate429, disableSimulationAfterFallback, shouldSimulate429, - createSimulated429Error, resetRequestCounter, } from './testUtils.js'; import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; -import { retryWithBackoff } from './retry.js'; -import { AuthType } from '../core/contentGenerator.js'; // Import the new types (Assuming this test file is in packages/core/src/utils/) import type { FallbackModelHandler } from '../fallback/types.js'; @@ -61,84 +58,6 @@ describe('Retry Utility Fallback Integration', () => { expect(result).toBe('retry'); }); - // This test validates the retry utility's logic for triggering the callback. - it('should trigger onPersistent429 after 2 consecutive 429 errors for OAuth users', async () => { - let fallbackCalled = false; - // Removed fallbackModel variable as it's no longer relevant here. - - // Mock function that simulates exactly 2 429 errors, then succeeds after fallback - const mockApiCall = vi - .fn() - .mockRejectedValueOnce(createSimulated429Error()) - .mockRejectedValueOnce(createSimulated429Error()) - .mockResolvedValueOnce('success after fallback'); - - // Mock the onPersistent429 callback (this is what client.ts/geminiChat.ts provides) - const mockPersistent429Callback = vi.fn(async (_authType?: string) => { - fallbackCalled = true; - // Return true to signal retryWithBackoff to reset attempts and continue. - return true; - }); - - // Test with OAuth personal auth type, with maxAttempts = 2 to ensure fallback triggers - const result = await retryWithBackoff(mockApiCall, { - maxAttempts: 2, - initialDelayMs: 1, - maxDelayMs: 10, - shouldRetryOnError: (error: Error) => { - const status = (error as Error & { status?: number }).status; - return status === 429; - }, - onPersistent429: mockPersistent429Callback, - authType: AuthType.LOGIN_WITH_GOOGLE, - }); - - // Verify fallback mechanism was triggered - expect(fallbackCalled).toBe(true); - expect(mockPersistent429Callback).toHaveBeenCalledWith( - AuthType.LOGIN_WITH_GOOGLE, - expect.any(Error), - ); - expect(result).toBe('success after fallback'); - // Should have: 2 failures, then fallback triggered, then 1 success after retry reset - expect(mockApiCall).toHaveBeenCalledTimes(3); - }); - - it('should not trigger onPersistent429 for API key users', async () => { - let fallbackCalled = false; - - // Mock function that simulates 429 errors - const mockApiCall = vi.fn().mockRejectedValue(createSimulated429Error()); - - // Mock the callback - const mockPersistent429Callback = vi.fn(async () => { - fallbackCalled = true; - return true; - }); - - // Test with API key auth type - should not trigger fallback - try { - await retryWithBackoff(mockApiCall, { - maxAttempts: 5, - initialDelayMs: 10, - maxDelayMs: 100, - shouldRetryOnError: (error: Error) => { - const status = (error as Error & { status?: number }).status; - return status === 429; - }, - onPersistent429: mockPersistent429Callback, - authType: AuthType.USE_GEMINI, // API key auth type - }); - } catch (error) { - // Expected to throw after max attempts - expect((error as Error).message).toContain('Rate limit exceeded'); - } - - // Verify fallback was NOT triggered for API key users - expect(fallbackCalled).toBe(false); - expect(mockPersistent429Callback).not.toHaveBeenCalled(); - }); - // This test validates the test utilities themselves. it('should properly disable simulation state after fallback (Test Utility)', () => { // Enable simulation diff --git a/packages/core/src/utils/retry.test.ts b/packages/core/src/utils/retry.test.ts index 27c09459..27090969 100644 --- a/packages/core/src/utils/retry.test.ts +++ b/packages/core/src/utils/retry.test.ts @@ -285,173 +285,6 @@ describe('retryWithBackoff', () => { }); }); - describe('Flash model fallback for OAuth users', () => { - it('should trigger fallback for OAuth personal users after persistent 429 errors', async () => { - const fallbackCallback = vi.fn().mockResolvedValue('gemini-2.5-flash'); - - let fallbackOccurred = false; - const mockFn = vi.fn().mockImplementation(async () => { - if (!fallbackOccurred) { - const error: HttpError = new Error('Rate limit exceeded'); - error.status = 429; - throw error; - } - return 'success'; - }); - - const promise = retryWithBackoff(mockFn, { - maxAttempts: 3, - initialDelayMs: 100, - onPersistent429: async (authType?: string) => { - fallbackOccurred = true; - return await fallbackCallback(authType); - }, - authType: 'oauth-personal', - }); - - // Advance all timers to complete retries - await vi.runAllTimersAsync(); - - // Should succeed after fallback - await expect(promise).resolves.toBe('success'); - - // Verify callback was called with correct auth type - expect(fallbackCallback).toHaveBeenCalledWith('oauth-personal'); - - // Should retry again after fallback - expect(mockFn).toHaveBeenCalledTimes(3); // 2 initial attempts + 1 after fallback - }); - - it('should NOT trigger fallback for API key users', async () => { - const fallbackCallback = vi.fn(); - - const mockFn = vi.fn(async () => { - const error: HttpError = new Error('Rate limit exceeded'); - error.status = 429; - throw error; - }); - - const promise = retryWithBackoff(mockFn, { - maxAttempts: 3, - initialDelayMs: 100, - onPersistent429: fallbackCallback, - authType: 'gemini-api-key', - }); - - // Handle the promise properly to avoid unhandled rejections - const resultPromise = promise.catch((error) => error); - await vi.runAllTimersAsync(); - const result = await resultPromise; - - // Should fail after all retries without fallback - expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('Rate limit exceeded'); - - // Callback should not be called for API key users - expect(fallbackCallback).not.toHaveBeenCalled(); - }); - - it('should reset attempt counter and continue after successful fallback', async () => { - let fallbackCalled = false; - const fallbackCallback = vi.fn().mockImplementation(async () => { - fallbackCalled = true; - return 'gemini-2.5-flash'; - }); - - const mockFn = vi.fn().mockImplementation(async () => { - if (!fallbackCalled) { - const error: HttpError = new Error('Rate limit exceeded'); - error.status = 429; - throw error; - } - return 'success'; - }); - - const promise = retryWithBackoff(mockFn, { - maxAttempts: 3, - initialDelayMs: 100, - onPersistent429: fallbackCallback, - authType: 'oauth-personal', - }); - - await vi.runAllTimersAsync(); - - await expect(promise).resolves.toBe('success'); - expect(fallbackCallback).toHaveBeenCalledOnce(); - }); - - it('should continue with original error if fallback is rejected', async () => { - const fallbackCallback = vi.fn().mockResolvedValue(null); // User rejected fallback - - const mockFn = vi.fn(async () => { - const error: HttpError = new Error('Rate limit exceeded'); - error.status = 429; - throw error; - }); - - const promise = retryWithBackoff(mockFn, { - maxAttempts: 3, - initialDelayMs: 100, - onPersistent429: fallbackCallback, - authType: 'oauth-personal', - }); - - // Handle the promise properly to avoid unhandled rejections - const resultPromise = promise.catch((error) => error); - await vi.runAllTimersAsync(); - const result = await resultPromise; - - // Should fail with original error when fallback is rejected - expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('Rate limit exceeded'); - expect(fallbackCallback).toHaveBeenCalledWith( - 'oauth-personal', - expect.any(Error), - ); - }); - - it('should handle mixed error types (only count consecutive 429s)', async () => { - const fallbackCallback = vi.fn().mockResolvedValue('gemini-2.5-flash'); - let attempts = 0; - let fallbackOccurred = false; - - const mockFn = vi.fn().mockImplementation(async () => { - attempts++; - if (fallbackOccurred) { - return 'success'; - } - if (attempts === 1) { - // First attempt: 500 error (resets consecutive count) - const error: HttpError = new Error('Server error'); - error.status = 500; - throw error; - } else { - // Remaining attempts: 429 errors - const error: HttpError = new Error('Rate limit exceeded'); - error.status = 429; - throw error; - } - }); - - const promise = retryWithBackoff(mockFn, { - maxAttempts: 5, - initialDelayMs: 100, - onPersistent429: async (authType?: string) => { - fallbackOccurred = true; - return await fallbackCallback(authType); - }, - authType: 'oauth-personal', - }); - - await vi.runAllTimersAsync(); - - await expect(promise).resolves.toBe('success'); - - // Should trigger fallback after 2 consecutive 429s (attempts 2-3) - expect(fallbackCallback).toHaveBeenCalledWith('oauth-personal'); - }); - }); - describe('Qwen OAuth 429 error handling', () => { it('should retry for Qwen OAuth 429 errors that are throttling-related', async () => { const errorWith429: HttpError = new Error('Rate limit exceeded'); diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index 215833d9..9e9412af 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -7,8 +7,6 @@ import type { GenerateContentResponse } from '@google/genai'; import { AuthType } from '../core/contentGenerator.js'; import { - isProQuotaExceededError, - isGenericQuotaExceededError, isQwenQuotaExceededError, isQwenThrottlingError, } from './quotaErrorDetection.js'; @@ -90,7 +88,6 @@ export async function retryWithBackoff( maxAttempts, initialDelayMs, maxDelayMs, - onPersistent429, authType, shouldRetryOnError, shouldRetryOnContent, @@ -123,59 +120,6 @@ export async function retryWithBackoff( } catch (error) { const errorStatus = getErrorStatus(error); - // Check for Pro quota exceeded error first - immediate fallback for OAuth users - if ( - errorStatus === 429 && - authType === AuthType.LOGIN_WITH_GOOGLE && - isProQuotaExceededError(error) && - onPersistent429 - ) { - try { - const fallbackModel = await onPersistent429(authType, error); - if (fallbackModel !== false && fallbackModel !== null) { - // Reset attempt counter and try with new model - attempt = 0; - consecutive429Count = 0; - currentDelay = initialDelayMs; - // With the model updated, we continue to the next attempt - continue; - } else { - // Fallback handler returned null/false, meaning don't continue - stop retry process - throw error; - } - } catch (fallbackError) { - // If fallback fails, continue with original error - console.warn('Fallback to Flash model failed:', fallbackError); - } - } - - // Check for generic quota exceeded error (but not Pro, which was handled above) - immediate fallback for OAuth users - if ( - errorStatus === 429 && - authType === AuthType.LOGIN_WITH_GOOGLE && - !isProQuotaExceededError(error) && - isGenericQuotaExceededError(error) && - onPersistent429 - ) { - try { - const fallbackModel = await onPersistent429(authType, error); - if (fallbackModel !== false && fallbackModel !== null) { - // Reset attempt counter and try with new model - attempt = 0; - consecutive429Count = 0; - currentDelay = initialDelayMs; - // With the model updated, we continue to the next attempt - continue; - } else { - // Fallback handler returned null/false, meaning don't continue - stop retry process - throw error; - } - } catch (fallbackError) { - // If fallback fails, continue with original error - console.warn('Fallback to Flash model failed:', fallbackError); - } - } - // Check for Qwen OAuth quota exceeded error - throw immediately without retry if (authType === AuthType.QWEN_OAUTH && isQwenQuotaExceededError(error)) { throw new Error( @@ -197,30 +141,7 @@ export async function retryWithBackoff( consecutive429Count = 0; } - // If we have persistent 429s and a fallback callback for OAuth - if ( - consecutive429Count >= 2 && - onPersistent429 && - authType === AuthType.LOGIN_WITH_GOOGLE - ) { - try { - const fallbackModel = await onPersistent429(authType, error); - if (fallbackModel !== false && fallbackModel !== null) { - // Reset attempt counter and try with new model - attempt = 0; - consecutive429Count = 0; - currentDelay = initialDelayMs; - // With the model updated, we continue to the next attempt - continue; - } else { - // Fallback handler returned null/false, meaning don't continue - stop retry process - throw error; - } - } catch (fallbackError) { - // If fallback fails, continue with original error - console.warn('Fallback to Flash model failed:', fallbackError); - } - } + console.debug('consecutive429Count', consecutive429Count); // Check if we've exhausted retries or shouldn't retry if (attempt >= maxAttempts || !shouldRetryOnError(error as Error)) { @@ -240,7 +161,7 @@ export async function retryWithBackoff( // Reset currentDelay for next potential non-429 error, or if Retry-After is not present next time currentDelay = initialDelayMs; } else { - // Fall back to exponential backoff with jitter + // Fallback to exponential backoff with jitter logRetryAttempt(attempt, error, errorStatus); // Add jitter: +/- 30% of currentDelay const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1); diff --git a/packages/core/src/utils/userAccountManager.test.ts b/packages/core/src/utils/userAccountManager.test.ts deleted file mode 100644 index 6a5ca720..00000000 --- a/packages/core/src/utils/userAccountManager.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { Mock } from 'vitest'; -import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { UserAccountManager } from './userAccountManager.js'; -import * as fs from 'node:fs'; -import * as os from 'node:os'; -import path from 'node:path'; - -vi.mock('os', async (importOriginal) => { - const os = await importOriginal(); - return { - ...os, - homedir: vi.fn(), - }; -}); - -describe('UserAccountManager', () => { - let tempHomeDir: string; - let userAccountManager: UserAccountManager; - let accountsFile: () => string; - - beforeEach(() => { - tempHomeDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'qwen-code-test-home-'), - ); - (os.homedir as Mock).mockReturnValue(tempHomeDir); - accountsFile = () => - path.join(tempHomeDir, '.qwen', 'google_accounts.json'); - userAccountManager = new UserAccountManager(); - }); - - afterEach(() => { - fs.rmSync(tempHomeDir, { recursive: true, force: true }); - vi.clearAllMocks(); - }); - - describe('cacheGoogleAccount', () => { - it('should create directory and write initial account file', async () => { - await userAccountManager.cacheGoogleAccount('test1@google.com'); - - // Verify Google Account ID was cached - expect(fs.existsSync(accountsFile())).toBe(true); - expect(fs.readFileSync(accountsFile(), 'utf-8')).toBe( - JSON.stringify({ active: 'test1@google.com', old: [] }, null, 2), - ); - }); - - it('should update active account and move previous to old', async () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify( - { active: 'test2@google.com', old: ['test1@google.com'] }, - null, - 2, - ), - ); - - await userAccountManager.cacheGoogleAccount('test3@google.com'); - - expect(fs.readFileSync(accountsFile(), 'utf-8')).toBe( - JSON.stringify( - { - active: 'test3@google.com', - old: ['test1@google.com', 'test2@google.com'], - }, - null, - 2, - ), - ); - }); - - it('should not add a duplicate to the old list', async () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify( - { active: 'test1@google.com', old: ['test2@google.com'] }, - null, - 2, - ), - ); - await userAccountManager.cacheGoogleAccount('test2@google.com'); - await userAccountManager.cacheGoogleAccount('test1@google.com'); - - expect(fs.readFileSync(accountsFile(), 'utf-8')).toBe( - JSON.stringify( - { active: 'test1@google.com', old: ['test2@google.com'] }, - null, - 2, - ), - ); - }); - - it('should handle corrupted JSON by starting fresh', async () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync(accountsFile(), 'not valid json'); - const consoleLogSpy = vi - .spyOn(console, 'log') - .mockImplementation(() => {}); - - await userAccountManager.cacheGoogleAccount('test1@google.com'); - - expect(consoleLogSpy).toHaveBeenCalled(); - expect(JSON.parse(fs.readFileSync(accountsFile(), 'utf-8'))).toEqual({ - active: 'test1@google.com', - old: [], - }); - }); - - it('should handle valid JSON with incorrect schema by starting fresh', async () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify({ active: 'test1@google.com', old: 'not-an-array' }), - ); - const consoleLogSpy = vi - .spyOn(console, 'log') - .mockImplementation(() => {}); - - await userAccountManager.cacheGoogleAccount('test2@google.com'); - - expect(consoleLogSpy).toHaveBeenCalled(); - expect(JSON.parse(fs.readFileSync(accountsFile(), 'utf-8'))).toEqual({ - active: 'test2@google.com', - old: [], - }); - }); - }); - - describe('getCachedGoogleAccount', () => { - it('should return the active account if file exists and is valid', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify({ active: 'active@google.com', old: [] }, null, 2), - ); - const account = userAccountManager.getCachedGoogleAccount(); - expect(account).toBe('active@google.com'); - }); - - it('should return null if file does not exist', () => { - const account = userAccountManager.getCachedGoogleAccount(); - expect(account).toBeNull(); - }); - - it('should return null if file is empty', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync(accountsFile(), ''); - const account = userAccountManager.getCachedGoogleAccount(); - expect(account).toBeNull(); - }); - - it('should return null and log if file is corrupted', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync(accountsFile(), '{ "active": "test@google.com"'); // Invalid JSON - const consoleLogSpy = vi - .spyOn(console, 'log') - .mockImplementation(() => {}); - - const account = userAccountManager.getCachedGoogleAccount(); - - expect(account).toBeNull(); - expect(consoleLogSpy).toHaveBeenCalled(); - }); - - it('should return null if active key is missing', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync(accountsFile(), JSON.stringify({ old: [] })); - const account = userAccountManager.getCachedGoogleAccount(); - expect(account).toBeNull(); - }); - }); - - describe('clearCachedGoogleAccount', () => { - it('should set active to null and move it to old', async () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify( - { active: 'active@google.com', old: ['old1@google.com'] }, - null, - 2, - ), - ); - - await userAccountManager.clearCachedGoogleAccount(); - - const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); - expect(stored.active).toBeNull(); - expect(stored.old).toEqual(['old1@google.com', 'active@google.com']); - }); - - it('should handle empty file gracefully', async () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync(accountsFile(), ''); - await userAccountManager.clearCachedGoogleAccount(); - const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); - expect(stored.active).toBeNull(); - expect(stored.old).toEqual([]); - }); - - it('should handle corrupted JSON by creating a fresh file', async () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync(accountsFile(), 'not valid json'); - const consoleLogSpy = vi - .spyOn(console, 'log') - .mockImplementation(() => {}); - - await userAccountManager.clearCachedGoogleAccount(); - - expect(consoleLogSpy).toHaveBeenCalled(); - const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); - expect(stored.active).toBeNull(); - expect(stored.old).toEqual([]); - }); - - it('should be idempotent if active account is already null', async () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify({ active: null, old: ['old1@google.com'] }, null, 2), - ); - - await userAccountManager.clearCachedGoogleAccount(); - - const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); - expect(stored.active).toBeNull(); - expect(stored.old).toEqual(['old1@google.com']); - }); - - it('should not add a duplicate to the old list', async () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify( - { - active: 'active@google.com', - old: ['active@google.com'], - }, - null, - 2, - ), - ); - - await userAccountManager.clearCachedGoogleAccount(); - - const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); - expect(stored.active).toBeNull(); - expect(stored.old).toEqual(['active@google.com']); - }); - }); - - describe('getLifetimeGoogleAccounts', () => { - it('should return 0 if the file does not exist', () => { - expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(0); - }); - - it('should return 0 if the file is empty', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync(accountsFile(), ''); - expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(0); - }); - - it('should return 0 if the file is corrupted', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync(accountsFile(), 'invalid json'); - const consoleDebugSpy = vi - .spyOn(console, 'log') - .mockImplementation(() => {}); - - expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(0); - expect(consoleDebugSpy).toHaveBeenCalled(); - }); - - it('should return 1 if there is only an active account', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify({ active: 'test1@google.com', old: [] }), - ); - expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(1); - }); - - it('should correctly count old accounts when active is null', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify({ - active: null, - old: ['test1@google.com', 'test2@google.com'], - }), - ); - expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(2); - }); - - it('should correctly count both active and old accounts', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify({ - active: 'test3@google.com', - old: ['test1@google.com', 'test2@google.com'], - }), - ); - expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(3); - }); - - it('should handle valid JSON with incorrect schema by returning 0', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify({ active: null, old: 1 }), - ); - const consoleLogSpy = vi - .spyOn(console, 'log') - .mockImplementation(() => {}); - - expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(0); - expect(consoleLogSpy).toHaveBeenCalled(); - }); - - it('should not double count if active account is also in old list', () => { - fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); - fs.writeFileSync( - accountsFile(), - JSON.stringify({ - active: 'test1@google.com', - old: ['test1@google.com', 'test2@google.com'], - }), - ); - expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(2); - }); - }); -}); diff --git a/packages/core/src/utils/userAccountManager.ts b/packages/core/src/utils/userAccountManager.ts deleted file mode 100644 index 28d3cef9..00000000 --- a/packages/core/src/utils/userAccountManager.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import path from 'node:path'; -import { promises as fsp, readFileSync } from 'node:fs'; -import { Storage } from '../config/storage.js'; - -interface UserAccounts { - active: string | null; - old: string[]; -} - -export class UserAccountManager { - private getGoogleAccountsCachePath(): string { - return Storage.getGoogleAccountsPath(); - } - - /** - * Parses and validates the string content of an accounts file. - * @param content The raw string content from the file. - * @returns A valid UserAccounts object. - */ - private parseAndValidateAccounts(content: string): UserAccounts { - const defaultState = { active: null, old: [] }; - if (!content.trim()) { - return defaultState; - } - - const parsed = JSON.parse(content); - - // Inlined validation logic - if (typeof parsed !== 'object' || parsed === null) { - console.log('Invalid accounts file schema, starting fresh.'); - return defaultState; - } - const { active, old } = parsed as Partial; - const isValid = - (active === undefined || active === null || typeof active === 'string') && - (old === undefined || - (Array.isArray(old) && old.every((i) => typeof i === 'string'))); - - if (!isValid) { - console.log('Invalid accounts file schema, starting fresh.'); - return defaultState; - } - - return { - active: parsed.active ?? null, - old: parsed.old ?? [], - }; - } - - private readAccountsSync(filePath: string): UserAccounts { - const defaultState = { active: null, old: [] }; - try { - const content = readFileSync(filePath, 'utf-8'); - return this.parseAndValidateAccounts(content); - } catch (error) { - if ( - error instanceof Error && - 'code' in error && - error.code === 'ENOENT' - ) { - return defaultState; - } - console.log('Error during sync read of accounts, starting fresh.', error); - return defaultState; - } - } - - private async readAccounts(filePath: string): Promise { - const defaultState = { active: null, old: [] }; - try { - const content = await fsp.readFile(filePath, 'utf-8'); - return this.parseAndValidateAccounts(content); - } catch (error) { - if ( - error instanceof Error && - 'code' in error && - error.code === 'ENOENT' - ) { - return defaultState; - } - console.log('Could not parse accounts file, starting fresh.', error); - return defaultState; - } - } - - async cacheGoogleAccount(email: string): Promise { - const filePath = this.getGoogleAccountsCachePath(); - await fsp.mkdir(path.dirname(filePath), { recursive: true }); - - const accounts = await this.readAccounts(filePath); - - if (accounts.active && accounts.active !== email) { - if (!accounts.old.includes(accounts.active)) { - accounts.old.push(accounts.active); - } - } - - // If the new email was in the old list, remove it - accounts.old = accounts.old.filter((oldEmail) => oldEmail !== email); - - accounts.active = email; - await fsp.writeFile(filePath, JSON.stringify(accounts, null, 2), 'utf-8'); - } - - getCachedGoogleAccount(): string | null { - const filePath = this.getGoogleAccountsCachePath(); - const accounts = this.readAccountsSync(filePath); - return accounts.active; - } - - getLifetimeGoogleAccounts(): number { - const filePath = this.getGoogleAccountsCachePath(); - const accounts = this.readAccountsSync(filePath); - const allAccounts = new Set(accounts.old); - if (accounts.active) { - allAccounts.add(accounts.active); - } - return allAccounts.size; - } - - async clearCachedGoogleAccount(): Promise { - const filePath = this.getGoogleAccountsCachePath(); - const accounts = await this.readAccounts(filePath); - - if (accounts.active) { - if (!accounts.old.includes(accounts.active)) { - accounts.old.push(accounts.active); - } - accounts.active = null; - } - - await fsp.writeFile(filePath, JSON.stringify(accounts, null, 2), 'utf-8'); - } -} diff --git a/packages/sdk-typescript/package.json b/packages/sdk-typescript/package.json index b071b8a3..7a3b6036 100644 --- a/packages/sdk-typescript/package.json +++ b/packages/sdk-typescript/package.json @@ -45,7 +45,8 @@ "node": ">=18.0.0" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.4" + "@modelcontextprotocol/sdk": "^1.25.1", + "zod": "^3.25.0" }, "devDependencies": { "@types/node": "^20.14.0", @@ -56,8 +57,7 @@ "esbuild": "^0.25.12", "eslint": "^8.57.0", "typescript": "^5.4.5", - "vitest": "^1.6.0", - "zod": "^3.23.8" + "vitest": "^1.6.0" }, "peerDependencies": { "typescript": ">=5.0.0" diff --git a/packages/sdk-typescript/src/mcp/tool.ts b/packages/sdk-typescript/src/mcp/tool.ts index 53e00399..ab5fa7e4 100644 --- a/packages/sdk-typescript/src/mcp/tool.ts +++ b/packages/sdk-typescript/src/mcp/tool.ts @@ -8,11 +8,9 @@ * Tool definition helper for SDK-embedded MCP servers */ -import type { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import type { z, ZodRawShape, ZodObject, ZodTypeAny } from 'zod'; -type CallToolResult = z.infer; - /** * SDK MCP Tool Definition with Zod schema type inference */ diff --git a/packages/sdk-typescript/tsconfig.json b/packages/sdk-typescript/tsconfig.json index 11fba047..86820243 100644 --- a/packages/sdk-typescript/tsconfig.json +++ b/packages/sdk-typescript/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { /* Language and Environment */ "target": "ES2022", - "lib": ["ES2022"], + "lib": ["ES2022", "DOM"], "module": "ESNext", "moduleResolution": "bundler", diff --git a/packages/vscode-ide-companion/NOTICES.txt b/packages/vscode-ide-companion/NOTICES.txt index afbf750d..9e312534 100644 --- a/packages/vscode-ide-companion/NOTICES.txt +++ b/packages/vscode-ide-companion/NOTICES.txt @@ -21,213 +21,6 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -============================================================ -@modelcontextprotocol/sdk@1.15.1 -(git+https://github.com/modelcontextprotocol/typescript-sdk.git) - -MIT License - -Copyright (c) 2024 Anthropic, PBC - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -============================================================ -ajv@6.12.6 -(https://github.com/ajv-validator/ajv.git) - -The MIT License (MIT) - -Copyright (c) 2015-2017 Evgeny Poberezkin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - - -============================================================ -fast-deep-equal@3.1.3 -(git+https://github.com/epoberezkin/fast-deep-equal.git) - -MIT License - -Copyright (c) 2017 Evgeny Poberezkin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -============================================================ -fast-json-stable-stringify@2.1.0 -(git://github.com/epoberezkin/fast-json-stable-stringify.git) - -This software is released under the MIT license: - -Copyright (c) 2017 Evgeny Poberezkin -Copyright (c) 2013 James Halliday - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -============================================================ -json-schema-traverse@0.4.1 -(git+https://github.com/epoberezkin/json-schema-traverse.git) - -MIT License - -Copyright (c) 2017 Evgeny Poberezkin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -============================================================ -uri-js@4.4.1 -(http://github.com/garycourt/uri-js) - -Copyright 2011 Gary Court. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY GARY COURT "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARY COURT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of Gary Court. - - -============================================================ -punycode@2.3.1 -(https://github.com/mathiasbynens/punycode.js.git) - -Copyright Mathias Bynens - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -============================================================ -content-type@1.0.5 -(No repository found) - -(The MIT License) - -Copyright (c) 2015 Douglas Christopher Wilson - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ============================================================ cors@2.8.5 (No repository found) @@ -311,175 +104,6 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -============================================================ -cross-spawn@7.0.6 -(git@github.com:moxystudio/node-cross-spawn.git) - -The MIT License (MIT) - -Copyright (c) 2018 Made With MOXY Lda - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - -============================================================ -path-key@3.1.1 -(No repository found) - -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -============================================================ -shebang-command@2.0.0 -(No repository found) - -MIT License - -Copyright (c) Kevin Mårtensson (github.com/kevva) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -============================================================ -shebang-regex@3.0.0 -(No repository found) - -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -============================================================ -which@2.0.2 -(git://github.com/isaacs/node-which.git) - -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - -============================================================ -isexe@2.0.0 -(git+https://github.com/isaacs/isexe.git) - -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - -============================================================ -eventsource@3.0.7 -(git://git@github.com/EventSource/eventsource.git) - -The MIT License - -Copyright (c) EventSource GitHub organisation - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -============================================================ -eventsource-parser@3.0.3 -(git+ssh://git@github.com/rexxars/eventsource-parser.git) - -MIT License - -Copyright (c) 2025 Espen Hovlandsdal - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ============================================================ express@4.21.2 (No repository found) @@ -712,6 +336,34 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +============================================================ +content-type@1.0.5 +(No repository found) + +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ============================================================ debug@4.4.1 (git://github.com/debug-js/debug.git) @@ -2238,106 +1890,6 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -============================================================ -express-rate-limit@7.5.1 -(git+https://github.com/express-rate-limit/express-rate-limit.git) - -# MIT License - -Copyright 2023 Nathan Friedly, Vedant K - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -============================================================ -pkce-challenge@5.0.0 -(git+https://github.com/crouchcd/pkce-challenge.git) - -MIT License - -Copyright (c) 2019 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -============================================================ -zod@3.25.76 -(git+https://github.com/colinhacks/zod.git) - -MIT License - -Copyright (c) 2025 Colin McDonnell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -============================================================ -zod-to-json-schema@3.24.6 -(https://github.com/StefanTerdell/zod-to-json-schema) - -ISC License - -Copyright (c) 2020, Stefan Terdell - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ============================================================ markdown-it@14.1.0 (No repository found) @@ -2855,3 +2407,30 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +============================================================ +zod@3.25.76 +(git+https://github.com/colinhacks/zod.git) + +MIT License + +Copyright (c) 2025 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index faffc3f5..ac44d673 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -153,7 +153,7 @@ }, "dependencies": { "semver": "^7.7.2", - "@modelcontextprotocol/sdk": "^1.15.1", + "@modelcontextprotocol/sdk": "^1.25.1", "cors": "^2.8.5", "express": "^5.1.0", "markdown-it": "^14.1.0",