---
title: "Migrating to v1.5.0"
description: "Breaking changes and migration instructions for the v1.5.0 release"
---

v1.5.0 introduces several breaking changes across provider key configuration, Virtual Key semantics, the Go SDK, and the REST API. This page consolidates every breaking change with before/after examples and a migration checklist.

<Warning>
**Make a database backup before upgrading.** Automatic database migrations run on startup and are not revertible. A backup is the only way to restore a previous state if anything goes wrong. A database successfully migrated to v1.5.0 cannot be used to run v1.4.x.
</Warning>

---

## 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 to `models: ["*"]`
- Virtual Key provider configs with `allowed_models: []` are converted to `allowed_models: ["*"]`
- Virtual Keys with no `provider_configs` are backfilled with all currently configured providers (`allowed_models: ["*"]`, `key_ids: ["*"]`)
- Virtual Keys with no `mcp_configs` are backfilled with all currently connected MCP clients (`tools_to_execute: ["*"]`)
- Per-provider `deployments` maps (Azure, Bedrock, Vertex, Replicate) are migrated into the unified `aliases` field

**The automatic migration only protects your existing data.** Any new configuration created after upgrading — via `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) |

This affects four fields:

| 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:**
```json
{ "value": "env.OPENAI_API_KEY", "models": [] }
```
`models: []` → key served all models

**After:**
```json
{ "value": "env.OPENAI_API_KEY", "models": ["*"] }
```

### Virtual Key `allowed_models`

**Before:**
```json
{ "provider": "openai", "weight": 1.0 }
```
Missing `allowed_models` → all models allowed

**After:**
```json
{ "provider": "openai", "allowed_models": ["*"], "key_ids": ["*"], "weight": 1.0 }
```

### Virtual Key MCP `tools_to_execute`

**Before:**
```json
{ "mcp_client_name": "my-tools", "tools_to_execute": [] }
```

**After:**
```json
{ "mcp_client_name": "my-tools", "tools_to_execute": ["*"] }
```

---

## 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.

<Note>
Unlike `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.
</Note>

**Before:**
```json
{ "provider": "openai", "allowed_keys": ["key-prod-001"], "weight": 1.0 }
```

**After:**
```json
{ "provider": "openai", "key_ids": ["key-prod-001"], "allowed_models": ["*"], "weight": 1.0 }
```

To allow all keys:
```json
{ "provider": "openai", "key_ids": ["*"], "allowed_models": ["*"], "weight": 1.0 }
```

---

## 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:
```json
{
  "provider_configs": [
    { "provider": "openai",    "allowed_models": ["*"], "key_ids": ["*"], "weight": 1.0 },
    { "provider": "anthropic", "allowed_models": ["*"], "key_ids": ["*"], "weight": 1.0 }
  ]
}
```

The automatic migration backfills all currently configured providers into any VK that has an empty `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 values**
```json
// ❌ Invalid
{ "allowed_models": ["*", "gpt-4o"] }

// ✅ Valid
{ "allowed_models": ["*"] }
```

**Rule 2: No duplicate values**
```json
// ❌ Invalid
{ "allowed_models": ["gpt-4o", "gpt-4o"] }
```

Applies to: `models`, `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 balancing
- `weight: null` / omitted — provider is accessible for direct routing but excluded from weighted selection

**API response change:** `weight` may now be `null`. Update any client code that assumes it is always a number.

---

## Breaking Change 6: Provider Keys API Separated

Provider key management now has dedicated endpoints. The `keys` 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:**
```bash
curl -X POST localhost:8080/api/providers -d '{
  "provider": "openai",
  "keys": [{"name": "main", "value": "sk-..."}]
}'
```

**After:** Create provider first, then add keys:
```bash
curl -X POST localhost:8080/api/providers -d '{"provider": "openai"}'
curl -X POST localhost:8080/api/providers/openai/keys -d '{"name": "main", "value": "sk-..."}'
```

**Reading 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:
```bash
curl -X PUT localhost:8080/api/providers/openai -d '{"keys": [{"id": "key-1", "value": "sk-new"}]}'
```

**After:** Individual key operations:
```bash
curl -X PUT localhost:8080/api/providers/openai/keys/key-1 -d '{"name": "updated", "value": "sk-new"}'
curl -X DELETE localhost:8080/api/providers/openai/keys/key-2
```

