How it works
object_storage is not a standalone feature. It is a sub-config of logs_store:
client_config.log_retention_days - see Retention policy below.
When logs_store.object_storage is set:
- Bifrost writes per-request metadata (timestamps, provider, model, latency, token counts, cost, status, IDs) to the logs database, along with each failed request’s error details (except on content-hidden logs, see below), so error type, error code and status code breakdowns keep working.
- Large payload fields (request body, response body, streamed chunks, tool call arguments, etc.) are uploaded to the configured bucket under
prefix/. - The database row stores the object key, so the UI and API can fetch the payload on demand.
object_storage_exclude_fieldslets you skip specific fields from offload (for example, when you do not want to retain raw user prompts at all).object_storage_exclude_request_typeslets you skip whole request types from offload (for example,list_models).
Error details stay in the database with three limits:
- Content-hidden logs (content logging disabled with
retain_content_in_object_storageon) keep no error details in the database. Their error details are offloaded with the rest of the payload, so these logs are left out of the error type, error code and status code breakdowns. - Raw provider bodies that error details carry when raw request/response storage is on are offloaded with the rest of the payload. Only the queryable error fields stay in the database row. Listing
error_detailsinobject_storage_exclude_fieldskeeps the whole value, bodies included, in the database. - Logs written by earlier versions with object storage on hold their error details only in object storage. They are not backfilled, so they stay out of the error breakdowns. Opening one of those logs still shows its error details.
Configuration via config.json
Amazon S3
Required IAM Permissions
The IAM user or role needs the following permissions on your bucket:env.<NAME> indirection to read from environment variables):
A live example is checked in at
examples/configs/withobjectstorages3/config.json.
Google Cloud Storage
A live example is checked in at
examples/configs/withobjectstoragegcs/config.json.
Azure Blob Storage
Set either
endpoint or connection_string, not both. bucket and both authentication fields accept environment or vault references and must resolve to nonempty values.
Identity mode supports managed identity, workload identity, or a service principal configured through AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET. Azure model-provider credentials are not reused.
Grant Storage Blob Data Contributor on the container, or equivalent read/write/list/delete permissions. Attributes are stored as URL-encoded blob metadata, not index tags: no index-tag permissions are needed. URL-decode values when reading metadata outside Bifrost.
See examples/configs/withobjectstorageazure/config.json for a connection-string example.
Payload format: JSON or Parquet
By default each offloaded payload is stored as a JSON object. Setformat to parquet to store each payload as a single-row Parquet file instead, so the bucket can be queried directly with compatible data tools. Parquet is supported on S3 and Azure; GCS does not yet support it. The Athena examples below are S3-specific.
For
json, the legacy compress: true is the same as compression: gzip.
MCP tool logs, A2A agent logs and Relay Logs follow the same layout under mcp-logs/, agent-logs/ and relay-logs/.
Each Parquet file holds one row. Every payload field becomes a nullable UTF8 column named after its database column (input_history, output_message, raw_request, token_usage, …). String values are stored as is; nested values (for example MCP tool arguments) are stored as JSON text, and their column names are listed in the file’s bifrost.json_columns key-value metadata. Objects are uploaded with Content-Type: application/vnd.apache.parquet.
Switching formats, in either direction, does not rewrite existing objects. Bifrost keeps serving logs written before the switch: when the object in the configured format is missing, it reads the object in the other format at the same path (.json.gz or .parquet), and deleting a log removes both. A log updated after the switch (for example, a streamed response finishing) is rewritten in the configured format, and the copy in the old format is deleted, so each log keeps a single object.
To query request-log payloads from Athena in a bucket that has only ever been written with format: parquet:
NULL. The log ID is the object’s file name, available in Athena as regexp_extract("$path", '([^/]+)\.parquet$', 1), for joining back to the logs database.
For a bucket that switched formats, use partition projection over the hour folders and start the range at the first full hour after the switch, so Athena never lists a folder that can hold JSON:
read_parquet('s3://<bucket>/<prefix>/logs/**/*.parquet'), and Spark can use spark.read.option("pathGlobFilter", "*.parquet").option("recursiveFileLookup", "true").parquet("s3://<bucket>/<prefix>/logs/").
Choosing what stays in the database vs. what is offloaded
By default, every offloadable payload field is uploaded to object storage.object_storage_exclude_fields lets you pin specific fields to the database only, so they are never written to the bucket. Listed fields are not dropped - they continue to live in the logs DB row as before. Everything not listed is offloaded.
Values must be database column names (not JSON paths). Common choices:
framework/logstore/hybrid_test.go.
Typical use cases:
- Data residency: keep raw prompts and responses inside the DB (which may itself sit inside a controlled VPC) while still benefiting from offload for less sensitive fields.
- Operational queries: keep
input_historyin the DB so SQL queries and the UI’s search can run against the full conversation without paying an object-fetch round trip.
Excluding request types
object_storage_exclude_request_types keeps every log of the listed request types out of the bucket. Use it for traffic you do not need in your data lake, such as model listing calls. Those logs are still written to the logs DB in full, so they keep showing up in the UI and API.
Values are request type names as they appear in the log object field, for example list_models, embedding, chat_completion or chat_completion_stream. Streaming and non-streaming variants are separate types, so list both if you want to exclude both.
retain_content_in_object_storage is on, logs of an excluded request type are still offloaded as hidden content, so that content stays out of the DB.
This is not the same as
client.hidden_request_types. That setting hides request types from the logs view, dashboard, and log read APIs. It does not change what is written to the DB or uploaded to object storage.Guardrail redaction
When Enterprise guardrail redaction is enabled, object storage receives the same persisted log payload fields that Bifrost logs use. That means redacted fields are offloaded as redacted fields. For reversible modes such aslogs_only and runtime_reversible, the offloaded payload contains placeholders like [EMAIL-1]. The reveal mapping stays with the Bifrost log row and is not written into the offloaded object. If disable_content_logging is enabled, Bifrost skips content logging and does not persist reveal data.
For the full mode matrix, see Guardrail Redaction.
Retention policy
Bifrost ships a background log cleaner that deletes old logs on a fixed schedule.Configuration
Retention is configured on the client config, not onlogs_store. The active key is client_config.log_retention_days:
Source:
framework/logstore/cleaner.go and transports/bifrost-http/server/server.go.
What gets deleted
The cleaner deletes database rows older thannow - retention_days (UTC). It deletes from the main Log table as well as the MCP tool logs table.
What does NOT get deleted automatically
The retention cleaner deletes database rows, not stored payloads. Configure lifecycle rules forlogs/, mcp-logs/, agent-logs/, and relay-logs/ under your configured prefix, with retention longer than log_retention_days to cover the cleaner’s 24-hour interval, 15-30 minute jitter, and pass duration. For example, use 32 days for a 30-day log_retention_days setting. Allow more time for cleanup backlogs, failures, or downtime: missing payloads leave database rows readable without their offloaded content. Do not expire skills/ or the whole container.
Amazon S3 lifecycle rule (example)
aws s3api put-bucket-lifecycle-configuration --bucket <bucket> --lifecycle-configuration file://lifecycle.json. See the AWS docs on object lifecycle management.
Google Cloud Storage lifecycle rule (example)
gcloud storage buckets update gs://<bucket> --lifecycle-file=lifecycle.json. See the GCS docs on Object Lifecycle Management.
Azure operations
Configure Azure Blob lifecycle management for block blobs. EachprefixMatch includes the container name. For container bifrost-logs and prefix: bifrost, use:
bifrost-logs/bifrost/logs/bifrost-logs/bifrost/mcp-logs/bifrost-logs/bifrost/agent-logs/bifrost-logs/bifrost/relay-logs/
skills/ or the whole container. Early expiry makes historical payloads unreadable; soft delete and version retention can delay permanent removal.
Restart Bifrost after changing storage configuration or credentials. The SDK refreshes identity tokens automatically, but does not reload configuration.
Changing backends does not move existing data. Before switching:
- Keep the destination’s
object_storage.prefixidentical to the source. Historical payload lookups use the currently configured prefix, so changing it makes objects copied with their original keys unreadable. For example, retainbifrost/logswhen migrating from the S3/GCS examples above rather than adopting the Azure example’sbifrost. - Copy objects with their exact keys,
Content-Type,Content-Encoding, and metadata. - Pause writes or synchronize remaining changes before restarting against the destination.
- Check historical payload reads before removing the source.
Manual deletes still cascade
Single and batch deletes triggered explicitly (via the UI’s Delete log action or the underlyingDeleteLog / DeleteLogs APIs) do remove the associated objects from S3, GCS, or Azure Blob Storage at the same time. Only the time-based retention sweep is bucket-blind.
Configuration via Helm
Use the chart’sstorage.logsStore.objectStorage values. For Azure identity mode:
endpoint with existingSecret naming a Kubernetes Secret. Its default key is connection-string; override it with connectionStringKey. See Helm storage for an example.
BYO config.json
If you prefer to manage the fullconfig.json yourself, mount it as a Secret and point the chart at it. Use the same logs_store.object_storage blocks shown in the config.json section verbatim.
Configuration via the UI
The web UI does not currently expose a form forlogs_store.object_storage. To enable payload offload:
- Edit
config.json(or the Helm values) using the snippets above. - Restart Bifrost so the new log store wiring takes effect.
- Confirm new requests are landing in the bucket by browsing the configured
prefix/path.
Verifying the setup
- Send a request through Bifrost.
- Confirm a row appears in the logs DB (visible via the UI’s Logs screen).
- List the bucket under your
prefix/. You should see one or more objects per request, ending in.json.gzor, withformat: parquet,.parquet. - Open the log entry in the UI. The payload pane should render the content fetched from object storage.
objectstore errors. The most common causes are missing credentials, a region/endpoint mismatch, or a bucket policy that blocks the credentials’ principal.
