diff --git a/.github/workflows/scripts/validate-helm-config-fields.sh b/.github/workflows/scripts/validate-helm-config-fields.sh index 40b61427d1e..d24079779e9 100755 --- a/.github/workflows/scripts/validate-helm-config-fields.sh +++ b/.github/workflows/scripts/validate-helm-config-fields.sh @@ -219,6 +219,24 @@ assert_field_value 'client.mcp_code_mode_binding_level' '.client.mcp_code_mode_b assert_field_value 'client.mcp_tool_sync_interval' '.client.mcp_tool_sync_interval' '60' assert_field_value 'client.hide_deleted_virtual_keys_in_filters' '.client.hide_deleted_virtual_keys_in_filters' 'true' +############################################################################### +# 1b. Server Config +############################################################################### +echo "" +echo -e "${CYAN}🖥️ 1b - Server Config${NC}" +echo "----------------------" + +cat > "$TMPDIR/values-server.yaml" << 'VALS' +image: + tag: v1.0.0 +bifrost: + server: + readBufferSize: 131072 +VALS + +render_config "$TMPDIR/values-server.yaml" +assert_field_value 'server.read_buffer_size' '.server.read_buffer_size' '131072' + ############################################################################### # 2. Framework (Pricing) ############################################################################### diff --git a/.github/workflows/scripts/validate-helm-templates.sh b/.github/workflows/scripts/validate-helm-templates.sh index ccab87838e7..c3730922446 100755 --- a/.github/workflows/scripts/validate-helm-templates.sh +++ b/.github/workflows/scripts/validate-helm-templates.sh @@ -310,6 +310,10 @@ test_template "cluster: region (Gap 7)" \ --set bifrost.cluster.gossip.config.failureThreshold=3 \ --set bifrost.cluster.region=us-east-1 +# Gap 9: Server config +test_template "server: readBufferSize (Gap 9)" \ + --set bifrost.server.readBufferSize=131072 + # Gap 8: Combined production-like with all new fields test_template "combined: all new Gap 1-8 fields" \ --set bifrost.client.asyncJobResultTTL=300 \ diff --git a/examples/k8s/examples/values-ingress-legacy.yaml b/examples/k8s/examples/values-ingress-legacy.yaml new file mode 100644 index 00000000000..62e6f1e5167 --- /dev/null +++ b/examples/k8s/examples/values-ingress-legacy.yaml @@ -0,0 +1,24 @@ +# Legacy ingress format — single Ingress resource named "". +# Use this when you only need one ingress controller. +# +# Test with: +# helm template bifrost ../bifrost/helm-charts/bifrost -f values-ingress-legacy.yaml +# +image: + repository: maximhq/bifrost + tag: latest + +ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + hosts: + - host: bifrost.example.com + paths: + - path: / + pathType: Prefix + tls: + - hosts: + - bifrost.example.com + secretName: bifrost-tls diff --git a/examples/k8s/examples/values-ingress-named.yaml b/examples/k8s/examples/values-ingress-named.yaml new file mode 100644 index 00000000000..1dde818f4cd --- /dev/null +++ b/examples/k8s/examples/values-ingress-named.yaml @@ -0,0 +1,36 @@ +# Named ingress map format — one Ingress resource per key, named "-". +# Use this when you need multiple ingress controllers (e.g. public + internal). +# +# Test with: +# helm template bifrost ../bifrost/helm-charts/bifrost -f values-ingress-named.yaml +# +image: + repository: maximhq/bifrost + tag: latest + +ingress: + public: + enabled: true + className: nginx-public + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + hosts: + - host: bifrost.example.com + paths: + - path: / + pathType: Prefix + tls: + - hosts: + - bifrost.example.com + secretName: bifrost-tls-public + + internal: + enabled: true + className: nginx-internal + annotations: {} + hosts: + - host: bifrost.internal.example.com + paths: + - path: / + pathType: Prefix + tls: [] diff --git a/helm-charts/bifrost/templates/NOTES.txt b/helm-charts/bifrost/templates/NOTES.txt index e24712edaa2..c4570475191 100644 --- a/helm-charts/bifrost/templates/NOTES.txt +++ b/helm-charts/bifrost/templates/NOTES.txt @@ -1,12 +1,28 @@ Bifrost has been installed! -{{- if .Values.ingress.enabled }} +{{- $isLegacy := true }} +{{- range $k, $v := .Values.ingress }} +{{- if and (kindIs "map" $v) (hasKey $v "enabled") }}{{- $isLegacy = false }}{{- end }} +{{- end }} +{{- if and .Values.ingress $isLegacy .Values.ingress.enabled }} 1. Access Bifrost at: {{- range .Values.ingress.hosts }} http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }} {{- end }} +{{- else if and .Values.ingress (not $isLegacy) }} + +1. Access Bifrost at: +{{- range (keys .Values.ingress | sortAlpha) }} +{{- $ing := index $.Values.ingress . }} +{{- if and (kindIs "map" $ing) $ing.enabled }} +{{- range $ing.hosts }} + http{{ if $ing.tls }}s{{ end }}://{{ .host }} +{{- end }} +{{- end }} +{{- end }} + {{- else if eq .Values.service.type "LoadBalancer" }} 1. Get the LoadBalancer IP: diff --git a/helm-charts/bifrost/templates/_helpers.tpl b/helm-charts/bifrost/templates/_helpers.tpl index 6612ba33488..04515d9b590 100644 --- a/helm-charts/bifrost/templates/_helpers.tpl +++ b/helm-charts/bifrost/templates/_helpers.tpl @@ -318,6 +318,16 @@ false {{- end }} {{- $_ := set $config "client" $client }} {{- end }} +{{- /* Server */ -}} +{{- if .Values.bifrost.server }} +{{- $server := dict }} +{{- if .Values.bifrost.server.readBufferSize }} +{{- $_ := set $server "read_buffer_size" .Values.bifrost.server.readBufferSize }} +{{- end }} +{{- if $server }} +{{- $_ := set $config "server" $server }} +{{- end }} +{{- end }} {{- /* Framework */ -}} {{- if .Values.bifrost.framework }} {{- $framework := dict }} @@ -332,7 +342,13 @@ false {{- if .Values.bifrost.framework.pricing.pricingSyncInterval }} {{- $_ := set $pricing "pricing_sync_interval" .Values.bifrost.framework.pricing.pricingSyncInterval }} {{- end }} -{{- if or $pricing.pricing_url $pricing.pricing_sync_interval }} +{{- if .Values.bifrost.framework.pricing.mcpLibraryUrl }} +{{- $_ := set $pricing "mcp_library_url" .Values.bifrost.framework.pricing.mcpLibraryUrl }} +{{- end }} +{{- if .Values.bifrost.framework.pricing.mcpLibrarySyncInterval }} +{{- $_ := set $pricing "mcp_library_sync_interval" .Values.bifrost.framework.pricing.mcpLibrarySyncInterval }} +{{- end }} +{{- if or $pricing.pricing_url $pricing.model_parameters_url $pricing.pricing_sync_interval $pricing.mcp_library_url $pricing.mcp_library_sync_interval }} {{- $_ := set $framework "pricing" $pricing }} {{- end }} {{- end }} diff --git a/helm-charts/bifrost/templates/ingress.yaml b/helm-charts/bifrost/templates/ingress.yaml index a9269cbc10e..0c3a2cef10b 100644 --- a/helm-charts/bifrost/templates/ingress.yaml +++ b/helm-charts/bifrost/templates/ingress.yaml @@ -1,3 +1,10 @@ +{{- if .Values.ingress }} +{{- $isLegacy := true }} +{{- range $k, $v := .Values.ingress }} +{{- if and (kindIs "map" $v) (hasKey $v "enabled") }}{{- $isLegacy = false }}{{- end }} +{{- end }} +{{- if $isLegacy }} +{{- /* Single ingress (legacy format) — no non-legacy keys present */}} {{- if .Values.ingress.enabled -}} apiVersion: networking.k8s.io/v1 kind: Ingress @@ -40,3 +47,54 @@ spec: {{- end }} {{- end }} {{- end }} +{{- else }} +{{- /* Named ingresses map (new format) */}} +{{- range (keys .Values.ingress | sortAlpha) }} +{{- $name := . }} +{{- $ing := index $.Values.ingress $name }} +{{- if and (kindIs "map" $ing) $ing.enabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "bifrost.fullname" $ }}-{{ $name }} + namespace: {{ $.Release.Namespace }} + labels: + {{- include "bifrost.labels" $ | nindent 4 }} + {{- with $ing.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if $ing.className }} + ingressClassName: {{ $ing.className }} + {{- end }} + {{- if $ing.tls }} + tls: + {{- range $ing.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range $ing.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "bifrost.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/helm-charts/bifrost/values.schema.json b/helm-charts/bifrost/values.schema.json index ddb7d97f104..5a98b39f3a0 100644 --- a/helm-charts/bifrost/values.schema.json +++ b/helm-charts/bifrost/values.schema.json @@ -108,47 +108,109 @@ } }, "ingress": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "className": { - "type": "string" - }, - "annotations": { - "type": "object" + "description": "Ingress configuration. Either a single ingress (legacy, with top-level 'enabled') or a map of named ingresses (new format, each key becomes a separate Ingress resource named -).", + "oneOf": [ + { + "type": "object", + "description": "Single ingress (legacy format).", + "properties": { + "enabled": { + "type": "boolean" + }, + "className": { + "type": "string" + }, + "annotations": { + "type": "object" + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "required": ["host", "paths"], + "properties": { + "host": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "type": "object", + "required": ["path", "pathType"], + "properties": { + "path": { + "type": "string" + }, + "pathType": { + "type": "string", + "enum": ["Prefix", "Exact", "ImplementationSpecific"] + } + } + } + } + } + } + }, + "tls": { + "type": "array" + } + }, + "required": ["enabled"] }, - "hosts": { - "type": "array", - "items": { + { + "type": "object", + "description": "Named ingresses map. Each key produces a separate Ingress named -. Keys must be valid DNS-1123 label segments (lowercase alphanumeric and hyphens only, no underscores or uppercase) so the resulting Ingress name is a valid Kubernetes resource name.", + "additionalProperties": { "type": "object", "properties": { - "host": { + "enabled": { + "type": "boolean" + }, + "className": { "type": "string" }, - "paths": { + "annotations": { + "type": "object" + }, + "hosts": { "type": "array", "items": { "type": "object", + "required": ["host", "paths"], "properties": { - "path": { + "host": { "type": "string" }, - "pathType": { - "type": "string", - "enum": ["Prefix", "Exact", "ImplementationSpecific"] + "paths": { + "type": "array", + "items": { + "type": "object", + "required": ["path", "pathType"], + "properties": { + "path": { + "type": "string" + }, + "pathType": { + "type": "string", + "enum": ["Prefix", "Exact", "ImplementationSpecific"] + } + } + } } } } + }, + "tls": { + "type": "array" } - } + }, + "required": ["enabled"] + }, + "not": { + "required": ["enabled"] } - }, - "tls": { - "type": "array" } - } + ] }, "resources": { "type": "object", @@ -466,6 +528,18 @@ }, "additionalProperties": false }, + "server": { + "type": "object", + "description": "HTTP server configuration", + "properties": { + "readBufferSize": { + "type": "integer", + "description": "Read buffer size in bytes. This controls the size of the buffer used for reading HTTP headers.", + "default": 65536 + } + }, + "additionalProperties": false + }, "framework": { "type": "object", "properties": { @@ -485,6 +559,19 @@ "description": "Pricing sync interval in seconds. Default is 24 hours. Minimum is 3600 seconds (1 hour).", "default": 86400, "minimum": 3600 + }, + "mcpLibraryUrl": { + "description": "URL to a custom MCP server catalog. Leave empty to use the default Bifrost catalog.", + "anyOf": [ + {"type": "string", "format": "uri"}, + {"const": ""} + ] + }, + "mcpLibrarySyncInterval": { + "type": "integer", + "description": "MCP library sync interval in seconds. Default is 24 hours. Minimum is 3600 seconds (1 hour).", + "default": 86400, + "minimum": 3600 } }, "additionalProperties": false @@ -2174,6 +2261,15 @@ "role": { "type": "string", "description": "Bifrost role to assign on match" + }, + "attributeType": { + "type": "string", + "enum": ["user", "group"], + "description": "SCIM provisioning type: 'user' matches SCIM User attributes, 'group' matches SCIM Group displayName" + }, + "attributeValue": { + "type": "string", + "description": "SCIM attribute value to match (for attributeType 'user': the SCIM user attribute value; for 'group': the SCIM group displayName, auto-set to 'displayName')" } }, "required": ["attribute", "value", "role"], @@ -2216,7 +2312,7 @@ "type": "object", "properties": { "attribute": { "type": "string", "description": "JWT claim name" }, - "value": { "type": "string", "description": "Claim value to match" }, + "value": { "type": "string", "description": "Claim value to match. Wildcard '*' will use the claim as is the name of the business unit to assign." }, "business_unit": { "type": "string", "description": "Bifrost business unit slug to assign" @@ -2231,7 +2327,7 @@ "description": "SCIM attribute value to match (for 'user': SCIM user attribute value; for 'group': SCIM group displayName, auto-set to 'displayName')" } }, - "required": ["attribute", "value", "business_unit"], + "required": ["attribute", "value"], "additionalProperties": false } } @@ -2319,6 +2415,15 @@ "role": { "type": "string", "description": "Bifrost role to assign on match" + }, + "attributeType": { + "type": "string", + "enum": ["user", "group"], + "description": "SCIM provisioning type: 'user' matches SCIM User attributes, 'group' matches SCIM Group displayName" + }, + "attributeValue": { + "type": "string", + "description": "SCIM attribute value to match (for attributeType 'user': the SCIM user attribute value; for 'group': the SCIM group displayName, auto-set to 'displayName')" } }, "required": ["attribute", "value", "role"], @@ -2327,13 +2432,22 @@ }, "attributeTeamMappings": { "type": "array", - "description": "Attribute -> team mappings (all matches apply). Use value '*' for pass-through (every claim value becomes a team name).", + "description": "Attribute -> team mappings (all matches apply). Use value '*' for pass-through. Add attributeType/attributeValue to enable SCIM provisioning for a mapping.", "items": { "type": "object", "properties": { - "attribute": { "type": "string" }, - "value": { "type": "string" }, - "team": { "type": "string" } + "attribute": { "type": "string", "description": "JWT claim name" }, + "value": { "type": "string", "description": "Claim value to match, or '*' for pass-through" }, + "team": { "type": "string", "description": "Bifrost team slug to assign. In case of '*' value, leave this empty" }, + "attributeType": { + "type": "string", + "enum": ["user", "group"], + "description": "SCIM provisioning type: 'user' matches SCIM User attributes, 'group' matches SCIM Group displayName" + }, + "attributeValue": { + "type": "string", + "description": "SCIM attribute value to match (for 'user': SCIM user attribute value; for 'group': SCIM group displayName, auto-set to 'displayName')" + } }, "required": ["attribute", "value", "team"], "additionalProperties": false @@ -2341,15 +2455,24 @@ }, "attributeBusinessUnitMappings": { "type": "array", - "description": "Attribute -> business-unit mappings (all matches apply).", + "description": "Attribute -> business-unit mappings (all matches apply). Add attributeType/attributeValue to enable SCIM provisioning for a mapping.", "items": { "type": "object", "properties": { - "attribute": { "type": "string" }, - "value": { "type": "string" }, - "business_unit": { "type": "string" } + "attribute": { "type": "string", "description": "JWT claim name" }, + "value": { "type": "string", "description": "Claim value to match. Wildcard '*' will use the claim as is the name of the business unit to assign." }, + "business_unit": { "type": "string", "description": "Bifrost business unit slug to assign. In case of '*' value, leave this empty" }, + "attributeType": { + "type": "string", + "enum": ["user", "group"], + "description": "SCIM provisioning type: 'user' matches SCIM User attributes, 'group' matches SCIM Group displayName" + }, + "attributeValue": { + "type": "string", + "description": "SCIM attribute value to match (for 'user': SCIM user attribute value; for 'group': SCIM group displayName, auto-set to 'displayName')" + } }, - "required": ["attribute", "value", "business_unit"], + "required": ["attribute", "value"], "additionalProperties": false } } @@ -2458,10 +2581,10 @@ "type": "object", "properties": { "attribute": { "type": "string" }, - "value": { "type": "string" }, + "value": { "type": "string", "description": "Claim value to match. Wildcard '*' will use the claim as is the name of the business unit to assign." }, "business_unit": { "type": "string" } }, - "required": ["attribute", "value", "business_unit"], + "required": ["attribute", "value"], "additionalProperties": false } } @@ -2560,10 +2683,10 @@ "type": "object", "properties": { "attribute": { "type": "string" }, - "value": { "type": "string" }, + "value": { "type": "string", "description": "Claim value to match. Wildcard '*' will use the claim as is the name of the business unit to assign." }, "business_unit": { "type": "string" } }, - "required": ["attribute", "value", "business_unit"], + "required": ["attribute", "value"], "additionalProperties": false } } @@ -2672,10 +2795,10 @@ "type": "object", "properties": { "attribute": { "type": "string" }, - "value": { "type": "string" }, + "value": { "type": "string", "description": "Claim value to match. Wildcard '*' will use the claim as is the name of the business unit to assign." }, "business_unit": { "type": "string" } }, - "required": ["attribute", "value", "business_unit"], + "required": ["attribute", "value"], "additionalProperties": false } } @@ -4292,6 +4415,78 @@ }, "required": ["url", "model_name"], "additionalProperties": false + }, + "aliases": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "description": "Legacy shape: a bare provider-specific identifier. Equivalent to {\"model_id\": \"\"}." + }, + { + "type": "object", + "properties": { + "model_id": { + "type": "string", + "minLength": 1, + "description": "Provider-specific identifier sent on the wire (deployment name, inference profile ID, fine-tuned model ID, etc.)." + }, + "model_name": { + "type": "string", + "description": "Canonical model name used for pricing, logging, and family inference." + }, + "model_family": { + "type": "string", + "enum": ["anthropic", "openai", "mistral", "cohere", "gemini", "nova", "titan"], + "description": "Underlying model family. Used by provider routing without substring-sniffing the wire model ID." + }, + "description": { + "type": "string" + }, + "region": { + "type": "string", + "description": "Per-alias region override (can use env. prefix)." + }, + "api_version": { + "type": "string", + "description": "Azure OpenAI api-version override for this alias." + }, + "anthropic_version": { + "type": "string", + "description": "Azure anthropic-version header override for Claude-on-Azure deployments." + }, + "endpoint": { + "type": "string", + "description": "Per-alias Azure endpoint override (can use env. prefix)." + }, + "project_id": { + "type": "string", + "description": "Per-alias Vertex project ID override (can use env. prefix)." + }, + "project_number": { + "type": "string", + "description": "Per-alias Vertex project number override (can use env. prefix)." + }, + "inference_profile_arn": { + "type": "string", + "description": "Per-alias Bedrock inference profile ARN (can use env. prefix)." + }, + "use_deployments_endpoint": { + "type": "boolean", + "description": "Replicate: use the deployments endpoint instead of the predictions endpoint for this alias." + } + }, + "required": ["model_id"], + "additionalProperties": false + } + ] + }, + "propertyNames": { + "minLength": 1 + }, + "description": "Model alias mappings: each entry maps a user-facing model name to either a bare provider identifier (legacy string shape) or an AliasConfig object carrying the wire identifier plus optional canonical name, family, and provider-specific overrides." } }, "required": ["name", "weight"], diff --git a/helm-charts/bifrost/values.yaml b/helm-charts/bifrost/values.yaml index 4f3100e0c54..4e65d70cf47 100644 --- a/helm-charts/bifrost/values.yaml +++ b/helm-charts/bifrost/values.yaml @@ -75,6 +75,7 @@ service: port: 8080 annotations: {} +# Single ingress (legacy format): ingress: enabled: false className: "" @@ -85,6 +86,30 @@ ingress: - path: / pathType: Prefix tls: [] +# Named ingresses map (new format) — replaces the single ingress above. +# Each key becomes a separate Ingress resource named "-". +# Use this when you need multiple ingress controllers (e.g. public + internal). +# ingress: +# public: +# enabled: true +# className: nginx-public +# annotations: {} +# hosts: +# - host: bifrost.example.com +# paths: +# - path: / +# pathType: Prefix +# tls: [] +# internal: +# enabled: true +# className: nginx-internal +# annotations: {} +# hosts: +# - host: bifrost.internal.example.com +# paths: +# - path: / +# pathType: Prefix +# tls: [] resources: limits: @@ -257,6 +282,10 @@ bifrost: # whitelistedRoutes: [] # Routes that bypass auth middleware # routingChainMaxDepth: 10 # Maximum depth for routing rule chain evaluation + # Server configuration + server: + readBufferSize: 65536 # Read buffer size in bytes for reading HTTP headers (default: 64 KiB) + # Framework configuration framework: pricing: @@ -266,6 +295,10 @@ bifrost: modelParametersUrl: "https://getbifrost.ai/datasheet/model-parameters" # Sync interval in seconds (default: 86400 = 24 hours, minimum: 3600) pricingSyncInterval: 86400 + # Custom MCP server catalog URL (optional, leave empty to use the default Bifrost catalog) + # mcpLibraryUrl: "" + # MCP library sync interval in seconds (default: 86400 = 24 hours, minimum: 3600) + # mcpLibrarySyncInterval: 86400 # Provider configurations (add your provider keys here) # You can specify API keys directly or use env.VAR_NAME syntax to reference environment variables