Skip to content

feat: adds support for multiple otel collectors - #3894

Merged
akshaydeo merged 1 commit into
devfrom
05-29-feat_adds_support_for_multiple_otel_collectors
Jun 2, 2026
Merged

akshaydeo merged 1 commit into
devfrom
05-29-feat_adds_support_for_multiple_otel_collectors

Conversation

@sammaji

@sammaji sammaji commented May 29, 2026

Copy link
Copy Markdown
Member

Summary

The OTEL plugin previously supported a single collector target per configuration. This PR introduces multi-profile support, allowing Bifrost to export traces and metrics to multiple OTEL collectors simultaneously, each with its own service name, endpoint, TLS settings, headers, and metrics configuration.

Changes

  • Introduced a Profile struct to hold per-collector configuration, replacing the flat fields on Config. Config now holds a Profiles []*Profile slice plus a shared PluginSpanFilter.
  • Config.UnmarshalJSON normalizes both the new {"profiles": [...]} wrapper shape and the legacy single-object shape, so existing stored configs continue to work without migration.
  • Added an otelTarget runtime struct that pairs a trace client with an optional metrics exporter and a resolved service name. OtelPlugin now holds a targets slice instead of a single client/exporter.
  • convertTraceToResourceSpan, getResourceAttributes, and getInstrumentationScope now accept a serviceName argument so each profile's resource attributes reflect its own identity. convertSpanToOTELSpan was promoted to a package-level function since it no longer needs plugin state.
  • TLS config construction was consolidated into a single buildTLSConfig helper in a new utils.go, eliminating duplicated crypto/tls and crypto/x509 setup across grpc.go, http.go, and metrics.go. validateCACertPath was also moved there.
  • injectEnvToHeaders replaces resolveHeaders: headers are now stored as plain strings using the "env.VAR_NAME" convention and resolved at Init time, removing the map[string]*schemas.EnvVar header type from Profile.
  • MarshalForStorage always emits the canonical {"profiles": [...]} wrapper regardless of input shape.
  • Redacted and redactHeaderValue were updated to operate over the profiles slice; literal header values are masked while "env." references are preserved.
  • recordMetricsFromTrace now accepts an explicit *MetricsExporter argument instead of reading from plugin state, allowing it to be called once per target.
  • Cleanup shuts down every target's metrics exporter and closes every trace client, returning the first close error.
  • The UI form was refactored around a profiles field array. Each profile renders as a collapsible OtelProfileSection with its own enable toggle and remove button. An "Add Profile" button appends a new empty profile. Both the new wrapper shape and the legacy single-object shape are normalized on load via buildDefaults. toHeaderStringMap and toEnvRefString helpers were added to envVarForm.ts to flatten EnvVar form values to the plain-string map the backend expects. The otelFormSchema was updated to validate a profiles array, with per-profile enabled gating skipping validation for disabled profiles.

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

- [x] https
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] grpc (same setup should work)
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] with single collector config
- [x] with multiple collector config
- [x] instance attributes intact
- [x] header injection logic intact
- [x] helm (single + multiple)

Breaking changes

  • Yes
  • No

Security considerations

  • validateCACertPath rejects relative paths, symlinks, and non-regular files to prevent path traversal when loading custom CA certificates.
  • buildTLSConfig enforces tls.VersionTLS12 as the minimum TLS version across all transports.
  • Header values that are literal strings are masked in Redacted() API responses; "env." references are returned as-is since they do not expose the secret value.
  • injectEnvToHeaders errors if a referenced environment variable is unset, preventing silent misconfiguration.

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

Summary by CodeRabbit

  • New Features

    • Multi-profile OTEL support: add/remove/manage multiple independent export profiles with per-profile collector, service name, headers, protocol, TLS, and metrics toggles.
  • Behavior Changes

    • TLS handling consolidated: custom CA and insecure modes only permit plaintext/insecure when no CA is provided; TLS config centrally built otherwise.
    • Export/metrics now operate per-profile.
  • Validation

    • Per-profile enabled flag (default true); enabled profiles require a collector URL.
  • UX

    • Form and header env-var handling improved for persistence and submission.

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a559bcba-cee3-47a9-85f0-04acac70ea7c

📥 Commits

Reviewing files that changed from the base of the PR and between dd219c7 and 9efa509.

📒 Files selected for processing (10)
  • plugins/otel/converter.go
  • plugins/otel/grpc.go
  • plugins/otel/http.go
  • plugins/otel/main.go
  • plugins/otel/metrics.go
  • plugins/otel/utils.go
  • ui/app/workspace/observability/fragments/otelFormFragment.tsx
  • ui/app/workspace/observability/views/plugins/otelView.tsx
  • ui/lib/types/schemas.ts
  • ui/lib/utils/envVarForm.ts

📝 Walkthrough

Walkthrough

Refactors the OTEL plugin to a per-profile model: extracts TLS/header helpers, delegates TLS to shared builders, adds Profile/Config and otelTarget runtime, emits spans/metrics per-profile, and updates frontend schemas/UI to edit and save multiple profiles.

Changes

Backend: TLS Extraction and Multi-Profile Runtime

Layer / File(s) Summary
TLS and header helpers
plugins/otel/utils.go
Adds injectEnvToHeaders, validateCACertPath, and buildTLSConfig to centralize env-var header injection and TLS client configuration.
Client/exporter TLS delegation
plugins/otel/grpc.go, plugins/otel/http.go, plugins/otel/metrics.go
Removes inline CA parsing/validation; clients and OTLP exporters call buildTLSConfig and use plaintext/insecure only when TLSCACert == \"\" && Insecure. Imports cleaned.
Multi-profile config types & storage
plugins/otel/main.go
Adds Protocol type/constants, Profile type, rewrites Config for profiles with JSON normalization for legacy single-profile shape, and updates storage/redaction for per-profile headers.
otelTarget runtime state
plugins/otel/main.go
Introduces otelTarget and changes OtelPlugin to store targets []*otelTarget.
Init and per-profile target build
plugins/otel/main.go
Init requires >=1 profile, builds one enabled otelTarget per profile (buildTarget validates fields, injects env headers, selects protocol clients, configures TLS, optionally inits metrics exporter) and tears down on failure.
Per-profile span conversion and metrics emission
plugins/otel/converter.go, plugins/otel/main.go
convertTraceToResourceSpan, getResourceAttributes, and getInstrumentationScope now accept serviceName; convertSpanToOTELSpan is standalone. Inject emits spans to every target and records metrics per target/exporter; recordMetricsFromTrace accepts an exporter.
Cleanup and exporter access
plugins/otel/main.go
Cleanup shuts down all exporters and closes all clients; GetMetricsExporter returns the first enabled exporter among targets.

Frontend: Multi-Profile Form and Schema

Layer / File(s) Summary
Schema updates and form utilities
ui/lib/types/schemas.ts, ui/lib/utils/envVarForm.ts
otelConfigSchema adds per-profile enabled with short-circuit validation; otelFormSchema uses profiles array (min 1). New toEnvRefString and toHeaderStringMap flatten EnvVar form values for backend.
Multi-profile form UI components
ui/app/workspace/observability/fragments/otelFormFragment.tsx
OtelFormFragment normalizes stored config into profiles, manages them with useFieldArray, renders OtelProfileSection per-profile collapsible editors, and adds add/remove controls and conditional TLS/metrics fields.
Form save and backend transformation
ui/app/workspace/observability/views/plugins/otelView.tsx
handleOtelConfigSave flattens each profile's headers via toHeaderStringMap, wraps profiles as { profiles: [...] } in data.config, and preserves enabled.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant Form as OtelFormFragment
  participant Profiles as Profiles array
  participant Section as OtelProfileSection
  participant View as OtelView
  participant Backend as Backend API

  User->>Form: Load multi-profile config
  Form->>Profiles: Populate via useFieldArray
  Profiles->>Section: Render collapsible per-profile
  User->>Section: Edit profile settings / Add profile
  Form->>View: Submit
  View->>View: toHeaderStringMap(profile.headers)
  View->>Backend: Send { config: { profiles: [...] }, enabled }
  Backend-->>View: Configuration saved
