fix(cli): configure Vertex GCP settings (#11390)

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
This commit is contained in:
Max
2026-06-09 16:05:09 -07:00
committed by GitHub
co-authored by Max Paulus 🥪
parent 2c4aeae4f3
commit 35ce6a3f26
10 changed files with 144 additions and 13 deletions
@@ -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<Record<ProviderConfigFieldKey, string>> = {
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://<subdomain>.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);
@@ -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({
@@ -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;
@@ -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",
+2
View File
@@ -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);
}
@@ -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;
}
@@ -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");
@@ -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<string, ProviderConfigFieldMetadata>
> = {
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:
+24
View File
@@ -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({
+25 -12
View File
@@ -8,22 +8,35 @@ import { ensureFetch, resolveApiKey } from "../http";
import { isClaudeModelId } from "../model-facts";
import type { ProviderFactoryResult } from "./types";
function readStringOption(options: Record<string, unknown> | undefined, key: string): string | undefined {
const value = options?.[key];
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
}
function readNestedStringOption(options: Record<string, unknown> | 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<string, unknown>)[key];
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
}
export async function createVertexProviderModule(
config: GatewayResolvedProviderConfig,
context: GatewayProviderContext,
): Promise<ProviderFactoryResult> {
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)) {