Skip to content

fix(gradium): identify the STT plugin to the API with x-api-source - #2750

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
port/gradium-api-source
Open

rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
port/gradium-api-source

Conversation

@rosetta-livekit-bot

Copy link
Copy Markdown
Contributor

Summary

  • add the missing Gradium streaming STT plugin infrastructure to agents-js
  • identify Gradium STT websocket sessions with x-api-source: livekit-plugin
  • centralize Gradium websocket authentication and source-attribution headers

The target did not have a Gradium plugin counterpart, so this PR includes the package, streaming STT transport, workspace registration, API report, and release changeset required to preserve the source behavior. No source-dependent behavior was dropped.

Source diff coverage
  • Adapted livekit-plugins/livekit-plugins-gradium/livekit/plugins/gradium/api.py to plugins/gradium/src/api.ts: preserves the shared API source constant and websocket header helper in TypeScript.
  • Adapted livekit-plugins/livekit-plugins-gradium/livekit/plugins/gradium/stt.py to plugins/gradium/src/stt.ts: the target had no Gradium counterpart, so the Gradium streaming STT infrastructure was added and its websocket connection delegates headers to the shared helper. Python seconds-based buffering is exposed as target-conventional bufferSizeMs.
  • Adapted tests/test_plugin_gradium_api_source.py to plugins/gradium/src/api_source.test.ts: uses a local Vitest websocket server, pins livekit-plugin rather than importing it, and asserts both x-api-key and x-api-source upgrade headers.

Testing

  • pnpm test plugins/gradium
  • pnpm --filter @livekit/agents-plugin-gradium typecheck
  • pnpm --filter @livekit/agents-plugin-gradium build
  • pnpm --filter @livekit/agents-plugin-gradium api:check
  • pnpm build
  • pnpm lint (passes with existing repository warnings)
  • pnpm format:check

Source: livekit/agents#7873

@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from a team as a code owner October 16, 2026 20:15
@changeset-bot

changeset-bot Bot commented Oct 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6e94213

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 42 packages
Name Type
@livekit/agents-plugin-gradium Minor
@livekit/agents Minor
@livekit/agents-plugin-anam Minor
@livekit/agents-plugin-anthropic Minor
@livekit/agents-plugin-assemblyai Minor
@livekit/agents-plugin-azure Minor
@livekit/agents-plugin-baseten Minor
@livekit/agents-plugin-bey Minor
@livekit/agents-plugin-cartesia Minor
@livekit/agents-plugin-cerebras Minor
@livekit/agents-plugin-deepgram Minor
@livekit/agents-plugin-did Minor
@livekit/agents-plugin-elevenlabs Minor
@livekit/agents-plugin-fishaudio Minor
@livekit/agents-plugin-google Minor
@livekit/agents-plugin-hume Minor
@livekit/agents-plugin-inworld Minor
@livekit/agents-plugin-krisp Minor
@livekit/agents-plugin-lemonslice Minor
@livekit/agents-plugin-liveavatar Minor
@livekit/agents-plugin-livekit Minor
@livekit/agents-plugin-meta Minor
@livekit/agents-plugin-minimax Minor
@livekit/agents-plugin-mistral Minor
@livekit/agents-plugin-mistralai Minor
@livekit/agents-plugin-neuphonic Minor
@livekit/agents-plugin-openai Minor
@livekit/agents-plugin-perplexity Minor
@livekit/agents-plugin-phonic Minor
@livekit/agents-plugin-protoface Minor
@livekit/agents-plugin-resemble Minor
@livekit/agents-plugin-rime Minor
@livekit/agents-plugin-runway Minor
@livekit/agents-plugin-sarvam Minor
@livekit/agents-plugin-silero Minor
@livekit/agents-plugin-soniox Minor
@livekit/agents-plugin-speechify Minor
@livekit/agents-plugin-synthesia Minor
@livekit/agents-plugin-tavus Minor
@livekit/agents-plugins-test Minor
@livekit/agents-plugin-trugen Minor
@livekit/agents-plugin-xai Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@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 4 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +286 to +287
async #sendAudio(ws: WebSocket, signal: AbortSignal): Promise<void> {
const audio = new AudioByteStream(this.#opts.sampleRate, 1, DEFAULT_FRAME_SIZE);

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.

🔴 Buffered speech disappears on reconnect

When a socket reconnects, #sendAudio discards audio consumed but not yet assembled into a full frame. Speech crossing that boundary loses its first buffered samples.

Learn more

AudioByteStream collects input until it has 1920 samples. The base SpeechStream retries run() with the same input queue, but this method constructs a new buffer for each attempt. Audio consumed into an incomplete frame cannot be replayed on the new socket.

Example: A 40 ms audio frame enters the stream, then the socket closes. The frame stays in the old 80 ms byte buffer; after reconnect, the next 40 ms starts a new buffer instead of completing the original frame.

Recommended fix: Keep the byte buffer across connection attempts, or requeue untransmitted samples before retrying. Validate behavior with a disconnect after a partial chunk.

Devin Review


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

if (signal.aborted) return;
throw error;
}
if (result.done) return;

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.

🔴 Trailing speech lost at input completion

When endInput() closes the input queue, #sendAudio returns without flushing its partial frame. The provider never receives speech shorter than the remaining 80 ms buffer.

Suggested change
if (result.done) return;
if (result.done) {
for (const frame of audio.flush()) this.#sendFrame(ws, frame.data);
return;
}

Devin Review


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

Comment on lines +178 to +189
// Input completion does not end a Gradium session. Keep receiving final
// provider events, while still propagating a sender failure immediately.
const senderFailure = sender.then(
() => new Promise<never>(() => {}),
(error: unknown) => Promise.reject(error),
);
await Promise.race([receiver, senderFailure]);
if (!this.abortSignal.aborted) {
throw new APIConnectionError({
message: 'Gradium connection closed unexpectedly',
options: { retryable: true },
});

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.

🔴 Completed speech streams never settle normally

After endInput(), run keeps waiting for the provider to close its socket. It treats that close as failure, so consumers wait indefinitely or receive a retry error.

Learn more

The base endInput closes only the audio input queue. #sendAudio returns, but senderFailure deliberately never settles on success. When the provider closes after processing the last audio, the receiver resolves and run() throws a retryable error instead of completing. A provider that keeps connections open leaves the stream pending forever.

Example: A caller pushes one utterance, calls endInput(), and iterates the stream to completion. Even after the provider finishes, iteration hangs if its socket stays open, or ends with a connection error if it closes.

Recommended fix: Define an input-completion protocol: send the provider's end-of-audio signal, drain its final responses, and return successfully on the corresponding completion event or expected close.

Devin Review


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

Comment on lines +162 to +164
this.#speaking = false;
this.#bufferedText = [];
this.#remainingVadSteps = undefined;

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.

🔴 Active transcripts vanish on socket loss

If a socket closes before VAD finalization, run clears #bufferedText during retry. Interim speech never becomes a final transcript, so the agent loses that utterance.

Learn more

The text handler emits only interim events and keeps words in #bufferedText. Final events require enough inactivity step messages. A network drop before that threshold restarts run(), clearing that state and preventing the partial utterance from ever becoming final.

Example: Gradium sends text: "hello" and then drops the connection before sending a VAD step. The stream retries and clears hello; the agent never receives a final hello transcript.

Recommended fix: Finalize or preserve pending transcript state before reconnecting, with an end-of-speech boundary when appropriate.

Devin Review


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

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.

0 participants