Loading
sequenceDiagram
  participant Init as Init()
  participant Config as Config struct
  participant Targets as otelTargets[]
  participant Build as buildTarget()
  participant Helpers as buildTLSConfig/injectEnvToHeaders
  participant Client as HTTP/gRPC Client
  participant Exporter as MetricsExporter

  Init->>Config: Validate Profiles array has 1+ entry
  Init->>Targets: For each enabled Profile
  Targets->>Build: buildTarget(profile)
  Build->>Helpers: injectEnvToHeaders(headers) / buildTLSConfig(tlsCACert, insecure)
  Helpers-->>Build: injected headers / tls.Config
  Build->>Client: Instantiate HTTP or gRPC client with tls.Config
  Build->>Exporter: Optionally init MetricsExporter
  Build-->>Targets: return otelTarget
  Targets-->>Init: store target
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • maximhq/bifrost#3382: Modifies convertTraceToResourceSpan and related span filtering/reparenting logic in the same conversion path.
  • maximhq/bifrost#3865: Related changes to trace→metrics attribute mapping and TTFT/token attribute handling.
  • maximhq/bifrost#3816: Overlaps on OTEL metrics / PostLLMHook and MetricsExporter plumbing changes.

Suggested reviewers

  • danpiths
  • roroghost17
  • akshaydeo

Poem

🐰 I hopped through code with nimble feet,

Split one config into profiles neat;
TLS threads sewn with care and art,
Each target plays its faithful part,
Frontend blooms and backends meet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change—adding support for multiple OTEL collectors—which aligns with the primary objective of this PR.
Description check ✅ Passed The PR description comprehensively covers all required template sections: summary, detailed changes, type of change, affected areas, testing performed, breaking changes assessment, security considerations, and checklist completion.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 05-29-feat_adds_support_for_multiple_otel_collectors

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 and usage tips.

@CLAassistant

CLAassistant commented May 29, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@sammaji
sammaji force-pushed the 05-29-feat_adds_support_for_multiple_otel_collectors branch from ab9a1f6 to af26c3d Compare May 29, 2026 15:26
Comment thread plugins/otel/utils.go Dismissed
Comment thread plugins/otel/utils.go Dismissed
@akshaydeo
akshaydeo force-pushed the dev branch 2 times, most recently from 8c3e42e to b95e8e7 Compare May 31, 2026 08:03
@sammaji
sammaji force-pushed the 05-29-feat_adds_support_for_multiple_otel_collectors branch from af26c3d to f66934b Compare June 1, 2026 07:29
@sammaji sammaji mentioned this pull request Jun 1, 2026
18 tasks
@sammaji
sammaji marked this pull request as ready for review June 1, 2026 08:45
@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 4/5

Safe to merge after addressing the plugin_span_filter data-loss in the UI save path.

The Go backend refactor is solid — legacy config normalisation, concurrent Inject fanout, and TLS consolidation are all correct. The one concrete defect is in the UI save path: otelView.tsx constructs the payload as config: { profiles }, which never carries plugin_span_filter. Because the backend's UnmarshalJSON treats any object with a "profiles" key as the canonical wrapper and reads the span filter from the top-level JSON, every UI save permanently erases any previously configured span filter. This is a present write path bug, not a theoretical one.

ui/app/workspace/observability/views/plugins/otelView.tsx — the save payload needs to forward the existing plugin_span_filter from the current stored config.

Important Files Changed

Filename Overview
plugins/otel/main.go Core refactor introducing Profile/Config structs, multi-target Inject loop with goroutines, and clean teardown in Cleanup; logic is sound, goroutine capture pattern is correct.
plugins/otel/utils.go New utility file consolidating TLS config, CA cert validation (symlink/relative-path rejection), and env-injection for headers; uses os.LookupEnv correctly to distinguish unset from empty.
plugins/otel/metrics.go Largely unchanged; introduces no new per-profile behavior, but otel.SetMeterProvider is called once per metrics-enabled profile, overwriting the process-global OTel provider each time.
plugins/otel/grpc.go Simplified by delegating TLS logic to buildTLSConfig; gRPC insecure path correctly bypasses buildTLSConfig to use plaintext credentials.
plugins/otel/http.go Simplified by delegating TLS to buildTLSConfig; HTTP client construction and header injection look correct.
plugins/otel/converter.go convertSpanToOTELSpan promoted to package-level; service name now threaded per profile into resource attributes — clean refactor.
ui/app/workspace/observability/views/plugins/otelView.tsx Save payload sends only { profiles }, permanently dropping any stored plugin_span_filter on each UI save — a data-loss bug.
ui/app/workspace/observability/fragments/otelFormFragment.tsx Profile array form with collapsible sections, open-state index remapping on remove, and legacy single-object normalization all look correct.
ui/lib/types/schemas.ts otelConfigSchema and otelFormSchema correctly model per-profile validation with disabled-profile bypass; plugin_span_filter is absent from the schema (not modelled in UI).
ui/lib/utils/envVarForm.ts New toHeaderStringMap and toEnvRefString helpers correctly flatten EnvVar form values to the plain-string header map the backend expects.

Reviews (5): Last reviewed commit: "feat: adds support for multiple otel col..." | Re-trigger Greptile

Comment thread plugins/otel/utils.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.

Actionable comments posted: 5

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

Inline comments:
In `@plugins/otel/main.go`:
- Around line 81-106: The UnmarshalJSON implementation for Profile currently
defaults Insecure to true when the field is omitted, which enables insecure TLS
by default; update Profile.UnmarshalJSON so that when aux.Insecure is nil you do
not set p.Insecure = true but instead leave p.Insecure as the safe zero value
(or explicitly set p.Insecure = false), and only assign p.Insecure when
aux.Insecure != nil (i.e., change the aux.Insecure handling in
Profile.UnmarshalJSON to remove the default-true behavior while leaving Enabled
behavior unchanged).
- Around line 573-580: In ValidateConfig, currently only empty Protocol strings
are rejected which lets invalid values (e.g., "htp") pass and fail later in
buildTarget; update the validation to explicitly accept only ProtocolHTTP or
ProtocolGRPC (referencing the Protocol constants) and return an error like
"profile %d: unsupported protocol" when profile.Protocol is not one of those
values; locate the validation logic inside ValidateConfig (and related profile
checks) and add the explicit whitelist check before returning success so bad
configs are rejected at validation time rather than during buildTarget.

In `@plugins/otel/utils.go`:
- Around line 37-38: validateCACertPath cleans certPath using filepath.Clean but
buildTLSConfig still reads tlsCACert verbatim, causing mismatch for inputs like
"/path/to/ca.pem/"; update buildTLSConfig to use the cleaned path returned or
produced by validateCACertPath (or call filepath.Clean on tlsCACert before
os.ReadFile) so the same normalized path (e.g., cleanPath) is used for both
validation and I/O; make the same change for the other occurrence around lines
74-79 where CA path is read.

In `@ui/app/workspace/observability/fragments/otelFormFragment.tsx`:
- Around line 271-283: Add stable data-testid attributes to all interactive and
per-profile display elements so E2E can target each profile instance reliably:
add data-testid attributes (e.g., `data-testid={`profile-${index}-header`}`,
`profile-${index}-serviceName`, `profile-${index}-collectorPreview`,
`profile-${index}-enabled-toggle`, `profile-${index}-remove-btn`, etc.) to the
CollapsibleTrigger button and contained spans/buttons and likewise add matching
testids to the inputs/selectors referenced in this fragment (service name field,
collector URL input, format/protocol selectors, TLS controls, metrics fields)
and to any Badge elements that indicate disabled/error; use the `index` and
unique field names to construct testids so identical controls across profiles
are distinguishable (affecting CollapsibleTrigger/ChevronDown/button,
serviceName, collectorPreview, enabled, hasError and corresponding
inputs/selectors in the other ranges mentioned).

In `@ui/lib/types/schemas.ts`:
- Around line 854-857: Update otelFormSchema so profiles validation is
conditional on enabled: keep enabled: z.boolean().default(true) but make
profiles optional (e.g., profiles: z.array(otelConfigSchema).optional()) and add
a schema-level check using .refine or .superRefine on otelFormSchema that
enforces profiles is present and has at least one otelConfigSchema entry only
when enabled === true; reference the otelFormSchema, enabled, profiles, and
otelConfigSchema symbols when making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4aa263c0-3c02-4c3a-8849-8e13e5cd9791

