Skip to content

feat: add allowlist for private-use redirect URI schemes (RFC 8252 §7.1) with cursor:// as initial entry - #4994

Merged
Pratham-Mishra04 merged 1 commit into
devfrom
07-07-feat_adds_support_for_cursor_scheme_in_mcp_oauth
Jul 9, 2026
Merged

feat: add allowlist for private-use redirect URI schemes (RFC 8252 §7.1) with cursor:// as initial entry#4994
Pratham-Mishra04 merged 1 commit into
devfrom
07-07-feat_adds_support_for_cursor_scheme_in_mcp_oauth

Conversation

@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Summary

Adds a default-deny allowlist for private-use ("custom") URI schemes in OAuth2 redirect URI validation, enabling native app clients like Cursor to use schemes such as cursor://anysphere.cursor-mcp/oauth/callback (per RFC 8252 §7.1) without opening the door to dangerous schemes like javascript:, data:, or file:.

Changes

  • Introduced allowedPrivateUseRedirectSchemes, a map-based allowlist of permitted private-use URI schemes. Currently contains cursor as the only entry.
  • Updated isAllowedRedirectScheme to accept URIs whose scheme appears in the allowlist, provided the URI also includes an authority component (scheme://host/...). Opaque forms such as cursor:whatever are still rejected.
  • Added TestPrivateUseRedirectSchemes covering allowlisted schemes, loopback/https cases, non-allowlisted custom schemes, and dangerous schemes to ensure the default-deny behavior holds.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

go test ./transports/bifrost-http/handlers/...

Expected: all tests pass, including the new TestPrivateUseRedirectSchemes test which validates that:

  • cursor://anysphere.cursor-mcp/oauth/callback is accepted
  • https://example.com/cb and http://127.0.0.1:49152/cb are accepted
  • com.example.app://oauth/callback, myapp://callback, vscode://callback are rejected
  • javascript:, data:, file:, and opaque forms of allowlisted schemes are rejected

Breaking changes

  • Yes
  • No

Security considerations

The allowlist is intentionally default-deny. Only schemes explicitly added to allowedPrivateUseRedirectSchemes are permitted beyond https and http-loopback. An authority component is required even for allowlisted schemes, preventing opaque URI forms from being written into a Location header. Dangerous schemes (javascript:, data:, file:) remain rejected regardless of any allowlist entry. New native app clients requiring a custom scheme must be explicitly added to the allowlist.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a97e4f75-32c7-413f-a65c-63274dfb5744

📥 Commits

Reviewing files that changed from the base of the PR and between 5b32a70 and 51c8c10.

📒 Files selected for processing (2)
  • transports/bifrost-http/handlers/localhostcheck_test.go
  • transports/bifrost-http/handlers/mcpoauth2issuance.go

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Tightened redirect URI scheme validation to default-deny all non-https/non-http schemes, while allowing an approved private-use scheme when used in a full scheme://host/... (authority-form) format.
    • Continued to allow https universally, http only for loopback, and rejected unsafe/opaque or malformed private-use variants.
  • Tests
    • Added coverage for private-use scheme allowance/denial and for redirect URI matching, including mismatches and cases involving ports and incorrect structure.

Walkthrough

Adds a deny-by-default allowlist for private-use redirect URI schemes in isAllowedRedirectScheme, requires authority-form private-use URIs, and extends tests for allow/deny and redirect matching behavior.

Changes

Private-use Redirect Scheme Validation

Layer / File(s) Summary
Allowlist and validation logic
transports/bifrost-http/handlers/mcpoauth2issuance.go
Adds allowedPrivateUseRedirectSchemes and updates isAllowedRedirectScheme to deny non-https/http schemes not in the allowlist, and to require a non-empty Host for allowlisted private-use schemes.
Test coverage for scheme validation
transports/bifrost-http/handlers/localhostcheck_test.go
Adds tests covering isAllowedRedirectScheme allow/deny cases and matchRedirectURI behavior for registered private-use redirect URIs, including exact matches and mismatches.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • maximhq/bifrost#4523: Adds redirect-URI matching tests in the same handler area and exercises related matching behavior.

Suggested reviewers: akshaydeo, danpiths, roroghost17

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a private-use redirect URI allowlist with cursor:// as the initial entry.
Description check ✅ Passed The PR description is mostly complete and covers summary, changes, testing, type, security, and checklist; only related issues/screenshots are omitted.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 07-07-feat_adds_support_for_cursor_scheme_in_mcp_oauth

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from roroghost17 July 7, 2026 11:18
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge — the allowlist logic is correct, the authority-component guard prevents opaque-form bypass, and the matching path for private-use scheme URIs falls into the existing exact-match branch without touching loopback logic.

The scheme validation correctly gates registration on both allowlist membership and the presence of an authority component, blocking opaque forms like cursor:whatever. The matchRedirectURI function requires an exact string match for non-loopback hosts, so a registered cursor:// URI cannot be reached by a differently-hosted candidate. Both new test functions cover the critical boundary cases. The previously flagged stale comment and error message are cosmetic and were noted in an earlier review round.

The inline comment and error message in mcpoauth2issuance.go around lines 87–92 remain stale from a prior review; no files require blocking attention for this change.

Important Files Changed

Filename Overview
transports/bifrost-http/handlers/mcpoauth2issuance.go Adds allowedPrivateUseRedirectSchemes allowlist and updates isAllowedRedirectScheme; logic is correct but the inline comment and error message at lines 87–92 are now stale (noted in a prior review round).
transports/bifrost-http/handlers/localhostcheck_test.go Adds TestPrivateUseRedirectSchemes and TestMatchRedirectURIPrivateUseSchemes; both requested by prior review and now present with thorough case coverage.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Registration Request\nredirect_uri provided] --> B[isAllowedRedirectScheme]
    B --> C{scheme?}
    C -- https --> D[Allow ✓]
    C -- http --> E{isLoopbackRedirectHost?}
    E -- yes --> D
    E -- no --> F[Reject ✗]
    C -- other --> G{in allowedPrivateUseRedirectSchemes?}
    G -- no --> F
    G -- yes --> H{parsed.Host != empty?}
    H -- no\nopaque form --> F
    H -- yes\nauthority present --> D
    D --> I[URI stored in registration]
    J[Authorization Request\nredirect_uri candidate] --> K[matchRedirectURI]
    K --> L{isLoopback host?}
    L -- yes --> M[Match scheme + path\nignore port]
    L -- no --> N[Exact string match]
    M --> O{match found?}
    N --> O
    O -- yes --> P[Allow redirect ✓]
    O -- no --> Q[Reject ✗]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Registration Request\nredirect_uri provided] --> B[isAllowedRedirectScheme]
    B --> C{scheme?}
    C -- https --> D[Allow ✓]
    C -- http --> E{isLoopbackRedirectHost?}
    E -- yes --> D
    E -- no --> F[Reject ✗]
    C -- other --> G{in allowedPrivateUseRedirectSchemes?}
    G -- no --> F
    G -- yes --> H{parsed.Host != empty?}
    H -- no\nopaque form --> F
    H -- yes\nauthority present --> D
    D --> I[URI stored in registration]
    J[Authorization Request\nredirect_uri candidate] --> K[matchRedirectURI]
    K --> L{isLoopback host?}
    L -- yes --> M[Match scheme + path\nignore port]
    L -- no --> N[Exact string match]
    M --> O{match found?}
    N --> O
    O -- yes --> P[Allow redirect ✓]
    O -- no --> Q[Reject ✗]
Loading

Reviews (3): Last reviewed commit: "feat: adds support for cursor scheme in ..." | Re-trigger Greptile

Comment thread transports/bifrost-http/handlers/mcpoauth2issuance.go Outdated
Comment thread transports/bifrost-http/handlers/localhostcheck_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
transports/bifrost-http/handlers/mcpoauth2issuance.go (1)

685-690: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

transports/bifrost-http/handlers/mcpoauth2issuance.go:685-690 — Allow RFC 8252 private-use redirects by URI shape, not host presence. parsed.Host != "" rejects reverse-domain redirects like com.example.app:/oauth2redirect/callback because url.Parse leaves Host empty for that form. Use parsed.Opaque == "" instead to still reject opaque inputs such as cursor:whatever while accepting the canonical single-slash form.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transports/bifrost-http/handlers/mcpoauth2issuance.go` around lines 685 -
690, The private-use redirect validation in the parsed URI check is too strict
because `parsed.Host != ""` rejects valid RFC 8252 reverse-domain redirects like
`com.example.app:/oauth2redirect/callback`. Update the logic in the redirect
validation path to allow the canonical single-slash form by checking URI shape
with `parsed.Opaque == ""` while still keeping the allowlisted scheme check in
place, so opaque inputs like `cursor:whatever` remain rejected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@transports/bifrost-http/handlers/mcpoauth2issuance.go`:
- Around line 685-690: The private-use redirect validation in the parsed URI
check is too strict because `parsed.Host != ""` rejects valid RFC 8252
reverse-domain redirects like `com.example.app:/oauth2redirect/callback`. Update
the logic in the redirect validation path to allow the canonical single-slash
form by checking URI shape with `parsed.Opaque == ""` while still keeping the
allowlisted scheme check in place, so opaque inputs like `cursor:whatever`
remain rejected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5d1dcab6-3130-4f2e-a4f6-e64a9716526c

📥 Commits

Reviewing files that changed from the base of the PR and between d9577ba and 2a1e1f4.

📒 Files selected for processing (2)
  • transports/bifrost-http/handlers/localhostcheck_test.go
  • transports/bifrost-http/handlers/mcpoauth2issuance.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 7, 2026
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-07-feat_adds_support_for_cursor_scheme_in_mcp_oauth branch from 2a1e1f4 to 5b32a70 Compare July 7, 2026 16:27
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-07-feat_adds_support_for_cursor_scheme_in_mcp_oauth branch from 5b32a70 to 51c8c10 Compare July 9, 2026 07:17

Pratham-Mishra04 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Merge activity

  • Jul 9, 7:20 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 9, 7:20 AM UTC: @Pratham-Mishra04 merged this pull request with Graphite.

