Skip to content

feat: add StepFun Realtime plugin - #7352

Open
yaosongding wants to merge 7 commits into
livekit:mainfrom
yaosongding:feat/stepfun-realtime-plugin
Open

yaosongding wants to merge 7 commits into
livekit:mainfrom
yaosongding:feat/stepfun-realtime-plugin

Conversation

@yaosongding

@yaosongding yaosongding commented Sep 19, 2026

Copy link
Copy Markdown

Summary

Adds the StepFun (阶跃星辰) Realtime Speech-to-Speech plugin (livekit-plugins-stepfun) supporting StepAudio full-duplex speech models.

Key Features & Protocol Adaptations

  • Supported Models: stepaudio-3-realtime-preview (duplex reasoning model), stepaudio-2.5-realtime (production low-latency S2S), and step-1o-audio.
  • Dual Clusters & Voice Normalization: Automatic bidirectional voice mapping between international (api.stepfun.ai) and domestic (api.stepfun.com) endpoints.
  • Provider Tools: Cloud-native stepfun.tools.WebSearch and stepfun.tools.Retrieval (ProviderTool subclasses) integrated with session.update.
  • Function Calling Compatibility:
    • Normalizes tool declarations to StepFun's required nested function schema ({"type": "function", "function": {...}}).
    • Overrides update_tools to inspect nested function names (t["function"]["name"]), preserving local tools in self._tools across updates and reconnects.
    • Filters redundant client-side function_call items in send_event to prevent 400 Bad Request (item.type must be message or function_call_output).
    • Maps server-assigned UUIDs in conversation.item.created to pending client futures, eliminating update_chat_ctx delays.
  • Omission Semantics for Partial Updates: _wrap_session_update preserves field omissions (e.g. update_instructions never erases configured tools).
  • Acoustic Echo Gate: Built-in echo_gate_threshold to suppress laptop speaker acoustic bleed during active speech while allowing intentional user interruptions.
  • PII Redaction: Redacts thinking traces and provider errors via extra={"lk.pii.*"} attributes to adhere to LiveKit log sanitization guidelines.

Installation

pip install livekit-plugins-stepfun

Validation

  • Unit Tests: 26 passed in 0.12s (uv run pytest tests/test_plugin_stepfun_realtime.py)
  • Linting: Passed (uv run ruff check livekit-plugins/livekit-plugins-stepfun tests/test_plugin_stepfun_realtime.py)
  • Type Checking: Passed (uv run mypy -p livekit.plugins.stepfun, 7 source files checked)
  • Live E2E Verification: Full-duplex conversation, ServerVAD, WebSearch, and local @function_tool execution validated on local WebRTC meet session.

@yaosongding
yaosongding requested a review from a team as a code owner September 19, 2026 09:59
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

5 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Pending replies survive interruption

Calling interrupt() before response.created leaves the queued response undiscarded. StepFun can ignore the early cancel, so the interrupted reply later becomes live.

(Refers to this code)

Learn more

A pending generation is a response.create whose response.created acknowledgement has not arrived. The base interrupt sends response.cancel whenever a response-created future exists, but it does not cancel that future or add its event ID to _discarded_event_ids. StepFun can reject that cancel because no response exists yet. The removed state was the only path that remembered this race and discarded the eventual response.

Example: generate_reply() queues event A, then session.interrupt() runs before StepFun acknowledges A. StepFun returns “no ongoing response to cancel” and later sends response.created for A. The session now resolves A as a live generation, although the caller interrupted it.

Recommended fix: Preserve cancellation state for pending creates, or explicitly add the appropriate pending event ID to _discarded_event_ids when interrupt() targets an unacknowledged generation. Keep active-generation cancellation from marking unrelated queued replies.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@yaosongding

Copy link
Copy Markdown
Author

Human speaking. I'm also reviewing these comments/updates simultaneously, I think I need more time.

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.

1 participant