📥 Commits

Reviewing files that changed from the base of the PR and between d4c96b8 and f66934b.

📒 Files selected for processing (10)
  • plugins/otel/converter.go
  • plugins/otel/grpc.go
  • plugins/otel/http.go
  • plugins/otel/main.go
  • plugins/otel/metrics.go
  • plugins/otel/utils.go
  • ui/app/workspace/observability/fragments/otelFormFragment.tsx
  • ui/app/workspace/observability/views/plugins/otelView.tsx
  • ui/lib/types/schemas.ts
  • ui/lib/utils/envVarForm.ts

Comment thread plugins/otel/main.go
Comment thread plugins/otel/main.go Outdated
Comment thread plugins/otel/utils.go
Comment thread ui/app/workspace/observability/fragments/otelFormFragment.tsx
Comment thread ui/lib/types/schemas.ts
@sammaji
sammaji force-pushed the 05-29-feat_adds_support_for_multiple_otel_collectors branch from f66934b to 44c643e Compare June 1, 2026 10:33

@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)
plugins/otel/main.go (1)

250-254: 💤 Low value

Preserving nil profiles in redacted output may surprise callers.

When a profile is nil, appending nil to redacted.Profiles preserves slice indices but downstream code iterating over the redacted config may not expect nil elements. Consider skipping nil profiles entirely (like MarshalForStorage does) for consistency.

♻️ Suggested change
 		if p == nil {
-			redacted.Profiles = append(redacted.Profiles, nil)
 			continue
 		}
🤖 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 `@plugins/otel/main.go` around lines 250 - 254, The redaction loop currently
appends nil entries to redacted.Profiles when encountering a nil element
(iterating over c.Profiles); change this to skip nil profiles entirely so the
redacted slice contains only non-nil profiles (matching the behavior of
MarshalForStorage). Locate the loop that iterates over c.Profiles and remove the
branch that does redacted.Profiles = append(redacted.Profiles, nil); instead,
simply continue without appending when p == nil and append only the redacted
non-nil profile objects.
🤖 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 `@plugins/otel/main.go`:
- Around line 250-254: The redaction loop currently appends nil entries to
redacted.Profiles when encountering a nil element (iterating over c.Profiles);
change this to skip nil profiles entirely so the redacted slice contains only
non-nil profiles (matching the behavior of MarshalForStorage). Locate the loop
that iterates over c.Profiles and remove the branch that does redacted.Profiles
= append(redacted.Profiles, nil); instead, simply continue without appending
when p == nil and append only the redacted non-nil profile objects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc6c7cad-a378-414f-babf-8b7b4978f3ef

📥 Commits

Reviewing files that changed from the base of the PR and between f66934b and 44c643e.

📒 Files selected for processing (10)
  • plugins/otel/converter.go
  • plugins/otel/grpc.go
  • plugins/otel/http.go
  • plugins/otel/main.go
  • plugins/otel/metrics.go
  • plugins/otel/utils.go
  • ui/app/workspace/observability/fragments/otelFormFragment.tsx
  • ui/app/workspace/observability/views/plugins/otelView.tsx
  • ui/lib/types/schemas.ts
  • ui/lib/utils/envVarForm.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • plugins/otel/converter.go
  • ui/app/workspace/observability/views/plugins/otelView.tsx
  • ui/lib/types/schemas.ts
  • plugins/otel/http.go
  • ui/app/workspace/observability/fragments/otelFormFragment.tsx
  • plugins/otel/grpc.go
  • ui/lib/utils/envVarForm.ts

@sammaji
sammaji force-pushed the 05-29-feat_adds_support_for_multiple_otel_collectors branch from 44c643e to bfe9f6e Compare June 1, 2026 13:22
@sammaji sammaji mentioned this pull request Jun 1, 2026
18 tasks
@sammaji
sammaji force-pushed the 05-29-feat_adds_support_for_multiple_otel_collectors branch from bfe9f6e to dd219c7 Compare June 2, 2026 05:26

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

Actionable comments posted: 6

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

Inline comments:
In `@plugins/otel/converter.go`:
- Around line 210-213: getInstrumentationScope currently sets
InstrumentationScope.Name to the per-profile serviceName which fragments
downstream grouping; change getInstrumentationScope(serviceName string) to use a
stable scope identity (e.g., set commonpb.InstrumentationScope.Name to
"bifrost") and keep Version as p.bifrostVersion, leaving serviceName only for
the Resource attributes elsewhere; update the function getInstrumentationScope
and any callers if needed so the scope name is constant while serviceName
continues to be emitted as a resource attribute.

In `@plugins/otel/main.go`:
- Around line 836-844: GetMetricsExporter on OtelPlugin currently returns only
the first non-nil t.metricsExporter from p.targets, collapsing multi-profile
exporters; change it to return a fan-out wrapper (e.g., a new composite type
that implements the MetricsExporter interface and forwards calls to all non-nil
t.metricsExporter instances found in p.targets) or alternatively change the API
to return a slice of *MetricsExporter so callers can send to all collectors;
update GetMetricsExporter to collect all non-nil t.metricsExporter from
p.targets and return either the composite fan-out MetricsExporter or the slice,
ensuring the returned type still satisfies callers' expectations.
- Around line 815-833: The Cleanup method for OtelPlugin currently calls
t.metricsExporter.Shutdown(context.Background()) which can block and only logs
errors; change OtelPlugin.Cleanup to create a per-exporter timeout context (use
context.WithTimeout and defer cancel) when calling t.metricsExporter.Shutdown so
shutdowns cannot hang indefinitely, and if Shutdown returns an error assign it
to firstErr (if firstErr is nil) instead of merely logging; preserve existing
behavior of closing t.client and returning firstErr so the first external-call
failure is propagated.

In `@plugins/otel/utils.go`:
- Around line 45-56: The current check only Lstat's final element (cleanPath)
and misses symlinked ancestor directories; update the validation so it walks
each path component from the root (or working dir) to cleanPath and calls
os.Lstat on every joined prefix, returning an error if any component's Mode()
has os.ModeSymlink set; keep the existing checks for the final element
(IsRegular() etc.) and continue to use cleanPath and certPath names as the
targets for errors so you can locate and replace the single-call
os.Lstat(cleanPath) with a loop that tests each component for symlinks.

In `@ui/app/workspace/observability/fragments/otelFormFragment.tsx`:
- Around line 152-155: The onSubmit handler currently calls
onSave(data).finally(...) but does not handle rejections, leading to unhandled
promise rejections when the mutation fails; update onSubmit to either (A) make
it async and await onSave(data) inside a try/catch/finally block (use try {
await onSave(data) } catch (err) { /* handle or log error and run toast/error
path if needed */ } finally { setIsSaving(false) }) or (B) attach a .catch(err
=> { /* handle/log error to avoid unhandled rejection */ }).ensure
setIsSaving(false) always runs (keep finally) and reference the existing
onSubmit, onSave, and setIsSaving symbols when implementing the change.

In `@ui/app/workspace/observability/views/plugins/otelView.tsx`:
- Around line 18-31: handleOtelConfigSave is currently sending updatePlugin({
name: "otel", data: { enabled: ..., config: { profiles } } }) which overwrites
any top-level OTEL config (e.g., plugin_span_filter). Instead, preserve existing
top-level fields by merging the stored OTEL config with the new profiles: obtain
the current OTEL plugin config (or use the original server-provided config
object available in the component state/props), spread its properties and
replace only profiles with the flattened profiles array, then pass that merged
object as data.config to updatePlugin; reference handleOtelConfigSave,
updatePlugin, profiles and plugin_span_filter when locating and applying the
change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4b06193b-cd5c-4af8-adb0-b8363bc0e446

📥 Commits

Reviewing files that changed from the base of the PR and between bfe9f6e and dd219c7.

📒 Files selected for processing (10)
  • plugins/otel/converter.go
  • plugins/otel/grpc.go
  • plugins/otel/http.go
  • plugins/otel/main.go
  • plugins/otel/metrics.go
  • plugins/otel/utils.go
  • ui/app/workspace/observability/fragments/otelFormFragment.tsx
  • ui/app/workspace/observability/views/plugins/otelView.tsx
  • ui/lib/types/schemas.ts
  • ui/lib/utils/envVarForm.ts

