From 04c280c37d952a27a20f5458e6c97fa42aedf4c0 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Sat, 27 Dec 2025 19:21:04 +0100 Subject: [PATCH 01/11] wip --- src/_api_client.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/_api_client.ts b/src/_api_client.ts index fe4b2cbe1..af0e1c3d6 100644 --- a/src/_api_client.ts +++ b/src/_api_client.ts @@ -12,6 +12,7 @@ import {uploadToFileSearchStoreConfigToMldev} from './converters/_filesearchstor import {ApiError} from './errors.js'; import {GeminiNextGenAPIClientAdapter} from './interactions/client-adapter.js'; import * as types from './types.js'; +import { Agent, type RequestInit as UndiciRequestInit } from 'undici'; const CONTENT_TYPE_HEADER = 'Content-Type'; const SERVER_TIMEOUT_HEADER = 'X-Server-Timeout'; @@ -462,6 +463,12 @@ export class ApiClient implements GeminiNextGenAPIClientAdapter { // https://nodejs.org/api/timers.html#timeoutunref timeoutHandle.unref(); } + if (typeof process !== 'undefined' && process.versions?.node) { + (requestInit as UndiciRequestInit).dispatcher = new Agent({ + headersTimeout: httpOptions.timeout, + bodyTimeout: httpOptions.timeout, + }); + } } if (abortSignal) { abortSignal.addEventListener('abort', () => { From be6a20520650cf5d577a14bba3a9addbb67f1e62 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Sat, 27 Dec 2025 19:34:29 +0100 Subject: [PATCH 02/11] test: dispatcher timeout set in Node.js --- test/unit/api_client_test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/unit/api_client_test.ts b/test/unit/api_client_test.ts index 8803c4dcf..0698c77f9 100644 --- a/test/unit/api_client_test.ts +++ b/test/unit/api_client_test.ts @@ -14,6 +14,7 @@ import {CrossDownloader} from '../../src/cross/_cross_downloader.js'; import {CrossUploader} from '../../src/cross/_cross_uploader.js'; import * as types from '../../src/types.js'; import {FakeAuth} from '../_fake_auth.js'; +import {Agent, type RequestInit as UndiciRequestInit } from 'undici'; const fetchOkOptions = { status: 200, @@ -809,6 +810,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: 1000}, + 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'), From 80d89ee5507f7ad2d88e0839bb6e586ddce8fd5c Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Sat, 27 Dec 2025 21:03:40 +0100 Subject: [PATCH 03/11] chore: make undici optional peer deps --- package.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e107fe1ab..d3993b6aa 100644 --- a/package.json +++ b/package.json @@ -109,8 +109,6 @@ "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" }, @@ -119,11 +117,19 @@ "ws": "^8.18.0" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.24.0" + "@modelcontextprotocol/sdk": "^1.24.0", + "undici": "^7.16.0", + "undici-types": "^7.16.0" }, "peerDependenciesMeta": { "@modelcontextprotocol/sdk": { "optional": true + }, + "undici": { + "optional": true + }, + "undici-types": { + "optional": true } }, "repository": { From b17210701db30eb3f27fe120f406849e51c653c7 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Sat, 27 Dec 2025 21:13:57 +0100 Subject: [PATCH 04/11] refactor: dynamic import undici --- src/_api_client.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/_api_client.ts b/src/_api_client.ts index af0e1c3d6..458d8d5d9 100644 --- a/src/_api_client.ts +++ b/src/_api_client.ts @@ -12,7 +12,7 @@ import {uploadToFileSearchStoreConfigToMldev} from './converters/_filesearchstor import {ApiError} from './errors.js'; import {GeminiNextGenAPIClientAdapter} from './interactions/client-adapter.js'; import * as types from './types.js'; -import { Agent, type RequestInit as UndiciRequestInit } from 'undici'; +import type { RequestInit as UndiciRequestInit } from 'undici'; const CONTENT_TYPE_HEADER = 'Content-Type'; const SERVER_TIMEOUT_HEADER = 'X-Server-Timeout'; @@ -464,10 +464,15 @@ export class ApiClient implements GeminiNextGenAPIClientAdapter { timeoutHandle.unref(); } if (typeof process !== 'undefined' && process.versions?.node) { - (requestInit as UndiciRequestInit).dispatcher = new Agent({ - headersTimeout: httpOptions.timeout, - bodyTimeout: httpOptions.timeout, - }); + try { + const { Agent } = await import('undici'); + (requestInit as UndiciRequestInit).dispatcher = new Agent({ + headersTimeout: httpOptions.timeout, + bodyTimeout: httpOptions.timeout, + }); + } catch { + // Ignore errors, undici might not be available. + } } } if (abortSignal) { From 70c324aac068def17bd4def3895dd48b2a12e1c7 Mon Sep 17 00:00:00 2001 From: Vachmara Date: Tue, 13 Jan 2026 21:23:42 +0100 Subject: [PATCH 05/11] Make undici a direct dependency to simplify usage --- package.json | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 13163ce40..192d9be7e 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "api-extractor:prod:node": "api-extractor run -c api-extractor.node.json --verbose", "api-extractor:prod:web": "api-extractor run -c api-extractor.web.json --verbose", "api-extractor:prod:tokenizer-node": "api-extractor run -c api-extractor.tokenizer-node.json --verbose", - "unit-test": "tsc && cp src/cross/sentencepiece/sentencepiece_model.pb.js dist/src/cross/sentencepiece/ && jasmine dist/test/unit/**/*_test.js dist/test/unit/**/**/*_test.js dist/test/unit/*_test.js", + "unit-test": "tsc && cp src/cross/sentencepiece/sentencepiece_model.pb.js dist/src/cross/sentencepiece/ && jasmine dist/test/unit/**/*_test.js dist/test/unit/**/**/*_test.js dist/test/unit/*_t[...]", "system-test": "tsc && jasmine dist/test/system/**/*_test.js", "test-server-tests": "tsc && GOOGLE_CLOUD_PROJECT=googcloudproj GOOGLE_CLOUD_LOCATION=googcloudloc jasmine dist/test/system/node/*_test.js -- --test-server", "test-server-tests:record": "tsc && jasmine --fail-fast dist/test/system/node/*_test.js -- --test-server --record", @@ -77,7 +77,7 @@ "lint": "eslint '**/*.ts'", "lint-fix": "eslint --fix '**/*.ts'", "coverage-report": "./test/generate_report.sh", - "generate-proto": "pbjs -t static-module -w es6 -o src/cross/sentencepiece/sentencepiece_model.pb.js src/cross/sentencepiece/sentencepiece_model.proto && pbts -o src/cross/sentencepiece/sentencepiece_model.pb.d.ts src/cross/sentencepiece/sentencepiece_model.pb.js && sed -i.bak 's/import \\* as \\$protobuf from \"protobufjs\\/minimal\"/import \\$protobuf from \"protobufjs\\/minimal.js\"/' src/cross/sentencepiece/sentencepiece_model.pb.js && rm src/cross/sentencepiece/sentencepiece_model.pb.js.bak" + "generate-proto": "pbjs -t static-module -w es6 -o src/cross/sentencepiece/sentencepiece_model.pb.js src/cross/sentencepiece/sentencepiece_model.proto && pbts -o src/cross/sentencepiece/senten[...]" }, "engines": { "node": ">=20.0.0" @@ -141,22 +141,15 @@ "dependencies": { "google-auth-library": "^10.3.0", "protobufjs": "^7.5.4", - "ws": "^8.18.0" + "ws": "^8.18.0", + "undici": "^7.16.0" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.24.0", - "undici": "^7.16.0", - "undici-types": "^7.16.0" + "@modelcontextprotocol/sdk": "^1.24.0" }, "peerDependenciesMeta": { "@modelcontextprotocol/sdk": { "optional": true - }, - "undici": { - "optional": true - }, - "undici-types": { - "optional": true } }, "repository": { @@ -169,4 +162,4 @@ "homepage": "https://github.com/googleapis/js-genai#readme", "author": "", "license": "Apache-2.0" -} +} \ No newline at end of file From db30911a0f25bf22a4ddfc58f116ec32d7de66a3 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 23 Jan 2026 11:50:02 +0100 Subject: [PATCH 06/11] refacor: only call undici agent when timeout > 300s --- src/_api_client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_api_client.ts b/src/_api_client.ts index 42d28709d..ac7580d75 100644 --- a/src/_api_client.ts +++ b/src/_api_client.ts @@ -492,7 +492,8 @@ export class ApiClient implements GeminiNextGenAPIClientAdapter { // https://nodejs.org/api/timers.html#timeoutunref timeoutHandle.unref(); } - if (typeof process !== 'undefined' && process.versions?.node) { + if (typeof process !== 'undefined' && process.versions?.node && httpOptions.timeout > 300_000) { + // For Node.js, use undici agent when timeout > 300s to handle long timeouts properly. try { const { Agent } = await import('undici'); (requestInit as UndiciRequestInit).dispatcher = new Agent({ From f8b4c207b77077583f61b5b87617693174809c23 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 23 Jan 2026 11:59:54 +0100 Subject: [PATCH 07/11] chore: move to peerDep + update doc --- README.md | 8 ++++++++ package.json | 9 ++++++--- src/_api_client.ts | 4 ++-- 3 files changed, 16 insertions(+), 5 deletions(-) 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.json b/package.json index 138b6085c..d3018fdbf 100644 --- a/package.json +++ b/package.json @@ -141,15 +141,18 @@ "dependencies": { "google-auth-library": "^10.3.0", "protobufjs": "^7.5.4", - "ws": "^8.18.0", - "undici": "^7.16.0" + "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/src/_api_client.ts b/src/_api_client.ts index ac7580d75..cc8dcca74 100644 --- a/src/_api_client.ts +++ b/src/_api_client.ts @@ -500,8 +500,8 @@ export class ApiClient implements GeminiNextGenAPIClientAdapter { headersTimeout: httpOptions.timeout, bodyTimeout: httpOptions.timeout, }); - } catch { - // Ignore errors, undici might not be available. + } 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); } } } From 95c0da4db236cf26f0b581e6f6fdd7f2195b02d0 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Tue, 27 Jan 2026 10:28:55 +0100 Subject: [PATCH 08/11] refactor: cache node agent --- package-lock.json | 115 ++++++++++++++++++++++----------------------- package.json | 4 +- src/_api_client.ts | 28 +++++++---- 3 files changed, 77 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11f887d2d..5eabeb20e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,11 +53,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 } } }, @@ -89,6 +93,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", @@ -337,7 +342,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", @@ -1464,8 +1470,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.55.1", @@ -1478,8 +1483,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.55.1", @@ -1492,8 +1496,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.55.1", @@ -1506,8 +1509,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.55.1", @@ -1520,8 +1522,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.55.1", @@ -1534,8 +1535,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.55.1", @@ -1548,8 +1548,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.55.1", @@ -1562,8 +1561,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.55.1", @@ -1576,8 +1574,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.55.1", @@ -1590,8 +1587,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.55.1", @@ -1604,8 +1600,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { "version": "4.55.1", @@ -1618,8 +1613,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.55.1", @@ -1632,8 +1626,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { "version": "4.55.1", @@ -1646,8 +1639,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.55.1", @@ -1660,8 +1652,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.55.1", @@ -1674,8 +1665,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.55.1", @@ -1688,8 +1678,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.55.1", @@ -1702,8 +1691,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.55.1", @@ -1716,8 +1704,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-openbsd-x64": { "version": "4.55.1", @@ -1730,8 +1717,7 @@ "optional": true, "os": [ "openbsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.55.1", @@ -1744,8 +1730,7 @@ "optional": true, "os": [ "openharmony" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.55.1", @@ -1758,8 +1743,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.55.1", @@ -1772,8 +1756,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.55.1", @@ -1786,8 +1769,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.55.1", @@ -1800,8 +1782,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rushstack/node-core-library": { "version": "5.19.1", @@ -1998,6 +1979,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" @@ -2019,6 +2001,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" } @@ -2114,6 +2097,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", @@ -2324,6 +2308,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" }, @@ -2710,6 +2695,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3787,6 +3773,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", @@ -3842,6 +3829,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" }, @@ -4199,6 +4187,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", @@ -6517,6 +6506,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", @@ -7868,6 +7858,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" }, @@ -7923,6 +7914,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", @@ -9795,6 +9787,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" @@ -9859,6 +9852,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", @@ -10088,19 +10082,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", @@ -10569,6 +10565,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 d3018fdbf..8094a18ea 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,9 @@ "typescript": "~5.4.0", "typescript-eslint": "8.24.1", "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", diff --git a/src/_api_client.ts b/src/_api_client.ts index cc8dcca74..cdc920d6f 100644 --- a/src/_api_client.ts +++ b/src/_api_client.ts @@ -12,7 +12,7 @@ import {uploadToFileSearchStoreConfigToMldev} from './converters/_filesearchstor import {ApiError} from './errors.js'; import {GeminiNextGenAPIClientAdapter} from './interactions/client-adapter.js'; import * as types from './types.js'; -import type { RequestInit as UndiciRequestInit } from 'undici'; +import type { RequestInit as UndiciRequestInit, Agent } from 'undici'; const CONTENT_TYPE_HEADER = 'Content-Type'; const SERVER_TIMEOUT_HEADER = 'X-Server-Timeout'; @@ -137,6 +137,7 @@ export interface HttpRequest { export class ApiClient implements GeminiNextGenAPIClientAdapter { readonly clientOptions: ApiClientInitOptions; private readonly customBaseUrl?: string; + private cachedNodeAgent?: Agent; constructor(opts: ApiClientInitOptions) { this.clientOptions = { ...opts, @@ -492,17 +493,24 @@ export class ApiClient implements GeminiNextGenAPIClientAdapter { // https://nodejs.org/api/timers.html#timeoutunref timeoutHandle.unref(); } - if (typeof process !== 'undefined' && process.versions?.node && httpOptions.timeout > 300_000) { + + 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. - try { - const { Agent } = await import('undici'); - (requestInit as UndiciRequestInit).dispatcher = new Agent({ - headersTimeout: httpOptions.timeout, - bodyTimeout: httpOptions.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); + 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); + } } + (requestInit as UndiciRequestInit).dispatcher = this.cachedNodeAgent; } } if (abortSignal) { From c45657f06e9f5a241fc143e98766afc7686e0c01 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Wed, 28 Jan 2026 00:09:03 +0100 Subject: [PATCH 09/11] chore: format + types --- package.json | 2 +- rollup.config.mjs | 1 + src/_api_client.ts | 16 ++++++++++------ test/unit/api_client_test.ts | 4 ++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 8094a18ea..415d36390 100644 --- a/package.json +++ b/package.json @@ -167,4 +167,4 @@ "homepage": "https://github.com/googleapis/js-genai#readme", "author": "", "license": "Apache-2.0" -} \ No newline at end of file +} diff --git a/rollup.config.mjs b/rollup.config.mjs index e3113ae00..64b6db9ce 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -34,6 +34,7 @@ const externalDeps = [ 'os', 'protobufjs/minimal', 'protobufjs/minimal.js', + 'undici', ]; export default [ diff --git a/src/_api_client.ts b/src/_api_client.ts index cdc920d6f..472a22e22 100644 --- a/src/_api_client.ts +++ b/src/_api_client.ts @@ -12,7 +12,6 @@ import {uploadToFileSearchStoreConfigToMldev} from './converters/_filesearchstor import {ApiError} from './errors.js'; import {GeminiNextGenAPIClientAdapter} from './interactions/client-adapter.js'; import * as types from './types.js'; -import type { RequestInit as UndiciRequestInit, Agent } from 'undici'; const CONTENT_TYPE_HEADER = 'Content-Type'; const SERVER_TIMEOUT_HEADER = 'X-Server-Timeout'; @@ -137,7 +136,8 @@ export interface HttpRequest { export class ApiClient implements GeminiNextGenAPIClientAdapter { readonly clientOptions: ApiClientInitOptions; private readonly customBaseUrl?: string; - private cachedNodeAgent?: Agent; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private cachedNodeAgent?: any; constructor(opts: ApiClientInitOptions) { this.clientOptions = { ...opts, @@ -493,7 +493,7 @@ export class ApiClient implements GeminiNextGenAPIClientAdapter { // https://nodejs.org/api/timers.html#timeoutunref timeoutHandle.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. @@ -501,16 +501,20 @@ export class ApiClient implements GeminiNextGenAPIClientAdapter { // Cache undici agent if (!this.cachedNodeAgent) { try { - const { Agent } = await import('undici'); + 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); + 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, + ); } } - (requestInit as UndiciRequestInit).dispatcher = this.cachedNodeAgent; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (requestInit as any).dispatcher = this.cachedNodeAgent; } } if (abortSignal) { diff --git a/test/unit/api_client_test.ts b/test/unit/api_client_test.ts index 207e6f079..6906f032d 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, @@ -14,7 +15,6 @@ import {CrossDownloader} from '../../src/cross/_cross_downloader.js'; import {CrossUploader} from '../../src/cross/_cross_uploader.js'; import * as types from '../../src/types.js'; import {FakeAuth} from '../_fake_auth.js'; -import {Agent, type RequestInit as UndiciRequestInit } from 'undici'; const fetchOkOptions = { status: 200, @@ -871,7 +871,7 @@ describe('ApiClient', () => { ), ), ); - + await client.request({path: 'test-path', httpMethod: 'POST'}); const fetchArgs = fetchSpy.calls.first().args; const requestInit = fetchArgs[1] as UndiciRequestInit; From dcb96a5455e9d6e5d839b48ee5156a57caff2ed8 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Wed, 28 Jan 2026 12:47:11 +0100 Subject: [PATCH 10/11] fix: package json scripts --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 415d36390..e308d0a68 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "api-extractor:prod:node": "api-extractor run -c api-extractor.node.json --verbose", "api-extractor:prod:web": "api-extractor run -c api-extractor.web.json --verbose", "api-extractor:prod:tokenizer-node": "api-extractor run -c api-extractor.tokenizer-node.json --verbose", - "unit-test": "tsc && cp src/cross/sentencepiece/sentencepiece_model.pb.js dist/src/cross/sentencepiece/ && jasmine dist/test/unit/**/*_test.js dist/test/unit/**/**/*_test.js dist/test/unit/*_t[...]", + "unit-test": "tsc && cp src/cross/sentencepiece/sentencepiece_model.pb.js dist/src/cross/sentencepiece/ && jasmine dist/test/unit/**/*_test.js dist/test/unit/**/**/*_test.js dist/test/unit/*_test.js", "system-test": "tsc && jasmine dist/test/system/**/*_test.js", "test-server-tests": "tsc && GOOGLE_CLOUD_PROJECT=googcloudproj GOOGLE_CLOUD_LOCATION=googcloudloc jasmine dist/test/system/node/*_test.js -- --test-server", "test-server-tests:record": "tsc && jasmine --fail-fast dist/test/system/node/*_test.js -- --test-server --record", @@ -77,7 +77,7 @@ "lint": "eslint '**/*.ts'", "lint-fix": "eslint --fix '**/*.ts'", "coverage-report": "./test/generate_report.sh", - "generate-proto": "pbjs -t static-module -w es6 -o src/cross/sentencepiece/sentencepiece_model.pb.js src/cross/sentencepiece/sentencepiece_model.proto && pbts -o src/cross/sentencepiece/senten[...]" + "generate-proto": "pbjs -t static-module -w es6 -o src/cross/sentencepiece/sentencepiece_model.pb.js src/cross/sentencepiece/sentencepiece_model.proto && pbts -o src/cross/sentencepiece/sentencepiece_model.pb.d.ts src/cross/sentencepiece/sentencepiece_model.pb.js && sed -i.bak 's/import \\* as \\$protobuf from \"protobufjs\\/minimal\"/import \\$protobuf from \"protobufjs\\/minimal.js\"/' src/cross/sentencepiece/sentencepiece_model.pb.js && rm src/cross/sentencepiece/sentencepiece_model.pb.js.bak" }, "engines": { "node": ">=20.0.0" From 32927b68cfe7e0f1c462273dbf0f76d39437412b Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Wed, 28 Jan 2026 13:05:10 +0100 Subject: [PATCH 11/11] test: update the test to the latest version --- test/unit/api_client_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/api_client_test.ts b/test/unit/api_client_test.ts index 6906f032d..7ffaf97f2 100644 --- a/test/unit/api_client_test.ts +++ b/test/unit/api_client_test.ts @@ -859,7 +859,7 @@ describe('ApiClient', () => { const client = new ApiClient({ auth: new FakeAuth('test-api-key'), apiKey: 'test-api-key', - httpOptions: {timeout: 1000}, + httpOptions: {timeout: 300_001}, // above 5 minutes to trigger dispatcher uploader: new CrossUploader(), downloader: new CrossDownloader(), });