From b32c4efe04b1eb15f5c0261c975a2a70264dd353 Mon Sep 17 00:00:00 2001 From: Eugenio Zuccarelli <11176606+jayzuccarelli@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:09:57 +0000 Subject: [PATCH] broker: REASONING_EFFORT knob for reasoning-line Realtime models gpt-realtime-2.1 is a reasoning-line model; its server default effort (low) pads spoken replies and adds latency. Pipecat 0.0.97's SessionProperties has no reasoning field and pydantic serializes by declared type, so the broker injects reasoning.effort into the session.update wire dict when REASONING_EFFORT is set. Unset = field not sent (required for non-reasoning models like gpt-realtime); zero behavior change by default. Benched 2026-07-21 (isolated 8766/8767, hygiene off, paired same-hour runs): minimal effort fixes 2.1's verbosity (one-liner replies) but first-audio p50 stays 500-680ms vs 97-292ms on live gpt-realtime, so the live model keeps MODEL=gpt-realtime for now. Part of JAY-92. Co-Authored-By: Claude Fable 5 --- README.md | 1 + broker/realtime_broker/agent.py | 19 +++++++++++++++++++ broker/realtime_broker/config.py | 6 ++++++ 3 files changed, 26 insertions(+) diff --git a/README.md b/README.md index 7a853d2..bcab1f0 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ The broker fetches HA's tools at startup and registers them on the Realtime sess |---|---|---| | `OPENAI_API_KEY` | — | required | | `MODEL` | `gpt-realtime` | Realtime model | +| `REASONING_EFFORT` | unset | reasoning effort for reasoning-line models (`gpt-realtime-2.1`+): `minimal`/`low`/`medium`/`high`/`xhigh`; leave unset for non-reasoning models | | `VOICE` | `marin` | Realtime voice | | `INSTRUCTIONS` | generic | system prompt / persona | | `WS_HOST` / `WS_PORT` | `0.0.0.0` / `8765` | where the device connects | diff --git a/broker/realtime_broker/agent.py b/broker/realtime_broker/agent.py index 93babd7..00fb282 100644 --- a/broker/realtime_broker/agent.py +++ b/broker/realtime_broker/agent.py @@ -16,8 +16,10 @@ AudioConfiguration, AudioInput, AudioOutput, + ClientEvent, InputAudioTranscription, SessionProperties, + SessionUpdateEvent, TurnDetection, ) from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMService @@ -42,11 +44,27 @@ class VoicePERealtimeService(OpenAIRealtimeLLMService): its own response.create). """ + def __init__(self, *args, reasoning_effort: str | None = None, **kwargs): + super().__init__(*args, **kwargs) + self._reasoning_effort = reasoning_effort + async def _handle_context(self, context: LLMContext) -> None: self._context = context self._llm_needs_conversation_setup = False await self._process_completed_function_calls(send_new_results=True) + async def send_client_event(self, event: ClientEvent) -> None: + # Pipecat 0.0.97's SessionProperties has no `reasoning` field and + # pydantic serializes by declared type, so a subclass field would be + # dropped — inject into the wire dict instead. Applies to every + # session.update (initial setup and mid-session). + if self._reasoning_effort and isinstance(event, SessionUpdateEvent): + dump = event.model_dump(exclude_none=True) + dump["session"]["reasoning"] = {"effort": self._reasoning_effort} + await self._ws_send(dump) + return + await super().send_client_event(event) + # Custom broker tools, registered with handlers by the server. CUSTOM_TOOLS = [ { @@ -206,6 +224,7 @@ async def build_agent(config: Config, mcp: MCPClient | None) -> OpenAIRealtimeLL model=config.model, session_properties=session, start_audio_paused=False, + reasoning_effort=config.reasoning_effort, ) if mcp is not None and tools_schema is not None: diff --git a/broker/realtime_broker/config.py b/broker/realtime_broker/config.py index 2c55e08..ddaf96d 100644 --- a/broker/realtime_broker/config.py +++ b/broker/realtime_broker/config.py @@ -14,6 +14,11 @@ class Config: model: str = "gpt-realtime" voice: str = "marin" instructions: str = "You are a helpful voice assistant." + # Reasoning effort for reasoning-line Realtime models (gpt-realtime-2.1+): + # minimal/low/medium/high/xhigh. The server default (low) makes the model + # deliberate and pad its spoken replies; "minimal" suits command-and-control. + # None = field not sent, required for non-reasoning models (gpt-realtime). + reasoning_effort: str | None = None ws_host: str = "0.0.0.0" ws_port: int = 8765 @@ -80,6 +85,7 @@ def from_env(cls) -> "Config": openai_api_key=api_key, model=os.environ.get("MODEL", "gpt-realtime"), voice=os.environ.get("VOICE", "marin"), + reasoning_effort=os.environ.get("REASONING_EFFORT") or None, instructions=os.environ.get("INSTRUCTIONS", cls.instructions), ws_host=os.environ.get("WS_HOST", "0.0.0.0"), ws_port=int(os.environ.get("WS_PORT", "8765")),