Comment thread plugins/otel/converter.go
Comment thread plugins/otel/main.go
Comment thread plugins/otel/main.go
Comment thread plugins/otel/utils.go
Comment thread ui/app/workspace/observability/fragments/otelFormFragment.tsx
Comment thread ui/app/workspace/observability/views/plugins/otelView.tsx
Comment thread plugins/otel/main.go Outdated
Comment thread plugins/otel/main.go
Comment thread plugins/otel/main.go
@sammaji
sammaji force-pushed the 05-29-feat_adds_support_for_multiple_otel_collectors branch from dd219c7 to 9efa509 Compare June 2, 2026 09:11
Comment thread ui/app/workspace/observability/views/plugins/otelView.tsx

akshaydeo commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • Jun 2, 4:44 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jun 2, 4:44 PM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo merged commit b45fc34 into dev Jun 2, 2026
14 checks passed
@akshaydeo
akshaydeo deleted the 05-29-feat_adds_support_for_multiple_otel_collectors branch June 2, 2026 16:44
akshaydeo pushed a commit that referenced this pull request Jun 2, 2026
## Summary

The OTEL plugin previously supported a single collector target per configuration. This PR introduces multi-profile support, allowing Bifrost to export traces and metrics to multiple OTEL collectors simultaneously, each with its own service name, endpoint, TLS settings, headers, and metrics configuration.

## Changes

- Introduced a `Profile` struct to hold per-collector configuration, replacing the flat fields on `Config`. `Config` now holds a `Profiles []*Profile` slice plus a shared `PluginSpanFilter`.
- `Config.UnmarshalJSON` normalizes both the new `{"profiles": [...]}` wrapper shape and the legacy single-object shape, so existing stored configs continue to work without migration.
- Added an `otelTarget` runtime struct that pairs a trace client with an optional metrics exporter and a resolved service name. `OtelPlugin` now holds a `targets` slice instead of a single client/exporter.
- `convertTraceToResourceSpan`, `getResourceAttributes`, and `getInstrumentationScope` now accept a `serviceName` argument so each profile's resource attributes reflect its own identity. `convertSpanToOTELSpan` was promoted to a package-level function since it no longer needs plugin state.
- TLS config construction was consolidated into a single `buildTLSConfig` helper in a new `utils.go`, eliminating duplicated `crypto/tls` and `crypto/x509` setup across `grpc.go`, `http.go`, and `metrics.go`. `validateCACertPath` was also moved there.
- `injectEnvToHeaders` replaces `resolveHeaders`: headers are now stored as plain strings using the `"env.VAR_NAME"` convention and resolved at `Init` time, removing the `map[string]*schemas.EnvVar` header type from `Profile`.
- `MarshalForStorage` always emits the canonical `{"profiles": [...]}` wrapper regardless of input shape.
- `Redacted` and `redactHeaderValue` were updated to operate over the profiles slice; literal header values are masked while `"env."` references are preserved.
- `recordMetricsFromTrace` now accepts an explicit `*MetricsExporter` argument instead of reading from plugin state, allowing it to be called once per target.
- `Cleanup` shuts down every target's metrics exporter and closes every trace client, returning the first close error.
- The UI form was refactored around a `profiles` field array. Each profile renders as a collapsible `OtelProfileSection` with its own enable toggle and remove button. An "Add Profile" button appends a new empty profile. Both the new wrapper shape and the legacy single-object shape are normalized on load via `buildDefaults`. `toHeaderStringMap` and `toEnvRefString` helpers were added to `envVarForm.ts` to flatten `EnvVar` form values to the plain-string map the backend expects. The `otelFormSchema` was updated to validate a `profiles` array, with per-profile `enabled` gating skipping validation for disabled profiles.

## Type of change

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

## Affected areas

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

## How to test

```
- [x] https
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] grpc (same setup should work)
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] with single collector config
- [x] with multiple collector config
- [x] instance attributes intact
- [x] header injection logic intact
- [x] helm (single + multiple)
```

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

- `validateCACertPath` rejects relative paths, symlinks, and non-regular files to prevent path traversal when loading custom CA certificates.
- `buildTLSConfig` enforces `tls.VersionTLS12` as the minimum TLS version across all transports.
- Header values that are literal strings are masked in `Redacted()` API responses; `"env."` references are returned as-is since they do not expose the secret value.
- `injectEnvToHeaders` errors if a referenced environment variable is unset, preventing silent misconfiguration.

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

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Multi-profile OTEL support: add/remove/manage multiple independent export profiles with per-profile collector, service name, headers, protocol, TLS, and metrics toggles.

- **Behavior Changes**
  - TLS handling consolidated: custom CA and insecure modes only permit plaintext/insecure when no CA is provided; TLS config centrally built otherwise.
  - Export/metrics now operate per-profile.

- **Validation**
  - Per-profile enabled flag (default true); enabled profiles require a collector URL.

- **UX**
  - Form and header env-var handling improved for persistence and submission.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
akshaydeo pushed a commit that referenced this pull request Jun 4, 2026
## Summary

The OTEL plugin previously supported a single collector target per configuration. This PR introduces multi-profile support, allowing Bifrost to export traces and metrics to multiple OTEL collectors simultaneously, each with its own service name, endpoint, TLS settings, headers, and metrics configuration.

## Changes

- Introduced a `Profile` struct to hold per-collector configuration, replacing the flat fields on `Config`. `Config` now holds a `Profiles []*Profile` slice plus a shared `PluginSpanFilter`.
- `Config.UnmarshalJSON` normalizes both the new `{"profiles": [...]}` wrapper shape and the legacy single-object shape, so existing stored configs continue to work without migration.
- Added an `otelTarget` runtime struct that pairs a trace client with an optional metrics exporter and a resolved service name. `OtelPlugin` now holds a `targets` slice instead of a single client/exporter.
- `convertTraceToResourceSpan`, `getResourceAttributes`, and `getInstrumentationScope` now accept a `serviceName` argument so each profile's resource attributes reflect its own identity. `convertSpanToOTELSpan` was promoted to a package-level function since it no longer needs plugin state.
- TLS config construction was consolidated into a single `buildTLSConfig` helper in a new `utils.go`, eliminating duplicated `crypto/tls` and `crypto/x509` setup across `grpc.go`, `http.go`, and `metrics.go`. `validateCACertPath` was also moved there.
- `injectEnvToHeaders` replaces `resolveHeaders`: headers are now stored as plain strings using the `"env.VAR_NAME"` convention and resolved at `Init` time, removing the `map[string]*schemas.EnvVar` header type from `Profile`.
- `MarshalForStorage` always emits the canonical `{"profiles": [...]}` wrapper regardless of input shape.
- `Redacted` and `redactHeaderValue` were updated to operate over the profiles slice; literal header values are masked while `"env."` references are preserved.
- `recordMetricsFromTrace` now accepts an explicit `*MetricsExporter` argument instead of reading from plugin state, allowing it to be called once per target.
- `Cleanup` shuts down every target's metrics exporter and closes every trace client, returning the first close error.
- The UI form was refactored around a `profiles` field array. Each profile renders as a collapsible `OtelProfileSection` with its own enable toggle and remove button. An "Add Profile" button appends a new empty profile. Both the new wrapper shape and the legacy single-object shape are normalized on load via `buildDefaults`. `toHeaderStringMap` and `toEnvRefString` helpers were added to `envVarForm.ts` to flatten `EnvVar` form values to the plain-string map the backend expects. The `otelFormSchema` was updated to validate a `profiles` array, with per-profile `enabled` gating skipping validation for disabled profiles.

## Type of change

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

## Affected areas

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

## How to test

```
- [x] https
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] grpc (same setup should work)
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] with single collector config
- [x] with multiple collector config
- [x] instance attributes intact
- [x] header injection logic intact
- [x] helm (single + multiple)
```

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

- `validateCACertPath` rejects relative paths, symlinks, and non-regular files to prevent path traversal when loading custom CA certificates.
- `buildTLSConfig` enforces `tls.VersionTLS12` as the minimum TLS version across all transports.
- Header values that are literal strings are masked in `Redacted()` API responses; `"env."` references are returned as-is since they do not expose the secret value.
- `injectEnvToHeaders` errors if a referenced environment variable is unset, preventing silent misconfiguration.

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

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Multi-profile OTEL support: add/remove/manage multiple independent export profiles with per-profile collector, service name, headers, protocol, TLS, and metrics toggles.

