Compare commits

...

1 Commits

Author SHA1 Message Date
Bhagya Amarasinghe c5d629ef25 feat(ai): support Vertex AI ADC credentials 2026-05-05 18:04:30 +05:30
5 changed files with 100 additions and 20 deletions
+3 -2
View File
@@ -183,11 +183,12 @@ AZUREAD_TENANT_ID=
# Configure Formbricks AI at the instance level
# Set the provider used for AI features on this instance.
# Accepted values for AI_PROVIDER: aws, google, azure
# Set AI_MODEL to the provider-specific model or deployment name and configure the matching credentials below.
# Set AI_MODEL to the provider-specific model or deployment name and configure the matching provider settings below.
# AI_PROVIDER=google
# AI_MODEL=gemini-2.5-flash
# Google Cloud credentials for Gemini models
# Google Cloud settings for Gemini models
# Credentials are optional when Application Default Credentials are available.
# AI_GOOGLE_CLOUD_PROJECT=
# AI_GOOGLE_CLOUD_LOCATION=
# AI_GOOGLE_CLOUD_CREDENTIALS_JSON=
+29
View File
@@ -79,6 +79,35 @@ describe("env", () => {
expect(env.DEBUG_SHOW_RESET_LINK).toBe("1");
});
test("allows Google Cloud AI configuration to rely on ADC credentials", async () => {
setTestEnv({
AI_PROVIDER: "google",
AI_MODEL: "gemini-2.5-flash",
AI_GOOGLE_CLOUD_PROJECT: "test-project",
AI_GOOGLE_CLOUD_LOCATION: "us-central1",
AI_GOOGLE_CLOUD_CREDENTIALS_JSON: undefined,
AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS: undefined,
});
const { env } = await import("./env");
expect(env.AI_PROVIDER).toBe("google");
expect(env.AI_GOOGLE_CLOUD_PROJECT).toBe("test-project");
expect(env.AI_GOOGLE_CLOUD_LOCATION).toBe("us-central1");
});
test("fails to load when Google Cloud credentials JSON is invalid", async () => {
setTestEnv({
AI_PROVIDER: "google",
AI_MODEL: "gemini-2.5-flash",
AI_GOOGLE_CLOUD_PROJECT: "test-project",
AI_GOOGLE_CLOUD_LOCATION: "us-central1",
AI_GOOGLE_CLOUD_CREDENTIALS_JSON: "{not-json}",
});
await expect(import("./env")).rejects.toThrow("AI_GOOGLE_CLOUD_CREDENTIALS_JSON");
});
test("uses the configured Cube environment variables", async () => {
setTestEnv();
const { env } = await import("./env");
-8
View File
@@ -68,14 +68,6 @@ const validateGoogleAIConfiguration = (values: TAIConfigurationEnv, ctx: z.Refin
);
}
if (!values.AI_GOOGLE_CLOUD_CREDENTIALS_JSON && !values.AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS) {
addEnvIssue(
ctx,
"AI_GOOGLE_CLOUD_CREDENTIALS_JSON",
"AI_GOOGLE_CLOUD_CREDENTIALS_JSON or AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS is required when AI_PROVIDER=google"
);
}
if (values.AI_GOOGLE_CLOUD_CREDENTIALS_JSON) {
try {
const parsedCredentials = JSON.parse(values.AI_GOOGLE_CLOUD_CREDENTIALS_JSON) as unknown;
+66
View File
@@ -83,6 +83,30 @@ describe("packages/ai provider helpers", () => {
});
});
test("reports a fully configured Google Cloud instance when ADC provides credentials", () => {
expect(
getAiConfigurationStatus({
AI_PROVIDER: "google",
AI_MODEL: "gemini-2.5-flash",
AI_GOOGLE_CLOUD_PROJECT: "test-project",
AI_GOOGLE_CLOUD_LOCATION: "us-central1",
})
).toEqual({
provider: "google",
model: "gemini-2.5-flash",
isConfigured: true,
missingFields: [],
invalidFields: [],
providerStatus: {
provider: "google",
model: "gemini-2.5-flash",
isConfigured: true,
missingFields: [],
invalidFields: [],
},
});
});
test("treats the instance as not configured when AI_PROVIDER is missing", () => {
expect(
isAiConfigured({
@@ -207,6 +231,48 @@ describe("packages/ai provider helpers", () => {
expect(mocks.createVertex).toHaveBeenCalledTimes(1);
});
test("creates a Google Cloud model with application credentials file", () => {
const vertexProvider = createMockProvider("google");
mocks.createVertex.mockReturnValue(vertexProvider);
const model = getAiModel({
AI_PROVIDER: "google",
AI_MODEL: "gemini-2.5-flash",
AI_GOOGLE_CLOUD_PROJECT: "test-project",
AI_GOOGLE_CLOUD_LOCATION: "us-central1",
AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS: "/tmp/google-cloud.json",
});
expect(model).toEqual({ providerName: "google", modelName: "gemini-2.5-flash" });
expect(mocks.createVertex).toHaveBeenCalledWith({
project: "test-project",
location: "us-central1",
googleAuthOptions: {
keyFilename: "/tmp/google-cloud.json",
},
});
expect(vertexProvider).toHaveBeenCalledWith("gemini-2.5-flash");
});
test("creates a Google Cloud model using ADC when no credential override is configured", () => {
const vertexProvider = createMockProvider("google");
mocks.createVertex.mockReturnValue(vertexProvider);
const model = getAiModel({
AI_PROVIDER: "google",
AI_MODEL: "gemini-2.5-flash",
AI_GOOGLE_CLOUD_PROJECT: "test-project",
AI_GOOGLE_CLOUD_LOCATION: "us-central1",
});
expect(model).toEqual({ providerName: "google", modelName: "gemini-2.5-flash" });
expect(mocks.createVertex).toHaveBeenCalledWith({
project: "test-project",
location: "us-central1",
});
expect(vertexProvider).toHaveBeenCalledWith("gemini-2.5-flash");
});
test("creates an AWS model with explicit AWS credentials", () => {
const bedrockProvider = createMockProvider("aws");
mocks.createAmazonBedrock.mockReturnValue(bedrockProvider);
+2 -10
View File
@@ -39,11 +39,6 @@ export const googleProviderAdapter: AIProviderAdapter = {
}
const credentialsJson = normalizeValue(environment.AI_GOOGLE_CLOUD_CREDENTIALS_JSON);
const applicationCredentials = normalizeValue(environment.AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS);
if (!credentialsJson && !applicationCredentials) {
missingFields.push("AI_GOOGLE_CLOUD_CREDENTIALS_JSON or AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS");
}
if (credentialsJson) {
try {
@@ -73,15 +68,12 @@ export const googleProviderAdapter: AIProviderAdapter = {
const credentialsJson = normalizeValue(environment.AI_GOOGLE_CLOUD_CREDENTIALS_JSON);
const applicationCredentials = normalizeValue(environment.AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS);
if (!project || !location || (!credentialsJson && !applicationCredentials)) {
throw new AIConfigurationError("providerNotConfigured", "Google Cloud AI credentials are incomplete", {
if (!project || !location) {
throw new AIConfigurationError("providerNotConfigured", "Google Cloud AI configuration is incomplete", {
provider: "google",
missingFields: [
...(!project ? ["AI_GOOGLE_CLOUD_PROJECT"] : []),
...(!location ? ["AI_GOOGLE_CLOUD_LOCATION"] : []),
...(!credentialsJson && !applicationCredentials
? ["AI_GOOGLE_CLOUD_CREDENTIALS_JSON or AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS"]
: []),
],
});
}