---

## Breaking Change 7: Compact Plugin Restructured

The `enable_litellm_fallbacks` option has been removed and replaced with three granular options.

**Before:**
```json
{ "compat": { "enable_litellm_fallbacks": true } }
```

**After:**
```json
{
  "compat": {
    "convert_text_to_chat": true,
    "convert_chat_to_responses": true,
    "should_drop_params": true
  }
}
```

| 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 |

**Response field changes:**

| 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 8: 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`.

<Note>
Support for image editing via `/v1/images/edits` on Replicate is also being removed in a follow-up release. Plan to migrate to an alternative provider.
</Note>

---

## Breaking Change 9: 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:**
```json
{
  "providers": {
    "azure": {
      "keys": [{
        "value": "env.AZURE_API_KEY",
        "azure_key_config": {
          "endpoint": "env.AZURE_ENDPOINT",
          "deployments": {
            "gpt-4o": "my-gpt4o-deployment",
            "gpt-4o-mini": "my-mini-deployment"
          }
        }
      }]
    }
  }
}
```

**After:**
```json
{
  "providers": {
    "azure": {
      "keys": [{
        "value": "env.AZURE_API_KEY",
        "azure_key_config": {
          "endpoint": "env.AZURE_ENDPOINT"
        },
        "aliases": {
          "gpt-4o": "my-gpt4o-deployment",
          "gpt-4o-mini": "my-mini-deployment"
        }
      }]
    }
  }
}
```

### Bedrock

<Warning>
`bedrock_key_config.deployments` is a legacy field and is **removed** in v1.5.0 config semantics. Some setups/builds may still accept it for backward compatibility, but do not rely on it — migrate to `aliases` to avoid silent breakage and future removal.
</Warning>

**Before:**
```json
{
  "bedrock_key_config": {
    "region": "env.AWS_REGION",
    "deployments": {
      "claude-3-5-sonnet": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0"
    }
  }
}
```

**After:**
```json
{
  "bedrock_key_config": {
    "region": "env.AWS_REGION"
  },
  "aliases": {
    "claude-3-5-sonnet": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0"
  }
}
```

### Vertex

**Before:**
```json
{
  "vertex_key_config": {
    "project_id": "env.VERTEX_PROJECT_ID",
    "project_number": "env.VERTEX_PROJECT_NUMBER",
    "region": "env.VERTEX_REGION",
    "auth_credentials": "env.VERTEX_AUTH_CREDENTIALS",
    "deployments": {
      "gemini-2.0-flash": "projects/my-project/locations/us-central1/endpoints/123456"
    }
  }
}
```

**After:**
```json
{
  "vertex_key_config": {
    "project_id": "env.VERTEX_PROJECT_ID",
    "project_number": "env.VERTEX_PROJECT_NUMBER",
    "region": "env.VERTEX_REGION",
    "auth_credentials": "env.VERTEX_AUTH_CREDENTIALS"
  },
  "aliases": {
    "gemini-2.0-flash": "projects/my-project/locations/us-central1/endpoints/123456"
  }
}
```

### Replicate

The Replicate key config is also restructured. The `deployments` map is gone. A new boolean `use_deployments_endpoint` controls whether requests are routed through the [Deployments API](https://replicate.com/docs/reference/http#deployments.predictions.create) (private, fixed hardware) or the standard Models API.

**Before:**
```json
{
  "replicate_key_config": {
    "deployments": {
      "my-model": "owner/model-name/version-hash"
    }
  }
}
```

**After:**
```json
{
  "replicate_key_config": {
    "use_deployments_endpoint": true
  },
  "aliases": {
    "my-model": "owner/model-name"
  }
}
```

| 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 10: Go SDK — `ExtraFields` Model Fields Renamed

`ModelRequested string` has been replaced by two fields on `BifrostResponseExtraFields` and `BifrostErrorExtraFields`.

**Before:**
```go
model := response.ExtraFields.ModelRequested
```

**After:**
```go
// The alias the caller passed as "model" in the request
original := response.ExtraFields.OriginalModelRequested