@Pratham-Mishra04
Pratham-Mishra04 merged commit fe69407 into dev Jul 9, 2026
13 of 15 checks passed
@Pratham-Mishra04
Pratham-Mishra04 deleted the 07-07-feat_adds_support_for_cursor_scheme_in_mcp_oauth branch July 9, 2026 07:20
akshaydeo pushed a commit that referenced this pull request Jul 14, 2026
….1) with `cursor://` as initial entry (#4994)

## Summary

Adds a default-deny allowlist for private-use ("custom") URI schemes in OAuth2 redirect URI validation, enabling native app clients like Cursor to use schemes such as `cursor://anysphere.cursor-mcp/oauth/callback` (per RFC 8252 §7.1) without opening the door to dangerous schemes like `javascript:`, `data:`, or `file:`.

## Changes

- Introduced `allowedPrivateUseRedirectSchemes`, a map-based allowlist of permitted private-use URI schemes. Currently contains `cursor` as the only entry.
- Updated `isAllowedRedirectScheme` to accept URIs whose scheme appears in the allowlist, provided the URI also includes an authority component (`scheme://host/...`). Opaque forms such as `cursor:whatever` are still rejected.
- Added `TestPrivateUseRedirectSchemes` covering allowlisted schemes, loopback/https cases, non-allowlisted custom schemes, and dangerous schemes to ensure the default-deny behavior holds.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/handlers/...
```

Expected: all tests pass, including the new `TestPrivateUseRedirectSchemes` test which validates that:
- `cursor://anysphere.cursor-mcp/oauth/callback` is accepted
- `https://example.com/cb` and `http://127.0.0.1:49152/cb` are accepted
- `com.example.app://oauth/callback`, `myapp://callback`, `vscode://callback` are rejected
- `javascript:`, `data:`, `file:`, and opaque forms of allowlisted schemes are rejected

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

The allowlist is intentionally default-deny. Only schemes explicitly added to `allowedPrivateUseRedirectSchemes` are permitted beyond `https` and `http`-loopback. An authority component is required even for allowlisted schemes, preventing opaque URI forms from being written into a `Location` header. Dangerous schemes (`javascript:`, `data:`, `file:`) remain rejected regardless of any allowlist entry. New native app clients requiring a custom scheme must be explicitly added to the allowlist.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akshaydeo added a commit that referenced this pull request Jul 14, 2026
* upgrades clickhouse library version

* allow v2.0.0 to publish prerelease cuts

* updates clickhouse main library

* feat(logging): add transient redaction data field and context key for guardrails (#4169)

## Summary

This PR introduces reversible redaction support across the Bifrost stack, allowing enterprise guardrails plugins to redact PII from log content while preserving an encrypted reversible mapping that authorized users can later reveal inline in the log detail view.

## Changes

- Added `RedactionPayload` schema type and associated context helpers (`RedactionPayloadFromContext`, `SetRedactionPayloadOnContext`, `ApplyLiteralReplacements`) to carry request-scoped redaction data from guardrails to log sinks
- Added `BifrostContextKeyRedactionData` context key for guardrails plugins to attach redaction payloads (marked DO NOT SET MANUALLY)
- Added `RedactionData` (transient), `RedactionMapping` (persisted), and `HasReversibleRedaction` (virtual) fields to the `Log` table struct
- Added `migrationAddRedactionMappingColumn` to persist the reversible mapping alongside the log row so it shares the row's lifecycle
- Updated `FindByID` to use `ScopedDB` so point lookups honor caller-supplied query scope (e.g. Enterprise DAC), preventing out-of-scope ID access
- Added `attachLogRedactionData` in the logging plugin to copy guardrail redaction payloads into log entries before async writes, gated on content logging being enabled
- Exposed `HasReversibleRedaction` on log detail and list endpoints so the UI knows when a reveal toggle is applicable
- Added a `Reveal` RBAC operation and `canReveal` prop threading through `LogDetailSheet` → `LogDetailView`
- Added a "Show original values" toggle in the log detail header that calls a new `POST /logs/:id/reveal` endpoint and applies the returned mapping inline to all message text, reasoning, and refusal fields without mutating stored data
- Added `useRevealLogRedactionMappingMutation` RTK Query mutation and `LogRedactionRevealResponse` type
- Literal replacement applies longest-match-first ordering to avoid partial substitution of overlapping tokens

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

```sh
# Core/Transports
go test ./core/schemas/... ./framework/logstore/... ./plugins/logging/...

# UI
cd ui
pnpm i
pnpm build
```

To validate end-to-end:
1. Configure an enterprise guardrails plugin that sets `BifrostContextKeyRedactionData` with a `RedactionPayload` containing `ReversibleMappings`
2. Send a request containing PII through Bifrost
3. Open the log detail view — the "Show original values" toggle should appear only for users with the `Reveal` RBAC permission on `Logs`
4. Toggle reveal — placeholders like `[EMAIL-1]` should be replaced inline with their original values
5. Navigate to a different log — the toggle resets and the mapping is cleared from state

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

- The `RedactionMapping` column stores the reversible mapping encrypted when an encryption key is configured; the mapping is deleted when the log row is deleted, preventing orphaned sensitive data
- The reveal endpoint is gated behind a new `Reveal` RBAC operation so only authorized users can recover original PII values
- `BifrostContextKeyRedactionData` is explicitly marked DO NOT SET MANUALLY to prevent plugins from injecting arbitrary mappings
- `attachLogRedactionData` is a no-op when content logging is disabled, preventing sensitive payloads from leaking through the async write path
- `FindByID` now enforces query scope, closing a gap where a scoped caller could retrieve out-of-scope log rows by ID

## Checklist

- [x] I added/updated tests where appropriate
- [x] I verified builds succeed (Go and UI)

* feat(logging): redact trace content before connector export (#4417)

## Summary

Adds trace-level redaction of span content attributes before traces are exported to observability plugins. Connectors can register raw-to-placeholder replacement maps on a trace; when the trace completes, all content-bearing span attributes (messages, prompts, tool arguments, etc.) are rewritten in-place before any plugin receives the trace. The replacement map is stored in an unexported field so it is never serialized or leaked to connectors.

## Changes

- Added `IsContentAttribute(key string) bool` to classify which span attribute keys may carry user or model content (messages, prompts, embeddings, tool arguments, reasoning text, etc.).
- Added `RedactAttributeValue(value any, replacements map[string]string) any` to apply literal replacements across `string`, `[]string`, and `[]any` attribute shapes.
- Added `redactionReplacements` as an unexported field on `Trace` so the map is never JSON-serialized and cannot be observed by connectors.
- Added `Trace.SetRedactionReplacements` to store a defensive copy of the replacement map, stripping empty keys.
- Added `Trace.ApplyRedactionReplacements` to walk every span, redact content attributes, and clear the map atomically.
- Added `Trace.Reset` cleanup to ensure pooled traces cannot carry redaction data across requests.
- Added `redactSpanAttributes` as a package-private helper that locks a single span and rewrites its content attributes.
- Added `SetTraceRedactionReplacements` to the `Tracer` interface and its `NoOpTracer` implementation.
- Wired `ApplyRedactionReplacements` into `Tracer.CompleteAndFlushTrace` so redaction runs before any observability plugin `Inject` call.
- Added `Tracer.SetTraceRedactionReplacements` in the framework tracing layer to look up the live trace and delegate to `Trace.SetRedactionReplacements`.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/schemas/... ./framework/tracing/...
```

Key scenarios covered by new tests:

- `TestIsContentAttribute` — verifies the content attribute classifier includes message, prompt, embedding, and tool fields while excluding metadata fields like model name and session ID.
- `TestTraceApplyRedactionReplacementsRedactsContentAttributes` — verifies replacements are applied to all spans and that non-content attributes are left untouched.
- `TestTraceRedactionReplacementsDoNotSerialize` — verifies the replacement map never appears in JSON output.
- `TestTraceResetClearsRedactionReplacements` — verifies pooled traces cannot retain replacement data.
- `TestTracer_CompleteAndFlushTraceRedactsContentBeforeInject` — end-to-end: replacements set before span population are applied before the observability plugin receives the trace.
- `TestTracer_SetTraceRedactionReplacementsSurvivesLaterObservabilityPlugins` — replacements set before plugin registration still take effect at flush time.

## Breaking changes

- [x] Yes
- [ ] No

The `Tracer` interface gains a new method `SetTraceRedactionReplacements`. Any external implementation of `Tracer` must add this method. The `NoOpTracer` implementation is provided as a reference no-op.

## Security considerations

The replacement map is stored in an unexported struct field (`redactionReplacements`) and is explicitly cleared after `ApplyRedactionReplacements` runs and during `Reset`. This prevents PII or secret values used as redaction keys from being serialized into trace payloads, retained across pooled trace reuse, or observed by observability plugin authors inspecting the exported `Trace` struct.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* docs for redaction (#4565)

## Summary

Adds documentation for Bifrost-managed guardrail redaction, two new PII guardrail providers (Microsoft Presidio and Azure AI Language PII), and a `POST /api/logs/{id}/reveal` endpoint for revealing reversible redaction mappings from Bifrost logs.

## Changes

- Added a new `Guardrail Redaction` reference page (`enterprise/guardrails/redaction.mdx`) covering the three redaction modes (`runtime`, `logs_only`, `runtime_reversible`), redaction strategies (`replace`, `mask`, `hash`), the reveal permission model, and connector export behavior.
- Added integration pages for Microsoft Presidio (`integrations/guardrails/presidio.mdx`) and Azure AI Language PII (`integrations/guardrails/azure-language-pii.mdx`), including configuration fields, authentication modes, and all four config formats (Web UI, API, config.json, Helm).
- Extended the Regex and Secrets Detection provider docs and config examples to include per-pattern `action`, `redaction_strategy`, `redaction_mode`, and `entity_type` fields.
- Updated the guardrails overview to list Presidio and Azure AI Language PII in the provider capability matrix, added a warning against combining provider-managed transformation with Bifrost-managed redaction on the same phase, and added a Redaction section summarizing the three modes.
- Updated the nav (`docs.json`) to add a `Providers` sub-group under Guardrails and surface the new Redaction, Presidio, and Azure AI Language PII pages.
- Added `POST /api/logs/{id}/reveal` to the OpenAPI spec (YAML and compiled JSON), gated by `Logs:Reveal`, returning a `LogRevealResponse` with a placeholder-to-original-value mapping. Added `has_reversible_redaction` to `LogEntry`.
- Added `Logs:Reveal` and `MCPToolGroups`/`MCPLogs` to the RBAC resource table.
- Added guardrail redaction notes to the Datadog connector, OTel, default observability, and log-exports pages explaining that exported content receives redacted or placeholderized values and that reveal mappings are not forwarded to connectors.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [x] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

Review the rendered docs to confirm:

- The Guardrail Redaction page renders the mode matrix table and the redaction mode selector screenshot correctly.
- The Presidio and Azure AI Language PII pages appear under the Guardrails > Providers nav group.
- The `POST /api/logs/{id}/reveal` endpoint appears in the API reference with correct request/response schemas and a `403` for missing `Logs:Reveal` permission.
- The Regex and Secrets Detection config examples include `action`, `redaction_strategy`, and `redaction_mode` fields.
- Cross-links between the redaction page and provider pages resolve without 404s.

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

- The `POST /api/logs/{id}/reveal` endpoint returns original sensitive values and is gated by the `Logs:Reveal` RBAC permission. The response is marked `Cache-Control: no-store`.
- Reveal mappings are stored only in Bifrost logs and are never forwarded to trace-export connectors, object storage payloads, or external observability destinations.
- When an encryption key is configured, the reveal mapping is encrypted before storage.
- If `disable_content_logging` is enabled, no reveal data is persisted.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* [fix]: preserve streaming finish_reason in the accumulated response when forwarded on a content chunk (#4964)

The streaming chat accumulator reads finish_reason only from the highest-index
chunk (getLastChatChunkLocked). For providers that send finish_reason on the
final content chunk, the OpenAI-compatible handler forwards it on that chunk and
appends a synthetic terminal chunk (index + 1) whose finish_reason is nil to
avoid a duplicate client emission (the forwardedTerminalFinishReason guard from
#1995). The highest-index chunk therefore has a nil finish_reason while the real
one sits one index lower, so the accumulated response records null. Unlike the
sibling TokenUsage, Cost and CacheDebug fields at the same site, finish_reason
was assigned without a nil check. The accumulated value feeds the logging plugin
(entry.StopReason) and Maxim, so streaming logs recorded an empty stop reason
for these providers; non-streaming is unaffected.

Fall back to the newest chunk that actually carries a finish_reason only when the
highest-index chunk has none. Regression tests cover the content-chunk case and
the standard terminal-chunk case.

closes #4963

Signed-off-by: Akshay Deo <akshay@akshaydeo.com>
Co-authored-by: Akshay Deo <akshay@akshaydeo.com>

* fix: pass azure auth headers in helpers (#4999)

## Summary

Azure media endpoints (Speech, Transcription, ImageGeneration, ImageEdit, VideoGeneration) were hardcoding `Authorization: Bearer <key>` authentication, ignoring Azure-specific auth mechanisms such as service principal tokens or `api-key` headers. This PR propagates Azure auth headers through the shared OpenAI handler functions so that Azure's authentication flow is respected for all media request types.

## Changes

- Added an `authHeaders map[string]string` parameter to `HandleOpenAISpeechRequest`, `HandleOpenAITranscriptionRequest`, `HandleOpenAIImageGenerationRequest`, `HandleOpenAIImageEditRequest`, and `HandleOpenAIVideoGenerationRequest`.
- Each handler now prefers caller-supplied `authHeaders` over the default `Bearer` token fallback. If `authHeaders` is empty or nil, it falls back to `BearerAuthHeader(key)` as before.
- The Azure provider now calls `getAzureAuthHeaders` before invoking each of these handlers and passes the result through.
- Non-Azure providers (OpenAI, Groq, vLLM, xAI) pass `nil` for `authHeaders`, preserving existing behavior.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./...
```

Validate by configuring an Azure provider with service principal credentials and invoking Speech, Transcription, ImageGeneration, ImageEdit, and VideoGeneration endpoints. Confirm that requests are authenticated using the Azure-specific headers rather than a `Bearer` token, and that non-Azure providers continue to authenticate with `Bearer` tokens as expected.

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

Auth headers sourced from `getAzureAuthHeaders` may contain short-lived tokens or API keys. These are passed only in-memory to the HTTP request headers and are not logged or persisted. Existing secret handling guarantees apply.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix(redaction): phase scoped redaction and revealing (#5007)

## Summary

Redaction replacements are now tracked separately for request-side (input) and response-side (output) content rather than in a single flat map. This prevents input-phase redaction tokens from being applied to output attributes and vice versa, ensuring each replacement set is scoped to the content it was derived from.

## Changes

- Introduced `RedactionPhase` (`input` / `output`) and `RedactionMapsByPhase` to replace the flat `map[string]string` used in `RedactionData` and `Trace.redactionReplacements`.
- `SetRedactionReplacements` and `SetTraceRedactionReplacements` now require a `RedactionPhase` argument so callers explicitly declare which lifecycle phase produced the replacements.
- Span attribute redaction (`redactSpanAttributes`) selects the correct replacement map per attribute using a new `traceContentAttributeScopeForKey` classifier:
    - Input-only attributes (e.g. `AttrInputMessages`, `AttrPrompt`) receive only input replacements.
    - Output-only attributes (e.g. `AttrOutputMessages`, `AttrRespReasoningText`) receive only output replacements.
    - Mixed attributes (e.g. `AttrToolCallArguments`, `AttrToolCallResult`) receive a merged map of both phases.
- `IsContentAttribute` is now derived from `traceContentAttributeScopeForKey` to keep the two in sync.
- `RevealRedactionMapping` on `logstore.Log` changed from `map[string]string` to `*schemas.RedactionMapsByPhase`, and `LogRedactionMappingResolver` returns the same type.
- The `redaction_mapping` field in the log API response and OpenAPI schema is now a `{ input, output }` object instead of a flat map.
- The UI `LogEntry` type reflects the new shape, and `logDetailView` applies input and output reveal mappings independently to the appropriate content sections (request body, input messages, response body, output messages, reasoning, refusals, Responses API items).

## Type of change

- [ ] Bug fix
- [x] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [x] UI (React)
- [x] Docs

## How to test

```sh
# Core/Transports
go test ./core/schemas/... ./framework/tracing/... ./plugins/logging/...

# UI
cd ui
pnpm i
pnpm build
```

Verify that:

- Input-phase redaction tokens (e.g. `[EMAIL-1]`) are applied only to input attributes and request bodies.
- Output-phase redaction tokens (e.g. `[EMAIL-2]`) are applied only to output attributes and response bodies.
- The log detail reveal toggle restores original values in the correct content sections.
- The `redaction_mapping` field in log detail API responses serializes as `{ "input": {...}, "output": {...} }`.

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

`SetTraceRedactionReplacements` now requires a `RedactionPhase` argument. Any custom `Tracer` or `LogRedactionMappingResolver` implementations must be updated to match the new signatures. The `redaction_mapping` field in log detail API responses has changed shape from a flat object to a `{ input, output }` object; API consumers that read this field will need to handle the new structure.

## Related issues

N/A

## Security considerations

Scoping replacements by phase reduces the risk of a redaction token from one phase incorrectly masking or revealing content in another phase. The reversible mapping (used for the `Logs:Reveal` feature) is now also phase-scoped, so revealed values are only substituted back into the content section they originated from.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix(openai): serialize compaction request `input` correctly (#5014)

OpenAICompactionRequest had no MarshalJSON, so its value-typed
OpenAIResponsesRequestInput field — whose only marshaler is a pointer
receiver — was emitted by default struct encoding as a JSON object
({"OpenAIResponsesRequestInputArray":null,"OpenAIResponsesRequestInputStr":null}),
which /v1/responses/compact rejects with "Invalid type for 'input':
expected a string, but got an object instead." omitempty on the value
field also never omitted an empty input.

Add a MarshalJSON mirroring OpenAIResponsesRequest: route `input` through
the union's marshaler (string/array) and omit it when empty, since a
previous_response_id-only compaction is valid.

* fix(schemas): add ExtraContent to ChatStreamResponseChoiceDelta (#4569)

Rebased onto core/v1.5.21 (includes EnvVar, AliasConfig, etc).

Adds ExtraContent json.RawMessage to ChatStreamResponseChoiceDelta so
Gemini extended thinking markers (google.thought, thought_signature)
survive streaming through any Bifrost-based gateway/proxy.

Also adds ExtraContent deep-copy in DeepCopyChatMessage for the
tool-call path to prevent shared backing-array mutations in concurrent
streaming pipelines.

Upstream PR: https://github.com/maximhq/bifrost/pull/4569

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* added enterprise fallback pages for alerting (#4685)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat(ui): add Microsoft Teams icon and alert API tags (#4826)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix(transcription): support diarized_json segments, fix ElevenLabs speaker passthrough (#5020)

* fix(transcription): support diarized_json segments, fix ElevenLabs speaker passthrough

OpenAI's response_format=diarized_json (gpt-4o-transcribe-diarize) returns
segments with a string id, plus speaker/type fields, which crashed
unmarshalling into TranscriptionSegment's int id (#5002). Adds a distinct
TranscriptionDiarizedSegment type and decodes diarized_json separately in
both the OpenAI provider's normal and large-payload-passthrough paths (Azure
inherits the fix via the shared handler).

Since Segments and DiarizedSegments serialize under the same "segments" key,
BifrostTranscriptionResponse gets a custom MarshalJSON/UnmarshalJSON pair so
the shape round-trips correctly both on the wire and through
framework/logstore's persist/reload cycle.

Also:
- ElevenLabs' per-word speaker_id was decoded but never propagated into the
  canonical TranscriptionWord; added a Speaker field and wired it through.
- Multipart transcription parsing only whitelisted OpenAI's own fields,
  silently dropping provider-specific extras like ElevenLabs' diarize; now
  passes through unrecognized fields via ExtraParams.
- TranscriptionUsage.Seconds was *int, but OpenAI's duration-usage variant is
  fractional (e.g. 521.5) and would fail to parse; widened to *float64.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(transcription): dedupe diarized_json decode struct

The diarized_json response shape was duplicated as two anonymous structs
(normal path and large-payload-passthrough path); pulled into a single
named type instead.

* fix(transcription): address review findings on round-trip and multipart parsing

- Empty diarized segment arrays (e.g. silent audio) were indistinguishable
  from empty verbose segments on reload, since both unmarshal successfully
  from "[]" - a diarized response with zero segments would silently lose its
  identity and, on re-marshal, drop the "segments" key OpenAI's diarized_json
  contract requires. Adds an "is_diarized" marker written whenever
  DiarizedSegments is set, used as the authoritative signal when present;
  falls back to the existing shape-sniffing for data persisted before the
  marker existed.
- Custom Marshal/UnmarshalJSON now use encoding/json instead of sonic, per
  this repo's core/schemas convention.
- transcription multipart parsing didn't extract temperature or
  timestamp_granularities into their typed fields (verified via the
  openai-python SDK's actual multipart encoding: plain "temperature" field,
  repeated "timestamp_granularities[]"), so they'd leak into ExtraParams
  instead of reaching the outbound OpenAI request. Extracted properly and
  excluded from the generic passthrough.
- new(expr) instead of an intermediate variable for the two *int/*float64
  seconds conversions, matching this repo's existing Go 1.26 convention.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Akshay Deo <akshay@akshaydeo.com>

* fix: pass container block from anthropic api (#5024)

## Summary

Adds support for Anthropic's `container_upload` content block type, which is used to stage files into the code-execution container. Previously, these blocks were silently dropped during conversion between Anthropic and Bifrost formats.

## Changes

- Added `ResponsesInputMessageContentBlockTypeContainerUpload` (`"container_upload"`) to the Bifrost responses schema constants.
- Added handling for `AnthropicContentBlockTypeContainerUpload` in both the standard and grouped Anthropic→Bifrost responses converters, preserving `file_id` and `cache_control`.
- Added `toBifrostResponsesContainerUploadBlock()` helper on `AnthropicContentBlock` to mirror the existing image/document block converters.
- Added the reverse conversion path in `convertContentBlockToAnthropic` so `container_upload` blocks round-trip correctly from Bifrost→Anthropic.
- Updated `isEffectivelyEmptyContent` in the cursor integration to treat a message containing only a `container_upload` block (with a non-nil `file_id`) as non-empty, preventing it from being replaced by the `"..."` placeholder.
- Added round-trip tests covering the standard converter, the grouped (Bedrock-routed) converter, and the full integration normalization pipeline.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/providers/anthropic/... -run TestRoundTrip_ContainerUpload
go test ./core/providers/anthropic/... -run TestRoundTrip_ContainerUpload_Grouped
go test ./transports/bifrost-http/integrations/... -run TestAnthropicContainerUploadSurvivesNormalization
go test ./...
```

The `container_upload` block should survive Anthropic→Bifrost→Anthropic conversion with its `file_id` and `cache_control` intact, and should not be replaced by the empty-content `"..."` placeholder during normalization.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None. `file_id` values are opaque references to files already staged in Anthropic's infrastructure; no new secrets or PII are introduced.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: force single region config in vertex key config (#5035)

* fix: pass container block from anthropic api

* feat: force single region config in vertex key config

---------

Co-authored-by: tejas ghatte <tejas@tejass-MacBook-Pro.local>
Co-authored-by: Akshay Deo <akshay@akshaydeo.com>

* fix: skip disabled keys when scheduling model-discovery fetches (#5046)

RefreshLiveModelsForProvider, OnKeyAdded, and OnKeyUpdated read the raw
(unfiltered) key list and scheduled a list-models fetch for every key,
including disabled ones. Core already filters disabled keys out of
ListModels key resolution, so a fetch scoped to a disabled key's ID was
guaranteed to fail with "no key found with id...", wasting per-key
goroutines and logging misleading "falling back onto the static
datasheet" warnings for every disabled key on a provider.

Closes #5037

Co-authored-by: Akshay Deo <akshay@akshaydeo.com>

* fix: fixes race conditions in tracer related to span locks (#5023)

## Summary

Fixes a fatal `concurrent map iteration and map write` panic in observability exporters (Datadog, OTEL, etc.) that cannot be caught by `recover()`. When `CompleteAndFlushTrace` hands a trace to exporters, late writers (streaming span finalization, redaction) may still be mutating span attribute maps under the span lock. Exporters iterating those live maps — directly or via marshaling — race those writes and crash the process.

## Changes

- Added `Trace.SnapshotForExport()` which produces a deep copy of a trace with all attribute maps (trace-level, span-level, and span event-level) cloned under their respective locks, giving exporters a safe, immutable view of the trace.
- Added `Span.snapshotForExport()` as the per-span equivalent, cloning `Attributes` and `Events` under the span lock.
- `CompleteAndFlushTrace` now takes a single snapshot after redaction and passes `exportTrace` to all observability plugin `Inject` calls instead of the live `completedTrace`.
- `Span.Reset()` now acquires `s.mu` before clearing fields, preventing a straggling writer from triggering a fatal concurrent map access on `s.Attributes` during pool release.
- Span pointer identity is preserved within the snapshot (`RootSpan` and `Spans` entries refer to the same copied `*Span` values), so pointer-equality checks within exporters continue to work.
- Updated the `ObservabilityPlugin.Inject` doc comment to remove the misleading reference to pool-reuse races, since the snapshot now insulates exporters from that concern.
- Added `trace_snapshot_test.go` with a race-detector test (`TestSnapshotForExport_ConcurrentWriter`) that reproduces the original crash, and an isolation test (`TestSnapshotForExport_IsolatedCopy`) verifying mutations to the original do not bleed into the snapshot.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test -race ./core/schemas/... ./framework/tracing/...
```

The `TestSnapshotForExport_ConcurrentWriter` test will fatal without the fix when run with `-race`. With the fix, all tests should pass cleanly under the race detector.

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

Attribute maps containing PII or secrets are cloned by reference — values are not deep-copied. Redaction is applied before the snapshot is taken, so no new PII exposure is introduced.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: fixes telemetry plugin cardinality explosion risk (#5041)

## Summary

Prometheus and OpenTelemetry HTTP metrics were using the raw URL path as the `path` label, causing metric cardinality to grow unboundedly as model names, batch IDs, file IDs, and other path parameters appeared in URLs. This PR replaces the raw path with the matched route template (e.g. `/v1/messages/batches/{batch_id}`) so cardinality is bounded by the number of registered routes.

## Changes

- Enabled `SaveMatchedRoutePath` on the fasthttp router so the matched route template is captured per request.
- Added a middleware in `PrepareCommonMiddlewares` that copies the router's matched route template into a stable, router-agnostic user value (`BifrostContextKeyHTTPRoute`) and removes the router's internal key to prevent it from leaking into request path params.
- Updated the OpenTelemetry plugin middleware in `server.go` to prefer the route template over the raw path when recording HTTP metrics.
- Updated `collectPrometheusKeyValues` in `plugins/telemetry/utils.go` to prefer the route template over the raw path.
- Added `BifrostContextKeyHTTPRoute` to the bifrost context key schema with documentation.
- Added a note to the Prometheus observability docs explaining that the `path` label reflects the route template, not the raw URL, and directing users to `model`/`provider` labels for per-model breakdowns.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

```sh
go test ./...
```

1. Start the Bifrost HTTP server with Prometheus metrics enabled.
2. Send requests to parameterized routes, e.g. `/v1/messages/batches/batch_abc123` and `/v1/messages/batches/batch_xyz789`.
3. Scrape `/metrics` and confirm both requests are recorded under a single `path="/v1/messages/batches/{batch_id}"` label value rather than two distinct raw paths.
4. Confirm `model` and `provider` labels on `bifrost_*` metrics still reflect per-model detail.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None. The route template is derived from the router's internal matched path and contains no user-supplied data.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: fixes OTEL metrics not sending status code (#5043)

## Summary

Adds `http.response.status_code` as a dimension on error metrics so that error requests can be broken down by HTTP status code (e.g. 400, 429, 500) rather than all being grouped under `"unknown"`.

## Changes

- Introduced `AttrHTTPResponseStatusCode = "http.response.status_code"` constant following OTel semconv conventions.
- `PopulateErrorAttributes` now includes the HTTP status code from `BifrostError.StatusCode` in the returned attribute map when present.
- `recordMetricsFromTrace` in the OTel plugin reads the `http.response.status_code` attribute from the span and attaches it as a `status_code` dimension when recording error requests. Falls back to `"unknown"` if the attribute is absent.

## Type of change

- [x] Feature

## Affected areas

- [x] Core (Go)
- [x] Plugins

## How to test

```sh
go test ./...
```

Trigger a request that results in a provider error (e.g. an invalid API key to produce a 401, or a bad request to produce a 400) and verify that the resulting error metric carries the correct `status_code` label rather than `"unknown"`.

## Breaking changes

- [x] No

## Related issues

## Security considerations

None. HTTP status codes are non-sensitive numeric values.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add allowlist for private-use redirect URI schemes (RFC 8252 §7.1) with `cursor://` as initial entry (#4994)

## Summary

Adds a default-deny allowlist for private-use ("custom") URI schemes in OAuth2 redirect URI validation, enabling native app clients like Cursor to use schemes such as `cursor://anysphere.cursor-mcp/oauth/callback` (per RFC 8252 §7.1) without opening the door to dangerous schemes like `javascript:`, `data:`, or `file:`.

## Changes

- Introduced `allowedPrivateUseRedirectSchemes`, a map-based allowlist of permitted private-use URI schemes. Currently contains `cursor` as the only entry.
- Updated `isAllowedRedirectScheme` to accept URIs whose scheme appears in the allowlist, provided the URI also includes an authority component (`scheme://host/...`). Opaque forms such as `cursor:whatever` are still rejected.
- Added `TestPrivateUseRedirectSchemes` covering allowlisted schemes, loopback/https cases, non-allowlisted custom schemes, and dangerous schemes to ensure the default-deny behavior holds.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/handlers/...
```

Expected: all tests pass, including the new `TestPrivateUseRedirectSchemes` test which validates that:
- `cursor://anysphere.cursor-mcp/oauth/callback` is accepted
- `https://example.com/cb` and `http://127.0.0.1:49152/cb` are accepted
- `com.example.app://oauth/callback`, `myapp://callback`, `vscode://callback` are rejected
- `javascript:`, `data:`, `file:`, and opaque forms of allowlisted schemes are rejected

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

The allowlist is intentionally default-deny. Only schemes explicitly added to `allowedPrivateUseRedirectSchemes` are permitted beyond `https` and `http`-loopback. An authority component is required even for allowlisted schemes, preventing opaque URI forms from being written into a `Location` header. Dangerous schemes (`javascript:`, `data:`, `file:`) remain rejected regardless of any allowlist entry. New native app clients requiring a custom scheme must be explicitly added to the allowlist.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add `shouldSweep` gate to OAuth2 sweep worker and expose `StartOAuth2SweepWorker` (#4995)

## Summary

Exposes the OAuth2 sweep worker startup as a public method (`StartOAuth2SweepWorker`) and adds a `shouldSweep` gate so multi-node deployments can restrict database sweeping to a single node at a time.

## Changes

- Added a `shouldSweep func() bool` field to `oauth2SweepWorker`. When non-nil, it is consulted before each sweep pass; returning `false` skips the pass entirely. This allows multi-node deployments to elect a single sweeping node without disabling the worker on others, and the gate is re-evaluated every interval so leadership can change at runtime.
- Updated `newOAuth2SweepWorker` to accept and store the `shouldSweep` callback.
- Extracted sweep worker creation and startup into a new public method `StartOAuth2SweepWorker(ctx, shouldSweep)` on `BifrostHTTPServer`. The method is a no-op if a worker is already running or no config store is present, preventing double-starts.
- `Bootstrap` now delegates to `StartOAuth2SweepWorker(ctx, nil)` (always sweep), replacing the inline construction logic.
- Elevated sweep failure log messages from `Debug` to `Warn` so errors surface in production logs.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/server/...
```

- Verify that a server bootstrapped normally still runs the sweep worker and cleans up expired OAuth2 records.
- In a multi-node setup, pass a `shouldSweep` function that returns `false` on non-leader nodes and confirm those nodes skip sweep passes while the leader node continues sweeping.
- Confirm that sweep errors now appear at `WARN` level rather than `DEBUG`.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

No changes to auth logic or token issuance. The sweep worker only removes already-expired or revoked records; restricting it to a single node in a cluster does not affect correctness of token validation on other nodes.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* Forward ScopedDB from HybridLogStore (#5052)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* changed alerting icon from bell to a siren (#5054)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* docs: Bigquery integration docs (#5055)

## Summary

Adds a new BigQuery observability plugin for Bifrost Enterprise that streams every LLM trace into a Google BigQuery table as a single denormalized row, enabling SQL-based analytics, cost attribution, and long-term retention.

## Changes

- Added `docs/features/observability/bigquery.mdx` — full documentation for the BigQuery plugin covering authentication (ADC and service account key), configuration reference, table schema with all columns grouped by category, example SQL queries, plugin span filtering, and troubleshooting guidance.
- Registered `features/observability/bigquery` in `docs/docs.json` under the Observability section alongside the existing Kafka entry.
- Reformatted several single-item and short `pages` arrays in `docs/docs.json` to inline style for consistency.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [x] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

Navigate to the Bifrost docs site and verify:

1. The BigQuery page appears under **Observability** in the sidebar.
2. All accordion sections expand and render the column tables correctly.
3. Code blocks for `config.json`, SQL examples, and the `CREATE TABLE` statement render without errors.
4. Tabs (Web UI / config.json) toggle correctly.
5. All cross-links (OTel, Datadog, Plugin Versioning) resolve.

## Screenshots/Recordings

N/A — documentation-only change.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

N/A

## Security considerations

The documentation explicitly warns against embedding raw service account JSON in stored configuration and instructs users to pass credentials via `env.VAR_NAME` references. It also warns that using `*` for `request_headers` captures all headers including `Authorization`, and recommends scoped patterns instead.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* chore: fixes OTEL tests and strengthens harness (#5056)

## Summary

`SpanKindMCPClient` was falling through to `SPAN_KIND_UNSPECIFIED` in the OTEL converter because no explicit case existed for it. This PR adds the missing mapping and introduces a comprehensive unit test suite for the OTEL mapping layer to catch this class of drift in the future. The e2e observability test is also extended to assert that content stripping holds end-to-end and that previously unasserted metadata attributes (`gen_ai.response.model`, `gen_ai.response.finish_reasons`) are present in exported traces.

## Changes

- Added `schemas.SpanKindMCPClient → tracepb.Span_SPAN_KIND_CLIENT` case in `convertSpanKind` to fix the unspecified span kind bug.
- Added `plugins/otel/mapping_test.go` with the following coverage:
  - **Drift guard** (`TestConvertSpanKindExhaustive`): every `schemas.SpanKind*` constant must map to a non-`UNSPECIFIED` OTEL kind; this is how the `SpanKindMCPClient` gap was detected.
  - **Content stripping** (`TestIsContentAttributeCoversCanonicalSet`, `TestConvertAttributesStripsContentAllSpans`): canonical content keys and OTEL-specific tool-content keys are stripped when `disableContentLogging` is true; metadata keys survive.
  - **Value/type fidelity** (`TestAnyToKeyValueFidelity`): all Go type branches in `anyToKeyValue` (scalars, slices, maps, struct fallback) land in the correct OTEL `AnyValue` variant with correct values.
  - **Edge/nil safety** (`TestConvertAttributesEdgeCases`): nil maps, nil values, empty strings, and empty slices produce no attribute rather than a zero-value or panic.
  - **Request header filtering** (`TestConvertTraceRequestHeaderFiltering`): only allow-listed headers are emitted, prefixed `http.request.header.*`, and only on the root span.
  - **Status mapping** (`TestConvertSpanStatus`): ok/error/unset codes and error message propagation.
  - **Event content stripping** (`TestConvertSpanEventsStripContent`): `disableContentLogging` applies inside event attributes.
  - **Content fidelity** (`TestConvertTraceContentFidelity`): realistic `llm.call` span attributes (JSON message strings, `[]string` finish reasons, int token counts) survive conversion with correct types and values.
- Extended the e2e observability runner to assert `gen_ai.response.model`, `gen_ai.response.finish_reasons`, and the `"stop"` finish reason value are present in the exported trace, and to assert that `"hello world"` message content does **not** appear when `disable_content_logging: true`.

## Type of change

- [x] Bug fix
- [x] Chore/CI

## Affected areas

- [x] Plugins

## How to test

```sh
go test ./plugins/otel/...
```

The e2e observability suite can be run with the local runner:

```sh
node tests/e2e/api/runners/run-observability-local.mjs
```

Expected: all mapping tests pass, the e2e runner confirms `gen_ai.response.model` and `gen_ai.response.finish_reasons` are present in the OTEL export, and `"hello world"` is absent from the exported trace body.

## Breaking changes

- [x] No

## Security considerations

The `assertBufferContainsNone` assertion in the e2e runner validates the privacy guarantee that user message content (`"hello world"`) does not reach the OTEL collector when content logging is disabled. The check distinguishes content from the model name (`"hello-world"`, hyphenated) to avoid false negatives.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable

* chore: adds metrics vs logs sync check and tests for telemetry plugin (#5057)

## Summary

Token usage for compaction, image generation, and passthrough responses was never recorded in the Prometheus counters, causing a mismatch between what appeared in Grafana dashboards and what Bifrost's logging plugin reported. This PR fixes the gap in `PostLLMHook` and adds both unit and E2E test coverage to prevent future regressions.

## Changes

- Added three missing `case` branches to the `PostLLMHook` token-extraction switch in `plugins/telemetry/main.go` to handle `CompactionResponse`, `ImageGenerationResponse`, and `PassthroughResponse` usage fields — the same response types that the logging plugin already records.
- Added `plugins/telemetry/main_test.go` with a regression suite:
  - `TestTokenExtractionParityWithLogging` drives `PostLLMHook` with every usage-bearing response type and asserts `bifrost_input_tokens_total` / `bifrost_output_tokens_total` match exactly. The three previously missing types are explicitly called out as the regression cases.
  - `TestPostLLMHookRequiresStartTime` guards the documented early-return when `PreLLMHook` has not run.
  - `TestMetricsEnabledGating` covers the `MetricsEnabled` config flag and its default-on back-compat behaviour.
  - `TestGetMetricsGathererCombinesRegistries` asserts the `/metrics` scrape gatherer exposes both Bifrost and Go/process runtime metrics.
  - `TestPushGatewayLifecycle` covers enable/disable/re-enable of the push gateway without goroutine leaks.
  - `TestPushGatewayPushesBifrostButNotRuntimeCollectors` stands up a fake push gateway and asserts the pushed payload contains Bifrost metrics but not Go/process runtime collectors.
- Added `assertMetricsMatchLogs` to the E2E observability runner (`run-observability-local.mjs`), which cross-checks the `/metrics` scrape counters against the logging trace for the same call. `assertPrometheusScrape` and `assertLoggingTrace` now return their data so the reconciliation can compare both sides; a mismatch fails the E2E run with a descriptive error.

## Type of change

- [x] Bug fix
- [x] Feature

## Affected areas

- [x] Plugins

## How to test

```sh
# Run the new telemetry unit tests
go test ./plugins/telemetry/...

# Run the full E2E observability check (requires local stack)
node tests/e2e/api/runners/run-observability-local.mjs
```

The E2E run will now print `Metrics/logs token usage reconciled (scrape == logs)` on success and fail with a descriptive mismatch error if the counters diverge from the logged usage.

## Breaking changes

- [x] No

## Related issues

Closes the customer-reported Grafana dashboard vs. Bifrost logs token usage mismatch.

## Security considerations

None. No auth, secrets, or PII are involved.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable

* feat: add durable background-job `sidekiq` table, store methods, and runner with recovery and reaper (#4989)

## Summary

Introduces a generic durable background-job system ("sidekiq") that persists job state to the database, enabling long-running tasks to survive process restarts and crashes by resuming from a stored checkpoint cursor.

## Changes

- Added `TableSidekiqJob` model with status constants (`pending`, `running`, `completed`, `failed`) and a `sidekiq` table backed by explicit raw SQL DDL for both Postgres and SQLite dialects, with a composite index on `(status, updated_at)` to support reaper and recovery scans.
- Added a `add_sidekiq_table` migration step that creates the table and index idempotently, falling back to GORM auto-DDL for unsupported dialects.
- Added CRUD and lifecycle methods on `RDBConfigStore`: `CreateSidekiqJob`, `GetSidekiqJob`, `MarkSidekiqJobRunning`, `UpdateSidekiqJobProgress`, `CompleteSidekiqJob`, `FailSidekiqJob`, `ListIncompleteSidekiqJobs`, and `MarkStaleSidekiqJobsFailed`.
- Extended the `ConfigStore` interface with the above methods.
- Added `framework/sidekiq` package containing a `Runner` that manages handler registration, concurrency-bounded goroutine dispatch, panic recovery, crash recovery (`RecoverIncomplete`), and a periodic reaper (`StartReaper`) that flips stale running jobs to failed when their heartbeat exceeds a configurable threshold.
- The `Runner` depends on a narrow `Store` interface rather than the full configstore, keeping it independently testable.
- Added unit tests covering: successful completion, handler errors, panic recovery, unknown-kind rejection, incomplete-job recovery, and reaper invocation.
- Added no-op sidekiq method stubs to `MockConfigStore` in the HTTP transport test suite to satisfy the updated interface.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./framework/sidekiq/...
go test ./framework/configstore/...
go test ./transports/bifrost-http/lib/...
```

- Verify the `add_sidekiq_table` migration runs cleanly against both SQLite and Postgres backends and that the `sidekiq` table and `idx_sidekiq_status_updated` index are created.
- Enqueue a job via `Runner.Enqueue`, confirm it transitions through `pending → running → completed` in the database.
- Kill the process mid-job and restart; call `Runner.RecoverIncomplete` and confirm the job resumes from its last checkpoint metadata.
- Start the reaper with a short `staleAfter` duration, leave a job in `running` without heartbeating, and confirm it is flipped to `failed`.

## Breaking changes

- [x] Yes
- [ ] No

The `ConfigStore` interface gains eight new methods. Any existing mock or alternative implementation of `ConfigStore` must add stubs for all eight sidekiq methods.

## Related issues

## Security considerations

Job metadata is stored as a plain text JSON blob. Callers must ensure no secrets or PII are written into the metadata field, as it is persisted in plaintext and returned in query results without redaction.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: show canonical model names in dashboard model rankings (#4941)

* feat: show canonical model names in dashboard model rankings

Model Rankings and the Top Models chart previously displayed raw wire
model values, which for AWS Bedrock application inference profiles are
opaque resource IDs (e.g. "4xg7dq2mkz9v"), making the dashboard hard to
read. The logs table already stores canonical_model_name per row (from
deployments/key aliases with model_name set), but no aggregation path
surfaced it.

- GetModelRankings (raw + matview paths) selects
  MAX(NULLIF(canonical_model_name, '')) per model+provider group and
  returns it as canonical_model_name on ModelRankingEntry; grouping
  stays keyed by the raw model. The previous-period trend query keeps
  the canonical-free clause since it never reads the column.
- mv_logs_hourly gains canonical_model_name as a dimension (DDL, unique
  index, required columns); repairMatViewShapes rebuilds old-shape
  views on startup, same as the alias dimension added for #4071.
- The rankings table renders the canonical name with the raw profile
  ID as muted secondary text; the Top Models legend and tooltip resolve
  labels through a shared displayModelLabel helper in chartUtils. CSV
  export gains a "Canonical Model" column.

Tested on SQLite, Postgres (raw + matview), and ClickHouse via the
logstore parity suite and new TestCanonicalModelRankings_* tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: note canonical_model_name cardinality trade-off in mv_logs_hourly DDL comment

Addresses CodeRabbit's review note on PR #4941: the dimension is
effectively functionally dependent on model, buckets only split
transiently while a model's canonical value churns, and readers
re-aggregate per model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Pratham Mishra <99235987+Pratham-Mishra04@users.noreply.github.com>

* add model catalog pricing (#5033)

* add model catalog pricing

* address review: extract pricing formatters and add model param to source URL

Move duplicated token price formatting into ui/lib/utils/numbers.ts and
append ?model= to the default datasheet pricing source link.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: John Brett <johnbrett@MAC-A5A852.station>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Pratham Mishra <99235987+Pratham-Mishra04@users.noreply.github.com>

* fix: forwards request id and trace id through telemetry (#5058)

## Summary

Callers currently have no reliable way to correlate a Bifrost HTTP response back to its structured access log entry or distributed trace. This PR surfaces `x-request-id` and `x-bifrost-trace-id` as response headers on every traced request, and ensures both values are written as fields on the access log so they can be searched directly in Loki, Tempo, Grafana, or any similar observability stack.

## Changes

- `TracingMiddleware` now sets `x-request-id` (echoed from the caller or the generated UUID) and `x-bifrost-trace-id` (inherited from an incoming W3C `traceparent` or generated) on every response, including error responses.
- `CorsMiddleware` access-log path now emits `request_id` alongside the existing `trace_id` field so both correlation IDs appear in structured stdout logs.
- Documentation added to `docs/providers/request-options.mdx` describing the two response headers and their relationship to the access log fields.
- Tests added for: header generation when no `x-request-id` is supplied, header echo when one is supplied, header survival through the error path, and access-log emission of both `trace_id` and `request_id`.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

```sh
go version
go test ./transports/bifrost-http/handlers/...
```

Send a request without `x-request-id` and confirm both `x-request-id` and `x-bifrost-trace-id` appear in the response headers with non-empty values.

Send a request with `x-request-id: my-id` and confirm the response echoes `x-request-id: my-id` and includes a non-empty `x-bifrost-trace-id`.

Check the structured access log output and confirm both `request_id` and `trace_id` fields are present and match the response headers.

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

Closes BF-1041

## Security considerations

The `x-request-id` value supplied by the caller is echoed back verbatim in the response header and written to the access log. No sanitisation beyond what fasthttp already applies to header values is performed. Callers should not embed sensitive data in request IDs.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add `GetInFlightSidekiqJobByKind` to config store interface (#5004)

## Summary

Adds a `GetInFlightSidekiqJobByKind` method to the config store that looks up the most recently created pending or running Sidekiq job of a given kind. This allows callers to check whether a job of the same kind is already active before enqueuing a new one, preventing duplicate in-flight jobs.

## Changes

- Added `GetInFlightSidekiqJobByKind` to `RDBConfigStore` in `framework/configstore/sidekiq.go`, querying for the latest job matching the given kind with a `pending` or `running` status, returning `nil` when none exists.
- Added `GetInFlightSidekiqJobByKind` to the `ConfigStore` interface in `framework/configstore/store.go` so all implementations must satisfy the contract.
- Added a no-op stub implementation to `MockConfigStore` in `transports/bifrost-http/lib/config_test.go` to keep the mock in sync with the updated interface.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./framework/configstore/...
go test ./transports/bifrost-http/...
```

Verify that:
1. A job of a given kind that is `pending` or `running` is returned by `GetInFlightSidekiqJobByKind`.
2. `nil, nil` is returned when no matching in-flight job exists.
3. The most recently created job is returned when multiple in-flight jobs of the same kind exist.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None. This is a read-only query scoped to job kind and status with no exposure of sensitive data.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: migrate cost recalculation to durable background sidekiq job with resume and dedup (#5005)

## Summary

Cost recalculation is migrated from a synchronous (and optionally SSE-streamed) HTTP handler into a durable background sidekiq job. This prevents long-running recalculations from timing out or being lost on server restart, and gives the UI a stable job ID to poll for progress.

## Changes

- **`plugins/logging/costrecalc.go`** — New file implementing the sidekiq job body. `BuildCostRecalcJobMeta` counts in-scope rows and serialises the initial `CostRecalcJobMeta` (frozen time window, scope, counters, cursor). `RunCostRecalcJob` walks the window in timestamp-ascending batches of 1 000, recomputes costs via the existing pricing manager, bulk-updates the store, and checkpoints the cursor after each batch so a crash or restart can resume without reprocessing from the beginning. An anti-stall nudge (`+1 ns`) prevents an infinite loop when an entire batch shares the same …
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
….1) with `cursor://` as initial entry (maximhq#4994)

## Summary

Adds a default-deny allowlist for private-use ("custom") URI schemes in OAuth2 redirect URI validation, enabling native app clients like Cursor to use schemes such as `cursor://anysphere.cursor-mcp/oauth/callback` (per RFC 8252 §7.1) without opening the door to dangerous schemes like `javascript:`, `data:`, or `file:`.

## Changes

- Introduced `allowedPrivateUseRedirectSchemes`, a map-based allowlist of permitted private-use URI schemes. Currently contains `cursor` as the only entry.
- Updated `isAllowedRedirectScheme` to accept URIs whose scheme appears in the allowlist, provided the URI also includes an authority component (`scheme://host/...`). Opaque forms such as `cursor:whatever` are still rejected.
- Added `TestPrivateUseRedirectSchemes` covering allowlisted schemes, loopback/https cases, non-allowlisted custom schemes, and dangerous schemes to ensure the default-deny behavior holds.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/handlers/...
```

Expected: all tests pass, including the new `TestPrivateUseRedirectSchemes` test which validates that:
- `cursor://anysphere.cursor-mcp/oauth/callback` is accepted
- `https://example.com/cb` and `http://127.0.0.1:49152/cb` are accepted
- `com.example.app://oauth/callback`, `myapp://callback`, `vscode://callback` are rejected
- `javascript:`, `data:`, `file:`, and opaque forms of allowlisted schemes are rejected

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

The allowlist is intentionally default-deny. Only schemes explicitly added to `allowedPrivateUseRedirectSchemes` are permitted beyond `https` and `http`-loopback. An authority component is required even for allowlisted schemes, preventing opaque URI forms from being written into a `Location` header. Dangerous schemes (`javascript:`, `data:`, `file:`) remain rejected regardless of any allowlist entry. New native app clients requiring a custom scheme must be explicitly added to the allowlist.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
* upgrades clickhouse library version

* allow v2.0.0 to publish prerelease cuts

* updates clickhouse main library

* feat(logging): add transient redaction data field and context key for guardrails (#4169)

## Summary

This PR introduces reversible redaction support across the Bifrost stack, allowing enterprise guardrails plugins to redact PII from log content while preserving an encrypted reversible mapping that authorized users can later reveal inline in the log detail view.

## Changes

- Added `RedactionPayload` schema type and associated context helpers (`RedactionPayloadFromContext`, `SetRedactionPayloadOnContext`, `ApplyLiteralReplacements`) to carry request-scoped redaction data from guardrails to log sinks
- Added `BifrostContextKeyRedactionData` context key for guardrails plugins to attach redaction payloads (marked DO NOT SET MANUALLY)
- Added `RedactionData` (transient), `RedactionMapping` (persisted), and `HasReversibleRedaction` (virtual) fields to the `Log` table struct
- Added `migrationAddRedactionMappingColumn` to persist the reversible mapping alongside the log row so it shares the row's lifecycle
- Updated `FindByID` to use `ScopedDB` so point lookups honor caller-supplied query scope (e.g. Enterprise DAC), preventing out-of-scope ID access
- Added `attachLogRedactionData` in the logging plugin to copy guardrail redaction payloads into log entries before async writes, gated on content logging being enabled
- Exposed `HasReversibleRedaction` on log detail and list endpoints so the UI knows when a reveal toggle is applicable
- Added a `Reveal` RBAC operation and `canReveal` prop threading through `LogDetailSheet` → `LogDetailView`
- Added a "Show original values" toggle in the log detail header that calls a new `POST /logs/:id/reveal` endpoint and applies the returned mapping inline to all message text, reasoning, and refusal fields without mutating stored data
- Added `useRevealLogRedactionMappingMutation` RTK Query mutation and `LogRedactionRevealResponse` type
- Literal replacement applies longest-match-first ordering to avoid partial substitution of overlapping tokens

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

```sh
# Core/Transports
go test ./core/schemas/... ./framework/logstore/... ./plugins/logging/...

# UI
cd ui
pnpm i
pnpm build
```

To validate end-to-end:
1. Configure an enterprise guardrails plugin that sets `BifrostContextKeyRedactionData` with a `RedactionPayload` containing `ReversibleMappings`
2. Send a request containing PII through Bifrost
3. Open the log detail view — the "Show original values" toggle should appear only for users with the `Reveal` RBAC permission on `Logs`
4. Toggle reveal — placeholders like `[EMAIL-1]` should be replaced inline with their original values
5. Navigate to a different log — the toggle resets and the mapping is cleared from state

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

- The `RedactionMapping` column stores the reversible mapping encrypted when an encryption key is configured; the mapping is deleted when the log row is deleted, preventing orphaned sensitive data
- The reveal endpoint is gated behind a new `Reveal` RBAC operation so only authorized users can recover original PII values
- `BifrostContextKeyRedactionData` is explicitly marked DO NOT SET MANUALLY to prevent plugins from injecting arbitrary mappings
- `attachLogRedactionData` is a no-op when content logging is disabled, preventing sensitive payloads from leaking through the async write path
- `FindByID` now enforces query scope, closing a gap where a scoped caller could retrieve out-of-scope log rows by ID

## Checklist

- [x] I added/updated tests where appropriate
- [x] I verified builds succeed (Go and UI)

* feat(logging): redact trace content before connector export (#4417)

## Summary

Adds trace-level redaction of span content attributes before traces are exported to observability plugins. Connectors can register raw-to-placeholder replacement maps on a trace; when the trace completes, all content-bearing span attributes (messages, prompts, tool arguments, etc.) are rewritten in-place before any plugin receives the trace. The replacement map is stored in an unexported field so it is never serialized or leaked to connectors.

## Changes

- Added `IsContentAttribute(key string) bool` to classify which span attribute keys may carry user or model content (messages, prompts, embeddings, tool arguments, reasoning text, etc.).
- Added `RedactAttributeValue(value any, replacements map[string]string) any` to apply literal replacements across `string`, `[]string`, and `[]any` attribute shapes.
- Added `redactionReplacements` as an unexported field on `Trace` so the map is never JSON-serialized and cannot be observed by connectors.
- Added `Trace.SetRedactionReplacements` to store a defensive copy of the replacement map, stripping empty keys.
- Added `Trace.ApplyRedactionReplacements` to walk every span, redact content attributes, and clear the map atomically.
- Added `Trace.Reset` cleanup to ensure pooled traces cannot carry redaction data across requests.
- Added `redactSpanAttributes` as a package-private helper that locks a single span and rewrites its content attributes.
- Added `SetTraceRedactionReplacements` to the `Tracer` interface and its `NoOpTracer` implementation.
- Wired `ApplyRedactionReplacements` into `Tracer.CompleteAndFlushTrace` so redaction runs before any observability plugin `Inject` call.
- Added `Tracer.SetTraceRedactionReplacements` in the framework tracing layer to look up the live trace and delegate to `Trace.SetRedactionReplacements`.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/schemas/... ./framework/tracing/...
```

Key scenarios covered by new tests:

- `TestIsContentAttribute` — verifies the content attribute classifier includes message, prompt, embedding, and tool fields while excluding metadata fields like model name and session ID.
- `TestTraceApplyRedactionReplacementsRedactsContentAttributes` — verifies replacements are applied to all spans and that non-content attributes are left untouched.
- `TestTraceRedactionReplacementsDoNotSerialize` — verifies the replacement map never appears in JSON output.
- `TestTraceResetClearsRedactionReplacements` — verifies pooled traces cannot retain replacement data.
- `TestTracer_CompleteAndFlushTraceRedactsContentBeforeInject` — end-to-end: replacements set before span population are applied before the observability plugin receives the trace.
- `TestTracer_SetTraceRedactionReplacementsSurvivesLaterObservabilityPlugins` — replacements set before plugin registration still take effect at flush time.

## Breaking changes

- [x] Yes
- [ ] No

The `Tracer` interface gains a new method `SetTraceRedactionReplacements`. Any external implementation of `Tracer` must add this method. The `NoOpTracer` implementation is provided as a reference no-op.

## Security considerations

The replacement map is stored in an unexported struct field (`redactionReplacements`) and is explicitly cleared after `ApplyRedactionReplacements` runs and during `Reset`. This prevents PII or secret values used as redaction keys from being serialized into trace payloads, retained across pooled trace reuse, or observed by observability plugin authors inspecting the exported `Trace` struct.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* docs for redaction (#4565)

## Summary

Adds documentation for Bifrost-managed guardrail redaction, two new PII guardrail providers (Microsoft Presidio and Azure AI Language PII), and a `POST /api/logs/{id}/reveal` endpoint for revealing reversible redaction mappings from Bifrost logs.

## Changes

- Added a new `Guardrail Redaction` reference page (`enterprise/guardrails/redaction.mdx`) covering the three redaction modes (`runtime`, `logs_only`, `runtime_reversible`), redaction strategies (`replace`, `mask`, `hash`), the reveal permission model, and connector export behavior.
- Added integration pages for Microsoft Presidio (`integrations/guardrails/presidio.mdx`) and Azure AI Language PII (`integrations/guardrails/azure-language-pii.mdx`), including configuration fields, authentication modes, and all four config formats (Web UI, API, config.json, Helm).
- Extended the Regex and Secrets Detection provider docs and config examples to include per-pattern `action`, `redaction_strategy`, `redaction_mode`, and `entity_type` fields.
- Updated the guardrails overview to list Presidio and Azure AI Language PII in the provider capability matrix, added a warning against combining provider-managed transformation with Bifrost-managed redaction on the same phase, and added a Redaction section summarizing the three modes.
- Updated the nav (`docs.json`) to add a `Providers` sub-group under Guardrails and surface the new Redaction, Presidio, and Azure AI Language PII pages.
- Added `POST /api/logs/{id}/reveal` to the OpenAPI spec (YAML and compiled JSON), gated by `Logs:Reveal`, returning a `LogRevealResponse` with a placeholder-to-original-value mapping. Added `has_reversible_redaction` to `LogEntry`.
- Added `Logs:Reveal` and `MCPToolGroups`/`MCPLogs` to the RBAC resource table.
- Added guardrail redaction notes to the Datadog connector, OTel, default observability, and log-exports pages explaining that exported content receives redacted or placeholderized values and that reveal mappings are not forwarded to connectors.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [x] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

Review the rendered docs to confirm:

- The Guardrail Redaction page renders the mode matrix table and the redaction mode selector screenshot correctly.
- The Presidio and Azure AI Language PII pages appear under the Guardrails > Providers nav group.
- The `POST /api/logs/{id}/reveal` endpoint appears in the API reference with correct request/response schemas and a `403` for missing `Logs:Reveal` permission.
- The Regex and Secrets Detection config examples include `action`, `redaction_strategy`, and `redaction_mode` fields.
- Cross-links between the redaction page and provider pages resolve without 404s.

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

- The `POST /api/logs/{id}/reveal` endpoint returns original sensitive values and is gated by the `Logs:Reveal` RBAC permission. The response is marked `Cache-Control: no-store`.
- Reveal mappings are stored only in Bifrost logs and are never forwarded to trace-export connectors, object storage payloads, or external observability destinations.
- When an encryption key is configured, the reveal mapping is encrypted before storage.
- If `disable_content_logging` is enabled, no reveal data is persisted.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* [fix]: preserve streaming finish_reason in the accumulated response when forwarded on a content chunk (#4964)

The streaming chat accumulator reads finish_reason only from the highest-index
chunk (getLastChatChunkLocked). For providers that send finish_reason on the
final content chunk, the OpenAI-compatible handler forwards it on that chunk and
appends a synthetic terminal chunk (index + 1) whose finish_reason is nil to
avoid a duplicate client emission (the forwardedTerminalFinishReason guard from
#1995). The highest-index chunk therefore has a nil finish_reason while the real
one sits one index lower, so the accumulated response records null. Unlike the
sibling TokenUsage, Cost and CacheDebug fields at the same site, finish_reason
was assigned without a nil check. The accumulated value feeds the logging plugin
(entry.StopReason) and Maxim, so streaming logs recorded an empty stop reason
for these providers; non-streaming is unaffected.

Fall back to the newest chunk that actually carries a finish_reason only when the
highest-index chunk has none. Regression tests cover the content-chunk case and
the standard terminal-chunk case.

closes #4963

Signed-off-by: Akshay Deo <akshay@akshaydeo.com>
Co-authored-by: Akshay Deo <akshay@akshaydeo.com>

* fix: pass azure auth headers in helpers (#4999)

## Summary

Azure media endpoints (Speech, Transcription, ImageGeneration, ImageEdit, VideoGeneration) were hardcoding `Authorization: Bearer <key>` authentication, ignoring Azure-specific auth mechanisms such as service principal tokens or `api-key` headers. This PR propagates Azure auth headers through the shared OpenAI handler functions so that Azure's authentication flow is respected for all media request types.

## Changes

- Added an `authHeaders map[string]string` parameter to `HandleOpenAISpeechRequest`, `HandleOpenAITranscriptionRequest`, `HandleOpenAIImageGenerationRequest`, `HandleOpenAIImageEditRequest`, and `HandleOpenAIVideoGenerationRequest`.
- Each handler now prefers caller-supplied `authHeaders` over the default `Bearer` token fallback. If `authHeaders` is empty or nil, it falls back to `BearerAuthHeader(key)` as before.
- The Azure provider now calls `getAzureAuthHeaders` before invoking each of these handlers and passes the result through.
- Non-Azure providers (OpenAI, Groq, vLLM, xAI) pass `nil` for `authHeaders`, preserving existing behavior.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./...
```

Validate by configuring an Azure provider with service principal credentials and invoking Speech, Transcription, ImageGeneration, ImageEdit, and VideoGeneration endpoints. Confirm that requests are authenticated using the Azure-specific headers rather than a `Bearer` token, and that non-Azure providers continue to authenticate with `Bearer` tokens as expected.

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

Auth headers sourced from `getAzureAuthHeaders` may contain short-lived tokens or API keys. These are passed only in-memory to the HTTP request headers and are not logged or persisted. Existing secret handling guarantees apply.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix(redaction): phase scoped redaction and revealing (#5007)

## Summary

Redaction replacements are now tracked separately for request-side (input) and response-side (output) content rather than in a single flat map. This prevents input-phase redaction tokens from being applied to output attributes and vice versa, ensuring each replacement set is scoped to the content it was derived from.

## Changes

- Introduced `RedactionPhase` (`input` / `output`) and `RedactionMapsByPhase` to replace the flat `map[string]string` used in `RedactionData` and `Trace.redactionReplacements`.
- `SetRedactionReplacements` and `SetTraceRedactionReplacements` now require a `RedactionPhase` argument so callers explicitly declare which lifecycle phase produced the replacements.
- Span attribute redaction (`redactSpanAttributes`) selects the correct replacement map per attribute using a new `traceContentAttributeScopeForKey` classifier:
    - Input-only attributes (e.g. `AttrInputMessages`, `AttrPrompt`) receive only input replacements.
    - Output-only attributes (e.g. `AttrOutputMessages`, `AttrRespReasoningText`) receive only output replacements.
    - Mixed attributes (e.g. `AttrToolCallArguments`, `AttrToolCallResult`) receive a merged map of both phases.
- `IsContentAttribute` is now derived from `traceContentAttributeScopeForKey` to keep the two in sync.
- `RevealRedactionMapping` on `logstore.Log` changed from `map[string]string` to `*schemas.RedactionMapsByPhase`, and `LogRedactionMappingResolver` returns the same type.
- The `redaction_mapping` field in the log API response and OpenAPI schema is now a `{ input, output }` object instead of a flat map.
- The UI `LogEntry` type reflects the new shape, and `logDetailView` applies input and output reveal mappings independently to the appropriate content sections (request body, input messages, response body, output messages, reasoning, refusals, Responses API items).

## Type of change

- [ ] Bug fix
- [x] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [x] UI (React)
- [x] Docs

## How to test

```sh
# Core/Transports
go test ./core/schemas/... ./framework/tracing/... ./plugins/logging/...

# UI
cd ui
pnpm i
pnpm build
```

Verify that:

- Input-phase redaction tokens (e.g. `[EMAIL-1]`) are applied only to input attributes and request bodies.
- Output-phase redaction tokens (e.g. `[EMAIL-2]`) are applied only to output attributes and response bodies.
- The log detail reveal toggle restores original values in the correct content sections.
- The `redaction_mapping` field in log detail API responses serializes as `{ "input": {...}, "output": {...} }`.

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

`SetTraceRedactionReplacements` now requires a `RedactionPhase` argument. Any custom `Tracer` or `LogRedactionMappingResolver` implementations must be updated to match the new signatures. The `redaction_mapping` field in log detail API responses has changed shape from a flat object to a `{ input, output }` object; API consumers that read this field will need to handle the new structure.

## Related issues

N/A

## Security considerations

Scoping replacements by phase reduces the risk of a redaction token from one phase incorrectly masking or revealing content in another phase. The reversible mapping (used for the `Logs:Reveal` feature) is now also phase-scoped, so revealed values are only substituted back into the content section they originated from.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix(openai): serialize compaction request `input` correctly (#5014)

OpenAICompactionRequest had no MarshalJSON, so its value-typed
OpenAIResponsesRequestInput field — whose only marshaler is a pointer
receiver — was emitted by default struct encoding as a JSON object
({"OpenAIResponsesRequestInputArray":null,"OpenAIResponsesRequestInputStr":null}),
which /v1/responses/compact rejects with "Invalid type for 'input':
expected a string, but got an object instead." omitempty on the value
field also never omitted an empty input.

Add a MarshalJSON mirroring OpenAIResponsesRequest: route `input` through
the union's marshaler (string/array) and omit it when empty, since a
previous_response_id-only compaction is valid.

* fix(schemas): add ExtraContent to ChatStreamResponseChoiceDelta (#4569)

Rebased onto core/v1.5.21 (includes EnvVar, AliasConfig, etc).

Adds ExtraContent json.RawMessage to ChatStreamResponseChoiceDelta so
Gemini extended thinking markers (google.thought, thought_signature)
survive streaming through any Bifrost-based gateway/proxy.

Also adds ExtraContent deep-copy in DeepCopyChatMessage for the
tool-call path to prevent shared backing-array mutations in concurrent
streaming pipelines.

Upstream PR: https://github.com/maximhq/bifrost/pull/4569

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* added enterprise fallback pages for alerting (#4685)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat(ui): add Microsoft Teams icon and alert API tags (#4826)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix(transcription): support diarized_json segments, fix ElevenLabs speaker passthrough (#5020)

* fix(transcription): support diarized_json segments, fix ElevenLabs speaker passthrough

OpenAI's response_format=diarized_json (gpt-4o-transcribe-diarize) returns
segments with a string id, plus speaker/type fields, which crashed
unmarshalling into TranscriptionSegment's int id (#5002). Adds a distinct
TranscriptionDiarizedSegment type and decodes diarized_json separately in
both the OpenAI provider's normal and large-payload-passthrough paths (Azure
inherits the fix via the shared handler).

Since Segments and DiarizedSegments serialize under the same "segments" key,
BifrostTranscriptionResponse gets a custom MarshalJSON/UnmarshalJSON pair so
the shape round-trips correctly both on the wire and through
framework/logstore's persist/reload cycle.

Also:
- ElevenLabs' per-word speaker_id was decoded but never propagated into the
  canonical TranscriptionWord; added a Speaker field and wired it through.
- Multipart transcription parsing only whitelisted OpenAI's own fields,
  silently dropping provider-specific extras like ElevenLabs' diarize; now
  passes through unrecognized fields via ExtraParams.
- TranscriptionUsage.Seconds was *int, but OpenAI's duration-usage variant is
  fractional (e.g. 521.5) and would fail to parse; widened to *float64.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(transcription): dedupe diarized_json decode struct

The diarized_json response shape was duplicated as two anonymous structs
(normal path and large-payload-passthrough path); pulled into a single
named type instead.

* fix(transcription): address review findings on round-trip and multipart parsing

- Empty diarized segment arrays (e.g. silent audio) were indistinguishable
  from empty verbose segments on reload, since both unmarshal successfully
  from "[]" - a diarized response with zero segments would silently lose its
  identity and, on re-marshal, drop the "segments" key OpenAI's diarized_json
  contract requires. Adds an "is_diarized" marker written whenever
  DiarizedSegments is set, used as the authoritative signal when present;
  falls back to the existing shape-sniffing for data persisted before the
  marker existed.
- Custom Marshal/UnmarshalJSON now use encoding/json instead of sonic, per
  this repo's core/schemas convention.
- transcription multipart parsing didn't extract temperature or
  timestamp_granularities into their typed fields (verified via the
  openai-python SDK's actual multipart encoding: plain "temperature" field,
  repeated "timestamp_granularities[]"), so they'd leak into ExtraParams
  instead of reaching the outbound OpenAI request. Extracted properly and
  excluded from the generic passthrough.
- new(expr) instead of an intermediate variable for the two *int/*float64
  seconds conversions, matching this repo's existing Go 1.26 convention.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Akshay Deo <akshay@akshaydeo.com>

* fix: pass container block from anthropic api (#5024)

## Summary

Adds support for Anthropic's `container_upload` content block type, which is used to stage files into the code-execution container. Previously, these blocks were silently dropped during conversion between Anthropic and Bifrost formats.

## Changes

- Added `ResponsesInputMessageContentBlockTypeContainerUpload` (`"container_upload"`) to the Bifrost responses schema constants.
- Added handling for `AnthropicContentBlockTypeContainerUpload` in both the standard and grouped Anthropic→Bifrost responses converters, preserving `file_id` and `cache_control`.
- Added `toBifrostResponsesContainerUploadBlock()` helper on `AnthropicContentBlock` to mirror the existing image/document block converters.
- Added the reverse conversion path in `convertContentBlockToAnthropic` so `container_upload` blocks round-trip correctly from Bifrost→Anthropic.
- Updated `isEffectivelyEmptyContent` in the cursor integration to treat a message containing only a `container_upload` block (with a non-nil `file_id`) as non-empty, preventing it from being replaced by the `"..."` placeholder.
- Added round-trip tests covering the standard converter, the grouped (Bedrock-routed) converter, and the full integration normalization pipeline.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/providers/anthropic/... -run TestRoundTrip_ContainerUpload
go test ./core/providers/anthropic/... -run TestRoundTrip_ContainerUpload_Grouped
go test ./transports/bifrost-http/integrations/... -run TestAnthropicContainerUploadSurvivesNormalization
go test ./...
```

The `container_upload` block should survive Anthropic→Bifrost→Anthropic conversion with its `file_id` and `cache_control` intact, and should not be replaced by the empty-content `"..."` placeholder during normalization.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None. `file_id` values are opaque references to files already staged in Anthropic's infrastructure; no new secrets or PII are introduced.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: force single region config in vertex key config (#5035)

* fix: pass container block from anthropic api

* feat: force single region config in vertex key config

---------

Co-authored-by: tejas ghatte <tejas@tejass-MacBook-Pro.local>
Co-authored-by: Akshay Deo <akshay@akshaydeo.com>

* fix: skip disabled keys when scheduling model-discovery fetches (#5046)

RefreshLiveModelsForProvider, OnKeyAdded, and OnKeyUpdated read the raw
(unfiltered) key list and scheduled a list-models fetch for every key,
including disabled ones. Core already filters disabled keys out of
ListModels key resolution, so a fetch scoped to a disabled key's ID was
guaranteed to fail with "no key found with id...", wasting per-key
goroutines and logging misleading "falling back onto the static
datasheet" warnings for every disabled key on a provider.

Closes #5037

Co-authored-by: Akshay Deo <akshay@akshaydeo.com>

* fix: fixes race conditions in tracer related to span locks (#5023)

## Summary

Fixes a fatal `concurrent map iteration and map write` panic in observability exporters (Datadog, OTEL, etc.) that cannot be caught by `recover()`. When `CompleteAndFlushTrace` hands a trace to exporters, late writers (streaming span finalization, redaction) may still be mutating span attribute maps under the span lock. Exporters iterating those live maps — directly or via marshaling — race those writes and crash the process.

## Changes

- Added `Trace.SnapshotForExport()` which produces a deep copy of a trace with all attribute maps (trace-level, span-level, and span event-level) cloned under their respective locks, giving exporters a safe, immutable view of the trace.
- Added `Span.snapshotForExport()` as the per-span equivalent, cloning `Attributes` and `Events` under the span lock.
- `CompleteAndFlushTrace` now takes a single snapshot after redaction and passes `exportTrace` to all observability plugin `Inject` calls instead of the live `completedTrace`.
- `Span.Reset()` now acquires `s.mu` before clearing fields, preventing a straggling writer from triggering a fatal concurrent map access on `s.Attributes` during pool release.
- Span pointer identity is preserved within the snapshot (`RootSpan` and `Spans` entries refer to the same copied `*Span` values), so pointer-equality checks within exporters continue to work.
- Updated the `ObservabilityPlugin.Inject` doc comment to remove the misleading reference to pool-reuse races, since the snapshot now insulates exporters from that concern.
- Added `trace_snapshot_test.go` with a race-detector test (`TestSnapshotForExport_ConcurrentWriter`) that reproduces the original crash, and an isolation test (`TestSnapshotForExport_IsolatedCopy`) verifying mutations to the original do not bleed into the snapshot.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test -race ./core/schemas/... ./framework/tracing/...
```

The `TestSnapshotForExport_ConcurrentWriter` test will fatal without the fix when run with `-race`. With the fix, all tests should pass cleanly under the race detector.

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

Attribute maps containing PII or secrets are cloned by reference — values are not deep-copied. Redaction is applied before the snapshot is taken, so no new PII exposure is introduced.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: fixes telemetry plugin cardinality explosion risk (#5041)

## Summary

Prometheus and OpenTelemetry HTTP metrics were using the raw URL path as the `path` label, causing metric cardinality to grow unboundedly as model names, batch IDs, file IDs, and other path parameters appeared in URLs. This PR replaces the raw path with the matched route template (e.g. `/v1/messages/batches/{batch_id}`) so cardinality is bounded by the number of registered routes.

## Changes

- Enabled `SaveMatchedRoutePath` on the fasthttp router so the matched route template is captured per request.
- Added a middleware in `PrepareCommonMiddlewares` that copies the router's matched route template into a stable, router-agnostic user value (`BifrostContextKeyHTTPRoute`) and removes the router's internal key to prevent it from leaking into request path params.
- Updated the OpenTelemetry plugin middleware in `server.go` to prefer the route template over the raw path when recording HTTP metrics.
- Updated `collectPrometheusKeyValues` in `plugins/telemetry/utils.go` to prefer the route template over the raw path.
- Added `BifrostContextKeyHTTPRoute` to the bifrost context key schema with documentation.
- Added a note to the Prometheus observability docs explaining that the `path` label reflects the route template, not the raw URL, and directing users to `model`/`provider` labels for per-model breakdowns.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

```sh
go test ./...
```

1. Start the Bifrost HTTP server with Prometheus metrics enabled.
2. Send requests to parameterized routes, e.g. `/v1/messages/batches/batch_abc123` and `/v1/messages/batches/batch_xyz789`.
3. Scrape `/metrics` and confirm both requests are recorded under a single `path="/v1/messages/batches/{batch_id}"` label value rather than two distinct raw paths.
4. Confirm `model` and `provider` labels on `bifrost_*` metrics still reflect per-model detail.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None. The route template is derived from the router's internal matched path and contains no user-supplied data.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* fix: fixes OTEL metrics not sending status code (#5043)

## Summary

Adds `http.response.status_code` as a dimension on error metrics so that error requests can be broken down by HTTP status code (e.g. 400, 429, 500) rather than all being grouped under `"unknown"`.

## Changes

- Introduced `AttrHTTPResponseStatusCode = "http.response.status_code"` constant following OTel semconv conventions.
- `PopulateErrorAttributes` now includes the HTTP status code from `BifrostError.StatusCode` in the returned attribute map when present.
- `recordMetricsFromTrace` in the OTel plugin reads the `http.response.status_code` attribute from the span and attaches it as a `status_code` dimension when recording error requests. Falls back to `"unknown"` if the attribute is absent.

## Type of change

- [x] Feature

## Affected areas

- [x] Core (Go)
- [x] Plugins

## How to test

```sh
go test ./...
```

Trigger a request that results in a provider error (e.g. an invalid API key to produce a 401, or a bad request to produce a 400) and verify that the resulting error metric carries the correct `status_code` label rather than `"unknown"`.

## Breaking changes

- [x] No

## Related issues

## Security considerations

None. HTTP status codes are non-sensitive numeric values.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add allowlist for private-use redirect URI schemes (RFC 8252 §7.1) with `cursor://` as initial entry (#4994)

## Summary

Adds a default-deny allowlist for private-use ("custom") URI schemes in OAuth2 redirect URI validation, enabling native app clients like Cursor to use schemes such as `cursor://anysphere.cursor-mcp/oauth/callback` (per RFC 8252 §7.1) without opening the door to dangerous schemes like `javascript:`, `data:`, or `file:`.

## Changes

- Introduced `allowedPrivateUseRedirectSchemes`, a map-based allowlist of permitted private-use URI schemes. Currently contains `cursor` as the only entry.
- Updated `isAllowedRedirectScheme` to accept URIs whose scheme appears in the allowlist, provided the URI also includes an authority component (`scheme://host/...`). Opaque forms such as `cursor:whatever` are still rejected.
- Added `TestPrivateUseRedirectSchemes` covering allowlisted schemes, loopback/https cases, non-allowlisted custom schemes, and dangerous schemes to ensure the default-deny behavior holds.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/handlers/...
```

Expected: all tests pass, including the new `TestPrivateUseRedirectSchemes` test which validates that:
- `cursor://anysphere.cursor-mcp/oauth/callback` is accepted
- `https://example.com/cb` and `http://127.0.0.1:49152/cb` are accepted
- `com.example.app://oauth/callback`, `myapp://callback`, `vscode://callback` are rejected
- `javascript:`, `data:`, `file:`, and opaque forms of allowlisted schemes are rejected

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

The allowlist is intentionally default-deny. Only schemes explicitly added to `allowedPrivateUseRedirectSchemes` are permitted beyond `https` and `http`-loopback. An authority component is required even for allowlisted schemes, preventing opaque URI forms from being written into a `Location` header. Dangerous schemes (`javascript:`, `data:`, `file:`) remain rejected regardless of any allowlist entry. New native app clients requiring a custom scheme must be explicitly added to the allowlist.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add `shouldSweep` gate to OAuth2 sweep worker and expose `StartOAuth2SweepWorker` (#4995)

## Summary

Exposes the OAuth2 sweep worker startup as a public method (`StartOAuth2SweepWorker`) and adds a `shouldSweep` gate so multi-node deployments can restrict database sweeping to a single node at a time.

## Changes

- Added a `shouldSweep func() bool` field to `oauth2SweepWorker`. When non-nil, it is consulted before each sweep pass; returning `false` skips the pass entirely. This allows multi-node deployments to elect a single sweeping node without disabling the worker on others, and the gate is re-evaluated every interval so leadership can change at runtime.
- Updated `newOAuth2SweepWorker` to accept and store the `shouldSweep` callback.
- Extracted sweep worker creation and startup into a new public method `StartOAuth2SweepWorker(ctx, shouldSweep)` on `BifrostHTTPServer`. The method is a no-op if a worker is already running or no config store is present, preventing double-starts.
- `Bootstrap` now delegates to `StartOAuth2SweepWorker(ctx, nil)` (always sweep), replacing the inline construction logic.
- Elevated sweep failure log messages from `Debug` to `Warn` so errors surface in production logs.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [x] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/server/...
```

- Verify that a server bootstrapped normally still runs the sweep worker and cleans up expired OAuth2 records.
- In a multi-node setup, pass a `shouldSweep` function that returns `false` on non-leader nodes and confirm those nodes skip sweep passes while the leader node continues sweeping.
- Confirm that sweep errors now appear at `WARN` level rather than `DEBUG`.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

No changes to auth logic or token issuance. The sweep worker only removes already-expired or revoked records; restricting it to a single node in a cluster does not affect correctness of token validation on other nodes.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* Forward ScopedDB from HybridLogStore (#5052)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* changed alerting icon from bell to a siren (#5054)

## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

```sh
# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build
```

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* docs: Bigquery integration docs (#5055)

## Summary

Adds a new BigQuery observability plugin for Bifrost Enterprise that streams every LLM trace into a Google BigQuery table as a single denormalized row, enabling SQL-based analytics, cost attribution, and long-term retention.

## Changes

- Added `docs/features/observability/bigquery.mdx` — full documentation for the BigQuery plugin covering authentication (ADC and service account key), configuration reference, table schema with all columns grouped by category, example SQL queries, plugin span filtering, and troubleshooting guidance.
- Registered `features/observability/bigquery` in `docs/docs.json` under the Observability section alongside the existing Kafka entry.
- Reformatted several single-item and short `pages` arrays in `docs/docs.json` to inline style for consistency.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [x] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

Navigate to the Bifrost docs site and verify:

1. The BigQuery page appears under **Observability** in the sidebar.
2. All accordion sections expand and render the column tables correctly.
3. Code blocks for `config.json`, SQL examples, and the `CREATE TABLE` statement render without errors.
4. Tabs (Web UI / config.json) toggle correctly.
5. All cross-links (OTel, Datadog, Plugin Versioning) resolve.

## Screenshots/Recordings

N/A — documentation-only change.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

N/A

## Security considerations

The documentation explicitly warns against embedding raw service account JSON in stored configuration and instructs users to pass credentials via `env.VAR_NAME` references. It also warns that using `*` for `request_headers` captures all headers including `Authorization`, and recommends scoped patterns instead.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* chore: fixes OTEL tests and strengthens harness (#5056)

## Summary

`SpanKindMCPClient` was falling through to `SPAN_KIND_UNSPECIFIED` in the OTEL converter because no explicit case existed for it. This PR adds the missing mapping and introduces a comprehensive unit test suite for the OTEL mapping layer to catch this class of drift in the future. The e2e observability test is also extended to assert that content stripping holds end-to-end and that previously unasserted metadata attributes (`gen_ai.response.model`, `gen_ai.response.finish_reasons`) are present in exported traces.

## Changes

- Added `schemas.SpanKindMCPClient → tracepb.Span_SPAN_KIND_CLIENT` case in `convertSpanKind` to fix the unspecified span kind bug.
- Added `plugins/otel/mapping_test.go` with the following coverage:
  - **Drift guard** (`TestConvertSpanKindExhaustive`): every `schemas.SpanKind*` constant must map to a non-`UNSPECIFIED` OTEL kind; this is how the `SpanKindMCPClient` gap was detected.
  - **Content stripping** (`TestIsContentAttributeCoversCanonicalSet`, `TestConvertAttributesStripsContentAllSpans`): canonical content keys and OTEL-specific tool-content keys are stripped when `disableContentLogging` is true; metadata keys survive.
  - **Value/type fidelity** (`TestAnyToKeyValueFidelity`): all Go type branches in `anyToKeyValue` (scalars, slices, maps, struct fallback) land in the correct OTEL `AnyValue` variant with correct values.
  - **Edge/nil safety** (`TestConvertAttributesEdgeCases`): nil maps, nil values, empty strings, and empty slices produce no attribute rather than a zero-value or panic.
  - **Request header filtering** (`TestConvertTraceRequestHeaderFiltering`): only allow-listed headers are emitted, prefixed `http.request.header.*`, and only on the root span.
  - **Status mapping** (`TestConvertSpanStatus`): ok/error/unset codes and error message propagation.
  - **Event content stripping** (`TestConvertSpanEventsStripContent`): `disableContentLogging` applies inside event attributes.
  - **Content fidelity** (`TestConvertTraceContentFidelity`): realistic `llm.call` span attributes (JSON message strings, `[]string` finish reasons, int token counts) survive conversion with correct types and values.
- Extended the e2e observability runner to assert `gen_ai.response.model`, `gen_ai.response.finish_reasons`, and the `"stop"` finish reason value are present in the exported trace, and to assert that `"hello world"` message content does **not** appear when `disable_content_logging: true`.

## Type of change

- [x] Bug fix
- [x] Chore/CI

## Affected areas

- [x] Plugins

## How to test

```sh
go test ./plugins/otel/...
```

The e2e observability suite can be run with the local runner:

```sh
node tests/e2e/api/runners/run-observability-local.mjs
```

Expected: all mapping tests pass, the e2e runner confirms `gen_ai.response.model` and `gen_ai.response.finish_reasons` are present in the OTEL export, and `"hello world"` is absent from the exported trace body.

## Breaking changes

- [x] No

## Security considerations

The `assertBufferContainsNone` assertion in the e2e runner validates the privacy guarantee that user message content (`"hello world"`) does not reach the OTEL collector when content logging is disabled. The check distinguishes content from the model name (`"hello-world"`, hyphenated) to avoid false negatives.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable

* chore: adds metrics vs logs sync check and tests for telemetry plugin (#5057)

## Summary

Token usage for compaction, image generation, and passthrough responses was never recorded in the Prometheus counters, causing a mismatch between what appeared in Grafana dashboards and what Bifrost's logging plugin reported. This PR fixes the gap in `PostLLMHook` and adds both unit and E2E test coverage to prevent future regressions.

## Changes

- Added three missing `case` branches to the `PostLLMHook` token-extraction switch in `plugins/telemetry/main.go` to handle `CompactionResponse`, `ImageGenerationResponse`, and `PassthroughResponse` usage fields — the same response types that the logging plugin already records.
- Added `plugins/telemetry/main_test.go` with a regression suite:
  - `TestTokenExtractionParityWithLogging` drives `PostLLMHook` with every usage-bearing response type and asserts `bifrost_input_tokens_total` / `bifrost_output_tokens_total` match exactly. The three previously missing types are explicitly called out as the regression cases.
  - `TestPostLLMHookRequiresStartTime` guards the documented early-return when `PreLLMHook` has not run.
  - `TestMetricsEnabledGating` covers the `MetricsEnabled` config flag and its default-on back-compat behaviour.
  - `TestGetMetricsGathererCombinesRegistries` asserts the `/metrics` scrape gatherer exposes both Bifrost and Go/process runtime metrics.
  - `TestPushGatewayLifecycle` covers enable/disable/re-enable of the push gateway without goroutine leaks.
  - `TestPushGatewayPushesBifrostButNotRuntimeCollectors` stands up a fake push gateway and asserts the pushed payload contains Bifrost metrics but not Go/process runtime collectors.
- Added `assertMetricsMatchLogs` to the E2E observability runner (`run-observability-local.mjs`), which cross-checks the `/metrics` scrape counters against the logging trace for the same call. `assertPrometheusScrape` and `assertLoggingTrace` now return their data so the reconciliation can compare both sides; a mismatch fails the E2E run with a descriptive error.

## Type of change

- [x] Bug fix
- [x] Feature

## Affected areas

- [x] Plugins

## How to test

```sh
# Run the new telemetry unit tests
go test ./plugins/telemetry/...

# Run the full E2E observability check (requires local stack)
node tests/e2e/api/runners/run-observability-local.mjs
```

The E2E run will now print `Metrics/logs token usage reconciled (scrape == logs)` on success and fail with a descriptive mismatch error if the counters diverge from the logged usage.

## Breaking changes

- [x] No

## Related issues

Closes the customer-reported Grafana dashboard vs. Bifrost logs token usage mismatch.

## Security considerations

None. No auth, secrets, or PII are involved.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable

* feat: add durable background-job `sidekiq` table, store methods, and runner with recovery and reaper (#4989)

## Summary

Introduces a generic durable background-job system ("sidekiq") that persists job state to the database, enabling long-running tasks to survive process restarts and crashes by resuming from a stored checkpoint cursor.

## Changes

- Added `TableSidekiqJob` model with status constants (`pending`, `running`, `completed`, `failed`) and a `sidekiq` table backed by explicit raw SQL DDL for both Postgres and SQLite dialects, with a composite index on `(status, updated_at)` to support reaper and recovery scans.
- Added a `add_sidekiq_table` migration step that creates the table and index idempotently, falling back to GORM auto-DDL for unsupported dialects.
- Added CRUD and lifecycle methods on `RDBConfigStore`: `CreateSidekiqJob`, `GetSidekiqJob`, `MarkSidekiqJobRunning`, `UpdateSidekiqJobProgress`, `CompleteSidekiqJob`, `FailSidekiqJob`, `ListIncompleteSidekiqJobs`, and `MarkStaleSidekiqJobsFailed`.
- Extended the `ConfigStore` interface with the above methods.
- Added `framework/sidekiq` package containing a `Runner` that manages handler registration, concurrency-bounded goroutine dispatch, panic recovery, crash recovery (`RecoverIncomplete`), and a periodic reaper (`StartReaper`) that flips stale running jobs to failed when their heartbeat exceeds a configurable threshold.
- The `Runner` depends on a narrow `Store` interface rather than the full configstore, keeping it independently testable.
- Added unit tests covering: successful completion, handler errors, panic recovery, unknown-kind rejection, incomplete-job recovery, and reaper invocation.
- Added no-op sidekiq method stubs to `MockConfigStore` in the HTTP transport test suite to satisfy the updated interface.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./framework/sidekiq/...
go test ./framework/configstore/...
go test ./transports/bifrost-http/lib/...
```

- Verify the `add_sidekiq_table` migration runs cleanly against both SQLite and Postgres backends and that the `sidekiq` table and `idx_sidekiq_status_updated` index are created.
- Enqueue a job via `Runner.Enqueue`, confirm it transitions through `pending → running → completed` in the database.
- Kill the process mid-job and restart; call `Runner.RecoverIncomplete` and confirm the job resumes from its last checkpoint metadata.
- Start the reaper with a short `staleAfter` duration, leave a job in `running` without heartbeating, and confirm it is flipped to `failed`.

## Breaking changes

- [x] Yes
- [ ] No

The `ConfigStore` interface gains eight new methods. Any existing mock or alternative implementation of `ConfigStore` must add stubs for all eight sidekiq methods.

## Related issues

## Security considerations

Job metadata is stored as a plain text JSON blob. Callers must ensure no secrets or PII are written into the metadata field, as it is persisted in plaintext and returned in query results without redaction.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: show canonical model names in dashboard model rankings (#4941)

* feat: show canonical model names in dashboard model rankings

Model Rankings and the Top Models chart previously displayed raw wire
model values, which for AWS Bedrock application inference profiles are
opaque resource IDs (e.g. "4xg7dq2mkz9v"), making the dashboard hard to
read. The logs table already stores canonical_model_name per row (from
deployments/key aliases with model_name set), but no aggregation path
surfaced it.

- GetModelRankings (raw + matview paths) selects
  MAX(NULLIF(canonical_model_name, '')) per model+provider group and
  returns it as canonical_model_name on ModelRankingEntry; grouping
  stays keyed by the raw model. The previous-period trend query keeps
  the canonical-free clause since it never reads the column.
- mv_logs_hourly gains canonical_model_name as a dimension (DDL, unique
  index, required columns); repairMatViewShapes rebuilds old-shape
  views on startup, same as the alias dimension added for #4071.
- The rankings table renders the canonical name with the raw profile
  ID as muted secondary text; the Top Models legend and tooltip resolve
  labels through a shared displayModelLabel helper in chartUtils. CSV
  export gains a "Canonical Model" column.

Tested on SQLite, Postgres (raw + matview), and ClickHouse via the
logstore parity suite and new TestCanonicalModelRankings_* tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: note canonical_model_name cardinality trade-off in mv_logs_hourly DDL comment

Addresses CodeRabbit's review note on PR #4941: the dimension is
effectively functionally dependent on model, buckets only split
transiently while a model's canonical value churns, and readers
re-aggregate per model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Pratham Mishra <99235987+Pratham-Mishra04@users.noreply.github.com>

* add model catalog pricing (#5033)

* add model catalog pricing

* address review: extract pricing formatters and add model param to source URL

Move duplicated token price formatting into ui/lib/utils/numbers.ts and
append ?model= to the default datasheet pricing source link.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: John Brett <johnbrett@MAC-A5A852.station>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Pratham Mishra <99235987+Pratham-Mishra04@users.noreply.github.com>

* fix: forwards request id and trace id through telemetry (#5058)

## Summary

Callers currently have no reliable way to correlate a Bifrost HTTP response back to its structured access log entry or distributed trace. This PR surfaces `x-request-id` and `x-bifrost-trace-id` as response headers on every traced request, and ensures both values are written as fields on the access log so they can be searched directly in Loki, Tempo, Grafana, or any similar observability stack.

## Changes

- `TracingMiddleware` now sets `x-request-id` (echoed from the caller or the generated UUID) and `x-bifrost-trace-id` (inherited from an incoming W3C `traceparent` or generated) on every response, including error responses.
- `CorsMiddleware` access-log path now emits `request_id` alongside the existing `trace_id` field so both correlation IDs appear in structured stdout logs.
- Documentation added to `docs/providers/request-options.mdx` describing the two response headers and their relationship to the access log fields.
- Tests added for: header generation when no `x-request-id` is supplied, header echo when one is supplied, header survival through the error path, and access-log emission of both `trace_id` and `request_id`.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [x] Docs

## How to test

```sh
go version
go test ./transports/bifrost-http/handlers/...
```

Send a request without `x-request-id` and confirm both `x-request-id` and `x-bifrost-trace-id` appear in the response headers with non-empty values.

Send a request with `x-request-id: my-id` and confirm the response echoes `x-request-id: my-id` and includes a non-empty `x-bifrost-trace-id`.

Check the structured access log output and confirm both `request_id` and `trace_id` fields are present and match the response headers.

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

Closes BF-1041

## Security considerations

The `x-request-id` value supplied by the caller is echoed back verbatim in the response header and written to the access log. No sanitisation beyond what fasthttp already applies to header values is performed. Callers should not embed sensitive data in request IDs.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: add `GetInFlightSidekiqJobByKind` to config store interface (#5004)

## Summary

Adds a `GetInFlightSidekiqJobByKind` method to the config store that looks up the most recently created pending or running Sidekiq job of a given kind. This allows callers to check whether a job of the same kind is already active before enqueuing a new one, preventing duplicate in-flight jobs.

## Changes

- Added `GetInFlightSidekiqJobByKind` to `RDBConfigStore` in `framework/configstore/sidekiq.go`, querying for the latest job matching the given kind with a `pending` or `running` status, returning `nil` when none exists.
- Added `GetInFlightSidekiqJobByKind` to the `ConfigStore` interface in `framework/configstore/store.go` so all implementations must satisfy the contract.
- Added a no-op stub implementation to `MockConfigStore` in `transports/bifrost-http/lib/config_test.go` to keep the mock in sync with the updated interface.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./framework/configstore/...
go test ./transports/bifrost-http/...
```

Verify that:
1. A job of a given kind that is `pending` or `running` is returned by `GetInFlightSidekiqJobByKind`.
2. `nil, nil` is returned when no matching in-flight job exists.
3. The most recently created job is returned when multiple in-flight jobs of the same kind exist.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None. This is a read-only query scoped to job kind and status with no exposure of sensitive data.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

* feat: migrate cost recalculation to durable background sidekiq job with resume and dedup (#5005)

## Summary

Cost recalculation is migrated from a synchronous (and optionally SSE-streamed) HTTP handler into a durable background sidekiq job. This prevents long-running recalculations from timing out or being lost on server restart, and gives the UI a stable job ID to poll for progress.

## Changes

- **`plugins/logging/costrecalc.go`** — New file implementing the sidekiq job body. `BuildCostRecalcJobMeta` counts in-scope rows and serialises the initial `CostRecalcJobMeta` (frozen time window, scope, counters, cursor). `RunCostRecalcJob` walks the window in timestamp-ascending batches of 1 000, recomputes costs via the existing pricing manager, bulk-updates the store, and checkpoints the cursor after each batch so a crash or restart can resume without reprocessing from the beginning. An anti-stall nudge (`+1 ns`) prevents an infinite loop when an entire batch shares the same …
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants