Skip to content

fix(cartesia): redact API keys from websocket handshake errors - #2248

Open
rosetta-livekit-bot[bot] wants to merge 2 commits into
mainfrom
archaism-tubby-hyphen
Open

fix(cartesia): redact API keys from websocket handshake errors#2248
rosetta-livekit-bot[bot] wants to merge 2 commits into
mainfrom
archaism-tubby-hyphen

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Test plan

  • pnpm test agents (1587 passed, 5 skipped)
  • pnpm test plugins/cartesia (9 passed, 2 skipped)
  • pnpm --filter @livekit/agents build
  • pnpm --filter @livekit/agents lint (passes with pre-existing warnings)
  • pnpm --filter @livekit/agents-plugin-cartesia build
  • pnpm --filter @livekit/agents-plugin-cartesia lint
Source diff coverage
  • Adapted: livekit-agents/livekit/agents/utils/connection_pool.py -> agents/src/connection_pool.ts. agents-js already swallows prewarm rejection with .catch(() => {}) and logs no exception content, so only the concise credential-safety comment from the follow-up source change was needed.
  • Adapted: livekit-plugins/livekit-plugins-cartesia/livekit/plugins/cartesia/_recognize_streams/auto_finalize_recognize_stream.py -> plugins/cartesia/src/stt.ts. The sole agents-js Cartesia STT stream now maps HTTP handshake rejection to APIStatusError, applies its configured handshake timeout, replaces transport error content with a safe error type, and includes the follow-up's concise credential-safety comments.
  • Not applicable: livekit-plugins/livekit-plugins-cartesia/livekit/plugins/cartesia/_recognize_streams/legacy_recognize_stream.py. agents-js has no legacy Cartesia STT stream or legacy endpoint counterpart.
  • Adapted: livekit-plugins/livekit-plugins-cartesia/livekit/plugins/cartesia/tts.py -> plugins/cartesia/src/tts.ts. Handshake status, timeout, and transport failures are converted to sanitized framework errors with the follow-up's concise credential-safety comments; agents-js already rethrows APIError before generic receive-error handling.
  • Adapted: tests/test_connection_pool.py -> agents/src/connection_pool.test.ts. Node unhandled-rejection assertions replace Python task repr and caplog assertions for both header and URL credential failures.
  • Adapted: tests/test_plugin_cartesia_tts.py -> plugins/cartesia/src/tts.test.ts. Public stream tests use a local rejected WebSocket upgrade and a credential-bearing invalid URL to verify status mapping, sanitized error text, and absence of a JS error cause.

Sources: livekit/agents#6740 and comment updates from livekit/agents#6759

@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from a team as a code owner August 9, 2026 16:28
@changeset-bot

changeset-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d6d1999

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

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

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: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

Copy link
Copy Markdown
Member

Port verification: livekit/agents#6740 + #6759 → this PR

Compared this PR's diff against both merged Python sources (utils/connection_pool.py, both Cartesia STT recognize-stream variants, cartesia/tts.py, and stt/stt.py's retry loop) line-by-line.

Verdict: Correct and careful redaction port. No confirmed correctness regressions in the error-wrapping/redaction logic itself. One real, if minor, parity gap: the ported ConnectionPool.prewarm() doesn't reproduce the observability half of the source fix. Labeled effort:medium — mechanical in most places, but required cross-referencing two source PRs and tracing the new APIStatusError/retry interaction to confirm no regression.

Full analysis

What #6759 actually changed (important context)

livekit/agents#6759 isn't just a comment-trim PR — it also fixes stt/stt.py's retry loop (if not e.retryable or max_retries == 0) so a non-retryable APIStatusError (e.g. a 401) fails immediately instead of being retried max_retries times. This PR's description states it only ports "the concise credential-safety comment updates from #6759," which reads as if the whole PR were comment-only.

