mirror of
https://github.com/cline/cline.git
synced 2026-10-29 17:42:34 +08:00
fix(cli): configure Vertex GCP settings (#11390)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user