- **Behavior Changes**
  - TLS handling consolidated: custom CA and insecure modes only permit plaintext/insecure when no CA is provided; TLS config centrally built otherwise.
  - Export/metrics now operate per-profile.

- **Validation**
  - Per-profile enabled flag (default true); enabled profiles require a collector URL.

- **UX**
  - Form and header env-var handling improved for persistence and submission.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@akshaydeo akshaydeo mentioned this pull request Jun 7, 2026
akshaydeo pushed a commit that referenced this pull request Jun 7, 2026
## Summary

The OTEL plugin previously supported a single collector target per configuration. This PR introduces multi-profile support, allowing Bifrost to export traces and metrics to multiple OTEL collectors simultaneously, each with its own service name, endpoint, TLS settings, headers, and metrics configuration.

## Changes

- Introduced a `Profile` struct to hold per-collector configuration, replacing the flat fields on `Config`. `Config` now holds a `Profiles []*Profile` slice plus a shared `PluginSpanFilter`.
- `Config.UnmarshalJSON` normalizes both the new `{"profiles": [...]}` wrapper shape and the legacy single-object shape, so existing stored configs continue to work without migration.
- Added an `otelTarget` runtime struct that pairs a trace client with an optional metrics exporter and a resolved service name. `OtelPlugin` now holds a `targets` slice instead of a single client/exporter.
- `convertTraceToResourceSpan`, `getResourceAttributes`, and `getInstrumentationScope` now accept a `serviceName` argument so each profile's resource attributes reflect its own identity. `convertSpanToOTELSpan` was promoted to a package-level function since it no longer needs plugin state.
- TLS config construction was consolidated into a single `buildTLSConfig` helper in a new `utils.go`, eliminating duplicated `crypto/tls` and `crypto/x509` setup across `grpc.go`, `http.go`, and `metrics.go`. `validateCACertPath` was also moved there.
- `injectEnvToHeaders` replaces `resolveHeaders`: headers are now stored as plain strings using the `"env.VAR_NAME"` convention and resolved at `Init` time, removing the `map[string]*schemas.EnvVar` header type from `Profile`.
- `MarshalForStorage` always emits the canonical `{"profiles": [...]}` wrapper regardless of input shape.
- `Redacted` and `redactHeaderValue` were updated to operate over the profiles slice; literal header values are masked while `"env."` references are preserved.
- `recordMetricsFromTrace` now accepts an explicit `*MetricsExporter` argument instead of reading from plugin state, allowing it to be called once per target.
- `Cleanup` shuts down every target's metrics exporter and closes every trace client, returning the first close error.
- The UI form was refactored around a `profiles` field array. Each profile renders as a collapsible `OtelProfileSection` with its own enable toggle and remove button. An "Add Profile" button appends a new empty profile. Both the new wrapper shape and the legacy single-object shape are normalized on load via `buildDefaults`. `toHeaderStringMap` and `toEnvRefString` helpers were added to `envVarForm.ts` to flatten `EnvVar` form values to the plain-string map the backend expects. The `otelFormSchema` was updated to validate a `profiles` array, with per-profile `enabled` gating skipping validation for disabled profiles.

## Type of change

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

## Affected areas

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

## How to test

```
- [x] https
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] grpc (same setup should work)
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] with single collector config
- [x] with multiple collector config
- [x] instance attributes intact
- [x] header injection logic intact
- [x] helm (single + multiple)
```

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

- `validateCACertPath` rejects relative paths, symlinks, and non-regular files to prevent path traversal when loading custom CA certificates.
- `buildTLSConfig` enforces `tls.VersionTLS12` as the minimum TLS version across all transports.
- Header values that are literal strings are masked in `Redacted()` API responses; `"env."` references are returned as-is since they do not expose the secret value.
- `injectEnvToHeaders` errors if a referenced environment variable is unset, preventing silent misconfiguration.

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

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Multi-profile OTEL support: add/remove/manage multiple independent export profiles with per-profile collector, service name, headers, protocol, TLS, and metrics toggles.

- **Behavior Changes**
  - TLS handling consolidated: custom CA and insecure modes only permit plaintext/insecure when no CA is provided; TLS config centrally built otherwise.
  - Export/metrics now operate per-profile.

- **Validation**
  - Per-profile enabled flag (default true); enabled profiles require a collector URL.

- **UX**
  - Form and header env-var handling improved for persistence and submission.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
akshaydeo added a commit that referenced this pull request Jun 7, 2026
## ✨ Features

