diff --git a/packages/ai/README.md b/packages/ai/README.md index 32071faa..c339b234 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -37,7 +37,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an - [Environment Variables](#environment-variables-nodejs-only) - [Checking Environment Variables](#checking-environment-variables) - [OAuth Providers](#oauth-providers) - - [Vertex AI (ADC)](#vertex-ai-adc) + - [Vertex AI](#vertex-ai) - [CLI Login](#cli-login) - [Programmatic OAuth](#programmatic-oauth) - [Login Flow Example](#login-flow-example) @@ -907,7 +907,7 @@ In Node.js environments, you can set environment variables to avoid passing API | Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` or `AZURE_OPENAI_RESOURCE_NAME` (optional `AZURE_OPENAI_API_VERSION`, `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` like `model=deployment,model2=deployment2`) | | Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` | | Google | `GEMINI_API_KEY` | -| Vertex AI | `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC | +| Vertex AI | `GOOGLE_CLOUD_API_KEY` or `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC | | Mistral | `MISTRAL_API_KEY` | | Groq | `GROQ_API_KEY` | | Cerebras | `CEREBRAS_API_KEY` | @@ -975,14 +975,15 @@ Several providers require OAuth authentication instead of static API keys: For paid Cloud Code Assist subscriptions, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` to your project ID. -### Vertex AI (ADC) +### Vertex AI -Vertex AI models use Application Default Credentials (ADC): +Vertex AI models support either a Google Cloud API key or Application Default Credentials (ADC): -- **Local development**: Run `gcloud auth application-default login` -- **CI/Production**: Set `GOOGLE_APPLICATION_CREDENTIALS` to point to a service account JSON key file +- **API key**: Set `GOOGLE_CLOUD_API_KEY` or pass `apiKey` in the call options. +- **Local development (ADC)**: Run `gcloud auth application-default login` +- **CI/Production (ADC)**: Set `GOOGLE_APPLICATION_CREDENTIALS` to point to a service account JSON key file -Also set `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) and `GOOGLE_CLOUD_LOCATION`. You can also pass `project`/`location` in the call options. +When using ADC, also set `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) and `GOOGLE_CLOUD_LOCATION`. You can also pass `project`/`location` in the call options. When using `GOOGLE_CLOUD_API_KEY`, `project` and `location` are not required. Example: @@ -1003,6 +1004,8 @@ import { getModel, complete } from '@mariozechner/pi-ai'; const model = getModel('google-vertex', 'gemini-2.5-flash'); const response = await complete(model, { messages: [{ role: 'user', content: 'Hello from Vertex AI' }] + }, { + apiKey: process.env.GOOGLE_CLOUD_API_KEY, }); for (const block of response.content) { diff --git a/packages/ai/src/env-api-keys.ts b/packages/ai/src/env-api-keys.ts index 749e074f..95e91417 100644 --- a/packages/ai/src/env-api-keys.ts +++ b/packages/ai/src/env-api-keys.ts @@ -73,9 +73,13 @@ export function getEnvApiKey(provider: any): string | undefined { return process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY; } - // Vertex AI uses Application Default Credentials, not API keys. - // Auth is configured via `gcloud auth application-default login`. + // Vertex AI supports either an explicit API key or Application Default Credentials + // Auth is configured via `gcloud auth application-default login` if (provider === "google-vertex") { + if (process.env.GOOGLE_CLOUD_API_KEY) { + return process.env.GOOGLE_CLOUD_API_KEY; + } + const hasCredentials = hasVertexAdcCredentials(); const hasProject = !!(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT); const hasLocation = !!process.env.GOOGLE_CLOUD_LOCATION; diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index 7bdaf5eb..3f04b33f 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -84,9 +84,11 @@ export const streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOpt }; try { - const project = resolveProject(options); - const location = resolveLocation(options); - const client = createClient(model, project, location, options?.headers); + const apiKey = resolveApiKey(options); + // Create the client using either a Vertex API key, if provided, or ADC with project and location + const client = apiKey + ? createClientWithApiKey(model, apiKey, options?.headers) + : createClient(model, resolveProject(options), resolveLocation(options), options?.headers); let params = buildParams(model, context, options); const nextParams = await options?.onPayload?.(params, model); if (nextParams !== undefined) { @@ -341,6 +343,31 @@ function createClient( }); } +function createClientWithApiKey( + model: Model<"google-vertex">, + apiKey: string, + optionsHeaders?: Record, +): GoogleGenAI { + const httpOptions: { headers?: Record } = {}; + + if (model.headers || optionsHeaders) { + httpOptions.headers = { ...model.headers, ...optionsHeaders }; + } + + const hasHttpOptions = Object.values(httpOptions).some(Boolean); + + return new GoogleGenAI({ + vertexai: true, + apiKey, + apiVersion: API_VERSION, + httpOptions: hasHttpOptions ? httpOptions : undefined, + }); +} + +function resolveApiKey(options?: GoogleVertexOptions): string | undefined { + return options?.apiKey || process.env.GOOGLE_CLOUD_API_KEY; +} + function resolveProject(options?: GoogleVertexOptions): string { const project = options?.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT; if (!project) { diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 9b49f697..f4abb90a 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -380,6 +380,7 @@ describe("Generate E2E Tests", () => { describe("Google Vertex Provider (gemini-3-flash-preview)", () => { const vertexProject = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT; const vertexLocation = process.env.GOOGLE_CLOUD_LOCATION; + const vertexApiKey = process.env.GOOGLE_CLOUD_API_KEY; const isVertexConfigured = Boolean(vertexProject && vertexLocation); const vertexOptions = { project: vertexProject, location: vertexLocation } as const; const llm = getModel("google-vertex", "gemini-3-flash-preview"); @@ -388,6 +389,10 @@ describe("Generate E2E Tests", () => { await basicTextGeneration(llm, vertexOptions); }); + it.skipIf(!vertexApiKey)("should complete basic text generation with Vertex API key", { retry: 3 }, async () => { + await basicTextGeneration(llm, { apiKey: vertexApiKey! }); + }); + it.skipIf(!isVertexConfigured)("should handle tool calling", { retry: 3 }, async () => { await handleToolCall(llm, vertexOptions); });