diff --git a/README.md b/README.md index 7c679bd94..01ac1b3d6 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,14 @@ To install the SDK, run the following command: npm install @google/genai ``` +### Optional Dependencies + +The SDK has optional peer dependencies that enhance functionality in specific scenarios: + +- **`undici`** (Node.js only): Required for handling HTTP timeouts longer than 300 seconds. If not installed, long-running requests may fail. Install with `npm install undici`. + +- **`@modelcontextprotocol/sdk`**: Required for Model Context Protocol (MCP) server functionality. If not installed, MCP features will not be available. + ## Quickstart The simplest way to get started is to use an API key from diff --git a/package-lock.json b/package-lock.json index c0fec72c1..c38bd840d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,11 +54,15 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" + "@modelcontextprotocol/sdk": "^1.25.2", + "undici": "^7.16.0" }, "peerDependenciesMeta": { "@modelcontextprotocol/sdk": { "optional": true + }, + "undici": { + "optional": true } } }, @@ -90,6 +94,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -338,7 +343,8 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -1465,8 +1471,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.55.1", @@ -1479,8 +1484,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.55.1", @@ -1493,8 +1497,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.55.1", @@ -1507,8 +1510,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.55.1", @@ -1521,8 +1523,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.55.1", @@ -1535,8 +1536,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.55.1", @@ -1549,8 +1549,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.55.1", @@ -1563,8 +1562,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.55.1", @@ -1577,8 +1575,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.55.1", @@ -1591,8 +1588,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.55.1", @@ -1605,8 +1601,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { "version": "4.55.1", @@ -1619,8 +1614,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.55.1", @@ -1633,8 +1627,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { "version": "4.55.1", @@ -1647,8 +1640,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.55.1", @@ -1661,8 +1653,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.55.1", @@ -1675,8 +1666,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.55.1", @@ -1689,8 +1679,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.55.1", @@ -1703,8 +1692,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.55.1", @@ -1717,8 +1705,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-openbsd-x64": { "version": "4.55.1", @@ -1731,8 +1718,7 @@ "optional": true, "os": [ "openbsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.55.1", @@ -1745,8 +1731,7 @@ "optional": true, "os": [ "openharmony" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.55.1", @@ -1759,8 +1744,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.55.1", @@ -1773,8 +1757,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.55.1", @@ -1787,8 +1770,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.55.1", @@ -1801,8 +1783,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rushstack/node-core-library": { "version": "5.19.1", @@ -1999,6 +1980,7 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, + "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -2020,6 +2002,7 @@ "version": "20.19.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2121,6 +2104,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -2331,6 +2315,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2717,6 +2702,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3794,6 +3780,7 @@ "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3849,6 +3836,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4206,6 +4194,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -6524,6 +6513,7 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, + "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -7888,6 +7878,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7943,6 +7934,7 @@ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", "hasInstallScript": true, + "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -9824,6 +9816,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9888,6 +9881,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", @@ -10117,19 +10111,21 @@ "dev": true }, "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.1.tgz", + "integrity": "sha512-Gpq0iNm5M6cQWlyHQv9MV+uOj1jWk7LpkoE5vSp/7zjb4zMdAcUD+VL5y0nH4p9EbUklq00eVIIX/XcDHzu5xg==", "dev": true, + "license": "MIT", "engines": { "node": ">=20.18.1" } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.1.tgz", + "integrity": "sha512-z2f4eae6/P3L9bogRUfLEZfRRxyrH4ssRq8s2/NOOgXEwwM5w0hsaj+mtDJPN7sBXQQNlagCzYUfjHywUiTETw==", + "dev": true, + "license": "MIT" }, "node_modules/universalify": { "version": "2.0.1", @@ -10598,6 +10594,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 186ba540b..22edfdb98 100644 --- a/package.json +++ b/package.json @@ -147,10 +147,10 @@ "typedoc": "^0.27.0", "typescript": "~5.4.0", "typescript-eslint": "8.24.1", - "undici": "^7.16.0", - "undici-types": "^7.16.0", "zod": "^3.25.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.0", + "undici": "^7.16.0", + "undici-types": "^7.16.0" }, "dependencies": { "google-auth-library": "^10.3.0", @@ -159,11 +159,15 @@ "ws": "^8.18.0" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" + "@modelcontextprotocol/sdk": "^1.25.2", + "undici": "^7.16.0" }, "peerDependenciesMeta": { "@modelcontextprotocol/sdk": { "optional": true + }, + "undici": { + "optional": true } }, "repository": { diff --git a/rollup.config.mjs b/rollup.config.mjs index 508de8b7f..f5dc260bb 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -34,6 +34,7 @@ const externalDeps = [ 'os', 'protobufjs/minimal', 'protobufjs/minimal.js', + 'undici', 'p-retry', ]; diff --git a/src/_api_client.ts b/src/_api_client.ts index 685909f88..1dfab1a0b 100644 --- a/src/_api_client.ts +++ b/src/_api_client.ts @@ -172,6 +172,8 @@ export interface HttpRequest { export class ApiClient implements GeminiNextGenAPIClientAdapter { readonly clientOptions: ApiClientInitOptions; private readonly customBaseUrl?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private cachedNodeAgent?: any; constructor(opts: ApiClientInitOptions) { this.clientOptions = { ...opts, @@ -530,6 +532,29 @@ export class ApiClient implements GeminiNextGenAPIClientAdapter { // https://nodejs.org/api/timers.html#timeoutunref (timeoutHandle as unknown as NodeJSTimeout).unref(); } + + const isNode = typeof process !== 'undefined' && process.versions?.node; + if (isNode && httpOptions.timeout > 300_000) { + // For Node.js, use undici agent when timeout > 300s to handle long timeouts properly. + const timeout = httpOptions.timeout; + // Cache undici agent + if (!this.cachedNodeAgent) { + try { + const {Agent} = await import('undici'); + this.cachedNodeAgent = new Agent({ + headersTimeout: timeout, + bodyTimeout: timeout, + }); + } catch (e) { + console.warn( + 'undici is not available. Long timeouts (>300s) may not work properly in Node.js. Install undici as a peer dependency if needed.', + e, + ); + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (requestInit as any).dispatcher = this.cachedNodeAgent; + } } if (abortSignal) { abortSignal.addEventListener('abort', () => { diff --git a/test/unit/api_client_test.ts b/test/unit/api_client_test.ts index 234f8e838..9b7121bd6 100644 --- a/test/unit/api_client_test.ts +++ b/test/unit/api_client_test.ts @@ -6,6 +6,7 @@ import {Readable} from 'stream'; +import {Agent, type RequestInit as UndiciRequestInit} from 'undici'; import { ApiClient, includeExtraBodyToRequestInit, @@ -1041,6 +1042,29 @@ describe('ApiClient', () => { // @ts-expect-error TS2532: Object is possibly 'undefined'. expect(fetchArgs[0][1].signal.aborted).toBeTrue(); }); + it('should set dispatcher with timeouts in Node.js', async () => { + const client = new ApiClient({ + auth: new FakeAuth('test-api-key'), + apiKey: 'test-api-key', + httpOptions: {timeout: 300_001}, // above 5 minutes to trigger dispatcher + uploader: new CrossUploader(), + downloader: new CrossDownloader(), + }); + const fetchSpy = spyOn(global, 'fetch').and.returnValue( + Promise.resolve( + new Response( + JSON.stringify(mockGenerateContentResponse), + fetchOkOptions, + ), + ), + ); + + await client.request({path: 'test-path', httpMethod: 'POST'}); + const fetchArgs = fetchSpy.calls.first().args; + const requestInit = fetchArgs[1] as UndiciRequestInit; + expect(requestInit.dispatcher).toBeDefined(); + expect(requestInit.dispatcher).toBeInstanceOf(Agent); + }); it('should apply requestHttpOptions when provided', async () => { const client = new ApiClient({ auth: new FakeAuth('test-api-key'),