From 83c11e3f42e23db85d3bc925a7a4ecc404230c1f Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Wed, 25 Mar 2026 09:32:22 -0400 Subject: [PATCH 1/3] fix: eliminate process.env race condition in GoogleLlm and AiSdkLlm GoogleLlm, AiSdkLlm, and GeminiContextCacheManager lazily read process.env to build Google clients. In a multi-tenant server, concurrent requests needing different backends (Vertex AI vs API key) race on the shared mutable process.env. Add GoogleLlmConfig to allow explicit, request-scoped configuration (apiKey, vertexai/project/location, or a pre-built client) that bypasses env vars entirely. Env fallback is preserved when no config is provided, so the change is fully backward compatible. Co-Authored-By: Claude Opus 4.6 --- packages/adk/src/models/ai-sdk.ts | 39 +++-- packages/adk/src/models/google-llm.ts | 156 +++++++++++------- packages/adk/src/tests/models/ai-sdk.test.ts | 69 ++++++++ .../adk/src/tests/models/google-llm.test.ts | 143 ++++++++++++++-- 4 files changed, 324 insertions(+), 83 deletions(-) diff --git a/packages/adk/src/models/ai-sdk.ts b/packages/adk/src/models/ai-sdk.ts index 6f8a86d6d..a38f8c391 100644 --- a/packages/adk/src/models/ai-sdk.ts +++ b/packages/adk/src/models/ai-sdk.ts @@ -1,5 +1,5 @@ import { Logger } from "@adk/logger"; -import type { Content, Part } from "@google/genai"; +import { type Content, GoogleGenAI, type Part } from "@google/genai"; import { AssistantContent, generateText, @@ -52,14 +52,23 @@ interface AiSdkRequestParams { }; } +/** + * Options for AiSdkLlm. + */ +export interface AiSdkLlmOptions { + /** Pre-built Google GenAI client for context caching (avoids env race conditions) */ + googleGenaiClient?: GoogleGenAI; +} + /** * AI SDK integration that accepts a pre-configured LanguageModel. * Enables ADK-TS to work with any provider supported by Vercel's AI SDK. */ export class AiSdkLlm extends BaseLlm { - private modelInstance: LanguageModel; + #modelInstance: LanguageModel; protected logger = new Logger({ name: "AiSdkLlm" }); - private cacheManager: ContextCacheManager | null = null; + #cacheManager: ContextCacheManager | null = null; + #options?: AiSdkLlmOptions; /** * Model provider patterns for detection @@ -72,15 +81,17 @@ export class AiSdkLlm extends BaseLlm { /** * Constructor accepts a pre-configured LanguageModel instance - * @param model - Pre-configured LanguageModel from provider(modelName) + * @param modelInstance - Pre-configured LanguageModel from provider(modelName) + * @param options - Optional configuration (e.g. googleGenaiClient for caching) */ - constructor(modelInstance: LanguageModel) { + constructor(modelInstance: LanguageModel, options?: AiSdkLlmOptions) { let modelId = "ai-sdk-model"; if (typeof modelInstance !== "string") { modelId = modelInstance.modelId; } super(modelId); - this.modelInstance = modelInstance; + this.#modelInstance = modelInstance; + this.#options = options; } /** @@ -132,11 +143,13 @@ export class AiSdkLlm extends BaseLlm { /** * Initializes the cache manager for Google models - * The manager lazily initializes its Google GenAI client on first use */ private initializeCacheManager(): void { - if (!this.cacheManager) { - this.cacheManager = new GeminiContextCacheManager(this.logger); + if (!this.#cacheManager) { + this.#cacheManager = new GeminiContextCacheManager( + this.logger, + this.#options?.googleGenaiClient, + ); } } @@ -152,14 +165,14 @@ export class AiSdkLlm extends BaseLlm { this.initializeCacheManager(); // Normalize model ID for Google API compatibility - const modelId = this.getModelId(this.modelInstance); + const modelId = this.getModelId(this.#modelInstance); llmRequest.model = this.normalizeGoogleModelId(modelId); this.logger.debug(`Using model for caching: ${llmRequest.model}`); // Handle caching through the manager const cacheMetadata = - await this.cacheManager!.handleContextCaching(llmRequest); + await this.#cacheManager!.handleContextCaching(llmRequest); if (cacheMetadata?.cacheName) { this.logger.debug(`Using cache: ${cacheMetadata.cacheName}`); @@ -182,7 +195,7 @@ export class AiSdkLlm extends BaseLlm { cacheMetadata: CacheMetadata | null, ): AiSdkRequestParams { const params: AiSdkRequestParams = { - model: this.modelInstance, + model: this.#modelInstance, messages, maxTokens: llmRequest.config?.maxOutputTokens, temperature: llmRequest.config?.temperature, @@ -251,7 +264,7 @@ export class AiSdkLlm extends BaseLlm { stream = false, ): AsyncGenerator { try { - const provider = this.detectModelProvider(this.modelInstance); + const provider = this.detectModelProvider(this.#modelInstance); const messages = this.convertToAiSdkMessages(llmRequest); const systemMessage = llmRequest.getSystemInstructionText(); const tools = this.convertToAiSdkTools(llmRequest); diff --git a/packages/adk/src/models/google-llm.ts b/packages/adk/src/models/google-llm.ts index 1804a50d7..a3f927e28 100644 --- a/packages/adk/src/models/google-llm.ts +++ b/packages/adk/src/models/google-llm.ts @@ -107,20 +107,35 @@ class StreamingResponseAggregator { } } +/** + * Explicit configuration for GoogleLlm — avoids process.env race conditions + * in multi-tenant servers. + */ +export interface GoogleLlmConfig { + apiKey?: string; + vertexai?: boolean; + project?: string; + location?: string; + /** Pre-built client — bypasses all other config / env vars */ + client?: GoogleGenAI; +} + /** * Integration for Gemini models. */ export class GoogleLlm extends BaseLlm { - private _apiClient?: GoogleGenAI; - private _liveApiClient?: GoogleGenAI; - private _apiBackend?: GoogleLLMVariant; - private _trackingHeaders?: Record; + #apiClient?: GoogleGenAI; + #liveApiClient?: GoogleGenAI; + #apiBackend?: GoogleLLMVariant; + #trackingHeaders?: Record; + #config?: GoogleLlmConfig; /** * Constructor for Gemini */ - constructor(model = "gemini-2.5-flash") { + constructor(model = "gemini-2.5-flash", config?: GoogleLlmConfig) { super(model); + this.#config = config; } /** @@ -282,50 +297,36 @@ export class GoogleLlm extends BaseLlm { * Provides the api client. */ get apiClient(): GoogleGenAI { - if (!this._apiClient) { - const useVertexAI = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true"; - const apiKey = process.env.GOOGLE_API_KEY; - const project = process.env.GOOGLE_CLOUD_PROJECT; - const location = process.env.GOOGLE_CLOUD_LOCATION; - - if (useVertexAI && project && location) { - this._apiClient = new GoogleGenAI({ - vertexai: true, - project, - location, - }); - } else if (apiKey) { - this._apiClient = new GoogleGenAI({ - apiKey, - }); - } else { - throw new Error( - "Google API Key or Vertex AI configuration is required. " + - "Set GOOGLE_API_KEY or GOOGLE_GENAI_USE_VERTEXAI=true with GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION.", - ); - } + if (!this.#apiClient) { + this.#apiClient = this.#buildClient(); } - return this._apiClient; + return this.#apiClient; } /** * Gets the API backend type. */ get apiBackend(): GoogleLLMVariant { - if (!this._apiBackend) { - const useVertexAI = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true"; - this._apiBackend = useVertexAI - ? GoogleLLMVariant.VERTEX_AI - : GoogleLLMVariant.GEMINI_API; + if (!this.#apiBackend) { + if (this.#config?.vertexai === true) { + this.#apiBackend = GoogleLLMVariant.VERTEX_AI; + } else if (this.#config) { + this.#apiBackend = GoogleLLMVariant.GEMINI_API; + } else { + const useVertexAI = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true"; + this.#apiBackend = useVertexAI + ? GoogleLLMVariant.VERTEX_AI + : GoogleLLMVariant.GEMINI_API; + } } - return this._apiBackend; + return this.#apiBackend; } /** * Gets the tracking headers. */ get trackingHeaders(): Record { - if (!this._trackingHeaders) { + if (!this.#trackingHeaders) { let frameworkLabel = "google-adk/1.0.0"; // Replace with actual version if (process.env[AGENT_ENGINE_TELEMETRY_ENV_VARIABLE_NAME]) { frameworkLabel = `${frameworkLabel}+${AGENT_ENGINE_TELEMETRY_TAG}`; @@ -333,12 +334,12 @@ export class GoogleLlm extends BaseLlm { const languageLabel = `gl-node/${process.version}`; const versionHeaderValue = `${frameworkLabel} ${languageLabel}`; - this._trackingHeaders = { + this.#trackingHeaders = { "x-goog-api-client": versionHeaderValue, "user-agent": versionHeaderValue, }; } - return this._trackingHeaders; + return this.#trackingHeaders; } /** @@ -354,28 +355,65 @@ export class GoogleLlm extends BaseLlm { * Gets the live API client. */ get liveApiClient(): GoogleGenAI { - if (!this._liveApiClient) { - const useVertexAI = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true"; - const apiKey = process.env.GOOGLE_API_KEY; - const project = process.env.GOOGLE_CLOUD_PROJECT; - const location = process.env.GOOGLE_CLOUD_LOCATION; - - if (useVertexAI && project && location) { - this._liveApiClient = new GoogleGenAI({ - vertexai: true, - project, - location, - apiVersion: this.liveApiVersion, - }); - } else if (apiKey) { - this._liveApiClient = new GoogleGenAI({ - apiKey, - apiVersion: this.liveApiVersion, - }); - } else { - throw new Error("API configuration required for live client"); - } + if (!this.#liveApiClient) { + this.#liveApiClient = this.#buildClient({ + apiVersion: this.liveApiVersion, + }); + } + return this.#liveApiClient; + } + + /** + * Builds a GoogleGenAI client from explicit config (if provided) or env vars. + */ + #buildClient(overrides?: { apiVersion?: string }): GoogleGenAI { + const cfg = this.#config; + + // 1. Pre-built client injection — ignore overrides (caller should pre-configure) + if (cfg?.client) { + return cfg.client; } - return this._liveApiClient; + + // 2. Explicit config fields + if (cfg?.vertexai && cfg.project && cfg.location) { + return new GoogleGenAI({ + vertexai: true, + project: cfg.project, + location: cfg.location, + ...overrides, + }); + } + if (cfg?.apiKey) { + return new GoogleGenAI({ + apiKey: cfg.apiKey, + ...overrides, + }); + } + + // 3. Env fallback (existing behaviour) + const useVertexAI = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true"; + const apiKey = process.env.GOOGLE_API_KEY; + const project = process.env.GOOGLE_CLOUD_PROJECT; + const location = process.env.GOOGLE_CLOUD_LOCATION; + + if (useVertexAI && project && location) { + return new GoogleGenAI({ + vertexai: true, + project, + location, + ...overrides, + }); + } + if (apiKey) { + return new GoogleGenAI({ + apiKey, + ...overrides, + }); + } + + throw new Error( + "Google API Key or Vertex AI configuration is required. " + + "Set GOOGLE_API_KEY or GOOGLE_GENAI_USE_VERTEXAI=true with GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION.", + ); } } diff --git a/packages/adk/src/tests/models/ai-sdk.test.ts b/packages/adk/src/tests/models/ai-sdk.test.ts index 486783896..915618189 100644 --- a/packages/adk/src/tests/models/ai-sdk.test.ts +++ b/packages/adk/src/tests/models/ai-sdk.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { streamText, generateText } from "ai"; import { AiSdkLlm, type LlmRequest, LlmResponse } from "@adk/models"; +import { GeminiContextCacheManager } from "../../models/context-cache-manager"; import { textStreamFrom } from "@adk/utils/streaming-utils"; import type { Event } from "@adk/events/event"; @@ -19,6 +20,18 @@ vi.mock("@adk/helpers/logger", () => ({ })), })); +vi.mock("../../models/context-cache-manager", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + GeminiContextCacheManager: vi.fn().mockImplementation(() => ({ + handleContextCaching: vi.fn().mockResolvedValue(null), + populateCacheMetadataInResponse: vi.fn(), + getGenaiClient: vi.fn(), + })), + }; +}); + describe("AiSdkLlm", () => { const mockModelInstance = { modelId: "gpt-4o", @@ -313,4 +326,60 @@ describe("AiSdkLlm", () => { expect(responses[0].content?.parts?.[0]?.text).toBe(""); }); }); + + describe("googleGenaiClient option", () => { + it("passes googleGenaiClient to GeminiContextCacheManager", async () => { + const fakeClient = { models: {} } as any; + + const googleModel = { + modelId: "google/gemini-2.5-flash", + provider: "google", + specificationVersion: "v2", + } as any; + + const llm = new AiSdkLlm(googleModel, { + googleGenaiClient: fakeClient, + }); + + const mockTextStream = (async function* () { + yield "ok"; + })(); + + (streamText as any).mockReturnValue({ + textStream: mockTextStream, + toolCalls: Promise.resolve([]), + usage: Promise.resolve({ + inputTokens: 5, + outputTokens: 5, + totalTokens: 10, + }), + finishReason: Promise.resolve("stop"), + providerMetadata: Promise.resolve({}), + }); + + const request = { + contents: [{ role: "user", parts: [{ text: "Hi" }] }], + config: {}, + cacheConfig: { ttlSeconds: 300, cacheIntervals: 1, minTokens: 100 }, + getSystemInstructionText: vi.fn().mockReturnValue(""), + } as unknown as LlmRequest; + + // Trigger the flow that initializes the cache manager + const gen = llm["generateContentAsyncImpl"](request, true); + for await (const _ of gen) { + // consume + } + + // Verify GeminiContextCacheManager was constructed with our client + expect(GeminiContextCacheManager).toHaveBeenCalledWith( + expect.anything(), // logger + fakeClient, + ); + }); + + it("creates AiSdkLlm without options (backward compatible)", () => { + const llm = new AiSdkLlm(mockModelInstance); + expect(llm.model).toBe("gpt-4o"); + }); + }); }); diff --git a/packages/adk/src/tests/models/google-llm.test.ts b/packages/adk/src/tests/models/google-llm.test.ts index 042162a46..d704ddc4d 100644 --- a/packages/adk/src/tests/models/google-llm.test.ts +++ b/packages/adk/src/tests/models/google-llm.test.ts @@ -91,19 +91,22 @@ describe("GoogleLlm", () => { }); describe("trackingHeaders", () => { - it("returns correct headers with and without AGENT_ENGINE_TELEMETRY_ENV_VARIABLE_NAME", () => { - const llm = new GoogleLlm(); + it("returns correct headers without AGENT_ENGINE_TELEMETRY", () => { process.env.GOOGLE_CLOUD_AGENT_ENGINE_ID = undefined; - const headers1 = llm.trackingHeaders; - expect(headers1["x-goog-api-client"]).toMatch(/google-adk\/1\.0\.0/); - process.env.GOOGLE_CLOUD_AGENT_ENGINE_ID = "foo"; - - (llm as any)._trackingHeaders = undefined; - const headers2 = llm.trackingHeaders; - expect(headers2["x-goog-api-client"]).toMatch( - /\+remote_reasoning_engine/, + const llm = new GoogleLlm(); + const headers = llm.trackingHeaders; + expect(headers["x-goog-api-client"]).toMatch(/google-adk\/1\.0\.0/); + expect(headers["x-goog-api-client"]).not.toMatch( + /remote_reasoning_engine/, ); }); + + it("returns correct headers with AGENT_ENGINE_TELEMETRY", () => { + process.env.GOOGLE_CLOUD_AGENT_ENGINE_ID = "foo"; + const llm = new GoogleLlm(); + const headers = llm.trackingHeaders; + expect(headers["x-goog-api-client"]).toMatch(/\+remote_reasoning_engine/); + }); }); describe("liveApiVersion", () => { @@ -154,7 +157,7 @@ describe("GoogleLlm", () => { process.env.GOOGLE_CLOUD_LOCATION = undefined; const llm = new GoogleLlm(); expect(() => llm.liveApiClient).toThrow( - /API configuration required for live client/, + /Google API Key or Vertex AI configuration is required/, ); }); }); @@ -167,4 +170,122 @@ describe("GoogleLlm", () => { ); }); }); + + describe("explicit config (GoogleLlmConfig)", () => { + it("uses explicit apiKey config, bypasses env", () => { + process.env.GOOGLE_API_KEY = "env-key"; + const llm = new GoogleLlm("gemini-2.5-flash", { + apiKey: "explicit-key", + }); + llm.apiClient; + expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: "explicit-key" }); + expect(GoogleGenAI).toHaveBeenCalledTimes(1); + }); + + it("uses explicit vertexai config, bypasses env", () => { + process.env.GOOGLE_GENAI_USE_VERTEXAI = undefined; + process.env.GOOGLE_API_KEY = "env-key"; + const llm = new GoogleLlm("gemini-2.5-flash", { + vertexai: true, + project: "my-proj", + location: "us-central1", + }); + llm.apiClient; + expect(GoogleGenAI).toHaveBeenCalledWith({ + vertexai: true, + project: "my-proj", + location: "us-central1", + }); + }); + + it("uses pre-built client injection directly", () => { + const fakeClient = { models: {} } as unknown as GoogleGenAI; + const llm = new GoogleLlm("gemini-2.5-flash", { client: fakeClient }); + expect(llm.apiClient).toBe(fakeClient); + expect(GoogleGenAI).not.toHaveBeenCalled(); + }); + + it("pre-built client is returned for liveApiClient too", () => { + const fakeClient = { models: {} } as unknown as GoogleGenAI; + const llm = new GoogleLlm("gemini-2.5-flash", { client: fakeClient }); + expect(llm.liveApiClient).toBe(fakeClient); + expect(GoogleGenAI).not.toHaveBeenCalled(); + }); + + it("apiBackend returns VERTEX_AI when config.vertexai is true", () => { + const llm = new GoogleLlm("gemini-2.5-flash", { + vertexai: true, + project: "p", + location: "l", + }); + expect(llm.apiBackend).toBe("VERTEX_AI"); + }); + + it("apiBackend returns GEMINI_API when config is provided without vertexai", () => { + const llm = new GoogleLlm("gemini-2.5-flash", { + apiKey: "key", + }); + expect(llm.apiBackend).toBe("GEMINI_API"); + }); + + it("falls back to env when no config provided", () => { + process.env.GOOGLE_API_KEY = "fallback-key"; + const llm = new GoogleLlm("gemini-2.5-flash"); + llm.apiClient; + expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: "fallback-key" }); + }); + }); + + describe("race condition safety", () => { + it("concurrent instances with different configs resolve independently", () => { + // Simulate what would be a race in a multi-tenant server: + // Two concurrent requests with different Google backends + const vertexLlm = new GoogleLlm("gemini-2.5-flash", { + vertexai: true, + project: "proj-a", + location: "us-east1", + }); + const apiKeyLlm = new GoogleLlm("gemini-2.5-flash", { + apiKey: "key-b", + }); + + // Access clients — the order shouldn't matter + apiKeyLlm.apiClient; + vertexLlm.apiClient; + + expect(GoogleGenAI).toHaveBeenCalledTimes(2); + expect(GoogleGenAI).toHaveBeenCalledWith({ + apiKey: "key-b", + }); + expect(GoogleGenAI).toHaveBeenCalledWith({ + vertexai: true, + project: "proj-a", + location: "us-east1", + }); + + // Backends resolve independently too + expect(vertexLlm.apiBackend).toBe("VERTEX_AI"); + expect(apiKeyLlm.apiBackend).toBe("GEMINI_API"); + }); + + it("N concurrent instances each get their own client", () => { + const configs = Array.from({ length: 10 }, (_, i) => ({ + apiKey: `key-${i}`, + })); + + const llms = configs.map((cfg) => new GoogleLlm("gemini-2.5-flash", cfg)); + + // Access all clients + for (const llm of llms) { + llm.apiClient; + } + + expect(GoogleGenAI).toHaveBeenCalledTimes(10); + for (let i = 0; i < 10; i++) { + expect(GoogleGenAI).toHaveBeenCalledWith({ + apiKey: `key-${i}`, + }); + } + }); + }); }); From 9d1b4d2ad841edb2ae42583884b3b2e2c30c1d9d Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Wed, 25 Mar 2026 10:57:24 -0400 Subject: [PATCH 2/3] fix: fail fast on incomplete config, align apiBackend with buildClient Address review feedback: - When GoogleLlmConfig is provided but incomplete (e.g. vertexai without project/location), throw instead of silently falling through to process.env (which would reintroduce the race condition) - Align apiBackend getter with #buildClient: require vertexai + project + location to report VERTEX_AI, not just vertexai flag Co-Authored-By: Claude Opus 4.6 --- packages/adk/src/models/google-llm.ts | 41 +++++++++++-------- .../adk/src/tests/models/google-llm.test.ts | 18 ++++++++ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/adk/src/models/google-llm.ts b/packages/adk/src/models/google-llm.ts index a3f927e28..5ab9e010c 100644 --- a/packages/adk/src/models/google-llm.ts +++ b/packages/adk/src/models/google-llm.ts @@ -308,9 +308,10 @@ export class GoogleLlm extends BaseLlm { */ get apiBackend(): GoogleLLMVariant { if (!this.#apiBackend) { - if (this.#config?.vertexai === true) { + const cfg = this.#config; + if (cfg?.vertexai && cfg.project && cfg.location) { this.#apiBackend = GoogleLLMVariant.VERTEX_AI; - } else if (this.#config) { + } else if (cfg) { this.#apiBackend = GoogleLLMVariant.GEMINI_API; } else { const useVertexAI = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true"; @@ -375,22 +376,30 @@ export class GoogleLlm extends BaseLlm { } // 2. Explicit config fields - if (cfg?.vertexai && cfg.project && cfg.location) { - return new GoogleGenAI({ - vertexai: true, - project: cfg.project, - location: cfg.location, - ...overrides, - }); - } - if (cfg?.apiKey) { - return new GoogleGenAI({ - apiKey: cfg.apiKey, - ...overrides, - }); + if (cfg) { + if (cfg.vertexai && cfg.project && cfg.location) { + return new GoogleGenAI({ + vertexai: true, + project: cfg.project, + location: cfg.location, + ...overrides, + }); + } + if (cfg.apiKey) { + return new GoogleGenAI({ + apiKey: cfg.apiKey, + ...overrides, + }); + } + // Config was provided but is incomplete — fail fast rather than + // silently falling through to process.env (which would reintroduce + // the race condition this config is meant to prevent). + throw new Error( + "Incomplete GoogleLlmConfig: provide apiKey, vertexai + project + location, or a pre-built client.", + ); } - // 3. Env fallback (existing behaviour) + // 3. Env fallback (only when no config was provided) const useVertexAI = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true"; const apiKey = process.env.GOOGLE_API_KEY; const project = process.env.GOOGLE_CLOUD_PROJECT; diff --git a/packages/adk/src/tests/models/google-llm.test.ts b/packages/adk/src/tests/models/google-llm.test.ts index d704ddc4d..a039c7dee 100644 --- a/packages/adk/src/tests/models/google-llm.test.ts +++ b/packages/adk/src/tests/models/google-llm.test.ts @@ -234,6 +234,24 @@ describe("GoogleLlm", () => { llm.apiClient; expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: "fallback-key" }); }); + + it("throws on incomplete config instead of falling through to env", () => { + process.env.GOOGLE_API_KEY = "env-key"; + const llm = new GoogleLlm("gemini-2.5-flash", { + vertexai: true, + project: "p", + // missing location + }); + expect(() => llm.apiClient).toThrow(/Incomplete GoogleLlmConfig/); + }); + + it("apiBackend returns GEMINI_API when vertexai is true but project/location missing", () => { + const llm = new GoogleLlm("gemini-2.5-flash", { + vertexai: true, + apiKey: "key", + }); + expect(llm.apiBackend).toBe("GEMINI_API"); + }); }); describe("race condition safety", () => { From a800cc387dd197bb05d6a41c48b66d74aef0565b Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Wed, 25 Mar 2026 10:59:33 -0400 Subject: [PATCH 3/3] chore: add changeset for minor version bump Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-google-runtime-race.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-google-runtime-race.md diff --git a/.changeset/fix-google-runtime-race.md b/.changeset/fix-google-runtime-race.md new file mode 100644 index 000000000..534b006ab --- /dev/null +++ b/.changeset/fix-google-runtime-race.md @@ -0,0 +1,5 @@ +--- +"@iqai/adk": minor +--- + +Add `GoogleLlmConfig` and `AiSdkLlmOptions` for explicit, request-scoped Google client configuration. This eliminates process.env race conditions when multiple GoogleLlm or AiSdkLlm instances with different backends run concurrently in a multi-tenant server. Env-based fallback is preserved when no config is provided.