// The actual identifier sent to the provider API
// Equals OriginalModelRequested when no alias is configured
resolved := response.ExtraFields.ResolvedModelUsed
```

The same rename applies to `BifrostErrorExtraFields`.

**JSON tag changes:**

| Old | New |
|---|---|
| `"model_requested"` | `"original_model_requested"` + `"resolved_model_used"` |

---

## Breaking Change 11: Go SDK — `StreamAccumulatorResult` Field Renamed

`Model string` has been replaced by two fields on `StreamAccumulatorResult` (returned by tracer streaming accumulation methods).

**Before:**
```go
result.Model
```

**After:**
```go
result.RequestedModel // original alias from the caller
result.ResolvedModel  // actual model identifier used by the provider
```

---

## Breaking Change 12: `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 `{ key_id, key_name, fail_reason }` per attempt |

### Impact on logging and telemetry plugins

The built-in **logging plugin** writes `selected_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 read `selected_key_id` from plugin context to attribute failed requests:**

**Before:**
```go
keyID, _ := ctx.Value(schemas.BifrostContextKeySelectedKeyID).(string)
// keyID was always populated, even on error
```

**After:**
```go
// Populated on success (or for single-key / pinned / sticky flows on error):
keyID, _ := ctx.Value(schemas.BifrostContextKeySelectedKeyID).(string)

// For full attribution across all retry attempts (including failures):
if trail, ok := ctx.Value(schemas.BifrostContextKeyAttemptTrail).([]schemas.KeyAttemptRecord); ok {
    for _, record := range trail {
        // record.KeyID, record.KeyName, record.FailReason
    }
}
```

**If you consume `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.

<Note>
Single-key, pinned (`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.
</Note>

---

## 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:

```json
{
  "version": 1,
  "providers": { ... }
}
```

| Value | Behavior |
|---|---|
| `2` (default, omitted) | v1.5.0 semantics — empty = deny all, `["*"]` = allow all |
| `1` | v1.4.x semantics — empty = allow all |

**What `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: ["*"]` |

<Note>
`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.
</Note>

<Warning>
`version: 1` is a temporary compatibility shim. Plan to migrate your `config.json` to explicit `["*"]` wildcards and remove the `version` field before the next major release.
</Warning>

---

## Complete Migration Checklist

<Steps>
<Step title="Backup your database">
Make a copy of your config store database before starting the upgrade.
</Step>

<Step title="Update provider key models in config.json">
Replace `"models": []` or missing `models` fields with `"models": ["*"]` on every provider key.
</Step>

<Step title="Add allowed_models and key_ids to every VK provider config">
Add `"allowed_models": ["*"]` and `"key_ids": ["*"]` to every `provider_configs` entry (or list specific values). Rename any `allowed_keys` fields to `key_ids`.
</Step>

<Step title="Ensure every VK has at least one provider config">
Any Virtual Key with `"provider_configs": []` or no `provider_configs` will block all traffic.
</Step>

<Step title="Update tools_to_execute for MCP configs">
Replace `"tools_to_execute": []` with `"tools_to_execute": ["*"]`. Ensure every VK that needs MCP access has at least one `mcp_configs` entry.
</Step>

<Step title="Handle nullable weight in API consumers">
Update any client code that processes `weight` to accept `null` in addition to numbers.
</Step>

<Step title="Fix invalid WhiteList values">
Ensure no list mixes `"*"` with specific values (e.g., `["*", "gpt-4o"]`) and no list has duplicate entries.
</Step>

<Step title="Migrate key management to dedicated endpoints">
Stop sending `keys` in provider create/update payloads and stop reading `keys` from provider responses. Use `/api/providers/{provider}/keys` for all key operations.
</Step>

<Step title="Update compact plugin config">
Replace `enable_litellm_fallbacks` with the appropriate combination of `convert_text_to_chat`, `convert_chat_to_responses`, and `should_drop_params`.
</Step>

<Step title="Migrate provider deployments to aliases">
Move deployment mappings from provider-specific `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.
</Step>

<Step title="Update Go SDK references to ExtraFields.ModelRequested">
Replace `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"`.
</Step>

<Step title="Update Go SDK references to StreamAccumulatorResult.Model">
Replace `.Model` with `.RequestedModel` (and optionally `.ResolvedModel`) on any `StreamAccumulatorResult` usage.
</Step>

<Step title="Handle empty selected_key_id on terminal retry failures">
If your code reads `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.
</Step>
</Steps>

---

## Troubleshooting

**All requests returning 403/blocked after upgrade**

A provider key has `models: []`, 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.
