Automatic Database Migration
If you are running Bifrost with a database (SQLite or Postgres), existing data is automatically migrated on startup. You do not need to manually update your database records. The following automatic migrations run on upgrade:- Provider keys with
models: []are converted tomodels: ["*"] - Virtual Key provider configs with
allowed_models: []are converted toallowed_models: ["*"] - Virtual Keys with no
provider_configsare backfilled with all currently configured providers (allowed_models: ["*"],key_ids: ["*"]) - Virtual Keys with no
mcp_configsare backfilled with all currently connected MCP clients (tools_to_execute: ["*"]) - Per-provider
deploymentsmaps (Azure, Bedrock, Vertex, Replicate) are migrated into the unifiedaliasesfield
config.json or the REST API - must follow the new semantics described below.
Breaking Change 1: Empty Array Now Means “Deny All”
v1.5.0 flips the meaning of empty arrays across all allow-list fields:| What you write | v1.4.x meaning | v1.5.0 meaning |
|---|---|---|
[] (empty array) | Allow all | Allow none |
["*"] (wildcard) | Not applicable | Allow all |
["a", "b"] | Only a and b | Only a and b (unchanged) |
| Field | Where |
|---|---|
models | Provider key |
allowed_models | Virtual Key provider config |
key_ids | Virtual Key provider config |
tools_to_execute | Virtual Key MCP config |
Provider key models
Before:
models: [] → key served all models
After:
Virtual Key allowed_models
Before:
allowed_models → all models allowed
After:
Virtual Key MCP tools_to_execute
Before:
Breaking Change 2: allowed_keys Renamed to key_ids
The field used to restrict which provider API keys a Virtual Key can use has been renamed from allowed_keys to key_ids. The deny-by-default rule also applies - omitting the field or setting it to [] now blocks all keys.
allowed_models, there is no automatic database migration for key_ids. An empty or omitted key_ids disables all key selection. You must explicitly use ["*"] to restore allow-all behavior.Breaking Change 3: Virtual Key provider_configs is Deny-by-Default
In v1.4.x, a Virtual Key with no provider_configs had access to all providers. In v1.5.0, it blocks all providers.
Before: "provider_configs": [] → access to all providers
After: "provider_configs": [] → no provider access
To allow all providers, list each one explicitly:
provider_configs. However, any VK created after upgrading must include explicit provider configs.
Breaking Change 4: WhiteList Validation
Two new validation rules are enforced on all allow-list fields. The API returns HTTP 400 if either is violated. Rule 1: Wildcard cannot be mixed with other valuesmodels, allowed_models, key_ids, tools_to_execute, tools_to_auto_execute, allowed_extra_headers.
Breaking Change 5: weight is Now Nullable
The weight field on a Virtual Key provider config was previously a required float64. It is now an optional *float64.
weight: 0.5- provider participates in weighted load balancingweight: null/ omitted - provider is accessible for direct routing but excluded from weighted selection
weight may now be null. Update any client code that assumes it is always a number.
Breaking Change 6: Virtual Key budget Changed to Multi-Budget budgets
The budget model on Virtual Keys and provider configs has been restructured from a single budget to support multiple budgets with different reset intervals.
In v1.4.x, a Virtual Key had a single budget_id foreign key pointing to one budget, and each provider config also had a single budget_id. In v1.5.0, the association is inverted: budgets now reference their parent via virtual_key_id or provider_config_id, and both Virtual Keys and provider configs support an array of budgets.
The database migration runs automatically on startup, converting existing single-budget associations into the new multi-budget structure.
API request changes
Creating a Virtual Key with a budget: Before:API response changes
| Field | v1.4.x | v1.5.0 |
|---|---|---|
Virtual Key budget_id | Present | Removed |
Virtual Key budget | Single object | Removed - replaced by budgets array |
Virtual Key budgets | Not present | Array of budget objects |
Provider config budget_id | Present | Removed |
Provider config budget | Single object | Removed - replaced by budgets array |
Provider config budgets | Not present | Array of budget objects |
calendar_aligned has moved from the budget level to the Virtual Key level. It now applies uniformly to all budgets under a VK.Viewing budget for a given key
Budgets are returned as part of the Virtual Key response on all admin endpoints:GET /api/governance/virtual-keys- list all VKs (includesbudgetson each VK and each provider config)GET /api/governance/virtual-keys/{id}- get a single VK with full budget detailsGET /api/governance/virtual-keys/quota- self-service endpoint (authenticate with the VK value viax-bf-vkheader)
Breaking Change 7: Provider Keys API Separated
Provider key management now has dedicated endpoints. Thekeys field has been removed from all provider API requests and responses.
What changed
| Before (v1.4.x) | After (v1.5.0) |
|---|---|
GET /api/providers/{p} returns keys | keys field removed from provider response |
POST /api/providers accepts keys | keys field ignored - create keys separately |
PUT /api/providers/{p} accepts keys | keys field ignored - update keys via dedicated endpoints |
New endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/providers/{provider}/keys | List all keys |
GET | /api/providers/{provider}/keys/{key_id} | Get a single key |
POST | /api/providers/{provider}/keys | Create a key |
PUT | /api/providers/{provider}/keys/{key_id} | Update a key |
DELETE | /api/providers/{provider}/keys/{key_id} | Delete a key |
How to update
Creating a provider with keys: Before:curl localhost:8080/api/providers/openai | jq '.keys'
After: curl localhost:8080/api/providers/openai/keys | jq '.keys'
Updating / deleting keys:
Before: Bulk replace via provider update:
Breaking Change 8: Compat Plugin Restructured
Theenable_litellm_fallbacks option has been removed and replaced with three granular options.
Before:
| Old option | New option | Description |
|---|---|---|
enable_litellm_fallbacks | convert_text_to_chat | Text completion → chat completion fallback |
| (new) | convert_chat_to_responses | Chat completion → Responses API fallback |
| (new) | should_drop_params | Drop unsupported OpenAI-compatible params |
| Field | Change |
|---|---|
extra_fields.litellm_compat | Removed |
extra_fields.dropped_compat_plugin_params | Added - lists params dropped by this plugin |
extra_fields.converted_request_type | Added - the request type it was converted to |
Breaking Change 9: Replicate Image Edits Removed from Generations Endpoint
The/v1/images/generations endpoint on the Replicate provider no longer accepts image editing parameters (source image, mask). It now only handles text-to-image generation.
If you were passing image editing parameters to /v1/images/generations on Replicate, switch to /v1/images/edits.
/v1/images/edits on Replicate is also being removed in a follow-up release. Plan to migrate to an alternative provider.Breaking Change 10: Provider deployments removed - migrate to aliases
Provider deployment mappings should now live in the top-level aliases field on each key. Aliases work across all providers and map any model name to a provider-specific identifier (deployment name, inference profile ARN, fine-tuned model ID, etc.).
The database migration runs automatically on startup, migrating existing deployment data into aliases. Only config.json files need to be updated manually.
Azure
Before:Bedrock
Before:Vertex
Before:Replicate
The Replicate key config is also restructured. Thedeployments map is gone. A new boolean use_deployments_endpoint controls whether requests are routed through the Deployments API (private, fixed hardware) or the standard Models API.
Before:
| Old field | New field | Notes |
|---|---|---|
replicate_key_config.deployments | Removed | Use top-level aliases |
| (new) | replicate_key_config.use_deployments_endpoint | bool, default false |
Breaking Change 11: Go SDK - ExtraFields Model Fields Renamed
ModelRequested string has been replaced by two fields on BifrostResponseExtraFields and BifrostErrorExtraFields.
Before:
BifrostErrorExtraFields.
JSON tag changes:
| Old | New |
|---|---|
"model_requested" | "original_model_requested" + "resolved_model_used" |
Breaking Change 12: Go SDK - StreamAccumulatorResult Field Renamed
Model string has been replaced by two fields on StreamAccumulatorResult (returned by tracer streaming accumulation methods).
Before:
Breaking Change 13: selected_key_id Cleared on Terminal Retry Failures
With the introduction of multi-key retry rotation, selected_key_id (and selected_key_name) in the request context are cleared when all retry attempts fail. Previously, these fields always reflected the key that was selected for the request, even on error.
The attempt_trail is now the authoritative record of every key tried and why each attempt failed.
What changed
| Field | Before | After |
|---|---|---|
selected_key_id | Always set, even on error | Empty string when all retries exhausted |
selected_key_name | Always set, even on error | Empty string when all retries exhausted |
attempt_trail | Not present | Array of { attempt, key_id, key_name, fail_reason, triggered_rotation } per attempt. fail_reason is set on every failed attempt; triggered_rotation is always present, set to true only when this attempt’s per-key failure (rate-limit (429), auth (401/403), or billing (402)) caused the next retry to switch keys, false otherwise. |
Impact on logging and telemetry plugins
The built-in logging plugin writesselected_key_id and selected_key_name directly to each log record. For multi-key requests that exhaust all retries, both fields will be empty in the stored log entry. The attempt_trail column captures the full per-attempt key history and is the correct field to use for failure attribution.
The built-in telemetry plugin emits selected_key_id and selected_key_name as span attributes. For exhausted-retry failures these attributes will be empty strings on the error span. The attempt_trail span attribute contains the full rotation history.
If you run a custom plugin or downstream log consumer that filters or groups by selected_key_id to track which key caused a failure, you must update it to handle the empty-string case and read from attempt_trail when attribution is needed.
How to update
If you readselected_key_id from plugin context to attribute failed requests:
Before:
selected_key_id from the logging REST API:
The selected_key_id field on a LogEntry may now be an empty string when the request failed after exhausting all retries. Use attempt_trail for the full per-attempt key history.
x-bf-key-id / x-bf-key-name), and session-sticky requests are unaffected - they never rotate keys, so selected_key_id remains populated on failure for those flows.Breaking Change 14: Direct Key Bypass Removed (HTTP Gateway and Go SDK)
The “Direct Key Bypass” feature has been removed entirely in v1.5.0, on both surfaces:- HTTP gateway: the
allow_direct_keysconfig flag and the header pass-through (Authorization,x-api-key,x-goog-api-key, plus the Bedrockx-bf-bedrock-*and Azurex-bf-azure-endpointintegration paths) no longer forward keys to upstream providers. - Go SDK: the
schemas.BifrostContextKeyDirectKeycontext value has been deleted, along with the documented “Direct Key (Go SDK Only)” API.
BifrostContextKeyAPIKeyID / BifrostContextKeyAPIKeyName (Go SDK) or virtual keys (sk-bf-*, HTTP).
What changed
| Field / surface | Status |
|---|---|
client.allow_direct_keys in config.json | Removed — field is no longer recognized; ignored if present |
client_config.allow_direct_keys over PUT /api/config | Removed — field is dropped from the request payload |
| Web UI Settings → Security → “Allow Direct API Keys” toggle | Removed |
Authorization: Bearer sk-... header → upstream provider | No longer forwarded as a direct key. Bearer values starting with sk-bf- continue to work as virtual keys |
x-api-key / x-goog-api-key header → upstream provider | No longer forwarded as a direct key |
x-bf-bedrock-api-key / x-bf-bedrock-access-key / x-bf-bedrock-secret-key / x-bf-bedrock-session-token / x-bf-bedrock-region | No longer extracted by the Bedrock integration |
x-bf-azure-endpoint + bare Authorization header on Azure OpenAI integration routes | No longer extracted as a direct Azure key |
Database column config_client.allow_direct_keys | Auto-dropped on first startup of v1.5.0 |
schemas.BifrostContextKeyDirectKey (Go SDK constant) | Removed — code referencing it will fail to compile |
Bifrost.getAllSupportedKeys / getKeysForBatchAndFileOps / selectKeyFromProviderForModelWithPool DirectKey branches | Removed — these no longer special-case a caller-supplied key |
How to update
1. Remove the field fromconfig.json:
PUT /api/config: the field has been dropped from the client_config schema.
3. Migrate any HTTP caller that relied on header-passed keys:
- For per-tenant or per-user key isolation: create a Bifrost virtual key per tenant (with budgets, rate limits, and provider/model allow-lists) and have callers send
Authorization: Bearer sk-bf-<tenant-vk>. - For routing requests across multiple provider keys: add the keys to Bifrost via
POST /api/providers/{provider}/keysand let Bifrost handle weighted selection and rotation. - For Bedrock callers using AWS credentials in
x-bf-bedrock-*headers: add a Bedrock provider key with the same credentials (access_key,secret_key,region, optionalsession_token) via the providers API and reference it from your virtual key. - For Azure direct-key callers using
x-bf-azure-endpoint+ Authorization: add the Azure deployment as a provider key withazure_key_config.endpointand reference it from your virtual key.
BifrostContextKeyDirectKey:
The constant is gone. Replace any in-process injection with one of the following supported alternatives.
Before (no longer compiles):
POST /api/providers/{provider}/keys. If you maintain a custom in-memory Account, return the key from GetKeysForProvider. Both routes give you the same per-request pinning behaviour DirectKey provided, plus governance, rotation on retry, and per-key cost attribution.
For providers that allow keyless requests (ambient credentials, IAM roles, etc.), BifrostContextKeySkipKeySelection is unchanged.
Why it was removed
The direct-key path bypassed Bifrost’s key management entirely, which meant:- No governance — virtual key budgets, rate limits, provider/model allow-lists, and routing rules were not applied to direct-key traffic
- No rotation or fallback — direct keys were used as-is with no retry across alternate keys
- No observability attribution — header-provided keys had a synthetic
key_id: "header-provided"that defeated per-key cost and usage analytics - Security surface area — a misconfigured HTTP deployment could leak provider credentials through logs or proxy chains; the Go SDK equivalent had the same hazard for any caller logging context contents
BifrostContextKeyAPIKeyID / BifrostContextKeyAPIKeyName path covers the “pick a specific key per request” use case without the governance and observability gaps.
Breaking Change 15: Semantic Cache Clear API is Now Cache-ID Based
The semantic cache “clear by request ID” API has been removed. Storage IDs in the cache are deterministic UUIDv5 hashes derived from the request payload (so the same prompt across many requests maps to a single cache entry), which made the previous request-ID-based delete unable to match anything written by the direct-search path. The replacement is keyed on the cache entry’s storage ID, which is now stamped on every response inextra_fields.cache_debug.cache_id — on cache hits and cache misses. Hold onto that ID from the response if you ever need to invalidate the entry.
REST API
| Before (v1.4.x) | After (v1.5.0) |
|---|---|
DELETE /api/cache/clear/{requestId} | DELETE /api/cache/clear/{cacheId} |
DELETE /api/cache/clear-by-key/{cacheKey}) is unchanged.
Before:
Go SDK
TheClearCacheForRequestID method on *semanticcache.Plugin has been removed and replaced by ClearCacheForCacheID.
Before:
Why the rename
A single cache entry is reused across many request IDs (that is the point of caching). A request-ID-based delete only ever made sense for the original writer of the entry, and even that broke once direct search switched to deterministic storage IDs. The cache ID is the only stable handle that works for both writers and readers, so the API now reflects that.CacheDebug on misses
extra_fields.cache_debug is now populated on cache misses too — previously it was only emitted when semantic search ran. The new fields on a miss:
cache_hit: falsecache_id: the storage ID where the entry was written (use this withClearCacheForCacheID)provider_used/model_used/input_tokens: only present when semantic search actually ran (i.e. embedding model was invoked)
cache_debug and assumed it was either absent or had cache_hit: true, update your consumer to handle the cache_hit: false shape.
Breaking Change 16: Semantic Cache cleanup_on_shutdown Removed
The cleanup_on_shutdown option on the semantic cache plugin config has been removed. Cache entries and the vector store namespace are no longer deleted when Bifrost shuts down — cache data always persists between restarts.
Before:
config.json, Helm values, and any PUT /api/config payloads.
How to clear cache data
If you previously relied oncleanup_on_shutdown: true to drop the cache on restart, use one of the supported invalidation paths instead:
DELETE /api/cache/clear/{cacheId}— invalidate a single entryDELETE /api/cache/clear-by-key/{cacheKey}— invalidate all entries for a cache key- Drop the vector store class/collection or point
vector_store_namespaceat a fresh name to start clean
Dimension / provider / model changes
The previouscleanup_on_shutdown: true + restart workflow was the documented escape hatch for changing dimension (or switching to an embedding provider/model that produces a different vector size). That option is gone. To rotate the namespace now, either:
- point
vector_store_namespaceat a fresh name, or - drop the existing class/index in your vector store before restarting
Opting Out: version: 1 Compatibility Mode
If you are not ready to adopt the new deny-by-default semantics, you can add a single field to config.json to restore v1.4.x behavior for all allow-list fields loaded from that file:
| Value | Behavior |
|---|---|
2 (default, omitted) | v1.5.0 semantics - empty = deny all, ["*"] = allow all |
1 | v1.4.x semantics - empty = allow all |
version: 1 normalizes at startup (before any other processing):
| Field | Without version: 1 | With version: 1 |
|---|---|---|
Provider key models: [] | Deny all models | Allow all models (→ ["*"]) |
VK provider_configs: [] | No providers allowed | All configured providers added with allowed_models: ["*"] |
VK provider config allowed_models: [] | Deny all models | Allow all models (→ ["*"]) |
VK provider config key_ids: [] | No keys allowed | All keys allowed (→ key_ids: ["*"]) |
VK mcp_configs: [] | No MCP tools allowed | All configured MCP clients added with tools_to_execute: ["*"] |
version: 1 only applies to configuration loaded from config.json. Virtual Keys created or updated via the REST API always use v1.5.0 semantics regardless of this setting. The automatic database migration that runs on startup is also unaffected.Complete Migration Checklist
Update provider key models in config.json
"models": [] or missing models fields with "models": ["*"] on every provider key.Add allowed_models and key_ids to every VK provider config
"allowed_models": ["*"] and "key_ids": ["*"] to every provider_configs entry (or list specific values). Rename any allowed_keys fields to key_ids.Ensure every VK has at least one provider config
"provider_configs": [] or no provider_configs will block all traffic.Update tools_to_execute for MCP configs
"tools_to_execute": [] with "tools_to_execute": ["*"]. Ensure every VK that needs MCP access has at least one mcp_configs entry.Migrate budget to budgets on Virtual Keys
budgets array instead of the singular budget object. Existing budgets are migrated automatically on startup.Handle nullable weight in API consumers
weight to accept null in addition to numbers.Fix invalid WhiteList values
"*" with specific values (e.g., ["*", "gpt-4o"]) and no list has duplicate entries.Migrate key management to dedicated endpoints
keys in provider create/update payloads and stop reading keys from provider responses. Use /api/providers/{provider}/keys for all key operations.Update compat plugin config
enable_litellm_fallbacks with the appropriate combination of convert_text_to_chat, convert_chat_to_responses, and should_drop_params.Migrate provider deployments to aliases
deployments fields into the top-level aliases field on each key. For Replicate, set use_deployments_endpoint: true if you were using the deployments endpoint.Update Go SDK references to ExtraFields.ModelRequested
ExtraFields.ModelRequested with ExtraFields.OriginalModelRequested (and optionally read ExtraFields.ResolvedModelUsed). Update JSON consumers reading "model_requested" to use "original_model_requested" and "resolved_model_used".Update Go SDK references to StreamAccumulatorResult.Model
.Model with .RequestedModel (and optionally .ResolvedModel) on any StreamAccumulatorResult usage.Handle empty selected_key_id on terminal retry failures
selected_key_id / selected_key_name from the request context or log entries to attribute failed requests, add a null/empty check and fall back to attempt_trail for the full per-attempt key history.Migrate direct-key callers off both surfaces
allow_direct_keys from config.json and any PUT /api/config payloads. Audit HTTP callers that sent provider keys in Authorization / x-api-key / x-goog-api-key / x-bf-bedrock-* / x-bf-azure-endpoint headers — those keys are no longer forwarded. Audit Go SDK callers for any reference to schemas.BifrostContextKeyDirectKey — the constant is removed and code referencing it will not compile. Replace both flavours with a Bifrost-managed provider key, optionally pinned per request via BifrostContextKeyAPIKeyID / BifrostContextKeyAPIKeyName (Go SDK) or a virtual key (sk-bf-*, HTTP).Switch semantic cache invalidation to cache IDs
DELETE /api/cache/clear/{requestId} with DELETE /api/cache/clear/{cacheId}, and replace plugin.ClearCacheForRequestID(...) with plugin.ClearCacheForCacheID(...). Read the cache ID from extra_fields.cache_debug.cache_id on the response (now populated on misses too).Remove cleanup_on_shutdown from semantic cache config
cleanup_on_shutdown field from the semantic cache plugin config in config.json, Helm values, and any API payloads — it is no longer part of the schema. Cache data now always persists across restarts; use the cache clear endpoints or rotate vector_store_namespace to drop entries.Troubleshooting
All requests returning 403/blocked after upgrade A provider key hasmodels: [], a Virtual Key has no provider_configs, or a provider config has allowed_models: []. Check Bifrost logs - a blocked request logs which rule denied it. Fix: add "models": ["*"] on provider keys, "allowed_models": ["*"] on VK provider configs.
MCP tools not being injected / tool calls blocked
The VK needs an mcp_configs entry for the MCP client with "tools_to_execute": ["*"] (or specific tools).
API returning 400 on VK create/update
A whitelist validation failure - either mixing "*" with specific values, or duplicate values in a list.
“No keys available” or key selection errors
A provider config with key_ids omitted or [] now blocks all keys (allow_all_keys: false). Add "key_ids": ["*"].
Provider create/update errors about keys field
The keys field has been removed. Remove it from provider payloads and use /api/providers/{provider}/keys instead.
Replicate requests failing after upgrade
If you used replicate_key_config.deployments, move the mappings to the top-level aliases field and set use_deployments_endpoint: true if you were targeting the Deployments API.
Go SDK compilation errors on ModelRequested or StreamAccumulatorResult.Model
Rename to OriginalModelRequested/ResolvedModelUsed on ExtraFields, and RequestedModel/ResolvedModel on StreamAccumulatorResult.
Calls authenticating with raw provider keys started failing with auth errors
The HTTP gateway no longer extracts provider keys from Authorization / x-api-key / x-goog-api-key headers (or x-bf-bedrock-* / x-bf-azure-endpoint on the Bedrock and Azure integrations). Either issue a virtual key (sk-bf-*) per caller and have them send that, or register the provider credentials as a Bifrost-managed key and route via virtual keys. See Breaking Change 14.
Virtual Key response missing budgets field
If you are creating Virtual Keys via the API using the old singular budget field, it will be ignored. Use the new budgets array format instead. If the VK was migrated from v1.4.x, the automatic migration should have converted the existing budget. Check that the migration ran successfully on startup.
Go SDK build error: undefined: schemas.BifrostContextKeyDirectKey
The constant is removed in v1.5.0. Register the key on your Account implementation (or via POST /api/providers/{provider}/keys if you use the database-backed config store) and pin it per request with BifrostContextKeyAPIKeyID or BifrostContextKeyAPIKeyName. See Breaking Change 14 for a before/after example.
