diff --git a/apps/cli/src/tui/components/dialogs/provider-picker.tsx b/apps/cli/src/tui/components/dialogs/provider-picker.tsx index af5d02bac2..179f2f4ee4 100644 --- a/apps/cli/src/tui/components/dialogs/provider-picker.tsx +++ b/apps/cli/src/tui/components/dialogs/provider-picker.tsx @@ -27,6 +27,7 @@ import { getDefaultAwsRegion, type ProviderConfigValues, resolveProviderConfigAwsRegion, + resolveProviderConfigGcp, resolveProviderConfigSap, updateProviderConfigValue, } from "../../utils/provider-config-values"; @@ -317,6 +318,8 @@ const DEFAULT_FIELD_LABELS: Partial> = { baseUrl: "Base URL", awsRegion: "AWS Region", awsProfile: "AWS Profile Name", + gcpProjectId: "Google Cloud Project ID", + gcpRegion: "Google Cloud Region", sapClientId: "Client ID", sapClientSecret: "Client Secret", sapTokenUrl: "Token URL", @@ -331,6 +334,8 @@ const DEFAULT_FIELD_PLACEHOLDERS: Partial< baseUrl: "", awsRegion: "us-east-1", awsProfile: "default", + gcpProjectId: "my-gcp-project", + gcpRegion: "us-central1", sapClientId: "sb-...|xsuaa_std!b...", sapClientSecret: "SAP AI Core client secret", sapTokenUrl: "https://.authentication.sap.hana.ondemand.com", @@ -341,6 +346,8 @@ const DEFAULT_FIELD_PLACEHOLDERS: Partial< /** Render order for cycling focus with Tab. */ const FIELD_ORDER: ProviderConfigFieldKey[] = [ "awsRegion", + "gcpProjectId", + "gcpRegion", "baseUrl", "apiKey", "awsProfile", @@ -403,6 +410,13 @@ export function ProviderConfigInputContent( initial.awsRegion = existingSettings?.aws?.region?.trim() || getDefaultAwsRegion(ep); } + if (config.fields.gcpProjectId) + initial.gcpProjectId = existingSettings?.gcp?.projectId?.trim() ?? ""; + if (config.fields.gcpRegion) + initial.gcpRegion = + existingSettings?.gcp?.region?.trim() ?? + config.fields.gcpRegion.defaultValue ?? + "us-central1"; if (config.fields.apiKey) initial.apiKey = existingSettings?.apiKey?.trim() ?? ""; if (config.fields.awsProfile) @@ -431,6 +445,7 @@ export function ProviderConfigInputContent( const apiKey = values.apiKey?.trim(); const awsProfile = values.awsProfile?.trim(); const hasAwsFields = config.fields.awsRegion || config.fields.awsProfile; + const hasGcpFields = config.fields.gcpProjectId || config.fields.gcpRegion; const hasSapFields = config.fields.sapClientId || config.fields.sapClientSecret || @@ -448,6 +463,7 @@ export function ProviderConfigInputContent( profile: apiKey ? undefined : awsProfile || undefined, } : undefined, + gcp: hasGcpFields ? resolveProviderConfigGcp(values) : undefined, sap: hasSapFields ? resolveProviderConfigSap(values) : undefined, }); resolve(true); diff --git a/apps/cli/src/tui/utils/provider-config-values.test.ts b/apps/cli/src/tui/utils/provider-config-values.test.ts index 92b75f69c7..1be10ee330 100644 --- a/apps/cli/src/tui/utils/provider-config-values.test.ts +++ b/apps/cli/src/tui/utils/provider-config-values.test.ts @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { getDefaultAwsRegion, resolveProviderConfigAwsRegion, + resolveProviderConfigGcp, resolveProviderConfigSap, updateProviderConfigValue, } from "./provider-config-values"; @@ -66,6 +67,16 @@ describe("provider config values", () => { ).toBe("us-west-2"); }); + it("resolves Vertex GCP field values into GCP settings", () => { + expect(resolveProviderConfigGcp({ gcpRegion: "us-central1" })).toBeUndefined(); + expect( + resolveProviderConfigGcp({ + gcpProjectId: " project ", + gcpRegion: " europe-west4 ", + }), + ).toEqual({ projectId: "project", region: "europe-west4" }); + }); + it("resolves SAP AI Core field values into SAP settings", () => { expect( resolveProviderConfigSap({ diff --git a/apps/cli/src/tui/utils/provider-config-values.ts b/apps/cli/src/tui/utils/provider-config-values.ts index e78a089e11..7c1f9fc374 100644 --- a/apps/cli/src/tui/utils/provider-config-values.ts +++ b/apps/cli/src/tui/utils/provider-config-values.ts @@ -6,6 +6,7 @@ export type ProviderConfigValues = Partial< >; const DEFAULT_AWS_REGION = "us-east-1"; +const DEFAULT_GCP_REGION = "us-central1"; export function getDefaultAwsRegion(profile?: string): string { return ( @@ -20,6 +21,20 @@ export function resolveProviderConfigAwsRegion( return values.awsRegion?.trim() || getDefaultAwsRegion(values.awsProfile); } +export function resolveProviderConfigGcp(values: ProviderConfigValues): + | { + projectId?: string; + region?: string; + } + | undefined { + const projectId = values.gcpProjectId?.trim() || undefined; + if (!projectId) return undefined; + return { + projectId, + region: values.gcpRegion?.trim() || DEFAULT_GCP_REGION, + }; +} + export function resolveProviderConfigSap(values: ProviderConfigValues): | { clientId?: string; diff --git a/apps/cli/src/utils/provider-readiness.test.ts b/apps/cli/src/utils/provider-readiness.test.ts index 6536b5c969..d794e7f931 100644 --- a/apps/cli/src/utils/provider-readiness.test.ts +++ b/apps/cli/src/utils/provider-readiness.test.ts @@ -139,6 +139,12 @@ describe("provider readiness", () => { gcp: { projectId: "test-project" }, } satisfies ProviderSettings), ).toBe(true); + expect( + isProviderSettingsUsable("vertex", { + provider: "vertex", + gcp: { projectId: "test-project", region: "us-central1" }, + } satisfies ProviderSettings), + ).toBe(true); expect( isProviderSettingsUsable("sapaicore", { provider: "sapaicore", diff --git a/apps/cli/src/utils/provider-readiness.ts b/apps/cli/src/utils/provider-readiness.ts index d804419068..c99fd85551 100644 --- a/apps/cli/src/utils/provider-readiness.ts +++ b/apps/cli/src/utils/provider-readiness.ts @@ -33,6 +33,8 @@ function hasAwsRegion(settings: ProviderSettings): boolean { function hasGcpCredentials(settings: ProviderSettings): boolean { const gcp = settings.gcp; + // Vertex defaults to us-central1 at runtime when no region is stored, so keep + // existing project-only configs usable while new CLI saves include a region. return hasText(gcp?.projectId); } diff --git a/sdk/packages/core/src/services/local-runtime-bootstrap.ts b/sdk/packages/core/src/services/local-runtime-bootstrap.ts index 3dbb5e4197..ef313fee2f 100644 --- a/sdk/packages/core/src/services/local-runtime-bootstrap.ts +++ b/sdk/packages/core/src/services/local-runtime-bootstrap.ts @@ -189,6 +189,7 @@ function buildProviderConfig( ...(stored?.modelCatalog ?? {}), } : undefined; + const sessionProviderConfig = config.providerConfig?.providerId === config.providerId ? config.providerConfig : undefined; const settings: ProviderSettings = { ...(stored ?? {}), provider: config.providerId, @@ -209,7 +210,10 @@ function buildProviderConfig( reasoning: resolveReasoningSettings(config, stored?.reasoning), modelCatalog, }; - const providerConfig = toProviderConfig(settings); + const providerConfig: ProviderConfig = { + ...toProviderConfig(settings), + ...(sessionProviderConfig ?? {}), + }; if (config.knownModels) { providerConfig.knownModels = config.knownModels; } diff --git a/sdk/packages/core/src/services/providers/provider-config-fields.test.ts b/sdk/packages/core/src/services/providers/provider-config-fields.test.ts index a4cb014a2c..7149a57110 100644 --- a/sdk/packages/core/src/services/providers/provider-config-fields.test.ts +++ b/sdk/packages/core/src/services/providers/provider-config-fields.test.ts @@ -94,6 +94,21 @@ describe("getProviderConfigFields", () => { ); }); + it("returns Vertex GCP config fields with optional API key", () => { + const result = getProviderConfigFields("vertex"); + expect(result.providerId).toBe("vertex"); + expect(result.authMethod).toBe("api-key"); + expect(result.description).toMatch(/Application Default Credentials/i); + expect(Object.keys(result.fields)).toEqual([ + "gcpProjectId", + "gcpRegion", + "apiKey", + ]); + expect(result.fields.gcpProjectId?.label).toBe("Google Cloud Project ID"); + expect(result.fields.gcpRegion?.defaultValue).toBe("us-central1"); + expect(result.fields.apiKey?.optional).toBe(true); + }); + it("returns api-key auth with awsRegion, apiKey, and awsProfile for bedrock", () => { const result = getProviderConfigFields("bedrock"); expect(result.providerId).toBe("bedrock"); diff --git a/sdk/packages/core/src/services/providers/provider-config-fields.ts b/sdk/packages/core/src/services/providers/provider-config-fields.ts index 8a69cec32b..80dc01535b 100644 --- a/sdk/packages/core/src/services/providers/provider-config-fields.ts +++ b/sdk/packages/core/src/services/providers/provider-config-fields.ts @@ -6,6 +6,8 @@ export type ProviderConfigFieldKey = | "baseUrl" | "awsRegion" | "awsProfile" + | "gcpProjectId" + | "gcpRegion" | "sapClientId" | "sapClientSecret" | "sapTokenUrl" @@ -35,6 +37,8 @@ const FIELD_KEYS: ProviderConfigFieldKey[] = [ "baseUrl", "awsRegion", "awsProfile", + "gcpProjectId", + "gcpRegion", "sapClientId", "sapClientSecret", "sapTokenUrl", @@ -53,6 +57,27 @@ interface ProviderConfigFieldMetadata { const PROVIDER_CONFIG_FIELD_METADATA: Partial< Record > = { + vertex: { + mode: "replace", + description: + "Vertex AI can use Google Cloud Application Default Credentials with a project/region. An API key is optional for supported Gemini models.", + fields: { + gcpProjectId: { + label: "Google Cloud Project ID", + placeholder: "my-gcp-project", + }, + gcpRegion: { + label: "Google Cloud Region", + placeholder: "us-central1", + defaultValue: "us-central1", + }, + apiKey: { + label: "API Key (optional)", + placeholder: "Leave blank to use Google Cloud credentials", + optional: true, + }, + }, + }, bedrock: { mode: "replace", description: diff --git a/sdk/packages/llms/src/providers/vendors/vertex.test.ts b/sdk/packages/llms/src/providers/vendors/vertex.test.ts index 78c5373ef8..f6a19bc381 100644 --- a/sdk/packages/llms/src/providers/vendors/vertex.test.ts +++ b/sdk/packages/llms/src/providers/vendors/vertex.test.ts @@ -60,6 +60,30 @@ describe("createVertexProviderModule", () => { expect(createVertexAnthropicMock).not.toHaveBeenCalled(); }); + it("accepts nested gcp project and region options", async () => { + await createVertexProviderModule( + config({ + options: { + gcp: { + projectId: "nested-project", + region: "europe-west4", + }, + }, + }), + context("gemini-3-flash-preview"), + ); + + expect(createVertexMock).toHaveBeenCalledWith( + expect.objectContaining({ + project: "nested-project", + location: "europe-west4", + googleAuthOptions: { + projectId: "nested-project", + }, + }), + ); + }); + it("keeps API-key express mode when Gemini config has no GCP settings", async () => { await createVertexProviderModule( config({ diff --git a/sdk/packages/llms/src/providers/vendors/vertex.ts b/sdk/packages/llms/src/providers/vendors/vertex.ts index 2a5d5cadeb..d6df1ddc94 100644 --- a/sdk/packages/llms/src/providers/vendors/vertex.ts +++ b/sdk/packages/llms/src/providers/vendors/vertex.ts @@ -8,22 +8,35 @@ import { ensureFetch, resolveApiKey } from "../http"; import { isClaudeModelId } from "../model-facts"; import type { ProviderFactoryResult } from "./types"; +function readStringOption(options: Record | undefined, key: string): string | undefined { + const value = options?.[key]; + return typeof value === "string" && value.trim().length > 0 ? value : undefined; +} + +function readNestedStringOption(options: Record | undefined, objectKey: string, key: string): string | undefined { + const object = options?.[objectKey]; + if (!object || typeof object !== "object" || Array.isArray(object)) { + return undefined; + } + const value = (object as Record)[key]; + return typeof value === "string" && value.trim().length > 0 ? value : undefined; +} + export async function createVertexProviderModule( config: GatewayResolvedProviderConfig, context: GatewayProviderContext, ): Promise { - const project = String( - config.options?.project ?? config.options?.projectId ?? "", - ); - const location = String( - config.options?.location ?? config.options?.region ?? "us-central1", - ); - const googleAuthProjectId = - typeof config.options?.project === "string" - ? config.options.project - : typeof config.options?.projectId === "string" - ? config.options.projectId - : undefined; + const project = + readStringOption(config.options, "project") ?? + readStringOption(config.options, "projectId") ?? + readNestedStringOption(config.options, "gcp", "projectId") ?? + ""; + const location = + readStringOption(config.options, "location") ?? + readStringOption(config.options, "region") ?? + readNestedStringOption(config.options, "gcp", "region") ?? + "us-central1"; + const googleAuthProjectId = project || undefined; const fetch = ensureFetch(config.fetch); if (isClaudeModelId(context.model.id)) {