Compare commits

...

3 Commits

Author SHA1 Message Date
Bhagya Amarasinghe e6b6f5e6d3 feat(helm): add Hub worker and embeddings runtime 2026-05-07 01:45:45 +05:30
Dhruwang Jariwala ed42df34c4 feat(ai): support Vertex AI ADC credentials (#7938) 2026-05-06 12:37:24 +05:30
Bhagya Amarasinghe c5d629ef25 feat(ai): support Vertex AI ADC credentials 2026-05-05 18:04:30 +05:30
16 changed files with 1086 additions and 27 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;
+76
View File
@@ -54,6 +54,38 @@ This chart does not deploy Cube.js. XM Suite v5 dashboard and analysis features
- Provide `CUBEJS_API_SECRET` through your existing secret management flow, such as the generated app secret override or `deployment.envFrom`.
- Keep Hub enabled. Cube should point at the same feedback records database that Hub writes to, unless you intentionally split that storage.
## Hub worker and self-hosted embeddings
The chart deploys Hub API and, by default, a `hub-worker` deployment. Hub API is insert-only for River jobs; webhook dispatch and embedding jobs are processed by `hub-worker`.
Self-hosted embeddings are disabled by default. Set `hub.embeddings.enabled=true` to deploy an internal Hugging Face Text Embeddings Inference (TEI) service and wire Hub API plus Hub worker to it through the OpenAI-compatible endpoint added in Hub:
```yaml
hub:
worker:
enabled: true
embeddings:
enabled: true
model: google/embeddinggemma-300m
servedModelName: google/embeddinggemma-300m
huggingFace:
token: hf_...
```
The generated Hub embedding configuration is:
- `EMBEDDING_PROVIDER=openai`
- `EMBEDDING_MODEL=<hub.embeddings.servedModelName or hub.embeddings.model>`
- `EMBEDDING_BASE_URL=http://<release>-hub-embeddings:8080/v1`
- `EMBEDDING_PROVIDER_API_KEY` from a dedicated embeddings Secret
The TEI service is internal-only (`ClusterIP`) and not exposed through ingress. For gated models such as `google/embeddinggemma-300m`, provide `hub.embeddings.huggingFace.token` or set `hub.embeddings.huggingFace.existingSecret`.
When TEI auth is enabled, configure the shared key through `hub.embeddings.auth.apiKey` or `hub.embeddings.auth.existingSecret`; the chart manages both TEI `API_KEY` and Hub `EMBEDDING_PROVIDER_API_KEY` from that source.
Autoscaling is opt-in for Hub API, Hub worker, and the embeddings runtime. If you scale the embeddings runtime above one replica while persistence is enabled, the cache PVC must support `ReadWriteMany`; otherwise set `hub.embeddings.persistence.enabled=false` or provide a compatible `existingClaim`.
## Values
| Key | Type | Default | Description |
@@ -139,7 +171,40 @@ This chart does not deploy Cube.js. XM Suite v5 dashboard and analysis features
| externalSecret.secretStore.name | string | `"aws-secrets-manager"` | |
| formbricks.publicUrl | string | `""` | |
| formbricks.webappUrl | string | `""` | |
| hub.autoscaling.enabled | bool | `false` | |
| hub.autoscaling.maxReplicas | int | `3` | |
| hub.autoscaling.minReplicas | int | `1` | |
| hub.enabled | bool | `true` | |
| hub.embeddings.auth.enabled | bool | `true` | |
| hub.embeddings.auth.existingSecret | string | `""` | |
| hub.embeddings.auth.secretKey | string | `"EMBEDDING_PROVIDER_API_KEY"` | |
| hub.embeddings.autoscaling.enabled | bool | `false` | |
| hub.embeddings.autoscaling.maxReplicas | int | `2` | |
| hub.embeddings.autoscaling.minReplicas | int | `1` | |
| hub.embeddings.baseUrl | string | `""` | Defaults to the internal TEI service URL ending in `/v1`. |
| hub.embeddings.enabled | bool | `false` | |
| hub.embeddings.huggingFace.existingSecret | string | `""` | |
| hub.embeddings.huggingFace.token | string | `""` | |
| hub.embeddings.huggingFace.tokenKey | string | `"HF_TOKEN"` | |
| hub.embeddings.image.pullPolicy | string | `"IfNotPresent"` | |
| hub.embeddings.image.repository | string | `"ghcr.io/huggingface/text-embeddings-inference"` | |
| hub.embeddings.image.tag | string | `"cpu-1.9"` | |
| hub.embeddings.maxConcurrent | string | `"5"` | |
| hub.embeddings.model | string | `"google/embeddinggemma-300m"` | |
| hub.embeddings.persistence.enabled | bool | `true` | |
| hub.embeddings.persistence.mountPath | string | `"/data"` | |
| hub.embeddings.persistence.size | string | `"10Gi"` | |
| hub.embeddings.pdb.enabled | bool | `false` | |
| hub.embeddings.port | int | `8080` | |
| hub.embeddings.prometheusPort | int | `9000` | |
| hub.embeddings.replicas | int | `1` | |
| hub.embeddings.resources.limits.memory | string | `"8Gi"` | |
| hub.embeddings.resources.requests.cpu | string | `"4"` | |
| hub.embeddings.resources.requests.memory | string | `"8Gi"` | |
| hub.embeddings.runtime | string | `"tei"` | |
| hub.embeddings.servedModelName | string | `""` | Defaults to `hub.embeddings.model`. |
| hub.embeddings.service.port | int | `8080` | |
| hub.embeddings.service.type | string | `"ClusterIP"` | |
| hub.env | object | `{}` | |
| hub.existingSecret | string | `""` | |
| hub.image.digest | string | `"sha256:14db7b3d285b6e9165b55693f9b83d08beff840a255fd77dd12882ee0a62f5cb"` | When set, takes precedence over tag (immutable pin). |
@@ -149,10 +214,21 @@ This chart does not deploy Cube.js. XM Suite v5 dashboard and analysis features
| hub.migration.activeDeadlineSeconds | int | `900` | |
| hub.migration.backoffLimit | int | `3` | |
| hub.migration.ttlSecondsAfterFinished | int | `300` | |
| hub.pdb.enabled | bool | `false` | |
| hub.replicas | int | `1` | |
| hub.resources.limits.memory | string | `"512Mi"` | |
| hub.resources.requests.cpu | string | `"100m"` | |
| hub.resources.requests.memory | string | `"256Mi"` | |
| hub.worker.autoscaling.enabled | bool | `false` | |
| hub.worker.autoscaling.maxReplicas | int | `5` | |
| hub.worker.autoscaling.minReplicas | int | `1` | |
| hub.worker.enabled | bool | `true` | |
| hub.worker.env | object | `{}` | |
| hub.worker.pdb.enabled | bool | `false` | |
| hub.worker.replicas | int | `1` | |
| hub.worker.resources.limits.memory | string | `"512Mi"` | |
| hub.worker.resources.requests.cpu | string | `"100m"` | |
| hub.worker.resources.requests.memory | string | `"256Mi"` | |
| ingress.annotations | object | `{}` | |
| ingress.enabled | bool | `false` | |
| ingress.hosts[0].host | string | `"k8s.formbricks.com"` | |
+106 -7
View File
@@ -114,6 +114,105 @@ hub-worker) must use this helper so they cannot drift apart.
{{- end -}}
{{- end }}
{{/*
Hub worker resource name.
*/}}
{{- define "formbricks.hubWorkerName" -}}
{{- $base := include "formbricks.name" . | trunc 52 | trimSuffix "-" }}
{{- printf "%s-hub-worker" $base | trimSuffix "-" }}
{{- end }}
{{/*
Hub embeddings runtime resource name.
*/}}
{{- define "formbricks.hubEmbeddingsName" -}}
{{- $base := include "formbricks.name" . | trunc 48 | trimSuffix "-" }}
{{- printf "%s-hub-embeddings" $base | trimSuffix "-" }}
{{- end }}
{{/*
Secret used by Hub and the embeddings runtime for the embeddings API key.
*/}}
{{- define "formbricks.hubEmbeddingsSecretName" -}}
{{- default (printf "%s-secret" (include "formbricks.hubEmbeddingsName" .)) .Values.hub.embeddings.auth.existingSecret -}}
{{- end }}
{{/*
Secret used by the embeddings runtime for Hugging Face access.
*/}}
{{- define "formbricks.hubEmbeddingsHuggingFaceSecretName" -}}
{{- default (include "formbricks.hubEmbeddingsSecretName" .) .Values.hub.embeddings.huggingFace.existingSecret -}}
{{- end }}
{{/*
Model name Hub sends to the OpenAI-compatible embeddings endpoint.
*/}}
{{- define "formbricks.hubEmbeddingsServedModelName" -}}
{{- default .Values.hub.embeddings.model .Values.hub.embeddings.servedModelName -}}
{{- end }}
{{/*
OpenAI-compatible embeddings base URL used by Hub.
*/}}
{{- define "formbricks.hubEmbeddingsBaseURL" -}}
{{- if .Values.hub.embeddings.baseUrl -}}
{{- .Values.hub.embeddings.baseUrl -}}
{{- else -}}
{{- printf "http://%s:%v/v1" (include "formbricks.hubEmbeddingsName" .) (.Values.hub.embeddings.service.port | default .Values.hub.embeddings.port) -}}
{{- end -}}
{{- end }}
{{/*
Embedding API key value for the generated embeddings secret.
*/}}
{{- define "formbricks.hubEmbeddingsApiKey" -}}
{{- $secretName := include "formbricks.hubEmbeddingsSecretName" . }}
{{- $secretKey := .Values.hub.embeddings.auth.secretKey | default "EMBEDDING_PROVIDER_API_KEY" }}
{{- $secret := (lookup "v1" "Secret" .Release.Namespace $secretName) }}
{{- if and $secret (index $secret.data $secretKey) }}
{{- index $secret.data $secretKey | b64dec -}}
{{- else if .Values.hub.embeddings.auth.apiKey }}
{{- .Values.hub.embeddings.auth.apiKey -}}
{{- else }}
{{- randAlphaNum 32 -}}
{{- end -}}
{{- end }}
{{/*
Shared Hub embedding env. These values are managed from hub.embeddings when the
self-hosted runtime is enabled so Hub API and Hub worker cannot drift.
*/}}
{{- define "formbricks.hubEmbeddingEnv" -}}
{{- $root := .root -}}
{{- if $root.Values.hub.embeddings.enabled }}
- name: EMBEDDING_PROVIDER
value: "openai"
- name: EMBEDDING_MODEL
value: {{ include "formbricks.hubEmbeddingsServedModelName" $root | quote }}
- name: EMBEDDING_BASE_URL
value: {{ include "formbricks.hubEmbeddingsBaseURL" $root | quote }}
- name: EMBEDDING_PROVIDER_API_KEY
valueFrom:
secretKeyRef:
name: {{ include "formbricks.hubEmbeddingsSecretName" $root }}
key: {{ $root.Values.hub.embeddings.auth.secretKey | default "EMBEDDING_PROVIDER_API_KEY" }}
- name: EMBEDDING_MAX_CONCURRENT
value: {{ $root.Values.hub.embeddings.maxConcurrent | quote }}
- name: EMBEDDING_NORMALIZE
value: {{ $root.Values.hub.embeddings.normalize | quote }}
{{- end }}
{{- end }}
{{/*
Returns true when an env var is managed by hub.embeddings and should not be rendered from hub.env/worker.env.
*/}}
{{- define "formbricks.hubEmbeddingEnvManaged" -}}
{{- $key := .key -}}
{{- if has $key (list "EMBEDDING_PROVIDER" "EMBEDDING_MODEL" "EMBEDDING_BASE_URL" "EMBEDDING_PROVIDER_API_KEY" "EMBEDDING_MAX_CONCURRENT" "EMBEDDING_NORMALIZE") -}}
true
{{- end -}}
{{- end }}
{{- define "formbricks.postgresAdminPassword" -}}
{{- $secret := (lookup "v1" "Secret" .Release.Namespace (include "formbricks.appSecretName" .)) }}
@@ -142,13 +241,13 @@ hub-worker) must use this helper so they cannot drift apart.
{{- end -}}
{{- end }}
{{- define "formbricks.cronSecret" -}}
{{- $secret := (lookup "v1" "Secret" .Release.Namespace (include "formbricks.appSecretName" .)) }}
{{- if $secret }}
{{- index $secret.data "CRON_SECRET" | b64dec -}}
{{- else }}
{{- randAlphaNum 32 -}}
{{- end -}}
{{- define "formbricks.cronSecret" -}}
{{- $secret := (lookup "v1" "Secret" .Release.Namespace (include "formbricks.appSecretName" .)) }}
{{- if $secret }}
{{- index $secret.data "CRON_SECRET" | b64dec -}}
{{- else }}
{{- randAlphaNum 32 -}}
{{- end -}}
{{- end }}
{{- define "formbricks.encryptionKey" -}}
@@ -14,7 +14,9 @@ metadata:
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
spec:
{{- if not .Values.hub.autoscaling.enabled }}
replicas: {{ .Values.hub.replicas | default 1 }}
{{- end }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "formbricks.hubname" . }}
@@ -66,10 +68,13 @@ spec:
secretKeyRef:
name: {{ include "formbricks.hubSecretName" . }}
key: HUB_API_KEY
{{- include "formbricks.hubEmbeddingEnv" (dict "root" $ "env" .Values.hub.env) | nindent 12 }}
{{- range $key, $value := .Values.hub.env }}
{{- if not (and $.Values.hub.embeddings.enabled (include "formbricks.hubEmbeddingEnvManaged" (dict "key" $key))) }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
{{- if .Values.hub.resources }}
resources:
{{- toYaml .Values.hub.resources | nindent 12 }}
@@ -0,0 +1,158 @@
{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled }}
{{- $embeddingsReplicas := int (.Values.hub.embeddings.replicas | default 1) -}}
{{- $embeddingsMaxReplicas := int (.Values.hub.embeddings.autoscaling.maxReplicas | default 1) -}}
{{- if and .Values.hub.embeddings.persistence.enabled (not (has "ReadWriteMany" .Values.hub.embeddings.persistence.accessModes)) (or (gt $embeddingsReplicas 1) (and .Values.hub.embeddings.autoscaling.enabled (gt $embeddingsMaxReplicas 1))) }}
{{- fail "hub.embeddings persistence with multiple replicas requires persistence.accessModes to include ReadWriteMany, or set hub.embeddings.persistence.enabled=false/use a ReadWriteMany existingClaim" }}
{{- end }}
{{- if and .Values.hub.embeddings.auth.existingSecret .Values.hub.embeddings.huggingFace.token (not .Values.hub.embeddings.huggingFace.existingSecret) }}
{{- fail "hub.embeddings.huggingFace.token cannot be stored when hub.embeddings.auth.existingSecret is set; put HF_TOKEN in the existing auth secret or set hub.embeddings.huggingFace.existingSecret" }}
{{- end }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "formbricks.hubEmbeddingsName" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-embeddings
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
spec:
{{- if not .Values.hub.embeddings.autoscaling.enabled }}
replicas: {{ .Values.hub.embeddings.replicas | default 1 }}
{{- end }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-embeddings
{{- with .Values.hub.embeddings.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.hub.embeddings.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.hub.embeddings.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.hub.embeddings.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.hub.embeddings.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.hub.embeddings.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.hub.embeddings.topologySpreadConstraints }}
topologySpreadConstraints:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.deployment.imagePullSecrets }}
imagePullSecrets:
{{- toYaml .Values.deployment.imagePullSecrets | nindent 8 }}
{{- end }}
containers:
- name: hub-embeddings
image: "{{ .Values.hub.embeddings.image.repository }}:{{ .Values.hub.embeddings.image.tag }}"
imagePullPolicy: {{ .Values.hub.embeddings.image.pullPolicy }}
{{- with .Values.hub.embeddings.command }}
command:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.hub.embeddings.args }}
args:
{{- toYaml .Values.hub.embeddings.args | nindent 12 }}
{{- else }}
args:
- --model-id
- {{ .Values.hub.embeddings.model | quote }}
- --port
- {{ .Values.hub.embeddings.port | quote }}
- --huggingface-hub-cache
- {{ .Values.hub.embeddings.persistence.mountPath | quote }}
- --served-model-name
- {{ include "formbricks.hubEmbeddingsServedModelName" . | quote }}
{{- with .Values.hub.embeddings.revision }}
- --revision
- {{ . | quote }}
{{- end }}
{{- with .Values.hub.embeddings.extraArgs }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.hub.embeddings.port }}
protocol: TCP
- name: metrics
containerPort: {{ .Values.hub.embeddings.prometheusPort }}
protocol: TCP
{{- if or .Values.hub.embeddings.auth.enabled .Values.hub.embeddings.huggingFace.existingSecret .Values.hub.embeddings.huggingFace.token (gt (len .Values.hub.embeddings.env) 0) }}
env:
{{- if .Values.hub.embeddings.auth.enabled }}
- name: API_KEY
valueFrom:
secretKeyRef:
name: {{ include "formbricks.hubEmbeddingsSecretName" . }}
key: {{ .Values.hub.embeddings.auth.secretKey | default "EMBEDDING_PROVIDER_API_KEY" }}
{{- end }}
{{- if or .Values.hub.embeddings.huggingFace.existingSecret .Values.hub.embeddings.huggingFace.token }}
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "formbricks.hubEmbeddingsHuggingFaceSecretName" . }}
key: {{ .Values.hub.embeddings.huggingFace.tokenKey | default "HF_TOKEN" }}
{{- end }}
{{- range $key, $value := .Values.hub.embeddings.env }}
{{- if not (or (and $.Values.hub.embeddings.auth.enabled (eq $key "API_KEY")) (and (or $.Values.hub.embeddings.huggingFace.existingSecret $.Values.hub.embeddings.huggingFace.token) (eq $key "HF_TOKEN"))) }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- with .Values.hub.embeddings.probes.startupProbe }}
startupProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.hub.embeddings.probes.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.hub.embeddings.probes.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.hub.embeddings.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.hub.embeddings.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.hub.embeddings.persistence.enabled }}
volumeMounts:
- name: model-cache
mountPath: {{ .Values.hub.embeddings.persistence.mountPath }}
{{- end }}
{{- if .Values.hub.embeddings.persistence.enabled }}
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: {{ default (include "formbricks.hubEmbeddingsName" .) .Values.hub.embeddings.persistence.existingClaim }}
{{- end }}
{{- end }}
@@ -0,0 +1,23 @@
{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled .Values.hub.embeddings.persistence.enabled (not .Values.hub.embeddings.persistence.existingClaim) }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "formbricks.hubEmbeddingsName" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-embeddings
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
spec:
accessModes:
{{- toYaml .Values.hub.embeddings.persistence.accessModes | nindent 4 }}
resources:
requests:
storage: {{ .Values.hub.embeddings.persistence.size }}
{{- with .Values.hub.embeddings.persistence.storageClass }}
storageClassName: {{ . | quote }}
{{- end }}
{{- end }}
@@ -0,0 +1,20 @@
{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled (not .Values.hub.embeddings.auth.existingSecret) }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ include "formbricks.hubEmbeddingsSecretName" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-embeddings
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
type: Opaque
data:
{{ .Values.hub.embeddings.auth.secretKey | default "EMBEDDING_PROVIDER_API_KEY" }}: {{ include "formbricks.hubEmbeddingsApiKey" . | b64enc }}
{{- if and (not .Values.hub.embeddings.huggingFace.existingSecret) .Values.hub.embeddings.huggingFace.token }}
{{ .Values.hub.embeddings.huggingFace.tokenKey | default "HF_TOKEN" }}: {{ .Values.hub.embeddings.huggingFace.token | b64enc }}
{{- end }}
{{- end }}
@@ -0,0 +1,35 @@
{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "formbricks.hubEmbeddingsName" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-embeddings
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
{{- with .Values.hub.embeddings.service.additionalLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.hub.embeddings.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.hub.embeddings.service.type }}
selector:
app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
ports:
- name: http
port: {{ .Values.hub.embeddings.service.port }}
targetPort: http
protocol: TCP
- name: metrics
port: {{ .Values.hub.embeddings.prometheusPort }}
targetPort: metrics
protocol: TCP
{{- end }}
+102
View File
@@ -0,0 +1,102 @@
{{- if and .Values.hub.enabled .Values.hub.autoscaling.enabled }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "formbricks.hubname" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubname" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
{{- with .Values.hub.autoscaling.additionalLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.hub.autoscaling.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "formbricks.hubname" . }}
minReplicas: {{ .Values.hub.autoscaling.minReplicas }}
maxReplicas: {{ .Values.hub.autoscaling.maxReplicas }}
metrics:
{{- toYaml .Values.hub.autoscaling.metrics | nindent 4 }}
{{- with .Values.hub.autoscaling.behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- if and .Values.hub.enabled .Values.hub.worker.enabled .Values.hub.worker.autoscaling.enabled }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "formbricks.hubWorkerName" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-worker
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
{{- with .Values.hub.worker.autoscaling.additionalLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.hub.worker.autoscaling.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "formbricks.hubWorkerName" . }}
minReplicas: {{ .Values.hub.worker.autoscaling.minReplicas }}
maxReplicas: {{ .Values.hub.worker.autoscaling.maxReplicas }}
metrics:
{{- toYaml .Values.hub.worker.autoscaling.metrics | nindent 4 }}
{{- with .Values.hub.worker.autoscaling.behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled .Values.hub.embeddings.autoscaling.enabled }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "formbricks.hubEmbeddingsName" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-embeddings
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
{{- with .Values.hub.embeddings.autoscaling.additionalLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.hub.embeddings.autoscaling.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "formbricks.hubEmbeddingsName" . }}
minReplicas: {{ .Values.hub.embeddings.autoscaling.minReplicas }}
maxReplicas: {{ .Values.hub.embeddings.autoscaling.maxReplicas }}
metrics:
{{- toYaml .Values.hub.embeddings.autoscaling.metrics | nindent 4 }}
{{- with .Values.hub.embeddings.autoscaling.behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
+129
View File
@@ -0,0 +1,129 @@
{{- if and .Values.hub.enabled .Values.hub.pdb.enabled }}
{{- $hasMinAvailable := not (kindIs "invalid" .Values.hub.pdb.minAvailable) -}}
{{- $hasMaxUnavailable := not (kindIs "invalid" .Values.hub.pdb.maxUnavailable) -}}
{{- if and $hasMinAvailable $hasMaxUnavailable }}
{{- fail "hub.pdb.minAvailable and hub.pdb.maxUnavailable are mutually exclusive; set only one" }}
{{- end }}
{{- if not (or $hasMinAvailable $hasMaxUnavailable) }}
{{- fail "hub.pdb.enabled is true but neither hub.pdb.minAvailable nor hub.pdb.maxUnavailable is set; set exactly one" }}
{{- end }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "formbricks.hubname" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubname" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
{{- with .Values.hub.pdb.additionalLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.hub.pdb.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if $hasMinAvailable }}
minAvailable: {{ .Values.hub.pdb.minAvailable }}
{{- end }}
{{- if $hasMaxUnavailable }}
maxUnavailable: {{ .Values.hub.pdb.maxUnavailable }}
{{- end }}
{{- with .Values.hub.pdb.unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ . }}
{{- end }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "formbricks.hubname" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- if and .Values.hub.enabled .Values.hub.worker.enabled .Values.hub.worker.pdb.enabled }}
{{- $hasMinAvailable := not (kindIs "invalid" .Values.hub.worker.pdb.minAvailable) -}}
{{- $hasMaxUnavailable := not (kindIs "invalid" .Values.hub.worker.pdb.maxUnavailable) -}}
{{- if and $hasMinAvailable $hasMaxUnavailable }}
{{- fail "hub.worker.pdb.minAvailable and hub.worker.pdb.maxUnavailable are mutually exclusive; set only one" }}
{{- end }}
{{- if not (or $hasMinAvailable $hasMaxUnavailable) }}
{{- fail "hub.worker.pdb.enabled is true but neither hub.worker.pdb.minAvailable nor hub.worker.pdb.maxUnavailable is set; set exactly one" }}
{{- end }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "formbricks.hubWorkerName" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-worker
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
{{- with .Values.hub.worker.pdb.additionalLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.hub.worker.pdb.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if $hasMinAvailable }}
minAvailable: {{ .Values.hub.worker.pdb.minAvailable }}
{{- end }}
{{- if $hasMaxUnavailable }}
maxUnavailable: {{ .Values.hub.worker.pdb.maxUnavailable }}
{{- end }}
{{- with .Values.hub.worker.pdb.unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ . }}
{{- end }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- if and .Values.hub.enabled .Values.hub.embeddings.enabled .Values.hub.embeddings.pdb.enabled }}
{{- $hasMinAvailable := not (kindIs "invalid" .Values.hub.embeddings.pdb.minAvailable) -}}
{{- $hasMaxUnavailable := not (kindIs "invalid" .Values.hub.embeddings.pdb.maxUnavailable) -}}
{{- if and $hasMinAvailable $hasMaxUnavailable }}
{{- fail "hub.embeddings.pdb.minAvailable and hub.embeddings.pdb.maxUnavailable are mutually exclusive; set only one" }}
{{- end }}
{{- if not (or $hasMinAvailable $hasMaxUnavailable) }}
{{- fail "hub.embeddings.pdb.enabled is true but neither hub.embeddings.pdb.minAvailable nor hub.embeddings.pdb.maxUnavailable is set; set exactly one" }}
{{- end }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "formbricks.hubEmbeddingsName" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-embeddings
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
{{- with .Values.hub.embeddings.pdb.additionalLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.hub.embeddings.pdb.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if $hasMinAvailable }}
minAvailable: {{ .Values.hub.embeddings.pdb.minAvailable }}
{{- end }}
{{- if $hasMaxUnavailable }}
maxUnavailable: {{ .Values.hub.embeddings.pdb.maxUnavailable }}
{{- end }}
{{- with .Values.hub.embeddings.pdb.unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ . }}
{{- end }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "formbricks.hubEmbeddingsName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
@@ -0,0 +1,97 @@
{{- if and .Values.hub.enabled .Values.hub.worker.enabled }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "formbricks.hubWorkerName" . }}
labels:
helm.sh/chart: {{ include "formbricks.chart" . }}
app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-worker
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }}
spec:
{{- if not .Values.hub.worker.autoscaling.enabled }}
replicas: {{ .Values.hub.worker.replicas | default 1 }}
{{- end }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "formbricks.hubWorkerName" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: hub-worker
spec:
{{- with .Values.hub.worker.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.hub.worker.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.hub.worker.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.hub.worker.topologySpreadConstraints }}
topologySpreadConstraints:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.deployment.imagePullSecrets }}
imagePullSecrets:
{{- toYaml .Values.deployment.imagePullSecrets | nindent 8 }}
{{- end }}
initContainers:
- name: wait-for-hub-api
image: {{ include "formbricks.hubImage" . }}
imagePullPolicy: {{ .Values.hub.image.pullPolicy }}
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
command:
- sh
- -c
- |
until wget --no-verbose --tries=1 --spider http://{{ include "formbricks.hubname" . }}:8080/health; do
echo "Waiting for Hub API migrations and health check..."
sleep 5
done
containers:
- name: hub-worker
image: {{ include "formbricks.hubImage" . }}
imagePullPolicy: {{ .Values.hub.image.pullPolicy }}
command:
- /app/hub-worker
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
envFrom:
- secretRef:
name: {{ include "formbricks.hubSecretName" . }}
{{- if or .Values.hub.embeddings.enabled (gt (len .Values.hub.env) 0) (gt (len .Values.hub.worker.env) 0) }}
env:
{{- $workerEnv := merge (dict) .Values.hub.env .Values.hub.worker.env }}
{{- include "formbricks.hubEmbeddingEnv" (dict "root" $ "env" $workerEnv) | nindent 12 }}
{{- range $key, $value := .Values.hub.env }}
{{- if and (not (hasKey $.Values.hub.worker.env $key)) (not (and $.Values.hub.embeddings.enabled (include "formbricks.hubEmbeddingEnvManaged" (dict "key" $key)))) }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
{{- range $key, $value := .Values.hub.worker.env }}
{{- if not (and $.Values.hub.embeddings.enabled (include "formbricks.hubEmbeddingEnvManaged" (dict "key" $key))) }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.hub.worker.resources }}
resources:
{{- toYaml .Values.hub.worker.resources | nindent 12 }}
{{- end }}
{{- end }}
+235
View File
@@ -588,6 +588,241 @@ hub:
# Optional env vars (non-secret). Use existingSecret for secret values such as DATABASE_URL and HUB_API_KEY.
env: {}
# Optional autoscaling for the Hub API deployment.
autoscaling:
enabled: false
additionalLabels: {}
annotations: {}
minReplicas: 1
maxReplicas: 3
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior: {}
# Optional PDB for the Hub API deployment. Disabled by default because a single
# Hub replica with minAvailable: 1 blocks voluntary node drains.
pdb:
enabled: false
additionalLabels: {}
annotations: {}
minAvailable: 1
# maxUnavailable: 1
# unhealthyPodEvictionPolicy: AlwaysAllow
worker:
# Hub async jobs (webhook dispatch, embeddings) run in hub-worker. Keep this
# enabled unless another worker deployment processes the same River queues.
enabled: true
replicas: 1
# Optional env vars (non-secret) added only to hub-worker.
env: {}
resources:
limits:
memory: 512Mi
requests:
memory: 256Mi
cpu: "100m"
autoscaling:
enabled: false
additionalLabels: {}
annotations: {}
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 120
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 2
periodSeconds: 60
# Disabled by default because the default worker replica count is 1.
pdb:
enabled: false
additionalLabels: {}
annotations: {}
minAvailable: 1
# maxUnavailable: 1
# unhealthyPodEvictionPolicy: AlwaysAllow
nodeSelector: {}
tolerations: []
affinity: {}
topologySpreadConstraints: []
embeddings:
# Optional self-hosted OpenAI-compatible embeddings runtime for Hub.
enabled: false
runtime: tei
model: google/embeddinggemma-300m
revision: ""
# Defaults to `model` when empty. Used by TEI OpenAI-compatible responses
# and as Hub's EMBEDDING_MODEL.
servedModelName: ""
# Defaults to http://<release>-hub-embeddings:<service.port>/v1 when empty.
baseUrl: ""
maxConcurrent: "5"
normalize: "false"
replicas: 1
image:
repository: "ghcr.io/huggingface/text-embeddings-inference"
tag: "cpu-1.9"
pullPolicy: IfNotPresent
command: []
# When empty, the chart renders TEI args from model, servedModelName, port,
# revision, and persistence.mountPath. Set this to fully override args.
args: []
extraArgs: []
env: {}
port: 8080
prometheusPort: 9000
service:
type: ClusterIP
port: 8080
annotations: {}
additionalLabels: {}
auth:
# TEI can enforce bearer-token auth with API_KEY. Hub always receives the
# same key as EMBEDDING_PROVIDER_API_KEY because the OpenAI-compatible Hub
# provider requires an API key.
enabled: true
existingSecret: ""
secretKey: EMBEDDING_PROVIDER_API_KEY
apiKey: ""
huggingFace:
# Required for gated models such as google/embeddinggemma-300m unless the
# model is pre-cached or a different ungated model is configured.
existingSecret: ""
tokenKey: HF_TOKEN
token: ""
persistence:
enabled: true
existingClaim: ""
storageClass: ""
accessModes:
- ReadWriteOnce
size: 10Gi
mountPath: /data
resources:
requests:
cpu: "4"
memory: 8Gi
limits:
memory: 8Gi
autoscaling:
enabled: false
additionalLabels: {}
annotations: {}
minReplicas: 1
maxReplicas: 2
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 600
policies:
- type: Pods
value: 1
periodSeconds: 300
scaleUp:
stabilizationWindowSeconds: 120
policies:
- type: Pods
value: 1
periodSeconds: 120
# Disabled by default because the default embeddings replica count is 1.
pdb:
enabled: false
additionalLabels: {}
annotations: {}
minAvailable: 1
# maxUnavailable: 1
# unhealthyPodEvictionPolicy: AlwaysAllow
probes:
startupProbe:
failureThreshold: 60
periodSeconds: 10
tcpSocket:
port: http
readinessProbe:
failureThreshold: 6
periodSeconds: 10
timeoutSeconds: 5
tcpSocket:
port: http
livenessProbe:
failureThreshold: 6
periodSeconds: 20
timeoutSeconds: 5
tcpSocket:
port: http
podAnnotations: {}
podLabels: {}
podSecurityContext: {}
# Keep empty by default because upstream model-serving images may define
# their own user and need write access to the model cache path.
securityContext: {}
nodeSelector: {}
tolerations: []
affinity: {}
topologySpreadConstraints: []
# Helm does not deploy Cube. XM Suite v5 analytics requires operators to provide an external Cube instance,
# set deployment.env.CUBEJS_API_URL, and supply CUBEJS_API_SECRET via an existing secret.
+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"]
: []),
],
});
}