I verified this is not a missed behavioral port: agents/src/stt/stt.ts's SpeechStream.mainTask already has if (this._connOptions.maxRetry === 0 || !error.retryable) as pre-existing infrastructure, and APIStatusError's constructor already auto-sets retryable=false for 4xx codes outside [408, 429, 499]. So the Cartesia STT's new onUnexpectedResponseAPIStatusError correctly fails fast today without any additional change — the target already had the equivalent guard. Good catch by the porting agent even if the PR description undersells it.

connection_pool.ts — confirmed gap

Python's fix wraps the prewarm's swallowed exception with a new structured warning log:

logger.warning("failed to prewarm connection pool", extra={"exception_type": type(e).__name__})

This is a deliberate design choice in the source: log something (just the exception class name, never str(e)/repr(e)) so failed prewarms are diagnosable without leaking credentials.

The JS port only changes the comment on the existing empty catch:

this._prewarmImpl(controller.signal).catch(() => {
  // Exception details can contain request headers or URL credentials.
});

No logger.warn(...) call was added. The PR description frames this as "preserve the existing safe behavior," but that undersells the source's actual change — Python didn't merely make the log safe, it added a log where none existed. The JS port keeps prewarm failures completely silent, so a Cartesia TTS/STT prewarm that fails repeatedly (bad key, network issue) now produces zero operator-visible signal in agents-js, whereas the Python target logs a redacted warning every time. Confirmed by reading the full connection_pool.ts: there is no other logger call anywhere near prewarm()/_prewarmImpl().

This isn't a correctness regression (nothing behaves differently from before this PR), but it is an incomplete port of the source's intent, and worth a maintainer call on whether to add the equivalent log().warn(...) before merge.

Cartesia TTS (tts.ts) — verified correct

  • waitForWsOpen's new unexpected-response handler rejects with APIStatusError carrying only a synthesized message (Cartesia WebSocket connection rejected with status ${statusCode}) — never the raw handshake response/headers. Matches Python's aiohttp.ClientResponseErrorAPIStatusError(message=e.message, ...) branch (Python re-uses e.message, which for ClientResponseError is just the HTTP reason phrase, not headers — so both are safe).
  • The explicit handshakeTimeout timer now rejects with APITimeoutError (a typed APIError) instead of a bare Error('connect timeout'), so it flows through connectCartesiaWebSocket's connectError instanceof APIError fast path unmodified — parity with Python's except asyncio.TimeoutError: raise APITimeoutError().
  • The generic catch-all now checks !(e instanceof APIError) before attempting the pre-existing IPv4 happy-eyeballs retry, so a rejected/timed-out APIError is never retried at this layer and never had its message replaced — correct, and a reasonable target-specific addition (the retry mechanism itself predates this PR and has no Python analog).
  • Final fallback throws APIConnectionError({ message: sanitizedErrorName(error) }) — class name only, never the original message — matching Python's APIConnectionError(type(e).__name__). Confirmed via the two new tts.test.ts cases that assert the secret never appears in error.message/error.toString() and that .cause is undefined (no chaining), mirroring Python's from None and the new test_plugin_cartesia_tts.py assertions.

Cartesia STT (stt.ts) — verified correct, structurally different from TTS but equivalent

  • Same unexpected-responseAPIStatusError, generic errorAPIConnectionError({message: sanitizedErrorName(err), retryable: true}) pattern, applied directly in the connect promise rather than through a shared helper (STT doesn't have TTS's happy-eyeballs retry wrapper, which predates this PR and is TTS-only in both languages).
  • Note the onUnexpectedResponse handler additionally does ws.on('error', () => {}); ws.close(); before rejecting — TTS's equivalent leaves cleanup to the outer connectOnce/safeTerminateWebSocket. Different mechanics, same end state (socket torn down, no unhandled error). Not a bug.
  • No dedicated STT redaction test was added (only tts.test.ts got new cases) — but this matches the source: livekit/agents#6740/#6759 also only added test_plugin_cartesia_tts.py (TTS-only), no STT-specific test. Not a port-introduced gap.

Not independently re-verified

I did not re-run the test suites myself; I relied on the PR's own reported results (pnpm test agents / pnpm test plugins/cartesia passing) plus static tracing of the redaction and retry logic against both source PRs.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant