Skip to content

feat: add realtime provider interfaces, schemas, and engine hooks#2337

Merged
akshaydeo merged 1 commit intov1.5.0from
feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks
Apr 7, 2026
Merged

feat: add realtime provider interfaces, schemas, and engine hooks#2337
akshaydeo merged 1 commit intov1.5.0from
feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks

Conversation

@danpiths
Copy link
Copy Markdown
Collaborator

@danpiths danpiths commented Mar 27, 2026

Summary

Adds the foundation for provider-extensible Realtime API support in Bifrost. Expands the RealtimeProvider interface with new methods for turn lifecycle, event filtering, output accumulation, WebRTC/WebSocket protocol negotiation, and subprotocol handling. Introduces RealtimeUsageExtractor, RealtimeSessionProvider, and RealtimeLegacyWebRTCProvider optional interfaces. Adds ExtraParams round-tripping on all realtime event structs, ParseRealtimeEvent for unknown-field preservation, new context keys for session/turn tracking, SelectKeyForProviderRequestType for request-type-aware key selection, RealtimeTurnHooks for turn-scoped plugin pipelines, and CompleteAndFlushTrace on the Tracer interface for explicit trace flushing outside HTTP request lifecycle.

Changes

  • Expanded RealtimeProvider interface: replaced RealtimeWebRTCURL/RealtimeWebRTCHeaders with SupportsRealtimeWebRTC() bool and ExchangeRealtimeWebRTCSDP(ctx, key, model, sdp, session) — provider now owns the full SDP exchange
  • Added RealtimeLegacyWebRTCProvider opt-in interface for providers that support beta WebRTC endpoints (e.g. OpenAI /v1/realtime)
  • Added RealtimeUsageExtractor interface (ExtractRealtimeTurnUsage, ExtractRealtimeTurnOutput) for provider-specific usage/output parsing
  • Added RealtimeSessionProvider interface for client secret creation
  • Added ExtraParams map[string]json.RawMessage to BifrostRealtimeEvent, RealtimeSession, RealtimeItem, RealtimeError
  • Added ParseRealtimeEvent with nested extra-params extraction
  • Added RealtimeSessionEndpointType, RealtimeSessionRoute types for client secret routing
  • Added client secret body parsing helpers (ParseRealtimeClientSecretBody, ExtractRealtimeClientSecretModel)
  • Added 5 new BifrostContextKey constants for realtime session/turn tracking
  • Renamed SelectKeyForProviderSelectKeyForProviderRequestType with request-type parameter
  • Added RealtimeTurnHooks struct and RunRealtimeTurnPreHooks method
  • Added CompleteAndFlushTrace(traceID string) to Tracer interface and NoOpTracer
  • Added IsRealtimeConversationItemEventType helper with tests
  • Implemented all new interface methods in OpenAI provider including ExchangeRealtimeWebRTCSDP (GA /v1/realtime/calls) and ExchangeLegacyRealtimeWebRTCSDP (beta /v1/realtime)
  • Implemented all new interface methods in ElevenLabs provider — SupportsRealtimeWebRTC returns false, ExchangeRealtimeWebRTCSDP returns "not yet implemented"
  • Fixed ElevenLabs terminal event mapping: agent_responseRTEventResponseDone
  • Removed RawData short-circuit in both providers' ToProviderRealtimeEvent

Type of change

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

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (Next.js)
  • Docs

How to test

cd core
go build ./...
go test ./providers/openai/... -run TestRealtime
go test ./schemas/... -run TestIsRealtimeConversationItemEventType
go test ./schemas/... -run TestExtractRealtime

Screenshots/Recordings

N/A

Breaking changes

  • Yes
  • No

SelectKeyForProvider renamed to SelectKeyForProviderRequestType — callers must pass request type. RealtimeWebRTCURL/RealtimeWebRTCHeaders removed from RealtimeProvider in favor of SupportsRealtimeWebRTC/ExchangeRealtimeWebRTCSDP. Providers must now implement the SDP exchange directly.

Related issues

N/A

Security considerations

  • ExtraParams preserves arbitrary provider fields — no new PII exposure since these round-trip existing provider data
  • Client secret parsing validates and sanitizes model input
  • SDP exchange delegates to provider's own HTTP client with proper auth

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

@danpiths danpiths requested a review from akshaydeo March 27, 2026 17:19
@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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 27, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • WebRTC SDP and realtime client‑secret/session support; expanded realtime provider hooks and transport controls
    • Canonical realtime event types and ExtraParams to preserve unknown provider fields
    • New realtime/session context keys and explicit trace completion hook
  • Bug Fixes

    • Provider key selection now respects request type
    • Plugin logs and traces are drained/flushed on realtime hook and error paths
  • Tests

    • Added coverage for realtime event translation, normalization, and client‑secret handling

Walkthrough

Replaces provider key selection with request-type-aware API; adds exported realtime turn hook lifecycle (pre/post hooks + cleanup), tracing flush and plugin log draining on early exits; extends realtime schemas, context keys, and provider interfaces; adds OpenAI/ElevenLabs realtime & WebRTC handling, client-secret parsing, event (de)serialization, and unit tests.

Changes

Cohort / File(s) Summary
Core Bifrost hooks & key API
core/bifrost.go
Replaced SelectKeyForProvider with SelectKeyForProviderRequestType; added exported RealtimeTurnHooks type and RunRealtimeTurnPreHooks to run LLM pre-hooks, return a post-hook runner, drain plugin logs, complete/flush traces on early exits, and provide cleanup.
Internal LLMT tests
core/internal/llmtests/realtime.go, core/internal/llmtests/websocket_responses.go
Updated tests to call SelectKeyForProviderRequestType(...) with appropriate schemas.RequestType values.
OpenAI provider realtime
core/providers/openai/realtime.go, core/providers/openai/realtime_test.go
Added WebRTC SDP exchange (legacy+GA), client-secret/session normalization and creation helpers, endpoint-specific header/path logic, expanded realtime event schema mapping (ExtraParams, pointer fields, indices, audio end), delta detection/accumulation changes, usage/output extraction helpers, and extensive unit tests.
ElevenLabs provider realtime
core/providers/elevenlabs/realtime.go
Added WebRTC/WebSocket method stubs and routing controls, adjusted realtime event mapping (final event type, forwarding/accumulation rules), removed raw passthrough fast-path in conversions.
Realtime schemas & parsing
core/schemas/realtime.go, core/schemas/bifrost.go
Added five new BifrostContextKey constants; canonicalized realtime event types and classifier helpers; added ExtraParams maps to event/session/item/error structs; introduced ParseRealtimeEvent, session endpoint/route types, and expanded RealtimeProvider interface (WebRTC, turn lifecycle, forwarding/accumulation, transport config).
Client-secret parsing utilities & tests
core/schemas/realtime_client_secrets.go, core/schemas/realtime_client_secrets_test.go
Added ParseRealtimeClientSecretBody, ExtractRealtimeClientSecretModel, and NewRealtimeClientSecretBodyError; validation prefers session.model then top-level model; added unit tests for both code paths.
Realtime schema tests & tracer API
core/schemas/realtime_test.go, core/schemas/tracer.go
Added realtime classifier tests; extended Tracer interface with CompleteAndFlushTrace(traceID string) and added a no-op implementation to NoOpTracer.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Bifrost as Bifrost
    participant Pipeline as PluginPipeline
    participant Tracer as Tracer

    Client->>Bifrost: RunRealtimeTurnPreHooks(ctx, req)
    activate Bifrost
    Bifrost->>Bifrost: ensure request ID & trace ctx
    Bifrost->>Pipeline: RunLLMPreHooks(ctx, req)
    activate Pipeline
    Pipeline-->>Bifrost: (preReq, shortCircuit)
    deactivate Pipeline

    alt shortCircuit.error
        Bifrost->>Pipeline: RunPostLLMHooks(ctx, nil, shortCircuit.Error)
        Pipeline-->>Bifrost: post-hook results
        Bifrost->>Tracer: CompleteAndFlushTrace(traceID)
        Bifrost->>Bifrost: Drain plugin logs then Cleanup
        Bifrost-->>Client: Return error
    else shortCircuit.response
        Bifrost->>Bifrost: Drain plugin logs then Cleanup
        Bifrost-->>Client: Return short-circuit response-as-error
    else no shortCircuit
        Bifrost-->>Client: Return RealtimeTurnHooks {PostHookRunner, Cleanup}
    end

    Note over Bifrost,Pipeline: PostHookRunner later calls RunPostLLMHooks for finalization
    deactivate Bifrost
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through hooks and realtime threads tonight,
Keys picked by request type under moonlight,
Traces flushed, plugin whispers drained away,
Secrets parsed and events danced in play,
A rabbit cheers the realtime code—so bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main changes: adding realtime provider interfaces, schemas, and engine hooks for the Bifrost project.
Description check ✅ Passed The description comprehensively covers all required template sections: clear summary, detailed changes, type of change, affected areas, testing instructions, breaking changes documentation, security considerations, and completed checklist items.

✏️ 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 feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks

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

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 27, 2026

Confidence Score: 5/5

All previously flagged P1 issues are resolved; PR is safe to merge

No remaining P0 or P1 issues. All three previously identified bugs are fixed in this revision: json.RawMessage null injection, single-block content extraction, and redundant ParseRealtimeEvent unmarshaling. New code is well-tested and consistent with existing patterns across the codebase.

No files require special attention

Important Files Changed

Filename Overview
core/providers/openai/realtime.go Full RealtimeProvider interface implementation including GA SDP exchange, legacy WebRTC, usage/output extraction; setRealtimeExtraParam now properly guards json.RawMessage null values
core/providers/elevenlabs/realtime.go New interface stubs with SupportsRealtimeWebRTC returning false and correct agent_response→RTEventResponseDone terminal event mapping
core/schemas/realtime.go ParseRealtimeEvent with saved-key pattern avoids redundant unmarshal; ExtraParams added to BifrostRealtimeEvent/RealtimeSession/RealtimeItem/RealtimeError; new provider interfaces defined
core/schemas/bifrost.go Five new BifrostContextKey constants for realtime session/turn/source/event-type tracking
core/schemas/tracer.go CompleteAndFlushTrace added to Tracer interface and NoOpTracer for explicit trace flushing outside HTTP request lifecycle
core/bifrost.go SelectKeyForProviderRequestType replaces SelectKeyForProvider with request-type awareness; RealtimeTurnHooks and RunRealtimeTurnPreHooks add turn-scoped plugin pipelines
core/schemas/realtime_client_secrets.go New ParseRealtimeClientSecretBody and ExtractRealtimeClientSecretModel helpers with proper BifrostError types
core/schemas/realtime_client_secrets_test.go Covers nested session.model extraction and flat top-level model fallback
core/schemas/realtime_test.go Tests for IsRealtimeConversationItemEventType and canonical event classifier helpers
core/providers/openai/realtime_test.go Good coverage for client-secret normalization, event serialization round-trips, and ShouldStartRealtimeTurn
core/internal/llmtests/realtime.go Integration test harness updated to use SelectKeyForProviderRequestType
core/internal/llmtests/websocket_responses.go Updated to renamed SelectKeyForProviderRequestType API

Reviews (12): Last reviewed commit: "feat: add realtime provider interfaces, ..." | Re-trigger Greptile

Comment thread core/providers/openai/realtime.go Outdated
Comment thread core/providers/openai/realtime.go
Comment thread core/schemas/realtime.go Outdated
Comment thread core/providers/openai/realtime.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/bifrost.go (1)

3738-3750: ⚠️ Potential issue | 🔴 Critical

Update the remaining WebSocket caller before this rename lands.

transports/bifrost-http/handlers/wsresponses.go Line 231 still calls SelectKeyForProvider(ctx, req.Provider, req.Model), so removing the old symbol here leaves the branch uncompilable until that handler is switched to SelectKeyForProviderRequestType(..., schemas.WebSocketResponsesRequest, ...) or a compatibility shim is kept.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/bifrost.go` around lines 3738 - 3750, The callsite in
transports/bifrost-http/handlers/wsresponses.go still uses the removed
SelectKeyForProvider; update that caller to use the new
SelectKeyForProviderRequestType signature (pass
schemas.WebSocketResponsesRequest as the requestType) so it calls
SelectKeyForProviderRequestType(ctx, schemas.WebSocketResponsesRequest,
req.Provider, req.Model), or alternatively add a small compatibility wrapper
function SelectKeyForProvider that forwards to SelectKeyForProviderRequestType
(preserving original parameters) to avoid a break; reference
SelectKeyForProviderRequestType in core/bifrost.go and the caller in
wsresponses.go when making the change.
🧹 Nitpick comments (2)
core/providers/openai/realtime.go (2)

80-90: Consider defining constants for new Responses API event types.

Lines 84-85 use inline type casts for event types. If these event types are used elsewhere (e.g., in isRealtimeDeltaEvent at lines 825-829), consider defining them as constants in core/schemas/realtime.go alongside the existing RTEvent* constants for consistency and maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 80 - 90, The switch in
OpenAIProvider.ShouldAccumulateRealtimeOutput uses inline
schemas.RealtimeEventType casts for new Responses API event strings; define
named constants (e.g., RTEventResponseOutputTextDelta,
RTEventResponseOutputAudioTranscriptDelta) in core/schemas/realtime.go alongside
existing RTEvent* constants, then replace the inline type-casts in
ShouldAccumulateRealtimeOutput and any other usages (e.g., isRealtimeDeltaEvent)
to reference those new constants (using the same schemas.RealtimeEventType type)
so event names are centralized and consistent.

758-782: Nil json.RawMessage values will be stored as "null" in ExtraParams.

When json.RawMessage fields (like raw.Conversation, raw.Response, raw.Part) are nil—meaning they were absent in the original JSON—json.Marshal(nil) returns []byte("null"). This passes the length check and gets added to ExtraParams, potentially polluting it with explicit null values for absent fields.

Consider adding a case for json.RawMessage to skip nil/empty values:

Proposed fix
 func setRealtimeExtraParam(event *schemas.BifrostRealtimeEvent, key string, value any) {
 	if event == nil || key == "" || value == nil {
 		return
 	}

 	switch v := value.(type) {
 	case string:
 		if v == "" {
 			return
 		}
 	case *int:
 		if v == nil {
 			return
 		}
+	case json.RawMessage:
+		if len(v) == 0 {
+			return
+		}
 	}

 	raw, err := json.Marshal(value)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 758 - 782, The function
setRealtimeExtraParam stores json.Marshal(value) into event.ExtraParams, but
marshaling a nil json.RawMessage produces the bytes "null" and will be written
into ExtraParams; update setRealtimeExtraParam to detect json.RawMessage inputs
(and skip when the RawMessage is nil, empty, or marshals to "null") before
calling json.Marshal so keys like raw.Conversation, raw.Response, raw.Part are
not added as explicit nulls—i.e., add a type switch case for json.RawMessage
(and *json.RawMessage if used) and return early when len(raw)==0 or
string(raw)=="null" (or the pointer is nil) so only meaningful values get
marshaled and stored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/bifrost.go`:
- Around line 3873-3893: The short-circuit Response path exits before running
post-LLM hooks and cleanup, leaving per-turn plugin state/logs attached to the
reused *schemas.BifrostContext; update the shortCircuit.Response branch to call
pipeline.RunPostLLMHooks(ctx, shortCircuit.Response, nil, preCount), run cleanup
afterwards, handle any bifrostErr returned from RunPostLLMHooks (return it if
non-nil), and only then return the newBifrostErrorFromMsg("realtime turn
short-circuit responses are not supported"); ensure the Error branch already
runs RunPostLLMHooks and cleanup in the same pattern so all exit paths drain
plugin logs for RealtimeTurnHooks and the PostHookRunner remains
pipeline.RunPostLLMHooks.

In `@core/schemas/realtime_client_secrets.go`:
- Line 48: Update the error message passed to NewRealtimeClientSecretBodyError
so it reflects both accepted shapes by mentioning "session.model or model is
required" instead of only "session.model is required"; locate the return that
currently calls NewRealtimeClientSecretBodyError(400, "invalid_request_error",
"session.model is required", nil) and change the third argument to a combined
message (e.g., "session.model or model is required") so callers using legacy
top-level model are correctly guided.

In `@core/schemas/realtime.go`:
- Around line 202-210: ParseRealtimeEvent currently leaves a literal
"extra_params" map in root which causes nested extra_params to re-emit instead
of merging into the already-unmarshaled Session/Item/Error maps; fix by, inside
ParseRealtimeEvent in core/schemas/realtime.go, capture root["session"],
root["item"], and root["error"] (and the top-level root["extra_params"]) before
you delete known keys, decode any present "extra_params" blobs and merge their
key/values into the corresponding structs' ExtraParams maps
(Session.ExtraParams, Item.ExtraParams, Error.ExtraParams) so nested fields like
item_id or output_modalities are restored into those maps, then proceed to
delete the known keys and assign event.ExtraParams from any remaining root
entries.

---

Outside diff comments:
In `@core/bifrost.go`:
- Around line 3738-3750: The callsite in
transports/bifrost-http/handlers/wsresponses.go still uses the removed
SelectKeyForProvider; update that caller to use the new
SelectKeyForProviderRequestType signature (pass
schemas.WebSocketResponsesRequest as the requestType) so it calls
SelectKeyForProviderRequestType(ctx, schemas.WebSocketResponsesRequest,
req.Provider, req.Model), or alternatively add a small compatibility wrapper
function SelectKeyForProvider that forwards to SelectKeyForProviderRequestType
(preserving original parameters) to avoid a break; reference
SelectKeyForProviderRequestType in core/bifrost.go and the caller in
wsresponses.go when making the change.

---

Nitpick comments:
In `@core/providers/openai/realtime.go`:
- Around line 80-90: The switch in OpenAIProvider.ShouldAccumulateRealtimeOutput
uses inline schemas.RealtimeEventType casts for new Responses API event strings;
define named constants (e.g., RTEventResponseOutputTextDelta,
RTEventResponseOutputAudioTranscriptDelta) in core/schemas/realtime.go alongside
existing RTEvent* constants, then replace the inline type-casts in
ShouldAccumulateRealtimeOutput and any other usages (e.g., isRealtimeDeltaEvent)
to reference those new constants (using the same schemas.RealtimeEventType type)
so event names are centralized and consistent.
- Around line 758-782: The function setRealtimeExtraParam stores
json.Marshal(value) into event.ExtraParams, but marshaling a nil json.RawMessage
produces the bytes "null" and will be written into ExtraParams; update
setRealtimeExtraParam to detect json.RawMessage inputs (and skip when the
RawMessage is nil, empty, or marshals to "null") before calling json.Marshal so
keys like raw.Conversation, raw.Response, raw.Part are not added as explicit
nulls—i.e., add a type switch case for json.RawMessage (and *json.RawMessage if
used) and return early when len(raw)==0 or string(raw)=="null" (or the pointer
is nil) so only meaningful values get marshaled and stored.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 457602e8-12ad-4353-b416-f79accbfaab2

📥 Commits

Reviewing files that changed from the base of the PR and between 16efb70 and de28bc1.

📒 Files selected for processing (10)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go

Comment thread core/bifrost.go
Comment thread core/schemas/realtime_client_secrets.go Outdated
Comment thread core/schemas/realtime.go
@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from de28bc1 to 8b08931 Compare March 30, 2026 10:24
Comment thread core/providers/openai/realtime.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/bifrost.go (1)

3738-3750: ⚠️ Potential issue | 🔴 Critical

Update the remaining handler call site before this rename lands.

In the current stack, transports/bifrost-http/handlers/wsresponses.go still calls SelectKeyForProvider(ctx, req.Provider, req.Model), so removing the old method here leaves the build broken until that handler moves to the new signature.

Suggested follow-up
- key, err := h.client.SelectKeyForProvider(ctx, req.Provider, req.Model)
+ key, err := h.client.SelectKeyForProviderRequestType(ctx, schemas.WebSocketResponsesRequest, req.Provider, req.Model)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/bifrost.go` around lines 3738 - 3750, The build breaks because callers
still invoke the old SelectKeyForProvider signature; update any call sites
(e.g., in transports/bifrost-http/handlers/wsresponses.go) to call
SelectKeyForProviderRequestType instead, passing the request type along with the
existing arguments: supply the correct schemas.RequestType value (from the
incoming request/context), the provider argument (req.Provider), and the model
(req.Model), so the handler calls SelectKeyForProviderRequestType(ctx,
requestType, req.Provider, req.Model) which then forwards to
selectKeyFromProviderForModel.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/bifrost.go`:
- Around line 3854-3866: RunRealtimeTurnPreHooks currently may proceed without
seeding tracing metadata, causing drainAndAttachPluginLogs to return early and
per-turn plugin logs to leak into later turns; modify RunRealtimeTurnPreHooks to
ensure that when ctx is nil or missing tracer/trace-id it seeds
BifrostContextKeyTracer and BifrostContextKeyRequestID (as is done in
RunStreamPreHooks) before calling getPluginPipeline and before any
drainAndAttachPluginLogs invocations, so per-turn values are drained and
attached correctly; update the same seeding logic for the analogous block
referenced around lines 3893-3899 to prevent shared-context leakage.

---

Outside diff comments:
In `@core/bifrost.go`:
- Around line 3738-3750: The build breaks because callers still invoke the old
SelectKeyForProvider signature; update any call sites (e.g., in
transports/bifrost-http/handlers/wsresponses.go) to call
SelectKeyForProviderRequestType instead, passing the request type along with the
existing arguments: supply the correct schemas.RequestType value (from the
incoming request/context), the provider argument (req.Provider), and the model
(req.Model), so the handler calls SelectKeyForProviderRequestType(ctx,
requestType, req.Provider, req.Model) which then forwards to
selectKeyFromProviderForModel.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12c27f77-8a25-4141-ad58-2cad4232399f

📥 Commits

Reviewing files that changed from the base of the PR and between de28bc1 and 8b08931.

📒 Files selected for processing (10)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
✅ Files skipped from review due to trivial changes (3)
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/bifrost.go
  • core/providers/openai/realtime_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go

Comment thread core/bifrost.go
@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from 8b08931 to 6a8d154 Compare March 30, 2026 12:49
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/bifrost.go (1)

3738-3751: ⚠️ Potential issue | 🔴 Critical

Compile error: stale method call in wsresponses.go must be updated.

The method SelectKeyForProvider(ctx, providerKey, model) was removed and replaced with SelectKeyForProviderRequestType(ctx, requestType, providerKey, model) (4 parameters instead of 3). However, transports/bifrost-http/handlers/wsresponses.go:231 still calls the old 3-parameter version, which will fail to compile. Update this callsite to use the new method name and provide the required requestType parameter (likely schemas.WebSocketResponsesRequest based on context).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/bifrost.go` around lines 3738 - 3751, Update the stale call to the
removed SelectKeyForProvider method in
transports/bifrost-http/handlers/wsresponses.go to use the new
SelectKeyForProviderRequestType(ctx, requestType, providerKey, model) signature:
replace the old three-argument call with
bifrost.SelectKeyForProviderRequestType(ctx, schemas.WebSocketResponsesRequest,
providerKey, model) (or the appropriate RequestType constant) so the call
matches the new function SelectKeyForProviderRequestType.
🧹 Nitpick comments (4)
core/providers/openai/realtime.go (4)

703-721: Minor: TrimSpace is called redundantly.

Lines 711-716 check strings.TrimSpace(block.Text) != "" and then call sb.WriteString(strings.TrimSpace(block.Text)), trimming the same string twice. Consider storing the trimmed result.

♻️ Minor optimization
 for _, block := range output.Content {
-    switch {
-    case strings.TrimSpace(block.Text) != "":
-        sb.WriteString(strings.TrimSpace(block.Text))
-    case strings.TrimSpace(block.Transcript) != "":
-        sb.WriteString(strings.TrimSpace(block.Transcript))
-    case strings.TrimSpace(block.Refusal) != "":
-        sb.WriteString(strings.TrimSpace(block.Refusal))
-    }
+    if text := strings.TrimSpace(block.Text); text != "" {
+        sb.WriteString(text)
+    } else if transcript := strings.TrimSpace(block.Transcript); transcript != "" {
+        sb.WriteString(transcript)
+    } else if refusal := strings.TrimSpace(block.Refusal); refusal != "" {
+        sb.WriteString(refusal)
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 703 - 721, In
extractOpenAIRealtimeResponseDoneAssistantText, avoid trimming the same fields
twice: for each block, compute trimmedText := strings.TrimSpace(block.Text),
trimmedTranscript := strings.TrimSpace(block.Transcript), trimmedRefusal :=
strings.TrimSpace(block.Refusal) and then test the trimmed variables for != ""
and write the already-trimmed value into sb (via sb.WriteString) instead of
calling TrimSpace again; this reduces redundant work while preserving the
current selection order (Text, then Transcript, then Refusal).

48-57: Consider extracting shared header logic.

RealtimeWebRTCHeaders is nearly identical to RealtimeHeaders (lines 38-46). If both are expected to remain the same, consider extracting a shared helper to reduce duplication.

♻️ Optional refactor to reduce duplication
+func (provider *OpenAIProvider) baseRealtimeHeaders(key schemas.Key) map[string]string {
+	headers := map[string]string{
+		"Authorization": "Bearer " + key.Value.GetValue(),
+	}
+	for k, v := range provider.networkConfig.ExtraHeaders {
+		headers[k] = v
+	}
+	return headers
+}
+
 // RealtimeHeaders returns the headers required for the OpenAI Realtime WebSocket connection.
 func (provider *OpenAIProvider) RealtimeHeaders(key schemas.Key) map[string]string {
-	headers := map[string]string{
-		"Authorization": "Bearer " + key.Value.GetValue(),
-	}
-	for k, v := range provider.networkConfig.ExtraHeaders {
-		headers[k] = v
-	}
-	return headers
+	return provider.baseRealtimeHeaders(key)
 }
 
 // RealtimeWebRTCHeaders returns the headers required for the OpenAI Realtime WebRTC SDP exchange.
 func (provider *OpenAIProvider) RealtimeWebRTCHeaders(key schemas.Key) map[string]string {
-	headers := map[string]string{
-		"Authorization": "Bearer " + key.Value.GetValue(),
-	}
-	for k, v := range provider.networkConfig.ExtraHeaders {
-		headers[k] = v
-	}
-	return headers
+	return provider.baseRealtimeHeaders(key)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 48 - 57,
RealtimeWebRTCHeaders and RealtimeHeaders share header-building logic; extract a
single helper (e.g., buildRealtimeHeaders or baseRealtimeHeaders) that accepts
schemas.Key and provider.networkConfig.ExtraHeaders (or provider reference) and
returns map[string]string, then replace the bodies of RealtimeWebRTCHeaders and
RealtimeHeaders to call that helper so they both set "Authorization": "Bearer
"+key.Value.GetValue() and merge provider.networkConfig.ExtraHeaders from the
helper; ensure the helper signature matches usage and update both methods to
return the helper's result.

79-89: Consider defining constants for newer Responses API event types.

Lines 83-84 use string literals for "response.output_text.delta" and "response.output_audio_transcript.delta". If these are stable API event types, define them as constants in the schemas package alongside RTEventResponseTextDelta and RTEventResponseAudioTransDelta for consistency and maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 79 - 89, The switch in
OpenAIProvider.ShouldAccumulateRealtimeOutput uses hard-coded event type strings
"response.output_text.delta" and "response.output_audio_transcript.delta"; add
matching named constants in the schemas package (e.g.,
RTEventResponseOutputTextDelta and RTEventResponseOutputAudioTranscriptDelta)
next to RTEventResponseTextDelta/RTEventResponseAudioTransDelta, then replace
the string literals in ShouldAccumulateRealtimeOutput with those new constants
so all event types are defined and referenced consistently.

361-459: Consider using sonic for JSON unmarshaling in this hot path.

ToBifrostRealtimeEvent is called for every incoming realtime event. Per coding guidelines, github.com/bytedance/sonic should be preferred for JSON unmarshaling in hot paths. The provider already imports sonic and uses it consistently in other functions. Lines 364, 388, 409, and 428 currently use standard encoding/json.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 361 - 459, The function
ToBifrostRealtimeEvent uses encoding/json's json.Unmarshal in a hot path;
replace those calls with sonic.Unmarshal to improve performance: change
json.Unmarshal(providerEvent, &raw) to sonic.Unmarshal(providerEvent, &raw),
json.Unmarshal(raw.Session, &sess) to sonic.Unmarshal(raw.Session, &sess),
json.Unmarshal(raw.Item, &item) to sonic.Unmarshal(raw.Item, &item), and
json.Unmarshal(raw.Error, &rtErr) to sonic.Unmarshal(raw.Error, &rtErr)
(preserving the same error handling and variable names raw, sess, item, rtErr,
and the surrounding logic in
ToBifrostRealtimeEvent/RealtimeSession/RealtimeItem/RealtimeError).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/providers/openai/realtime.go`:
- Around line 176-184: The second error branch currently returns the same
message as the model check; update the providerKey check to return a distinct,
descriptive error indicating a missing or misconfigured provider/default
provider. Specifically, in the block that checks providerKey (after fallback to
defaultProvider) replace the newRealtimeClientSecretError call so it uses a
message like "provider key or default provider is required" (using the existing
newRealtimeClientSecretError function) and keep the same status and error type;
this ensures the checks for normalizedModel and providerKey/defaultProvider are
clearly distinguishable.

---

Outside diff comments:
In `@core/bifrost.go`:
- Around line 3738-3751: Update the stale call to the removed
SelectKeyForProvider method in transports/bifrost-http/handlers/wsresponses.go
to use the new SelectKeyForProviderRequestType(ctx, requestType, providerKey,
model) signature: replace the old three-argument call with
bifrost.SelectKeyForProviderRequestType(ctx, schemas.WebSocketResponsesRequest,
providerKey, model) (or the appropriate RequestType constant) so the call
matches the new function SelectKeyForProviderRequestType.

---

Nitpick comments:
In `@core/providers/openai/realtime.go`:
- Around line 703-721: In extractOpenAIRealtimeResponseDoneAssistantText, avoid
trimming the same fields twice: for each block, compute trimmedText :=
strings.TrimSpace(block.Text), trimmedTranscript :=
strings.TrimSpace(block.Transcript), trimmedRefusal :=
strings.TrimSpace(block.Refusal) and then test the trimmed variables for != ""
and write the already-trimmed value into sb (via sb.WriteString) instead of
calling TrimSpace again; this reduces redundant work while preserving the
current selection order (Text, then Transcript, then Refusal).
- Around line 48-57: RealtimeWebRTCHeaders and RealtimeHeaders share
header-building logic; extract a single helper (e.g., buildRealtimeHeaders or
baseRealtimeHeaders) that accepts schemas.Key and
provider.networkConfig.ExtraHeaders (or provider reference) and returns
map[string]string, then replace the bodies of RealtimeWebRTCHeaders and
RealtimeHeaders to call that helper so they both set "Authorization": "Bearer
"+key.Value.GetValue() and merge provider.networkConfig.ExtraHeaders from the
helper; ensure the helper signature matches usage and update both methods to
return the helper's result.
- Around line 79-89: The switch in OpenAIProvider.ShouldAccumulateRealtimeOutput
uses hard-coded event type strings "response.output_text.delta" and
"response.output_audio_transcript.delta"; add matching named constants in the
schemas package (e.g., RTEventResponseOutputTextDelta and
RTEventResponseOutputAudioTranscriptDelta) next to
RTEventResponseTextDelta/RTEventResponseAudioTransDelta, then replace the string
literals in ShouldAccumulateRealtimeOutput with those new constants so all event
types are defined and referenced consistently.
- Around line 361-459: The function ToBifrostRealtimeEvent uses encoding/json's
json.Unmarshal in a hot path; replace those calls with sonic.Unmarshal to
improve performance: change json.Unmarshal(providerEvent, &raw) to
sonic.Unmarshal(providerEvent, &raw), json.Unmarshal(raw.Session, &sess) to
sonic.Unmarshal(raw.Session, &sess), json.Unmarshal(raw.Item, &item) to
sonic.Unmarshal(raw.Item, &item), and json.Unmarshal(raw.Error, &rtErr) to
sonic.Unmarshal(raw.Error, &rtErr) (preserving the same error handling and
variable names raw, sess, item, rtErr, and the surrounding logic in
ToBifrostRealtimeEvent/RealtimeSession/RealtimeItem/RealtimeError).
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71639472-0c4a-4ce0-ac7a-b44ec244741f

📥 Commits

Reviewing files that changed from the base of the PR and between 8b08931 and 6a8d154.

📒 Files selected for processing (10)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
✅ Files skipped from review due to trivial changes (2)
  • core/schemas/bifrost.go
  • core/providers/openai/realtime_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/internal/llmtests/realtime.go
  • core/schemas/realtime_client_secrets_test.go
  • core/providers/elevenlabs/realtime.go

Comment thread core/providers/openai/realtime.go
@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from 6a8d154 to 4978f02 Compare March 30, 2026 14:50
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
core/providers/openai/realtime.go (1)

79-89: Consider defining constants for modern Responses API event types.

The inline casts work correctly, but defining constants would improve consistency with the existing schemas.RTEventResponseTextDelta and schemas.RTEventResponseAudioTransDelta constants.

♻️ Optional: Define constants in schemas for the modern event names
// In core/schemas/realtime.go (or similar):
const (
    RTEventResponseOutputTextDelta          RealtimeEventType = "response.output_text.delta"
    RTEventResponseOutputAudioTransDelta    RealtimeEventType = "response.output_audio_transcript.delta"
)

Then use them here:

 func (provider *OpenAIProvider) ShouldAccumulateRealtimeOutput(eventType schemas.RealtimeEventType) bool {
     switch eventType {
     case schemas.RTEventResponseTextDelta,
         schemas.RTEventResponseAudioTransDelta,
-        schemas.RealtimeEventType("response.output_text.delta"),
-        schemas.RealtimeEventType("response.output_audio_transcript.delta"):
+        schemas.RTEventResponseOutputTextDelta,
+        schemas.RTEventResponseOutputAudioTransDelta:
         return true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 79 - 89, The switch in
ShouldAccumulateRealtimeOutput currently uses inline casts for modern Responses
API event strings; add named constants in the schemas package (e.g.,
RTEventResponseOutputTextDelta and RTEventResponseOutputAudioTransDelta of type
RealtimeEventType) and replace the inline schemas.RealtimeEventType(...) cases
in ShouldAccumulateRealtimeOutput with these new constants so the code is
consistent with existing symbols like schemas.RTEventResponseTextDelta and
schemas.RTEventResponseAudioTransDelta.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@core/providers/openai/realtime.go`:
- Around line 79-89: The switch in ShouldAccumulateRealtimeOutput currently uses
inline casts for modern Responses API event strings; add named constants in the
schemas package (e.g., RTEventResponseOutputTextDelta and
RTEventResponseOutputAudioTransDelta of type RealtimeEventType) and replace the
inline schemas.RealtimeEventType(...) cases in ShouldAccumulateRealtimeOutput
with these new constants so the code is consistent with existing symbols like
schemas.RTEventResponseTextDelta and schemas.RTEventResponseAudioTransDelta.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 30cc89e0-5da3-4348-a180-2440ec7330af

📥 Commits

Reviewing files that changed from the base of the PR and between 6a8d154 and 4978f02.

📒 Files selected for processing (10)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
✅ Files skipped from review due to trivial changes (4)
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/bifrost.go
  • core/providers/openai/realtime_test.go
  • core/schemas/realtime.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/bifrost.go

@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from 4978f02 to 90ebca7 Compare March 30, 2026 17:00
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/bifrost.go`:
- Around line 3882-3884: The early-return path that handles pre-hook
nil/short-circuit must drain plugin logs before cleaning up: call
drainAndAttachPluginLogs(ctx) just prior to cleanup() in the branch that checks
(preReq == nil && shortCircuit == nil) so any logs produced by pre-hook are
attached and cleared from the BifrostContext before reusing it; leave the error
return via newBifrostErrorFromMsg unchanged.
- Around line 3907-3910: parseUpstreamWSEvent is stamping upstream websocket
responses as schemas.ResponsesStreamRequest causing realtime turns to skip
Cleanup; update the stamping logic in parseUpstreamWSEvent to detect realtime
context (check ctx for BifrostContextKeyRealtimeSessionID or equivalent) and set
request.RequestType = schemas.RealtimeRequest for those events. Alternatively,
if you prefer to centralize normalization, modify the PostHookRunner (the
anonymous func passed to pipeline.RunPostLLMHooks) to inspect ctx for
BifrostContextKeyRealtimeSessionID and overwrite result.RequestType to
schemas.RealtimeRequest before calling drainAndAttachPluginLogs so the later
guard (expecting schemas.RealtimeRequest) and Cleanup path behave correctly.

In `@core/schemas/realtime_client_secrets.go`:
- Around line 13-14: Replace direct calls to json.Unmarshal with the package
Unmarshal wrapper in this file so the sonic-optimized path is used; specifically
update each json.Unmarshal(raw, &root) (and similar json.Unmarshal usages that
return NewRealtimeClientSecretBodyError on failure) to call Unmarshal(raw,
&root) and propagate the same NewRealtimeClientSecretBodyError on error. Ensure
you change all occurrences in realtime_client_secrets.go (the functions that
parse the request body using variable names like raw and root and which return
NewRealtimeClientSecretBodyError) so behavior and error handling remain
identical but use the Unmarshal wrapper for performance.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e5a6abf4-ddc5-49e6-ab0a-93ae961fac9a

📥 Commits

Reviewing files that changed from the base of the PR and between 4978f02 and 90ebca7.

📒 Files selected for processing (10)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
✅ Files skipped from review due to trivial changes (3)
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/bifrost.go
  • core/providers/openai/realtime_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/schemas/realtime.go
  • core/providers/openai/realtime.go

Comment thread core/bifrost.go Outdated
Comment thread core/bifrost.go
Comment thread core/schemas/realtime_client_secrets.go Outdated
@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from 90ebca7 to f38794a Compare March 31, 2026 06:16
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/providers/elevenlabs/realtime.go (1)

232-254: ⚠️ Potential issue | 🟠 Major

Keep a passthrough path for provider-native events you don’t reconstruct.

After removing the RawData short-circuit, every event outside the two explicit cases is serialized as {"type": ...} only. That strips the payload from provider-native client events like client_tool_result and contextual_update, so those messages can no longer round-trip to ElevenLabs.

🛠️ Minimal fallback
 	default:
+		if len(bifrostEvent.RawData) > 0 {
+			return bifrostEvent.RawData, nil
+		}
 		out := map[string]interface{}{
 			"type": string(bifrostEvent.Type),
 		}
 		return schemas.MarshalSorted(out)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/elevenlabs/realtime.go` around lines 232 - 254, The
ToProviderRealtimeEvent function currently drops provider-native payloads for
events not explicitly handled; restore a passthrough path by returning the
original bifrostEvent.RawData (when non-nil) for any event types you don't
reconstruct (e.g., client_tool_result, contextual_update) instead of serializing
only {"type":...}; update ToProviderRealtimeEvent to check bifrostEvent.RawData
and return it directly before falling back to building a minimal {"type":..."}
map and calling schemas.MarshalSorted, so existing short-circuited provider
messages round-trip intact.
core/bifrost.go (1)

3738-3750: ⚠️ Potential issue | 🟠 Major

Keep a deprecated SelectKeyForProvider shim for the v1 line.

Renaming an exported *Bifrost method is a compile-time break for downstream Go callers. If v1.5.0 is still meant to stay source-compatible, re-add the old method and delegate to this one with the previous default request type.

Compatibility shim
+// Deprecated: use SelectKeyForProviderRequestType.
+func (bifrost *Bifrost) SelectKeyForProvider(ctx *schemas.BifrostContext, providerKey schemas.ModelProvider, model string) (schemas.Key, error) {
+	return bifrost.SelectKeyForProviderRequestType(ctx, schemas.WebSocketResponsesRequest, providerKey, model)
+}
+
 // SelectKeyForProviderRequestType selects an API key for the given provider, request type, and model.
 // Used by WebSocket handlers that need a key for upstream connections while honoring request-specific
 // AllowedRequests gates such as realtime-only support.
 func (bifrost *Bifrost) SelectKeyForProviderRequestType(ctx *schemas.BifrostContext, requestType schemas.RequestType, providerKey schemas.ModelProvider, model string) (schemas.Key, error) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/bifrost.go` around lines 3738 - 3750, Add a deprecated shim method named
SelectKeyForProvider on *Bifrost that preserves the old signature and delegates
to the new SelectKeyForProviderRequestType; the shim should accept (ctx
*schemas.BifrostContext, providerKey schemas.ModelProvider, model string),
default the requestType to the same constant/value the previous implementation
used (the prior default request type), call
bifrost.SelectKeyForProviderRequestType(ctx, <defaultRequestType>, providerKey,
model) and return its results, and mark it deprecated in a comment so downstream
callers remain source-compatible while pointing them to
SelectKeyForProviderRequestType.
🧹 Nitpick comments (1)
core/providers/openai/realtime.go (1)

597-601: Backfill TotalTokens when the terminal payload omits it.

This copies response.done.usage.total_tokens verbatim. If that field is absent/zero while input/output counts are populated, downstream tiering/billing in the new realtime-turn flow will see TotalTokens == 0.

♻️ Suggested fix
 	usage := &schemas.BifrostLLMUsage{
 		PromptTokens:     parsed.Response.Usage.InputTokens,
 		CompletionTokens: parsed.Response.Usage.OutputTokens,
 		TotalTokens:      parsed.Response.Usage.TotalTokens,
 	}
+	if usage.TotalTokens == 0 && (usage.PromptTokens > 0 || usage.CompletionTokens > 0) {
+		usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
+	}

Based on learnings, ImageUsage and other usage types guarantee that TotalTokens is populated (computed as InputTokens + OutputTokens if providers don’t supply TotalTokens).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 597 - 601, The usage struct
currently copies parsed.Response.Usage.TotalTokens verbatim which can be
zero/missing; update the assignment for TotalTokens in the block that creates
schemas.BifrostLLMUsage (the variable usage) to backfill it when absent by
computing parsed.Response.Usage.InputTokens + parsed.Response.Usage.OutputTokens
(i.e., if parsed.Response.Usage.TotalTokens == 0 then set TotalTokens =
InputTokens + OutputTokens, otherwise use the provided TotalTokens) so
downstream tiering/billing sees a correct total.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/bifrost.go`:
- Around line 5760-5763: GetResponseFields(nil, bifrostErr) can return an empty
RequestType so realtime error turns with BifrostContextKeyStreamStartTime set
will incorrectly be treated as streaming; update the isStreaming determination
to prefer the typed realtime context before falling back to bifrostErr: after
calling GetResponseFields check if requestType == "" and ctx contains the
realtime session/turn marker (or inspect the realtime turn/session type), and if
so set requestType = schemas.RealtimeRequest (or normalize RequestType to
schemas.RealtimeRequest) so RunPostLLMHooks uses the non-streaming realtime
finalization path; adjust the logic around BifrostContextKeyStreamStartTime,
GetResponseFields, bifrostErr and RunPostLLMHooks accordingly.

In `@core/providers/openai/realtime.go`:
- Around line 99-100: The error path hardcodes schemas.OpenAI when calling
providerUtils.CheckOperationAllowed and when constructing the
newRealtimeClientSecretError, which misattributes errors for OpenAI-compatible
custom providers; update the calls in realtime.go to fetch the actual provider
key via provider.GetProviderKey() (or equivalent) and pass that provider key
into providerUtils.CheckOperationAllowed and newRealtimeClientSecretError (and
any similar error/validation calls later in the file, e.g., the code mirrored
around the other realtime client secret/validation block) so the stamped
provider metadata reflects the configured provider alias instead of always
"openai".
- Around line 30-35: The code is still using deprecated beta endpoints/headers
for OpenAI Realtime; update RealtimeWebRTCURL to return
provider.networkConfig.BaseURL + "/v1/realtime/calls", change
realtimeSessionUpstreamPath to return "/v1/realtime/client_secrets" instead of
"/v1/realtime/sessions", and remove the deprecated header "OpenAI-Beta:
realtime=v1" from realtimeSessionHeaders (and any usage tied to
RealtimeSessionEndpointSessions) so the code uses GA endpoints
(/v1/realtime/calls and /v1/realtime/client_secrets) with no beta header.

---

Outside diff comments:
In `@core/bifrost.go`:
- Around line 3738-3750: Add a deprecated shim method named SelectKeyForProvider
on *Bifrost that preserves the old signature and delegates to the new
SelectKeyForProviderRequestType; the shim should accept (ctx
*schemas.BifrostContext, providerKey schemas.ModelProvider, model string),
default the requestType to the same constant/value the previous implementation
used (the prior default request type), call
bifrost.SelectKeyForProviderRequestType(ctx, <defaultRequestType>, providerKey,
model) and return its results, and mark it deprecated in a comment so downstream
callers remain source-compatible while pointing them to
SelectKeyForProviderRequestType.

In `@core/providers/elevenlabs/realtime.go`:
- Around line 232-254: The ToProviderRealtimeEvent function currently drops
provider-native payloads for events not explicitly handled; restore a
passthrough path by returning the original bifrostEvent.RawData (when non-nil)
for any event types you don't reconstruct (e.g., client_tool_result,
contextual_update) instead of serializing only {"type":...}; update
ToProviderRealtimeEvent to check bifrostEvent.RawData and return it directly
before falling back to building a minimal {"type":..."} map and calling
schemas.MarshalSorted, so existing short-circuited provider messages round-trip
intact.

---

Nitpick comments:
In `@core/providers/openai/realtime.go`:
- Around line 597-601: The usage struct currently copies
parsed.Response.Usage.TotalTokens verbatim which can be zero/missing; update the
assignment for TotalTokens in the block that creates schemas.BifrostLLMUsage
(the variable usage) to backfill it when absent by computing
parsed.Response.Usage.InputTokens + parsed.Response.Usage.OutputTokens (i.e., if
parsed.Response.Usage.TotalTokens == 0 then set TotalTokens = InputTokens +
OutputTokens, otherwise use the provided TotalTokens) so downstream
tiering/billing sees a correct total.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 48513933-6df8-48bd-b8a7-88ebc4125bdb

📥 Commits

Reviewing files that changed from the base of the PR and between 90ebca7 and f38794a.

📒 Files selected for processing (10)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
✅ Files skipped from review due to trivial changes (1)
  • core/schemas/realtime_client_secrets_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • core/schemas/bifrost.go
  • core/schemas/realtime.go

Comment thread core/bifrost.go Outdated
Comment thread core/providers/openai/realtime.go Outdated
Comment thread core/providers/openai/realtime.go
@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from f38794a to 8e9f5b5 Compare March 31, 2026 08:00
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/bifrost.go (2)

3738-3750: ⚠️ Potential issue | 🟠 Major

The new request-type-aware selector still isn't enforcing AllowedRequests.

requestType is forwarded here, but selectKeyFromProviderForModel still only uses it for batch/file special cases and skip-model behavior. Keys restricted to realtime-only vs. standard flows are still eligible outside their allowed request types until the generic key filter also checks AllowedRequests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/bifrost.go` around lines 3738 - 3750, SelectKeyForProviderRequestType
currently passes requestType to selectKeyFromProviderForModel but
AllowedRequests on keys isn't enforced; update the key-filter logic in
selectKeyFromProviderForModel (or the shared generic key filter it calls) to
check the candidate key's AllowedRequests (e.g., key.AllowedRequests) against
the incoming schemas.RequestType and reject keys that don't permit the requested
flow (realtime vs standard/batch/file). Ensure the filter uses the same
enums/types (schemas.RequestType) and treats realtime-only/standard-only flags
correctly so keys restricted to realtime aren't returned for standard requests
and vice‑versa.

3738-3750: ⚠️ Potential issue | 🔴 Critical

Update the remaining callers before removing SelectKeyForProvider.

In the provided stack context, transports/bifrost-http/handlers/wsresponses.go at Line 231 still calls SelectKeyForProvider(...). Removing the old symbol here makes the stack inconsistent and will fail to build until that caller is migrated or a temporary compatibility shim is kept.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/bifrost.go` around lines 3738 - 3750, The code introduced
SelectKeyForProviderRequestType but callers still use the old
SelectKeyForProvider; update remaining call sites (e.g., in
transports/bifrost-http/handlers/wsresponses.go) to call
SelectKeyForProviderRequestType with the appropriate context, requestType,
providerKey and model parameters (matching how SelectKeyForProviderRequestType
defers to bifrost.selectKeyFromProviderForModel), or add a small compatibility
wrapper method named SelectKeyForProvider on Bifrost that forwards to
SelectKeyForProviderRequestType (preserving the original signature) until all
callers are migrated.
🧹 Nitpick comments (2)
core/schemas/realtime.go (2)

176-205: Consider performance implications of double-unmarshal in hot path.

ParseRealtimeEvent unmarshals the raw bytes twice (lines 188 and 203). For high-frequency realtime events, this doubles the parsing cost. The current approach using encoding/json is correct, but worth noting if realtime throughput becomes a concern.

If profiling indicates this is a bottleneck, consider unmarshaling into the map first and then extracting known fields, or using a streaming decoder.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/schemas/realtime.go` around lines 176 - 205, ParseRealtimeEvent
currently unmarshals raw bytes twice (first into realtimeEventAlias then into
root), which doubles work on the hot path; change it to unmarshal into a
map[string]json.RawMessage (root) once and then decode individual known fields
(Type, EventID, session/item/delta/audio/error) from root into the
realtimeEventAlias or directly into the BifrostRealtimeEvent to avoid the second
full unmarshal; update references to realtimeEventAlias, ParseRealtimeEvent,
BifrostRealtimeEvent and root so the code reads root :=
map[string]json.RawMessage{}; json.Unmarshal(raw, &root) and then
json.Unmarshal(root["field"], &alias.Field) for each known field (or use
json.Decoder streaming) to eliminate the double unmarshal.

209-211: Known key lists are correctly synchronized with struct field tags.

The hardcoded key lists for root, session, item, and error correctly match their respective struct field JSON tags. Future struct changes will require updating these lists.

Consider adding a comment near each key list referencing the corresponding struct to aid maintenance, or generating these lists from struct tags at init time.

Also applies to: 218-223, 232-236, 245-247

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/schemas/realtime.go` around lines 209 - 211, The hardcoded key lists in
realtime.go (the slices used in the delete loops that remove keys like
"type","event_id","session","item","delta","audio","error","raw_data" and the
other similar lists at the nearby blocks) must be kept in sync with their
corresponding struct JSON tags; either add a clear comment above each key-list
referencing the exact struct name (e.g., Session, Item, Error structs) and the
field/tag that the list mirrors, or replace the literal lists by computing them
at package init by reflecting the struct tags (use reflect to pull `json` tags
from the struct types and build the key slices once). Ensure you update the
delete loops to use the generated/annotated key slices so future struct changes
won't diverge silently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@core/bifrost.go`:
- Around line 3738-3750: SelectKeyForProviderRequestType currently passes
requestType to selectKeyFromProviderForModel but AllowedRequests on keys isn't
enforced; update the key-filter logic in selectKeyFromProviderForModel (or the
shared generic key filter it calls) to check the candidate key's AllowedRequests
(e.g., key.AllowedRequests) against the incoming schemas.RequestType and reject
keys that don't permit the requested flow (realtime vs standard/batch/file).
Ensure the filter uses the same enums/types (schemas.RequestType) and treats
realtime-only/standard-only flags correctly so keys restricted to realtime
aren't returned for standard requests and vice‑versa.
- Around line 3738-3750: The code introduced SelectKeyForProviderRequestType but
callers still use the old SelectKeyForProvider; update remaining call sites
(e.g., in transports/bifrost-http/handlers/wsresponses.go) to call
SelectKeyForProviderRequestType with the appropriate context, requestType,
providerKey and model parameters (matching how SelectKeyForProviderRequestType
defers to bifrost.selectKeyFromProviderForModel), or add a small compatibility
wrapper method named SelectKeyForProvider on Bifrost that forwards to
SelectKeyForProviderRequestType (preserving the original signature) until all
callers are migrated.

---

Nitpick comments:
In `@core/schemas/realtime.go`:
- Around line 176-205: ParseRealtimeEvent currently unmarshals raw bytes twice
(first into realtimeEventAlias then into root), which doubles work on the hot
path; change it to unmarshal into a map[string]json.RawMessage (root) once and
then decode individual known fields (Type, EventID,
session/item/delta/audio/error) from root into the realtimeEventAlias or
directly into the BifrostRealtimeEvent to avoid the second full unmarshal;
update references to realtimeEventAlias, ParseRealtimeEvent,
BifrostRealtimeEvent and root so the code reads root :=
map[string]json.RawMessage{}; json.Unmarshal(raw, &root) and then
json.Unmarshal(root["field"], &alias.Field) for each known field (or use
json.Decoder streaming) to eliminate the double unmarshal.
- Around line 209-211: The hardcoded key lists in realtime.go (the slices used
in the delete loops that remove keys like
"type","event_id","session","item","delta","audio","error","raw_data" and the
other similar lists at the nearby blocks) must be kept in sync with their
corresponding struct JSON tags; either add a clear comment above each key-list
referencing the exact struct name (e.g., Session, Item, Error structs) and the
field/tag that the list mirrors, or replace the literal lists by computing them
at package init by reflecting the struct tags (use reflect to pull `json` tags
from the struct types and build the key slices once). Ensure you update the
delete loops to use the generated/annotated key slices so future struct changes
won't diverge silently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 99ea426a-a3d7-4df0-9fb7-f95a0c5a83e1

📥 Commits

Reviewing files that changed from the base of the PR and between f38794a and 8e9f5b5.

📒 Files selected for processing (10)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
✅ Files skipped from review due to trivial changes (4)
  • core/internal/llmtests/websocket_responses.go
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime_client_secrets.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/internal/llmtests/realtime.go
  • core/providers/openai/realtime_test.go
  • core/providers/openai/realtime.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Mar 31, 2026
@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from 8e9f5b5 to 8c17c3b Compare April 3, 2026 08:03
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
core/bifrost.go (1)

3786-3799: ⚠️ Potential issue | 🔴 Critical

Update the remaining callers before dropping the old selector API.

Line 3789 renames the public method, but the provided stack snippet at transports/bifrost-http/handlers/wsresponses.go:220-234 still calls h.client.SelectKeyForProvider(ctx, req.Provider, req.Model). That leaves the stack broken until the transport side is migrated too. Please update those callers in this PR/stack, or keep a temporary compatibility wrapper until the whole stack lands.

As per coding guidelines, "**: always check the stack if there is one for the current PR. do not give localized reviews for the PR, always see all changes in the light of the whole stack of PRs (if there is a stack, if there is no stack you can continue to make localized suggestions/reviews)"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/bifrost.go` around lines 3786 - 3799, The public API was renamed to
SelectKeyForProviderRequestType but callers (e.g.,
transports/bifrost-http/handlers/wsresponses.go calling
h.client.SelectKeyForProvider(ctx, req.Provider, req.Model)) still use the old
name; either update those callers to call SelectKeyForProviderRequestType with
the same args or add a temporary compatibility wrapper method
SelectKeyForProvider on Bifrost that accepts (ctx, providerKey, model) and
forwards to SelectKeyForProviderRequestType (preserving behavior and using
bifrost.selectKeyFromProviderForModel underneath) so the transport code keeps
compiling until the whole stack is migrated.
core/providers/elevenlabs/realtime.go (1)

234-255: ⚠️ Potential issue | 🔴 Critical

Reconstruct the full ElevenLabs event after removing RawData passthrough.

Line 237 now rejects canonical input_audio_buffer.append events because core/schemas/realtime.go parses wire audio into BifrostRealtimeEvent.Audio, not Delta.Audio. Lines 251-255 also emit only type, so provider-specific client events like client_tool_result and contextual_update lose their payload entirely. Please serialize the top-level Audio field and merge ExtraParams back into the outgoing object in every branch.

Based on learnings, client→provider realtime round-tripping relies on ToProviderRealtimeEvent flattening ExtraParams back to top-level keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/elevenlabs/realtime.go` around lines 234 - 255, In
ToProviderRealtimeEvent, reconstruct the full ElevenLabs event instead of
passthroughing only type or Delta: for input_audio_buffer.append use
bifrostEvent.Audio (not bifrostEvent.Delta.Audio) and include it as the
top-level "user_audio_chunk" (or appropriate key), and for every branch
(including the pong and default branches) merge bifrostEvent.ExtraParams into
the outgoing map so provider-specific events like client_tool_result and
contextual_update preserve their payload; locate symbols
ToProviderRealtimeEvent, BifrostRealtimeEvent, Audio, Delta, and ExtraParams to
update the map construction and serialization accordingly.
core/providers/openai/realtime.go (1)

670-676: ⚠️ Potential issue | 🔴 Critical

Serialize the canonical Audio field for input-audio events.

After removing the RawData fast path, this method only writes audio from bifrostEvent.Delta.Audio. schemas.ParseRealtimeEvent populates wire audio on BifrostRealtimeEvent.Audio, so canonical input_audio_buffer.append events are now forwarded without their payload. Please fall back to the top-level Audio field here.

Based on learnings, ParseRealtimeEvent only handles raw wire events, so client→provider forwarding has to be rebuilt from the canonical event fields rather than RawData.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 670 - 676, The code only
writes delta audio from bifrostEvent.Delta.Audio and misses the canonical
top-level Audio field for input-audio events; update the serialization logic in
the block handling bifrostEvent.Delta (in the method that also references out
and bifrostEvent) to fall back to bifrostEvent.Audio when
bifrostEvent.Delta.Audio is empty (or not set), so that canonical
input-audio_buffer.append events populated by ParseRealtimeEvent (which sets
BifrostRealtimeEvent.Audio) are forwarded with their payload.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/providers/openai/realtime.go`:
- Around line 242-243: The call to ParseOpenAIError in realtime.go is using four
arguments but the helper signature accepts fewer, causing a compilation error;
update the call inside the error branch (where resp.StatusCode is checked) to
match the ParseOpenAIError signature by removing the extra requestedModel
argument (i.e., call ParseOpenAIError(resp, schemas.RealtimeRequest,
provider.GetProviderKey()) ), or alternatively update the ParseOpenAIError
function signature to accept the extra requestedModel across its usages—ensure
consistency between the ParseOpenAIError definition and this call so the code
compiles.
- Around line 829-846: The function
extractOpenAIRealtimeResponseDoneAssistantText currently trims each block
(block.Text/block.Transcript/block.Refusal) before concatenation causing
adjacent blocks to run together; change it so you append the raw block values
(use block.Text, block.Transcript, block.Refusal without strings.TrimSpace) to
the strings.Builder to preserve intra-block whitespace, and after building the
full string return strings.TrimSpace(sb.String()) to only trim the final
assembled result; update references in that function accordingly.

In `@core/schemas/realtime_test.go`:
- Around line 44-46: The test assertion message is misleading: when checking
IsRealtimeToolOutputEvent(userEvent) for a conversation.item.added event, update
the t.Fatal message to reference "conversation.item.added" instead of
"conversation.item.done" so the failure text matches the tested event; locate
the assertion in realtime_test.go where IsRealtimeToolOutputEvent(userEvent) is
called and change the string accordingly.

In `@core/schemas/tracer.go`:
- Around line 120-123: The build breaks because schemas.Tracer now has
CompleteAndFlushTrace(traceID string) but the concrete framework/tracing.Tracer
does not; add a method CompleteAndFlushTrace(traceID string) to the Tracer type
in framework/tracing/tracer.go that delegates to the existing trace
completion/export path used by the current trace finish logic (i.e., the same
code path that ends a trace and exports it), ensuring the method signature
matches schemas.Tracer exactly so the compile-time interface assertion passes.

---

Outside diff comments:
In `@core/bifrost.go`:
- Around line 3786-3799: The public API was renamed to
SelectKeyForProviderRequestType but callers (e.g.,
transports/bifrost-http/handlers/wsresponses.go calling
h.client.SelectKeyForProvider(ctx, req.Provider, req.Model)) still use the old
name; either update those callers to call SelectKeyForProviderRequestType with
the same args or add a temporary compatibility wrapper method
SelectKeyForProvider on Bifrost that accepts (ctx, providerKey, model) and
forwards to SelectKeyForProviderRequestType (preserving behavior and using
bifrost.selectKeyFromProviderForModel underneath) so the transport code keeps
compiling until the whole stack is migrated.

In `@core/providers/elevenlabs/realtime.go`:
- Around line 234-255: In ToProviderRealtimeEvent, reconstruct the full
ElevenLabs event instead of passthroughing only type or Delta: for
input_audio_buffer.append use bifrostEvent.Audio (not bifrostEvent.Delta.Audio)
and include it as the top-level "user_audio_chunk" (or appropriate key), and for
every branch (including the pong and default branches) merge
bifrostEvent.ExtraParams into the outgoing map so provider-specific events like
client_tool_result and contextual_update preserve their payload; locate symbols
ToProviderRealtimeEvent, BifrostRealtimeEvent, Audio, Delta, and ExtraParams to
update the map construction and serialization accordingly.

In `@core/providers/openai/realtime.go`:
- Around line 670-676: The code only writes delta audio from
bifrostEvent.Delta.Audio and misses the canonical top-level Audio field for
input-audio events; update the serialization logic in the block handling
bifrostEvent.Delta (in the method that also references out and bifrostEvent) to
fall back to bifrostEvent.Audio when bifrostEvent.Delta.Audio is empty (or not
set), so that canonical input-audio_buffer.append events populated by
ParseRealtimeEvent (which sets BifrostRealtimeEvent.Audio) are forwarded with
their payload.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cddaff17-7b94-4801-81c5-67d2144e341d

📥 Commits

Reviewing files that changed from the base of the PR and between 8e9f5b5 and 8c17c3b.

📒 Files selected for processing (12)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/realtime_test.go
  • core/schemas/tracer.go
✅ Files skipped from review due to trivial changes (4)
  • core/internal/llmtests/websocket_responses.go
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/bifrost.go
  • core/providers/openai/realtime_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/internal/llmtests/realtime.go

Comment thread core/providers/openai/realtime.go Outdated
Comment thread core/providers/openai/realtime.go Outdated
Comment thread core/schemas/realtime_test.go
Comment thread core/schemas/tracer.go
@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from 8c17c3b to 861b89e Compare April 3, 2026 11:07
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/providers/openai/realtime.go (1)

473-569: ⚠️ Potential issue | 🟠 Major

Switch hot-path JSON unmarshaling to sonic for per-event realtime event handling.

The ToBifrostRealtimeEvent method and its helper functions (setRealtimeExtraParam, mergeRealtimeExtraParams, extractRealtimeNestedParams) handle realtime events on a per-event basis and should use github.com/bytedance/sonic instead of encoding/json to match repo standards. These functions perform simple struct marshaling/unmarshaling with no custom logic, making them ideal candidates for sonic optimization.

♻️ Proposed refactor (targeted hot-path replacements)
 import (
 	"bytes"
 	"encoding/json"
 	"fmt"
 	"mime/multipart"
 	"net/http"
 	"net/url"
 	"strings"
 
+	"github.com/bytedance/sonic"
 	providerUtils "github.com/maximhq/bifrost/core/providers/utils"
 	"github.com/maximhq/bifrost/core/schemas"
 	"github.com/valyala/fasthttp"
 )
@@
-	if err := json.Unmarshal(providerEvent, &raw); err != nil {
+	if err := sonic.Unmarshal(providerEvent, &raw); err != nil {
 		return nil, fmt.Errorf("failed to unmarshal OpenAI realtime event: %w", err)
 	}
@@
-	raw, err := json.Marshal(value)
+	raw, err := sonic.Marshal(value)
 	if err != nil {
 		return
 	}
@@
-		if err := json.Unmarshal(raw, &value); err != nil {
+		if err := sonic.Unmarshal(raw, &value); err != nil {
 			continue
 		}
@@
-	if err := json.Unmarshal(raw, &root); err != nil {
+	if err := sonic.Unmarshal(raw, &root); err != nil {
 		return nil
 	}

Apply to lines 473–569 and 884–950 (helper functions).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 473 - 569, Replace uses of
encoding/json unmarshaling/marshaling in the realtime hot path with
bytedance/sonic equivalents: in ToBifrostRealtimeEvent change json.Unmarshal
calls (for providerEvent -> raw, raw.Session -> sess, raw.Item -> item,
raw.Error -> rtErr) to sonic.Unmarshal, and update any places that marshal
nested params to use sonic.Marshal; likewise update the helper functions
setRealtimeExtraParam, mergeRealtimeExtraParams, and extractRealtimeNestedParams
to call sonic.Unmarshal/sonic.Marshal instead of json.Marshal/json.Unmarshal.
Ensure you add the sonic import and keep the same error handling and data types
(json.RawMessage stays usable), so only the marshal/unmarshal function calls
change.
🧹 Nitpick comments (1)
core/providers/openai/realtime_test.go (1)

198-216: Exercise the canonical output-text delta mapping in these round-trip tests.

After the schema rename, these assertions still feed the provider-native "response.output_text.delta" string into ToProviderRealtimeEvent and never check event.Type on the ToBifrostRealtimeEvent side. That means the tests will still pass if the converter stops translating to or from schemas.RTEventResponseTextDelta, which is the new contract this PR introduced.

🧪 Suggested tightening
 func TestToBifrostRealtimeEventParsesModernOutputTextDelta(t *testing.T) {
@@
 	if err != nil {
 		t.Fatalf("ToBifrostRealtimeEvent() error = %v", err)
 	}
+	if event.Type != schemas.RTEventResponseTextDelta {
+		t.Fatalf("Type = %q, want %q", event.Type, schemas.RTEventResponseTextDelta)
+	}
 	if event.Delta == nil || event.Delta.Text != "hello" {
 		t.Fatalf("Delta = %+v, want text=hello", event.Delta)
 	}
 }
@@
 	out, err := provider.ToProviderRealtimeEvent(&schemas.BifrostRealtimeEvent{
-		Type: schemas.RealtimeEventType("response.output_text.delta"),
+		Type: schemas.RTEventResponseTextDelta,
 		Delta: &schemas.RealtimeDelta{
 			Text:       "hello",
 			ItemID:     "item_123",

Also applies to: 260-290

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime_test.go` around lines 198 - 216, The test
TestToBifrostRealtimeEventParsesModernOutputTextDelta currently only checks
event.Delta.Text but never asserts the canonical event type mapping; update the
test to also assert that provider.ToBifrostRealtimeEvent(...) returns an event
with event.Type == schemas.RTEventResponseTextDelta (and similarly add this
assertion in the other related tests around the 260-290 block). Ensure you keep
the existing provider input string ("response.output_text.delta") passed through
ToProviderRealtimeEvent/ToBifrostRealtimeEvent but add the explicit check of
event.Type to verify the converter translates to
schemas.RTEventResponseTextDelta.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@core/providers/openai/realtime.go`:
- Around line 473-569: Replace uses of encoding/json unmarshaling/marshaling in
the realtime hot path with bytedance/sonic equivalents: in
ToBifrostRealtimeEvent change json.Unmarshal calls (for providerEvent -> raw,
raw.Session -> sess, raw.Item -> item, raw.Error -> rtErr) to sonic.Unmarshal,
and update any places that marshal nested params to use sonic.Marshal; likewise
update the helper functions setRealtimeExtraParam, mergeRealtimeExtraParams, and
extractRealtimeNestedParams to call sonic.Unmarshal/sonic.Marshal instead of
json.Marshal/json.Unmarshal. Ensure you add the sonic import and keep the same
error handling and data types (json.RawMessage stays usable), so only the
marshal/unmarshal function calls change.

---

Nitpick comments:
In `@core/providers/openai/realtime_test.go`:
- Around line 198-216: The test
TestToBifrostRealtimeEventParsesModernOutputTextDelta currently only checks
event.Delta.Text but never asserts the canonical event type mapping; update the
test to also assert that provider.ToBifrostRealtimeEvent(...) returns an event
with event.Type == schemas.RTEventResponseTextDelta (and similarly add this
assertion in the other related tests around the 260-290 block). Ensure you keep
the existing provider input string ("response.output_text.delta") passed through
ToProviderRealtimeEvent/ToBifrostRealtimeEvent but add the explicit check of
event.Type to verify the converter translates to
schemas.RTEventResponseTextDelta.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ce7f7386-e4f2-40dc-938c-ba855ff4b65d

📥 Commits

Reviewing files that changed from the base of the PR and between 8c17c3b and 861b89e.

📒 Files selected for processing (12)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/realtime_test.go
  • core/schemas/tracer.go
✅ Files skipped from review due to trivial changes (4)
  • core/internal/llmtests/realtime.go
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/realtime_test.go
  • core/bifrost.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 3, 2026
@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from 861b89e to a48651f Compare April 6, 2026 06:17
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/providers/openai/realtime.go (1)

551-565: ⚠️ Potential issue | 🟠 Major

Map streamed delta payloads by event type instead of always treating them as text.

OpenAI's response.output_text.delta, response.output_audio.delta, and response.output_audio_transcript.delta all carry their incremental payload in the delta field. The current ingress code (lines 551–565) unconditionally stores raw.Delta into event.Delta.Text, ignoring the event type. On egress (lines 670–691), the code serializes Delta.Text as "delta", Delta.Audio as "audio", and Delta.Transcript as "transcript", so audio and transcript deltas get lost during round-trips.

Branch on raw.Type to map raw.Delta to Text, Transcript, or Audio on ingress; apply the same logic on egress so each field serializes back to its corresponding API field. Add round-trip tests for response.output_audio.delta and response.output_audio_transcript.delta to catch regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 551 - 565, The ingress
currently treats all streamed raw.Delta as text; update the mapping in the
isRealtimeDeltaEvent branch (the block that sets event.Delta in realtime.go) to
switch on raw.Type and assign raw.Delta into the correct field: set
event.Delta.Text for response.output_text.delta, event.Delta.Audio for
response.output_audio.delta, and event.Delta.Transcript for
response.output_audio_transcript.delta (preserving the existing fallback that
uses raw.Text when appropriate). Mirror this logic on egress (the serialization
code that reads event.Delta.* and writes "delta"/"audio"/"transcript") so each
Delta field round-trips to its matching API field, and add unit tests exercising
response.output_audio.delta and response.output_audio_transcript.delta to
prevent regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/bifrost.go`:
- Around line 3895-3902: The post-hook recovery path currently discards a
non-nil resp returned by pipeline.RunPostLLMHooks and always returns bifrostErr;
update the error path where newBifrostErrorFromMsg is created and
pipeline.RunPostLLMHooks is invoked so that after calling
pipeline.RunPostLLMHooks(ctx, nil, bifrostErr, preCount) you check both return
values and: if resp != nil && bifrostErr == nil return resp, nil (treat
recovered response as success); else continue to drainAndAttachPluginLogs(ctx),
flush the trace with tracer.CompleteAndFlushTrace(...), call cleanup(), and
return nil, bifrostErr. Apply the same change to the analogous helper block that
calls pipeline.RunPostLLMHooks around the other error path.
- Around line 3999-4001: shortCircuit.Error currently only sets
ExtraFields.RequestType to schemas.RealtimeRequest before calling
pipeline.RunPostLLMHooks, which leaves provider/model metadata empty; update the
branch so you copy the full ExtraFields metadata (provider, model, and
RequestType) from the post-prehook request object used earlier (the post-prehook
request/response struct) into shortCircuit.Error.ExtraFields before invoking
pipeline.RunPostLLMHooks(ctx, nil, shortCircuit.Error, preCount), ensuring the
same metadata stamping logic used by the streaming helpers is applied here.
- Around line 3898-3900: The short-circuit success branch (shortCircuit.Response
case) currently returns a no-op cleanup and skips trace flushing; mirror the
nil/error branches by extracting the trace ID from ctx via
ctx.Value(schemas.BifrostContextKeyTraceID).(string) (trim it with
strings.TrimSpace) and call tracer.CompleteAndFlushTrace(trimmedTraceID) just
before invoking cleanup() and returning, ensuring cached/short-circuit responses
flush traces/logs the same way as other paths.

In `@core/providers/openai/realtime.go`:
- Around line 130-146: The helper realtimeWebRTCUpstreamError currently always
copies the upstream body into ExtraFields.RawResponse; change it so that non-2xx
SDP/handshake responses are routed through the normal OpenAI error
parsing/enrichment path (use providerUtils.HandleProviderAPIError / provider
parse helpers) and only attach RawResponse when provider.sendBackRawResponse is
true — otherwise clear or omit RawResponse before returning the BifrostError.
Update realtimeWebRTCUpstreamError (and any call sites) to call the provider
error parsing/enrichment flow (providerUtils.EnrichError) so RawResponse is
preserved or removed based on provider.sendBackRawResponse while still setting
RequestType/Provider and StatusCode and using provider.GetProviderKey() in the
message.

In `@core/schemas/realtime.go`:
- Around line 229-310: In ParseRealtimeEvent, replace direct calls to
json.Unmarshal with the package Unmarshal wrapper so the hot path uses the
optimized JSON implementation; specifically update the unmarshal of alias, root,
savedSession/sessionRoot, savedItem/itemRoot, and savedError/errorRoot (the
json.Unmarshal invocations around variables alias, root, savedSession,
savedItem, savedError and the nested sessionRoot/itemRoot/errorRoot) to call
Unmarshal(...) instead while preserving the current error handling and nil-check
logic so ExtraParams extraction behavior remains identical.

---

Outside diff comments:
In `@core/providers/openai/realtime.go`:
- Around line 551-565: The ingress currently treats all streamed raw.Delta as
text; update the mapping in the isRealtimeDeltaEvent branch (the block that sets
event.Delta in realtime.go) to switch on raw.Type and assign raw.Delta into the
correct field: set event.Delta.Text for response.output_text.delta,
event.Delta.Audio for response.output_audio.delta, and event.Delta.Transcript
for response.output_audio_transcript.delta (preserving the existing fallback
that uses raw.Text when appropriate). Mirror this logic on egress (the
serialization code that reads event.Delta.* and writes
"delta"/"audio"/"transcript") so each Delta field round-trips to its matching
API field, and add unit tests exercising response.output_audio.delta and
response.output_audio_transcript.delta to prevent regressions.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d7d19eb-abc6-4bf8-86c2-e210e2448f04

📥 Commits

Reviewing files that changed from the base of the PR and between 861b89e and a48651f.

📒 Files selected for processing (12)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/realtime_test.go
  • core/schemas/tracer.go
✅ Files skipped from review due to trivial changes (2)
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/realtime_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/internal/llmtests/websocket_responses.go
  • core/schemas/bifrost.go
  • core/providers/elevenlabs/realtime.go

Comment thread core/bifrost.go
Comment thread core/bifrost.go
Comment thread core/bifrost.go
Comment thread core/providers/openai/realtime.go Outdated
Comment thread core/schemas/realtime.go
@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from a48651f to f39f1ed Compare April 7, 2026 07:45
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/providers/elevenlabs/realtime.go (1)

233-256: ⚠️ Potential issue | 🟠 Major

Preserve provider-specific fields when serializing ElevenLabs client events.

After the stack-wide RawData passthrough removal, the default branch now serializes only type. Events like client_tool_result and contextual_update need additional top-level fields, so this path drops their payloads and breaks provider-native round-trips.

💡 Suggested direction
 func (provider *ElevenlabsProvider) ToProviderRealtimeEvent(bifrostEvent *schemas.BifrostRealtimeEvent) (json.RawMessage, error) {
+	mergeExtraParams := func(out map[string]interface{}) {
+		for key, raw := range bifrostEvent.ExtraParams {
+			var value interface{}
+			if err := json.Unmarshal(raw, &value); err == nil {
+				out[key] = value
+			}
+		}
+	}
+
 	switch bifrostEvent.Type {
 	case schemas.RTEventInputAudioAppend:
 		if bifrostEvent.Delta == nil {
 			return nil, fmt.Errorf("delta must be set for input_audio_buffer.append events")
 		}
 		out := map[string]interface{}{
 			"type":             elUserAudioChunk,
 			"user_audio_chunk": bifrostEvent.Delta.Audio,
 		}
+		mergeExtraParams(out)
 		return schemas.MarshalSorted(out)
 
 	case schemas.RealtimeEventType("pong"):
-		return schemas.MarshalSorted(map[string]interface{}{
+		out := map[string]interface{}{
 			"type": "pong",
-		})
+		}
+		mergeExtraParams(out)
+		return schemas.MarshalSorted(out)
 
 	default:
 		out := map[string]interface{}{
 			"type": string(bifrostEvent.Type),
 		}
+		mergeExtraParams(out)
 		return schemas.MarshalSorted(out)
 	}
 }

As per coding guidelines, always see all changes in the light of the whole stack of PRs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/elevenlabs/realtime.go` around lines 233 - 256, The default
branch of ToProviderRealtimeEvent currently emits only the "type" field and
drops provider-specific payloads; update it to merge and preserve
provider-native fields from the incoming bifrostEvent (e.g., bifrostEvent.Raw /
bifrostEvent.Delta / provider-specific payload) into the out map before calling
schemas.MarshalSorted so events like client_tool_result and contextual_update
retain their top-level fields; locate the default case in
ToProviderRealtimeEvent and merge the provider payload into out (keeping the
existing "type") rather than emitting only type.
♻️ Duplicate comments (3)
core/bifrost.go (1)

3987-4020: ⚠️ Potential issue | 🟡 Minor

Stamp realtime error metadata from the post-prehook route.

Line 3987 snapshots provider and model before RunLLMPreHooks. If a pre-hook rewrites routing and then returns nil or short-circuits, Lines 3992, 4003, and 4019 still log the original route.

🔧 Suggested adjustment
-	provider, model, _ := req.GetRequestFields()
-
 	preReq, shortCircuit, preCount := pipeline.RunLLMPreHooks(ctx, req)
+	postReq := req
+	if preReq != nil {
+		postReq = preReq
+	}
+	provider, model, _ := postReq.GetRequestFields()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/bifrost.go` around lines 3987 - 4020, The code captures provider and
model before calling pipeline.RunLLMPreHooks, but when preReq is rewritten or
short-circuited the subsequent error handling (the newBifrostErrorFromMsg
branches and the short-circuit error path) still uses the original
provider/model; update those branches to extract and use the post-hook routing
metadata from the hook result (use preReq.GetRequestFields() when preReq != nil,
and the equivalent fields on shortCircuit when present) before calling
PopulateExtraFields and RunPostLLMHooks so the stamped realtime metadata
reflects the post-prehook route (update locations around RunLLMPreHooks, the nil
preReq branch, the shortCircuit.Error branch, and the shortCircuit.Response
branch to use the rewritten provider/model).
core/providers/openai/realtime.go (2)

289-297: ⚠️ Potential issue | 🟡 Minor

Use the resolved provider alias in local client-secret validation errors.

normalizeRealtimeClientSecretRequest() already resolves/falls back to the target provider, but newRealtimeClientSecretError() still stamps ExtraFields.Provider = schemas.OpenAI. For OpenAI-compatible aliases, bad-request errors from this path will be returned and logged under the wrong provider before any upstream call happens.

💡 Suggested fix
+	resolvedProvider := providerKey
+	if resolvedProvider == "" {
+		resolvedProvider = defaultProvider
+	}
 	if normalizedModel == "" {
-		return nil, "", newRealtimeClientSecretError(fasthttp.StatusBadRequest, "invalid_request_error", "session.model is required", nil)
+		return nil, "", newRealtimeClientSecretError(resolvedProvider, fasthttp.StatusBadRequest, "invalid_request_error", "session.model is required", nil)
 	}
@@
-func newRealtimeClientSecretError(status int, errorType, message string, err error) *schemas.BifrostError {
+func newRealtimeClientSecretError(providerKey schemas.ModelProvider, status int, errorType, message string, err error) *schemas.BifrostError {
 	return &schemas.BifrostError{
 		IsBifrostError: false,
 		StatusCode:     schemas.Ptr(status),
@@
 		ExtraFields: schemas.BifrostErrorExtraFields{
 			RequestType: schemas.RealtimeRequest,
-			Provider:    schemas.OpenAI,
+			Provider:    providerKey,
 		},
 	}
 }

Based on learnings, use provider.GetProviderKey() (or the resolved provider key) for error-attribution fields so custom provider aliases are preserved.

Also applies to: 400-412

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 289 - 297, The validation
path in normalizeRealtimeClientSecretRequest resolves provider aliases
(providerKey) but error-attribution still hardcodes schemas.OpenAI; update all
calls to newRealtimeClientSecretError in normalizeRealtimeClientSecretRequest
(including the similar block around 400-412) to set ExtraFields.Provider to the
resolved provider key (use providerKey or provider.GetProviderKey()) instead of
schemas.OpenAI so errors are attributed to the actual resolved provider alias.

123-125: ⚠️ Potential issue | 🟠 Major

Propagate the upstream WebRTC error instead of rewriting it to 502.

The raw-response gating fix helps, but this branch still turns every non-2xx SDP response into the same 502 upstream_connection_error. Invalid auth, bad model/session input, and rate limits all become indistinguishable to the caller, which breaks retry/auth handling on the client side.

💡 Suggested direction
-	if resp.StatusCode() < fasthttp.StatusOK || resp.StatusCode() >= fasthttp.StatusMultipleChoices {
-		return "", provider.realtimeWebRTCUpstreamError(ctx, resp.StatusCode(), answerBody)
-	}
+	if resp.StatusCode() < fasthttp.StatusOK || resp.StatusCode() >= fasthttp.StatusMultipleChoices {
+		bifrostErr := ParseOpenAIError(resp)
+		bifrostErr.ExtraFields.RequestType = schemas.RealtimeRequest
+		bifrostErr.ExtraFields.Provider = provider.GetProviderKey()
+		if !providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse) {
+			bifrostErr.ExtraFields.RawResponse = nil
+		}
+		return "", bifrostErr
+	}

Also applies to: 130-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/realtime.go` around lines 123 - 125, The branch that
currently turns any non-2xx SDP response into a generic 502 should instead
propagate the actual upstream status and body so callers can react to auth,
rate-limit, and model errors; update the non-2xx handling in the realtime SDP
flow (the checks using resp.StatusCode() and answerBody) to return an error that
includes resp.StatusCode() and answerBody (or a typed error containing those
fields) instead of forcing a 502 via provider.realtimeWebRTCUpstreamError, and
apply the same change to the other non-2xx branches in the same function (the
130-149 region). If realtimeWebRTCUpstreamError currently hardcodes 502, change
it to accept and use the upstream status/body (or introduce a new upstreamError
type) so the exact upstream status and message are preserved in the returned
error.
🧹 Nitpick comments (2)
core/schemas/realtime.go (2)

264-301: Reduce key-stripping duplication to avoid future drift.

Known-field deletion is repeated in multiple blocks. Extracting a small helper + shared key sets would make this easier to maintain when schema fields evolve.

♻️ Suggested refactor sketch
+func deleteKnownKeys(m map[string]json.RawMessage, keys []string) {
+	for _, k := range keys {
+		delete(m, k)
+	}
+}
...
- for _, key := range []string{"type", "event_id", "session", "item", "delta", "audio", "error", "raw_data"} {
- 	delete(root, key)
- }
+deleteKnownKeys(root, []string{"type", "event_id", "session", "item", "delta", "audio", "error", "raw_data"})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/schemas/realtime.go` around lines 264 - 301, Refactor the repeated
known-field deletion logic in core/schemas/realtime.go by extracting a small
helper (e.g., a function like stripKnownKeys(root map[string]json.RawMessage,
keys []string)) and centralized key sets (e.g., sessionKeys, itemKeys,
errorKeys, rootKeys) so each block can call stripKnownKeys(root, rootKeys)
instead of duplicating the delete loop; update the blocks that currently operate
on root, sessionRoot, itemRoot, and errorRoot (and references like
event.ExtraParams, event.Session.ExtraParams, event.Item.ExtraParams,
savedSession, savedItem, savedError and Unmarshal) to use the helper and shared
slices to keep behavior identical while removing duplication.

67-83: Clarify “finalized” wording (or tighten predicate).

These helpers currently accept any conversation-item event type (including non-terminal ones), so the “finalized” wording is misleading. Consider updating comments or constraining to terminal event(s).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/schemas/realtime.go` around lines 67 - 83, The comments for
IsRealtimeUserInputEvent and IsRealtimeToolOutputEvent incorrectly say they
detect “finalized” items while the predicates accept any conversation-item event
type; either update the comments to remove “finalized” wording or tighten the
predicates to only accept terminal conversation-item event types (e.g., check
event.Type equals the concrete terminal type(s) your schema uses) by adding an
explicit comparison in IsRealtimeUserInputEvent and IsRealtimeToolOutputEvent
(or introduce a helper like IsRealtimeConversationItemTerminalEvent and call it
from both) so the behavior and documentation match; reference the functions
IsRealtimeUserInputEvent, IsRealtimeToolOutputEvent, and
IsRealtimeConversationItemEventType when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/bifrost.go`:
- Around line 3961-3988: RunRealtimeTurnPreHooks currently dereferences req
(calls req.GetRequestFields) without checking for nil; add an early guard at the
top of the function that returns nil and a properly constructed
*schemas.BifrostError when req == nil (mirroring the pattern used by other
public entry points in this package), so callers receive a controlled error
instead of a panic; update RunRealtimeTurnPreHooks to check req == nil before
using req and return the same error-construction pattern used elsewhere in the
codebase.

---

Outside diff comments:
In `@core/providers/elevenlabs/realtime.go`:
- Around line 233-256: The default branch of ToProviderRealtimeEvent currently
emits only the "type" field and drops provider-specific payloads; update it to
merge and preserve provider-native fields from the incoming bifrostEvent (e.g.,
bifrostEvent.Raw / bifrostEvent.Delta / provider-specific payload) into the out
map before calling schemas.MarshalSorted so events like client_tool_result and
contextual_update retain their top-level fields; locate the default case in
ToProviderRealtimeEvent and merge the provider payload into out (keeping the
existing "type") rather than emitting only type.

---

Duplicate comments:
In `@core/bifrost.go`:
- Around line 3987-4020: The code captures provider and model before calling
pipeline.RunLLMPreHooks, but when preReq is rewritten or short-circuited the
subsequent error handling (the newBifrostErrorFromMsg branches and the
short-circuit error path) still uses the original provider/model; update those
branches to extract and use the post-hook routing metadata from the hook result
(use preReq.GetRequestFields() when preReq != nil, and the equivalent fields on
shortCircuit when present) before calling PopulateExtraFields and
RunPostLLMHooks so the stamped realtime metadata reflects the post-prehook route
(update locations around RunLLMPreHooks, the nil preReq branch, the
shortCircuit.Error branch, and the shortCircuit.Response branch to use the
rewritten provider/model).

In `@core/providers/openai/realtime.go`:
- Around line 289-297: The validation path in
normalizeRealtimeClientSecretRequest resolves provider aliases (providerKey) but
error-attribution still hardcodes schemas.OpenAI; update all calls to
newRealtimeClientSecretError in normalizeRealtimeClientSecretRequest (including
the similar block around 400-412) to set ExtraFields.Provider to the resolved
provider key (use providerKey or provider.GetProviderKey()) instead of
schemas.OpenAI so errors are attributed to the actual resolved provider alias.
- Around line 123-125: The branch that currently turns any non-2xx SDP response
into a generic 502 should instead propagate the actual upstream status and body
so callers can react to auth, rate-limit, and model errors; update the non-2xx
handling in the realtime SDP flow (the checks using resp.StatusCode() and
answerBody) to return an error that includes resp.StatusCode() and answerBody
(or a typed error containing those fields) instead of forcing a 502 via
provider.realtimeWebRTCUpstreamError, and apply the same change to the other
non-2xx branches in the same function (the 130-149 region). If
realtimeWebRTCUpstreamError currently hardcodes 502, change it to accept and use
the upstream status/body (or introduce a new upstreamError type) so the exact
upstream status and message are preserved in the returned error.

---

Nitpick comments:
In `@core/schemas/realtime.go`:
- Around line 264-301: Refactor the repeated known-field deletion logic in
core/schemas/realtime.go by extracting a small helper (e.g., a function like
stripKnownKeys(root map[string]json.RawMessage, keys []string)) and centralized
key sets (e.g., sessionKeys, itemKeys, errorKeys, rootKeys) so each block can
call stripKnownKeys(root, rootKeys) instead of duplicating the delete loop;
update the blocks that currently operate on root, sessionRoot, itemRoot, and
errorRoot (and references like event.ExtraParams, event.Session.ExtraParams,
event.Item.ExtraParams, savedSession, savedItem, savedError and Unmarshal) to
use the helper and shared slices to keep behavior identical while removing
duplication.
- Around line 67-83: The comments for IsRealtimeUserInputEvent and
IsRealtimeToolOutputEvent incorrectly say they detect “finalized” items while
the predicates accept any conversation-item event type; either update the
comments to remove “finalized” wording or tighten the predicates to only accept
terminal conversation-item event types (e.g., check event.Type equals the
concrete terminal type(s) your schema uses) by adding an explicit comparison in
IsRealtimeUserInputEvent and IsRealtimeToolOutputEvent (or introduce a helper
like IsRealtimeConversationItemTerminalEvent and call it from both) so the
behavior and documentation match; reference the functions
IsRealtimeUserInputEvent, IsRealtimeToolOutputEvent, and
IsRealtimeConversationItemEventType when making 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d4732f3-82ac-4a4f-ad1b-7ad671b7c3e8

📥 Commits

Reviewing files that changed from the base of the PR and between a48651f and f39f1ed.

📒 Files selected for processing (12)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/realtime_test.go
  • core/schemas/tracer.go
✅ Files skipped from review due to trivial changes (4)
  • core/internal/llmtests/websocket_responses.go
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • core/internal/llmtests/realtime.go
  • core/schemas/realtime_client_secrets.go

Comment thread core/bifrost.go
@danpiths danpiths force-pushed the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch from f39f1ed to 217a6b5 Compare April 7, 2026 09:09
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/providers/elevenlabs/realtime.go`:
- Around line 47-53: Update ExchangeRealtimeWebRTCSDP to build the error message
using the provider key rather than hardcoding "ElevenLabs": call
provider.GetProviderKey(), convert it to lowercase (e.g., via strings.ToLower)
and interpolate it into the ErrorField.Message so the returned message reads
like "webrtc sdp exchange is not yet implemented for <providerkey>" using the
dynamic provider key; ensure the function still returns the same BifrostError
shape and add the strings import if needed.

In `@core/providers/openai/realtime.go`:
- Around line 47-59: In ExchangeRealtimeWebRTCSDP, the code only appends the
model query when session == nil, which drops provider/model for requests with a
non-nil session; change the logic to always append the model query when model is
non-empty (e.g., if strings.TrimSpace(model) != "" { path += "?model=" +
url.QueryEscape(model) }) so the path includes provider/model regardless of
session presence before calling provider.exchangeWebRTCSDP; keep using the same
path variable and exchangeWebRTCSDP call.
- Around line 323-329: The code only backfills session["type"] when missing, but
OpenAI requires session.type to be exactly "realtime"; always overwrite any
caller-supplied value by marshaling "realtime" and assigning it to
session["type"] unconditionally (replace the conditional branch that checks for
the key), handling marshal errors the same way (returning
newRealtimeClientSecretError on marshalErr) so the session map used by the
realtime client secrets endpoint is guaranteed to contain the JSON-encoded
"realtime" string.
- Around line 62-71: The current ExchangeLegacyRealtimeWebRTCSDP incorrectly
reuses exchangeWebRTCSDP which sends multipart/form-data; create a new helper
named exchangeLegacyWebRTCSDP that posts the raw SDP string as the request body,
sets Content-Type: application/sdp, adds the header "OpenAI-Beta: realtime=v1",
and otherwise mirrors auth/timeout/error handling used by exchangeWebRTCSDP;
then change ExchangeLegacyRealtimeWebRTCSDP to call exchangeLegacyWebRTCSDP(ctx,
key, "/v1/realtime?model="+url.QueryEscape(model), sdp, session) so the legacy
path uses the raw SDP + beta header while the GA path still uses
exchangeWebRTCSDP.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b90e9af-1c22-4ebb-8927-bd10bf97b3f1

📥 Commits

Reviewing files that changed from the base of the PR and between f39f1ed and 217a6b5.

📒 Files selected for processing (12)
  • core/bifrost.go
  • core/internal/llmtests/realtime.go
  • core/internal/llmtests/websocket_responses.go
  • core/providers/elevenlabs/realtime.go
  • core/providers/openai/realtime.go
  • core/providers/openai/realtime_test.go
  • core/schemas/bifrost.go
  • core/schemas/realtime.go
  • core/schemas/realtime_client_secrets.go
  • core/schemas/realtime_client_secrets_test.go
  • core/schemas/realtime_test.go
  • core/schemas/tracer.go
✅ Files skipped from review due to trivial changes (3)
  • core/schemas/realtime_test.go
  • core/schemas/realtime_client_secrets_test.go
  • core/bifrost.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • core/internal/llmtests/realtime.go
  • core/schemas/realtime.go

Comment thread core/providers/elevenlabs/realtime.go
Comment thread core/providers/openai/realtime.go
Comment thread core/providers/openai/realtime.go
Comment thread core/providers/openai/realtime.go
Copy link
Copy Markdown
Contributor

akshaydeo commented Apr 7, 2026

Merge activity

  • Apr 7, 2:56 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 7, 2:57 PM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo akshaydeo merged commit aa27216 into v1.5.0 Apr 7, 2026
22 of 23 checks passed
@akshaydeo akshaydeo deleted the feature/03-27-feat_add_realtime_provider_interfaces_schemas_and_engine_hooks branch April 7, 2026 14:57
@coderabbitai coderabbitai Bot mentioned this pull request Apr 17, 2026
18 tasks
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.

3 participants