- **OpenAI Compaction** — Added OpenAI conversation compaction support
across core, framework, logging, and the API surface (#4053)
- **Multi-Customer & Org Hierarchy** — Logs and usage tracking now
support multiple customers, teams, and business units, including
business unit CRUD, team assignment, and governance endpoints in the
OpenAPI spec (#4066, #4041, #4082)
- **Provider-Level Governance** — Budgets & limits are now scope-aware
and can be applied at the virtual-key top level and per provider, wired
from the model configs table, with UI filters for scope and providers
(#3938, #3937, #3939, #3981, #3962)
- **Customer Budgets** — Customers support multiple budgets and
`calendar_aligned` budget windows (#3998, #3997)
- **Virtual Key Attribution & Controls** — Added a `created_by` user
attribution column and a `blacklisted_models` column for virtual key
provider configs (#3672, #3653)
- **Request Header Capture** — OTel and Maxim observability plugins
capture `request_headers` by pattern, with wildcard support (e.g.
`x-custom-*`); logging gained the same wildcard header capture (#4012,
#3958)
- **OTel Content Controls & Collectors** — New `disable_content_logging`
option drops message/tool content from exported spans, plus support for
multiple OTel collectors (#4064, #3894)
- **xAI x_search** — Added xAI `x_search` tool support (#3976)
- **URL Validation** — Added fetch URL validation with private-network
configuration and link-local blocking (#3947, #3991)
- **File Scheme Pricing URLs** — Pricing source URLs now accept the
`file://` scheme for air-gapped and self-hosted deployments (#4045)
- **Paginated Virtual Keys** — Virtual key fetching is paginated to
handle deployments with very large numbers of keys (#3957)
- **Client IP Resolution** — Resolve client IP from
`X-Forwarded-For`/`X-Real-IP` headers
- **SCIM Provisioning** — Added `attributeType`/`attributeValue` SCIM
provisioning fields
- **Helm/Config Schema** — Added `roles` RBAC governance config and
`per_user_oauth` MCP auth to the Helm chart and config schema (#4004,
#4009)
- **Log Navigation UI** — Added a "View logs" menu item to customer,
team, and virtual key tables, clickable links in log detail views, a
customer detail sheet, and a reusable `BudgetDisplay` component (#4073,
#4054, #4026, #4055)
- **Faster First Paint** — Added an inline loading shell to `#root`
before React mounts (#4063)
- **Materialized View Alias** — Added an `alias` column to the
materialized view with filter support (#4078)

## 🐞 Fixed

- **Fetch URL IP Checks** — Hardened fetch URL IP checks against SSRF
(#4092)
- **Mantle Model Matching** — Broadened Mantle model matching to all
`gpt` variants (#4091)
- **Empty Thinking Blocks** — Strip thinking blocks when the signature
is empty (#4079)
- **OpenAI Stream Usage** — Removed usage from the `responses.created`
event in the OpenAI stream (#4080)
- **Prompt Cache Key** — Set the prompt cache key from the Anthropic
integration (#4086)
- **Upstream Failure Status** — Map upstream connection failures to 502
instead of 400 (#3929) (thanks
[@chris-colinsky](https://github.com/chris-colinsky)!)
- **Gemini Schema Constraints** — Accept numeric schema integer
constraints for Gemini (#3994) (thanks
[@yanhao98](https://github.com/yanhao98)!)
- **Files Provider Param** — Accept the `?provider=` query param on `GET
/v1/files` (#3971) (thanks [@alexef](https://github.com/alexef)!)
- **Optional Batch Model** — Made the `model` field optional on `POST
/v1/batches` (#3973) (thanks [@alexef](https://github.com/alexef)!)
- **Helm Azure Config** — Added missing `azure_key_config` fields to the
Helm schema (#3996) (thanks
[@axelray-dev](https://github.com/axelray-dev)!)
- **Text Completion Chunk Model** — Added the missing `Model` field to
`TextCompletionChunkResponse` (#3970) (thanks
[@kuishou68](https://github.com/kuishou68)!)
- **MCP Inline stdio Env** — MCP stdio server configs accept inline
environment variable assignments (#3861) (thanks
[@Shushmitaaaa](https://github.com/Shushmitaaaa)!)
- **Orphaned Tool Results** — Orphaned tool results in the OpenAI to
Anthropic conversion flow are no longer rejected by the Anthropic API
(#3919)
- **Node Usage Reconciliation** — Added a monotonic `inc_number` log
cursor so node usage reconciliation does not skip late async log writes
(#3664)
- **Bedrock Output Assessments** — Corrected the type of
`outputAssessments` in Bedrock responses (#4028)
- **Model Pool Pricing Reloads** — Preserve non-pricing model pool
entries across pricing reloads (#3999)
- **Ghost Node Reconciliation** — Replicate the VK hierarchy flow for
ghost node reconciliation (#4088)
- **VK Double Usage Counting** — Fixed double usage counting when
creating a virtual key (#4070)
- **Model Config Lifecycle** — Cascade deletes for model configs and
removal of stale in-memory model configs (#4051, #4043)
- **FTS Index Cap** — Reduced the FTS index `left()` cap from 800k to
250k chars to stay within the tsvector limit (#4057)
- **Sync Worker Drift** — Reduced the sync worker ticker period to 5m to
prevent threshold drift (#4023)
- **Passthrough** — Fixed passthrough budgets, gated passthrough models
per VK, model extraction for Azure passthrough, and restricted
fallbacks/provider selection to the VK boundary (#3941, #3988, #3983,
#3924)
- **Provider Response Headers** — Strip provider response headers and
add a content-type filter (#3955, #4024)
- **Stream Handling** — Drain non-SSE stream readers and retry stale
connections (#3956, #3967)
- **Azure Claude** — Strip Azure diagnostic property for Claude models
(#3925)
- **Compat max_tokens** — Preserve chat `max_tokens` during param
filtering (#3992)
- **Raw Request Flag** — Removed the raw request flag from providers
that don't support it (#4058)
- **UI Fixes** — Standardized page container layout, virtual key model
configs UI, and dashboard chart tooltips (#4046, #4052, #4044)

## 🔧 Maintenance

- **Dependency Upgrades** — Bumped transitive `golang.org/x`
dependencies (crypto, net, sys, text) for Docker Scout CVE remediation
and `recharts` to 3.8.1; cascaded version bumps across all modules
(#3900, #4003)
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
## Summary

The OTEL plugin previously supported a single collector target per configuration. This PR introduces multi-profile support, allowing Bifrost to export traces and metrics to multiple OTEL collectors simultaneously, each with its own service name, endpoint, TLS settings, headers, and metrics configuration.

## Changes

- Introduced a `Profile` struct to hold per-collector configuration, replacing the flat fields on `Config`. `Config` now holds a `Profiles []*Profile` slice plus a shared `PluginSpanFilter`.
- `Config.UnmarshalJSON` normalizes both the new `{"profiles": [...]}` wrapper shape and the legacy single-object shape, so existing stored configs continue to work without migration.
- Added an `otelTarget` runtime struct that pairs a trace client with an optional metrics exporter and a resolved service name. `OtelPlugin` now holds a `targets` slice instead of a single client/exporter.
- `convertTraceToResourceSpan`, `getResourceAttributes`, and `getInstrumentationScope` now accept a `serviceName` argument so each profile's resource attributes reflect its own identity. `convertSpanToOTELSpan` was promoted to a package-level function since it no longer needs plugin state.
- TLS config construction was consolidated into a single `buildTLSConfig` helper in a new `utils.go`, eliminating duplicated `crypto/tls` and `crypto/x509` setup across `grpc.go`, `http.go`, and `metrics.go`. `validateCACertPath` was also moved there.
- `injectEnvToHeaders` replaces `resolveHeaders`: headers are now stored as plain strings using the `"env.VAR_NAME"` convention and resolved at `Init` time, removing the `map[string]*schemas.EnvVar` header type from `Profile`.
- `MarshalForStorage` always emits the canonical `{"profiles": [...]}` wrapper regardless of input shape.
- `Redacted` and `redactHeaderValue` were updated to operate over the profiles slice; literal header values are masked while `"env."` references are preserved.
- `recordMetricsFromTrace` now accepts an explicit `*MetricsExporter` argument instead of reading from plugin state, allowing it to be called once per target.
- `Cleanup` shuts down every target's metrics exporter and closes every trace client, returning the first close error.
- The UI form was refactored around a `profiles` field array. Each profile renders as a collapsible `OtelProfileSection` with its own enable toggle and remove button. An "Add Profile" button appends a new empty profile. Both the new wrapper shape and the legacy single-object shape are normalized on load via `buildDefaults`. `toHeaderStringMap` and `toEnvRefString` helpers were added to `envVarForm.ts` to flatten `EnvVar` form values to the plain-string map the backend expects. The `otelFormSchema` was updated to validate a `profiles` array, with per-profile `enabled` gating skipping validation for disabled profiles.

## Type of change

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

## Affected areas

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

## How to test

```
- [x] https
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] grpc (same setup should work)
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] with single collector config
- [x] with multiple collector config
- [x] instance attributes intact
- [x] header injection logic intact
- [x] helm (single + multiple)
```

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

- `validateCACertPath` rejects relative paths, symlinks, and non-regular files to prevent path traversal when loading custom CA certificates.
- `buildTLSConfig` enforces `tls.VersionTLS12` as the minimum TLS version across all transports.
- Header values that are literal strings are masked in `Redacted()` API responses; `"env."` references are returned as-is since they do not expose the secret value.
- `injectEnvToHeaders` errors if a referenced environment variable is unset, preventing silent misconfiguration.

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

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Multi-profile OTEL support: add/remove/manage multiple independent export profiles with per-profile collector, service name, headers, protocol, TLS, and metrics toggles.

- **Behavior Changes**
  - TLS handling consolidated: custom CA and insecure modes only permit plaintext/insecure when no CA is provided; TLS config centrally built otherwise.
  - Export/metrics now operate per-profile.

- **Validation**
  - Per-profile enabled flag (default true); enabled profiles require a collector URL.

- **UX**
  - Form and header env-var handling improved for persistence and submission.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
## ✨ Features

- **OpenAI Compaction** — Added OpenAI conversation compaction support
across core, framework, logging, and the API surface (maximhq#4053)
- **Multi-Customer & Org Hierarchy** — Logs and usage tracking now
support multiple customers, teams, and business units, including
business unit CRUD, team assignment, and governance endpoints in the
OpenAPI spec (maximhq#4066, maximhq#4041, maximhq#4082)
- **Provider-Level Governance** — Budgets & limits are now scope-aware
and can be applied at the virtual-key top level and per provider, wired
from the model configs table, with UI filters for scope and providers
(maximhq#3938, maximhq#3937, maximhq#3939, maximhq#3981, maximhq#3962)
- **Customer Budgets** — Customers support multiple budgets and
`calendar_aligned` budget windows (maximhq#3998, maximhq#3997)
- **Virtual Key Attribution & Controls** — Added a `created_by` user
attribution column and a `blacklisted_models` column for virtual key
provider configs (maximhq#3672, maximhq#3653)
- **Request Header Capture** — OTel and Maxim observability plugins
capture `request_headers` by pattern, with wildcard support (e.g.
`x-custom-*`); logging gained the same wildcard header capture (maximhq#4012,
maximhq#3958)
- **OTel Content Controls & Collectors** — New `disable_content_logging`
option drops message/tool content from exported spans, plus support for
multiple OTel collectors (maximhq#4064, maximhq#3894)
- **xAI x_search** — Added xAI `x_search` tool support (maximhq#3976)
- **URL Validation** — Added fetch URL validation with private-network
configuration and link-local blocking (maximhq#3947, maximhq#3991)
- **File Scheme Pricing URLs** — Pricing source URLs now accept the
`file://` scheme for air-gapped and self-hosted deployments (maximhq#4045)
- **Paginated Virtual Keys** — Virtual key fetching is paginated to
handle deployments with very large numbers of keys (maximhq#3957)
- **Client IP Resolution** — Resolve client IP from
`X-Forwarded-For`/`X-Real-IP` headers
- **SCIM Provisioning** — Added `attributeType`/`attributeValue` SCIM
provisioning fields
- **Helm/Config Schema** — Added `roles` RBAC governance config and
`per_user_oauth` MCP auth to the Helm chart and config schema (maximhq#4004,
maximhq#4009)
- **Log Navigation UI** — Added a "View logs" menu item to customer,
team, and virtual key tables, clickable links in log detail views, a
customer detail sheet, and a reusable `BudgetDisplay` component (maximhq#4073,
maximhq#4054, maximhq#4026, maximhq#4055)
- **Faster First Paint** — Added an inline loading shell to `#root`
before React mounts (maximhq#4063)
- **Materialized View Alias** — Added an `alias` column to the
materialized view with filter support (maximhq#4078)

## 🐞 Fixed

- **Fetch URL IP Checks** — Hardened fetch URL IP checks against SSRF
(maximhq#4092)
- **Mantle Model Matching** — Broadened Mantle model matching to all
`gpt` variants (maximhq#4091)
- **Empty Thinking Blocks** — Strip thinking blocks when the signature
is empty (maximhq#4079)
- **OpenAI Stream Usage** — Removed usage from the `responses.created`
event in the OpenAI stream (maximhq#4080)
- **Prompt Cache Key** — Set the prompt cache key from the Anthropic
integration (maximhq#4086)
- **Upstream Failure Status** — Map upstream connection failures to 502
instead of 400 (maximhq#3929) (thanks
[@chris-colinsky](https://github.com/chris-colinsky)!)
- **Gemini Schema Constraints** — Accept numeric schema integer
constraints for Gemini (maximhq#3994) (thanks
[@yanhao98](https://github.com/yanhao98)!)
- **Files Provider Param** — Accept the `?provider=` query param on `GET
/v1/files` (maximhq#3971) (thanks [@alexef](https://github.com/alexef)!)
- **Optional Batch Model** — Made the `model` field optional on `POST
/v1/batches` (maximhq#3973) (thanks [@alexef](https://github.com/alexef)!)
- **Helm Azure Config** — Added missing `azure_key_config` fields to the
Helm schema (maximhq#3996) (thanks
[@axelray-dev](https://github.com/axelray-dev)!)
- **Text Completion Chunk Model** — Added the missing `Model` field to
`TextCompletionChunkResponse` (maximhq#3970) (thanks
[@kuishou68](https://github.com/kuishou68)!)
- **MCP Inline stdio Env** — MCP stdio server configs accept inline
environment variable assignments (maximhq#3861) (thanks
[@Shushmitaaaa](https://github.com/Shushmitaaaa)!)
- **Orphaned Tool Results** — Orphaned tool results in the OpenAI to
Anthropic conversion flow are no longer rejected by the Anthropic API
(maximhq#3919)
- **Node Usage Reconciliation** — Added a monotonic `inc_number` log
cursor so node usage reconciliation does not skip late async log writes
(maximhq#3664)
- **Bedrock Output Assessments** — Corrected the type of
`outputAssessments` in Bedrock responses (maximhq#4028)
- **Model Pool Pricing Reloads** — Preserve non-pricing model pool
entries across pricing reloads (maximhq#3999)
- **Ghost Node Reconciliation** — Replicate the VK hierarchy flow for
ghost node reconciliation (maximhq#4088)
- **VK Double Usage Counting** — Fixed double usage counting when
creating a virtual key (maximhq#4070)
- **Model Config Lifecycle** — Cascade deletes for model configs and
removal of stale in-memory model configs (maximhq#4051, maximhq#4043)
- **FTS Index Cap** — Reduced the FTS index `left()` cap from 800k to
250k chars to stay within the tsvector limit (maximhq#4057)
- **Sync Worker Drift** — Reduced the sync worker ticker period to 5m to
prevent threshold drift (maximhq#4023)
- **Passthrough** — Fixed passthrough budgets, gated passthrough models
per VK, model extraction for Azure passthrough, and restricted
fallbacks/provider selection to the VK boundary (maximhq#3941, maximhq#3988, maximhq#3983,
maximhq#3924)
- **Provider Response Headers** — Strip provider response headers and
add a content-type filter (maximhq#3955, maximhq#4024)
- **Stream Handling** — Drain non-SSE stream readers and retry stale
connections (maximhq#3956, maximhq#3967)
- **Azure Claude** — Strip Azure diagnostic property for Claude models
(maximhq#3925)
- **Compat max_tokens** — Preserve chat `max_tokens` during param
filtering (maximhq#3992)
- **Raw Request Flag** — Removed the raw request flag from providers
that don't support it (maximhq#4058)
- **UI Fixes** — Standardized page container layout, virtual key model
configs UI, and dashboard chart tooltips (maximhq#4046, maximhq#4052, maximhq#4044)

## 🔧 Maintenance

- **Dependency Upgrades** — Bumped transitive `golang.org/x`
dependencies (crypto, net, sys, text) for Docker Scout CVE remediation
and `recharts` to 3.8.1; cascaded version bumps across all modules
(maximhq#3900, maximhq#4003)
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
## Summary

The OTEL plugin previously supported a single collector target per configuration. This PR introduces multi-profile support, allowing Bifrost to export traces and metrics to multiple OTEL collectors simultaneously, each with its own service name, endpoint, TLS settings, headers, and metrics configuration.

## Changes

- Introduced a `Profile` struct to hold per-collector configuration, replacing the flat fields on `Config`. `Config` now holds a `Profiles []*Profile` slice plus a shared `PluginSpanFilter`.
- `Config.UnmarshalJSON` normalizes both the new `{"profiles": [...]}` wrapper shape and the legacy single-object shape, so existing stored configs continue to work without migration.
- Added an `otelTarget` runtime struct that pairs a trace client with an optional metrics exporter and a resolved service name. `OtelPlugin` now holds a `targets` slice instead of a single client/exporter.
- `convertTraceToResourceSpan`, `getResourceAttributes`, and `getInstrumentationScope` now accept a `serviceName` argument so each profile's resource attributes reflect its own identity. `convertSpanToOTELSpan` was promoted to a package-level function since it no longer needs plugin state.
- TLS config construction was consolidated into a single `buildTLSConfig` helper in a new `utils.go`, eliminating duplicated `crypto/tls` and `crypto/x509` setup across `grpc.go`, `http.go`, and `metrics.go`. `validateCACertPath` was also moved there.
- `injectEnvToHeaders` replaces `resolveHeaders`: headers are now stored as plain strings using the `"env.VAR_NAME"` convention and resolved at `Init` time, removing the `map[string]*schemas.EnvVar` header type from `Profile`.
- `MarshalForStorage` always emits the canonical `{"profiles": [...]}` wrapper regardless of input shape.
- `Redacted` and `redactHeaderValue` were updated to operate over the profiles slice; literal header values are masked while `"env."` references are preserved.
- `recordMetricsFromTrace` now accepts an explicit `*MetricsExporter` argument instead of reading from plugin state, allowing it to be called once per target.
- `Cleanup` shuts down every target's metrics exporter and closes every trace client, returning the first close error.
- The UI form was refactored around a `profiles` field array. Each profile renders as a collapsible `OtelProfileSection` with its own enable toggle and remove button. An "Add Profile" button appends a new empty profile. Both the new wrapper shape and the legacy single-object shape are normalized on load via `buildDefaults`. `toHeaderStringMap` and `toEnvRefString` helpers were added to `envVarForm.ts` to flatten `EnvVar` form values to the plain-string map the backend expects. The `otelFormSchema` was updated to validate a `profiles` array, with per-profile `enabled` gating skipping validation for disabled profiles.

## Type of change

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

## Affected areas

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

## How to test

```
- [x] https
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] grpc (same setup should work)
    - [x] single config (legacy)
    - [x] multiple services, singe collector
    - [x] multiple collector
    - [x] with tls, multiple services + multiple collector
- [x] with single collector config
- [x] with multiple collector config
- [x] instance attributes intact
- [x] header injection logic intact
- [x] helm (single + multiple)
```

## Breaking changes

- [ ] Yes
- [x] No

## Security considerations

- `validateCACertPath` rejects relative paths, symlinks, and non-regular files to prevent path traversal when loading custom CA certificates.
- `buildTLSConfig` enforces `tls.VersionTLS12` as the minimum TLS version across all transports.
- Header values that are literal strings are masked in `Redacted()` API responses; `"env."` references are returned as-is since they do not expose the secret value.
- `injectEnvToHeaders` errors if a referenced environment variable is unset, preventing silent misconfiguration.

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

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Multi-profile OTEL support: add/remove/manage multiple independent export profiles with per-profile collector, service name, headers, protocol, TLS, and metrics toggles.

- **Behavior Changes**
  - TLS handling consolidated: custom CA and insecure modes only permit plaintext/insecure when no CA is provided; TLS config centrally built otherwise.
  - Export/metrics now operate per-profile.

- **Validation**
  - Per-profile enabled flag (default true); enabled profiles require a collector URL.

- **UX**
  - Form and header env-var handling improved for persistence and submission.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
## ✨ Features

- **OpenAI Compaction** — Added OpenAI conversation compaction support
across core, framework, logging, and the API surface (maximhq#4053)
- **Multi-Customer & Org Hierarchy** — Logs and usage tracking now
support multiple customers, teams, and business units, including
business unit CRUD, team assignment, and governance endpoints in the
OpenAPI spec (maximhq#4066, maximhq#4041, maximhq#4082)
- **Provider-Level Governance** — Budgets & limits are now scope-aware
and can be applied at the virtual-key top level and per provider, wired
from the model configs table, with UI filters for scope and providers
(maximhq#3938, maximhq#3937, maximhq#3939, maximhq#3981, maximhq#3962)
- **Customer Budgets** — Customers support multiple budgets and
`calendar_aligned` budget windows (maximhq#3998, maximhq#3997)
- **Virtual Key Attribution & Controls** — Added a `created_by` user
attribution column and a `blacklisted_models` column for virtual key
provider configs (maximhq#3672, maximhq#3653)
- **Request Header Capture** — OTel and Maxim observability plugins
capture `request_headers` by pattern, with wildcard support (e.g.
`x-custom-*`); logging gained the same wildcard header capture (maximhq#4012,
maximhq#3958)
- **OTel Content Controls & Collectors** — New `disable_content_logging`
option drops message/tool content from exported spans, plus support for
multiple OTel collectors (maximhq#4064, maximhq#3894)
- **xAI x_search** — Added xAI `x_search` tool support (maximhq#3976)
- **URL Validation** — Added fetch URL validation with private-network
configuration and link-local blocking (maximhq#3947, maximhq#3991)
- **File Scheme Pricing URLs** — Pricing source URLs now accept the
`file://` scheme for air-gapped and self-hosted deployments (maximhq#4045)
- **Paginated Virtual Keys** — Virtual key fetching is paginated to
handle deployments with very large numbers of keys (maximhq#3957)
- **Client IP Resolution** — Resolve client IP from
`X-Forwarded-For`/`X-Real-IP` headers
- **SCIM Provisioning** — Added `attributeType`/`attributeValue` SCIM
provisioning fields
- **Helm/Config Schema** — Added `roles` RBAC governance config and
`per_user_oauth` MCP auth to the Helm chart and config schema (maximhq#4004,
maximhq#4009)
- **Log Navigation UI** — Added a "View logs" menu item to customer,
team, and virtual key tables, clickable links in log detail views, a
customer detail sheet, and a reusable `BudgetDisplay` component (maximhq#4073,
maximhq#4054, maximhq#4026, maximhq#4055)
- **Faster First Paint** — Added an inline loading shell to `#root`
before React mounts (maximhq#4063)
- **Materialized View Alias** — Added an `alias` column to the
materialized view with filter support (maximhq#4078)

## 🐞 Fixed

- **Fetch URL IP Checks** — Hardened fetch URL IP checks against SSRF
(maximhq#4092)
- **Mantle Model Matching** — Broadened Mantle model matching to all
`gpt` variants (maximhq#4091)
- **Empty Thinking Blocks** — Strip thinking blocks when the signature
is empty (maximhq#4079)
- **OpenAI Stream Usage** — Removed usage from the `responses.created`
event in the OpenAI stream (maximhq#4080)
- **Prompt Cache Key** — Set the prompt cache key from the Anthropic
integration (maximhq#4086)
- **Upstream Failure Status** — Map upstream connection failures to 502
instead of 400 (maximhq#3929) (thanks
[@chris-colinsky](https://github.com/chris-colinsky)!)
- **Gemini Schema Constraints** — Accept numeric schema integer
constraints for Gemini (maximhq#3994) (thanks
[@yanhao98](https://github.com/yanhao98)!)
- **Files Provider Param** — Accept the `?provider=` query param on `GET
/v1/files` (maximhq#3971) (thanks [@alexef](https://github.com/alexef)!)
- **Optional Batch Model** — Made the `model` field optional on `POST
/v1/batches` (maximhq#3973) (thanks [@alexef](https://github.com/alexef)!)
- **Helm Azure Config** — Added missing `azure_key_config` fields to the
Helm schema (maximhq#3996) (thanks
[@axelray-dev](https://github.com/axelray-dev)!)
- **Text Completion Chunk Model** — Added the missing `Model` field to
`TextCompletionChunkResponse` (maximhq#3970) (thanks
[@kuishou68](https://github.com/kuishou68)!)
- **MCP Inline stdio Env** — MCP stdio server configs accept inline
environment variable assignments (maximhq#3861) (thanks
[@Shushmitaaaa](https://github.com/Shushmitaaaa)!)
- **Orphaned Tool Results** — Orphaned tool results in the OpenAI to
Anthropic conversion flow are no longer rejected by the Anthropic API
(maximhq#3919)
- **Node Usage Reconciliation** — Added a monotonic `inc_number` log
cursor so node usage reconciliation does not skip late async log writes
(maximhq#3664)
- **Bedrock Output Assessments** — Corrected the type of
`outputAssessments` in Bedrock responses (maximhq#4028)
- **Model Pool Pricing Reloads** — Preserve non-pricing model pool
entries across pricing reloads (maximhq#3999)
- **Ghost Node Reconciliation** — Replicate the VK hierarchy flow for
ghost node reconciliation (maximhq#4088)
- **VK Double Usage Counting** — Fixed double usage counting when
creating a virtual key (maximhq#4070)
- **Model Config Lifecycle** — Cascade deletes for model configs and
removal of stale in-memory model configs (maximhq#4051, maximhq#4043)
- **FTS Index Cap** — Reduced the FTS index `left()` cap from 800k to
250k chars to stay within the tsvector limit (maximhq#4057)
- **Sync Worker Drift** — Reduced the sync worker ticker period to 5m to
prevent threshold drift (maximhq#4023)
- **Passthrough** — Fixed passthrough budgets, gated passthrough models
per VK, model extraction for Azure passthrough, and restricted
fallbacks/provider selection to the VK boundary (maximhq#3941, maximhq#3988, maximhq#3983,
maximhq#3924)
- **Provider Response Headers** — Strip provider response headers and
add a content-type filter (maximhq#3955, maximhq#4024)
- **Stream Handling** — Drain non-SSE stream readers and retry stale
connections (maximhq#3956, maximhq#3967)
- **Azure Claude** — Strip Azure diagnostic property for Claude models
(maximhq#3925)
- **Compat max_tokens** — Preserve chat `max_tokens` during param
filtering (maximhq#3992)
- **Raw Request Flag** — Removed the raw request flag from providers
that don't support it (maximhq#4058)
- **UI Fixes** — Standardized page container layout, virtual key model
configs UI, and dashboard chart tooltips (maximhq#4046, maximhq#4052, maximhq#4044)

## 🔧 Maintenance

- **Dependency Upgrades** — Bumped transitive `golang.org/x`
dependencies (crypto, net, sys, text) for Docker Scout CVE remediation
and `recharts` to 3.8.1; cascaded version bumps across all modules
(maximhq#3900, maximhq#4003)
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.

4 participants