feat(volcengine): support v3 TTS (ws bidir/uni, http chunked/sse) - #4710
feat(volcengine): support v3 TTS (ws bidir/uni, http chunked/sse)#4710rangerTao wants to merge 4 commits into
Conversation
Adds Volcengine v3 TTS support alongside the existing v1 ws_binary path. Channels can now select one of four v3 transports per-channel (or per-request via OpenAI metadata), with the appropriate auth header layout (new-console X-Api-Key vs legacy X-Api-App-Id + X-Api-Access-Key) and resource id (seed-tts-2.0 / seed-icl-2.0 / etc.). Backend: - protocols.go: align readers/writers ordering, add EventClientRequest helper for v3 event-tagged frames, fix writeSessionID symmetry for ConnectionFinished. - tts_v3_ws.go: implement bidirectional + unidirectional WS state machines (StartConnection -> StartSession -> TaskRequest? -> FinishSession), parse SessionFinished usage.text_words. - tts_v3_http.go: implement HTTP Chunked frame splitter (binary frames) and HTTP SSE passthrough (raw text/event-stream forwarded to client; usage side-parsed for billing). - adaptor.go: dispatch by resolveVolcTTSConfig(info) covering the four v3 endpoints + retain v1 default. Metadata override via volc_tts_*. - dto/channel_settings.go: add VolcTTSConfig (protocol/resource_id/auth_mode/ require_usage). No DDL — JSON-in-text on existing settings column. - relay/common/relay_info.go: VolcTTSOverride field for per-request override. - service/log_info_generate.go: log volc_tts_protocol / volc_resource_id. Frontend (both themes): - web/default/ (Base UI): Select / Input / Switch controls in channel-mutate-drawer; channel-form schema/defaults/parse/serialize. - web/classic/ (Semi UI): same four fields in EditChannelModal type=45 block. - i18n: en/zh keys for new labels and descriptions. Tests: round-trip + frame-splitter + SSE side-channel + auth-header tests in relay/channel/volcengine/protocols_v3_test.go (all green). Default behavior unchanged: empty Protocol falls back to v1 ws_binary.
…fault Resource ID with default voice map
Two follow-up fixes after end-to-end smoke testing v3 TTS:
1. new_console mode now accepts a SINGLE-SEGMENT API Key, sent verbatim as
X-Api-Key. Previously the code force-split on '|' and used segment[1] as
X-Api-Key, which fails because new-console-issued API Keys are independent
credentials (NOT the same as legacy access tokens). Operators using legacy
"<app_id>|<access_token>" format are still supported for backwards compat
(second segment is taken).
legacy mode is unchanged: still requires "<app_id>|<access_token>" and
emits X-Api-App-Id + X-Api-Access-Key.
2. VolcTTSDefaultResourceID changed from "seed-tts-2.0" to
"seed-tts-1.0-concurr" to match the default OpenAI->Volcengine voice map
(alloy/echo/fable/... all map to *_mars_bigtts, which are v1.0 voices).
The previous default produced upstream code=55000000 ("resource ID is
mismatched with speaker related resource") for any client that didn't
override voice. Users wanting v2.0 still set ResourceID=seed-tts-2.0 in
the channel config or via metadata.volc_tts_resource_id, AND must pass
a v2.0 voice such as zh_female_xiaohe_uranus_bigtts.
Tests updated and pass: go test ./relay/channel/volcengine/...
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughImplements Volcengine TTS v3 support: adds per-channel/request config, selects v3 transport (WS bidir/uni, HTTP chunked, HTTP SSE), implements v3 transport handlers and frame decoder, routes adapter logic, records audit fields, and adds modern and classic UI controls plus i18n strings. ChangesVolcengine v3 TTS Protocol Support
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (4)
relay/channel/volcengine/tts_v3_ws.go (1)
288-346: ⚡ Quick winDrain loop has no read deadline; a stalled upstream will pin the request goroutine forever.
ReceiveMessage(conn)blocks onconn.ReadMessage()indefinitely; nothing here propagatesc.Request.Context()cancellation into the gorilla WebSocket, and noSetReadDeadlineis set inside the loop (only the best-effort 2s deadline at Line 350 after we exit). If the Volcengine endpoint accepts the session but stops sending frames beforeSessionFinished, this handler hangs until the OS TCP keepalive eventually trips — which can be many minutes — holding an HTTP server worker the whole time.Consider setting a per-frame
SetReadDeadline(sliding deadline, e.g. 30s, refreshed on each successful read) and/or spawning a watcher goroutine that closesconnwhenc.Request.Context().Done()fires.♻️ Sketch — sliding deadline + context watcher
+ // Cancel the read if the client goes away. + go func() { + <-c.Request.Context().Done() + _ = conn.Close() + }() + + const frameIdleTimeout = 30 * time.Second + drainLoop: for { + _ = conn.SetReadDeadline(time.Now().Add(frameIdleTimeout)) msg, recvErr := ReceiveMessage(conn) if recvErr != nil { if websocket.IsCloseError(recvErr, websocket.CloseNormalClosure, websocket.CloseGoingAway) { break drainLoop } return nil, v3WrapError(recvErr, "recv frame failed") }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@relay/channel/volcengine/tts_v3_ws.go` around lines 288 - 346, The drainLoop can hang because ReceiveMessage(conn) blocks without a read deadline or context cancellation; before each ReceiveMessage call in drainLoop set a sliding per-frame read deadline on conn (e.g., conn.SetReadDeadline(time.Now().Add(30*time.Second))) and handle any SetReadDeadline error, and after each successful read refresh the deadline; additionally spawn a small watcher goroutine that listens for c.Request.Context().Done() and closes conn (or calls conn.Close()) so the blocking ReadMessage unblocks, making sure the watcher exits when drainLoop ends to avoid leaks (references: ReceiveMessage(conn), drainLoop, c.Request.Context()).relay/channel/volcengine/tts_v3_http.go (1)
79-89: ⚡ Quick winUse a dedicated
http.Clientwith header-read and dial timeouts instead ofhttp.DefaultClient.Both v3 HTTP handlers issue
http.DefaultClient.Do(req)with no transport-level timeouts. The request context (c.Request.Context()) covers client-side cancellation, but if Volcengine accepts the TCP connection and then stalls before sending response headers (or stops sending bytes), the goroutine and worker slot remain held until the kernel's TCP keepalive eventually fails.For TTS streaming you don't want a hard
Client.Timeout(it kills long audio streams), but a customhttp.Transportwith boundedDialContext,TLSHandshakeTimeout, andResponseHeaderTimeoutis essential. Implement a package-level client matching the pattern incontroller/ratio_sync.go:♻️ Sketch — package-level client with bounded handshake/header/dial timeouts
var v3HTTPClient = &http.Client{ Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, TLSHandshakeTimeout: 10 * time.Second, ResponseHeaderTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, IdleConnTimeout: 90 * time.Second, }, // No Client.Timeout — body streaming must remain open. }-resp, doErr := http.DefaultClient.Do(req) +resp, doErr := v3HTTPClient.Do(req)Applies to lines 85 and 196.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@relay/channel/volcengine/tts_v3_http.go` around lines 79 - 89, Replace the use of http.DefaultClient.Do(req) in volcengine/tts_v3_http.go with a package-level custom client (e.g., v3HTTPClient) that uses a custom http.Transport; define v3HTTPClient as a package var and configure Transport with a net.Dialer (Timeout and KeepAlive), TLSHandshakeTimeout, ResponseHeaderTimeout, ExpectContinueTimeout and IdleConnTimeout as suggested (do not set Client.Timeout so streaming bodies stay open), then call v3HTTPClient.Do(req) instead of http.DefaultClient.Do(req) wherever resp, doErr := http.DefaultClient.Do(req) appears.web/default/src/features/channels/constants.ts (1)
374-374: ⚡ Quick winPrefer a stable i18n key instead of a long inline prompt literal.
Using a full sentence literal here is brittle and easy to desync from locale files. Store an i18n key (e.g.,
channels.volcengine.keyPrompt) and move text to locale resources.As per coding guidelines,
web/default/**/*.{ts,tsx}requires user-facing text to support i18n, andweb/default/**/constants.tsrequires consistent key-based message handling for constants used in UI display.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/features/channels/constants.ts` at line 374, Replace the long inline literal in web/default/src/features/channels/constants.ts with a stable i18n key (e.g., "channels.volcengine.keyPrompt") and move the full sentence into the locale resource files; update any consumer of that constant to use the i18n lookup (the app's translation function) instead of rendering the raw string so constants.ts only exports the key, not the literal, ensuring all UI text goes through i18n as required by web/default/**/*.{ts,tsx} and the constants handling guideline for channels.web/default/src/i18n/locales/zh.json (1)
4290-4304: ⚡ Quick winUse hierarchical keys for these new TTS i18n entries.
Lines 4290-4304 continue the sentence-as-key pattern. For new additions, switch to semantic keys (e.g.,
channels.volcengine.tts.protocol.label) to prevent key drift and improve consistency.As per coding guidelines,
web/default/src/i18n/**/*.{ts,tsx,json}: Use hierarchical and semantically clear translation key names such asdashboard.overview.titleand maintain naming consistency.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/i18n/locales/zh.json` around lines 4290 - 4304, The current zh.json entries use sentence-as-key strings ("TTS Protocol", "TTS Resource ID", "TTS Auth Mode", "Auto / WS Binary (v1)", "WS Bidirectional (v3)", "WS Unidirectional (v3)", "HTTP Chunked (v3)", "HTTP SSE (v3, passthrough)", the long help text, and the two console/auth descriptions) — refactor these into hierarchical semantic keys (for example channels.volcengine.tts.protocol.label, channels.volcengine.tts.resourceId.label, channels.volcengine.tts.authMode.label, channels.volcengine.tts.transport.auto_ws_binary, channels.volcengine.tts.transport.ws_bidi, channels.volcengine.tts.transport.ws_unidi, channels.volcengine.tts.transport.http_chunked, channels.volcengine.tts.transport.http_sse, channels.volcengine.tts.help.sse_passthrough, channels.volcengine.tts.console.new, channels.volcengine.tts.console.legacy, channels.volcengine.tts.return_usage) and move the Chinese strings as their values; then update any code that references the old literal keys to use the new hierarchical keys (search for usages of "TTS Protocol", "TTS Resource ID", etc.) and run i18n lint/tests to ensure no missing keys remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web/classic/src/components/table/channels/modals/EditChannelModal.jsx`:
- Around line 3539-3541: The displayed helper text in EditChannelModal.jsx
mistakenly states the default resource ID is "seed-tts-2.0" while the backend
constant VolcTTSDefaultResourceID (in dto/channel_settings.go) uses
"seed-tts-1.0-concurr"; update the extraText for the X-Api-Resource-Id field (in
EditChannelModal.jsx) to reflect the backend default ("留空时默认
seed-tts-1.0-concurr") or otherwise make the text indicate it must match
VolcTTSDefaultResourceID so frontend and backend defaults stay consistent.
In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 1822-1830: Update the user-facing TTS resource strings in the
ChannelMutateDrawer component: change the displayed default/version text in the
FormDescription from "seed-tts-2.0" to "seed-tts-1.0-concurr" and wrap that
entire message with the t() i18n call, and also replace the hard-coded
placeholder prop value 'seed-tts-2.0' on the input (the JSX element using
{...field} and value={field.value || ''}) with a call to
t('seed-tts-1.0-concurr') (or another suitably keyed translation) so both the
placeholder and the description are localized and reflect the correct default
version.
- Around line 1788-1790: The SelectItem options in the ChannelMutateDrawer are
using empty-string values which `@base-ui/react` treats as a selected value;
replace any SelectItem value='' (e.g., the "Auto / WS Binary (v1)" option) with
value={null} and ensure there is a SelectItem value={null} representing the "no
selection" sentinel so placeholder and state styling behave correctly; also
update the raw TTS Resource ID placeholder string (and the TTS Auth Mode strings
at the other mentioned spots) to wrap them in the translation function t(...)
instead of leaving raw strings so they are localized (reference the SelectItem
JSX in channel-mutate-drawer.tsx and the TTS Resource ID / Auth Mode input
placeholders).
In `@web/default/src/features/channels/constants.ts`:
- Line 374: The prompt string that currently states "Format: AppId|AccessToken
(TTS / Realtime)..." is stale; update the constant in features/channels (the
descriptive message string) to clearly state both accepted credential formats:
for new-console v3 one-segment API keys may be provided as X-Api-Key, while
legacy mode still accepts AppId|AccessToken sent as X-Api-App-Id +
X-Api-Access-Key, and clarify that Chat/embedding still use the AccessToken as
Bearer; replace the old literal with this clarified wording to prevent
misconfiguration.
In `@web/default/src/i18n/locales/en.json`:
- Line 4299: The help string for the X-Api-Resource-Id entry is incorrect:
update the value for the JSON key "X-Api-Resource-Id header value. Defaults to
seed-tts-2.0 when empty. Common values: seed-tts-2.0, seed-tts-1.0,
seed-tts-1.0-concurr, seed-icl-2.0, seed-icl-1.0, seed-icl-1.0-concurr." to
reflect the backend default "seed-tts-1.0-concurr" (i.e., change the phrase
"Defaults to seed-tts-2.0" to "Defaults to seed-tts-1.0-concurr") so the
frontend help text matches the backend behavior.
- Line 4302: Update the i18n copy string that currently reads "New console:
AccessToken (second segment of the API key) is sent as X-Api-Key. Legacy: AppId
+ AccessToken are sent as X-Api-App-Id + X-Api-Access-Key." to a wording that
covers both single-segment and multi-segment API keys (e.g., "New console: the
AccessToken (or the second segment of a multi-segment API key) is sent as
X-Api-Key; legacy keys use AppId + AccessToken sent as X-Api-App-Id and
X-Api-Access-Key.") so the message for the JSON key "New console: AccessToken
(second segment of the API key) is sent as X-Api-Key. Legacy: AppId +
AccessToken are sent as X-Api-App-Id + X-Api-Access-Key." accurately reflects
current behavior for single- and multi-segment keys.
In `@web/default/src/i18n/locales/zh.json`:
- Line 4302: Update the zh.json translation for the exact key "New console:
AccessToken (second segment of the API key) is sent as X-Api-Key. Legacy: AppId
+ AccessToken are sent as X-Api-App-Id + X-Api-Access-Key." to reflect that the
new console accepts both single-segment API keys and the second segment of
multi-segment keys; modify the value to mention "single-segment key or the
second segment of the API key" and keep the legacy explanation for X-Api-App-Id
+ X-Api-Access-Key intact so the message covers both accepted new-console
formats and the old-console format.
- Line 4299: The translation for the tooltip/string that currently states
"Defaults to seed-tts-2.0 when empty" is out of sync; update the Chinese entry
(the JSON value whose English key starts with "X-Api-Resource-Id header value.
Defaults to seed-tts-2.0 when empty.") to say the backend default is
"seed-tts-1.0-concurr" instead of "seed-tts-2.0" so it matches the backend
change.
---
Nitpick comments:
In `@relay/channel/volcengine/tts_v3_http.go`:
- Around line 79-89: Replace the use of http.DefaultClient.Do(req) in
volcengine/tts_v3_http.go with a package-level custom client (e.g.,
v3HTTPClient) that uses a custom http.Transport; define v3HTTPClient as a
package var and configure Transport with a net.Dialer (Timeout and KeepAlive),
TLSHandshakeTimeout, ResponseHeaderTimeout, ExpectContinueTimeout and
IdleConnTimeout as suggested (do not set Client.Timeout so streaming bodies stay
open), then call v3HTTPClient.Do(req) instead of http.DefaultClient.Do(req)
wherever resp, doErr := http.DefaultClient.Do(req) appears.
In `@relay/channel/volcengine/tts_v3_ws.go`:
- Around line 288-346: The drainLoop can hang because ReceiveMessage(conn)
blocks without a read deadline or context cancellation; before each
ReceiveMessage call in drainLoop set a sliding per-frame read deadline on conn
(e.g., conn.SetReadDeadline(time.Now().Add(30*time.Second))) and handle any
SetReadDeadline error, and after each successful read refresh the deadline;
additionally spawn a small watcher goroutine that listens for
c.Request.Context().Done() and closes conn (or calls conn.Close()) so the
blocking ReadMessage unblocks, making sure the watcher exits when drainLoop ends
to avoid leaks (references: ReceiveMessage(conn), drainLoop,
c.Request.Context()).
In `@web/default/src/features/channels/constants.ts`:
- Line 374: Replace the long inline literal in
web/default/src/features/channels/constants.ts with a stable i18n key (e.g.,
"channels.volcengine.keyPrompt") and move the full sentence into the locale
resource files; update any consumer of that constant to use the i18n lookup (the
app's translation function) instead of rendering the raw string so constants.ts
only exports the key, not the literal, ensuring all UI text goes through i18n as
required by web/default/**/*.{ts,tsx} and the constants handling guideline for
channels.
In `@web/default/src/i18n/locales/zh.json`:
- Around line 4290-4304: The current zh.json entries use sentence-as-key strings
("TTS Protocol", "TTS Resource ID", "TTS Auth Mode", "Auto / WS Binary (v1)",
"WS Bidirectional (v3)", "WS Unidirectional (v3)", "HTTP Chunked (v3)", "HTTP
SSE (v3, passthrough)", the long help text, and the two console/auth
descriptions) — refactor these into hierarchical semantic keys (for example
channels.volcengine.tts.protocol.label,
channels.volcengine.tts.resourceId.label,
channels.volcengine.tts.authMode.label,
channels.volcengine.tts.transport.auto_ws_binary,
channels.volcengine.tts.transport.ws_bidi,
channels.volcengine.tts.transport.ws_unidi,
channels.volcengine.tts.transport.http_chunked,
channels.volcengine.tts.transport.http_sse,
channels.volcengine.tts.help.sse_passthrough,
channels.volcengine.tts.console.new, channels.volcengine.tts.console.legacy,
channels.volcengine.tts.return_usage) and move the Chinese strings as their
values; then update any code that references the old literal keys to use the new
hierarchical keys (search for usages of "TTS Protocol", "TTS Resource ID", etc.)
and run i18n lint/tests to ensure no missing keys remain.
🪄 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: 084de4c9-3bc4-4884-9281-a628da22ea24
📒 Files selected for processing (14)
dto/channel_settings.gorelay/channel/volcengine/adaptor.gorelay/channel/volcengine/protocols.gorelay/channel/volcengine/protocols_v3_test.gorelay/channel/volcengine/tts_v3_http.gorelay/channel/volcengine/tts_v3_ws.gorelay/common/relay_info.goservice/log_info_generate.goweb/classic/src/components/table/channels/modals/EditChannelModal.jsxweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/channels/constants.tsweb/default/src/features/channels/lib/channel-form.tsweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/zh.json
…pty Select, sync UI copy with default ResourceID Addresses 8 CodeRabbit review comments on PR QuantumNous#4710: - web/default drawer: replace SelectItem value="" with a non-empty sentinel (VOLC_TTS_DEFAULT_SENTINEL = "__default__") for both volc_tts_protocol and volc_tts_auth_mode. Base UI Select treats "" as a filled value and would break placeholder semantics; the sentinel maps back to "" before persisting. - Sync TTS Resource ID help text + placeholder across both themes (default drawer, classic EditChannelModal, en.json, zh.json) to reflect that the backend default is now seed-tts-1.0-concurr (matching the *_mars_bigtts v1.0 voice map) and that v2.0 voices require an explicit override. - Sync auth-mode help text to cover both single-segment new-console keys and multi-segment legacy AppId|AccessToken keys. - Update TYPE_TO_KEY_PROMPT[45] to document the single-segment new-console key format alongside the legacy two-segment format. - Wrap raw user-facing placeholder string in t() per i18n convention.
…frame deadline Addresses the CodeRabbit nit on PR QuantumNous#4710 about handleTTSV3WSResponse pinning a goroutine when the client disconnects or upstream stops sending frames: - Add a watcher goroutine that closes the upstream WS as soon as c.Request.Context() is cancelled (i.e. the gin client went away). Without this, ReceiveMessage blocked on the underlying TCP socket until OS keepalive eventually tripped, holding the gin worker for many minutes. - Apply a sliding 30s read deadline to every ReceiveMessage call inside drainLoop and inside v3ExpectEvent (handshake). A healthy stream keeps refreshing the deadline; a stalled upstream times out and surfaces a 502 / 504 instead of leaking the goroutine. - When the client cancellation closes the conn, the loop returns http.StatusGatewayTimeout with a clear "stream cancelled by client" error rather than a generic recv failure. HTTP chunked / SSE paths already react to client cancellation through http.NewRequestWithContext(c.Request.Context(), ...); only the bespoke WS path needed this hardening.
|
Addressed the review-body nit about the WS drain loop in
Tests + build still green. |
- bound v3 WS receive blocking with context cancel + per-frame deadline - address PR QuantumNous#4710 review (sentinel for empty Select, sync default ResourceID copy)
51fdfc5 to
2b6f1df
Compare
Summary
Closes #4709.
Adds Volcengine v3 TTS support alongside the existing v1
ws_binarypath.Four transports are now selectable per-channel (or per-request via OpenAI
metadata), with new-console (X-Api-Key) or legacy(
X-Api-App-Id+X-Api-Access-Key) auth modes.wss://openspeech.bytedance.com/api/v3/tts/bidirectionwss://openspeech.bytedance.com/api/v3/tts/unidirectional/streamhttps://openspeech.bytedance.com/api/v3/tts/unidirectionalhttps://openspeech.bytedance.com/api/v3/tts/unidirectional/sseDefault behavior is unchanged: leaving the new "TTS Protocol" field blank
falls back to v1
ws_binary, so existing channels keep working.Backend
relay/channel/volcengine/protocols.go: align readers/writers ordering forMsgTypeFlagWithEventframes; addEventClientRequesthelper andParseFrameshim. FixwriteSessionIDsymmetry forConnectionFinished.relay/channel/volcengine/tts_v3_ws.go: implement the bidirectional andunidirectional WS state machines (StartConnection -> StartSession ->
TaskRequest? -> FinishSession), parse
usage.text_wordsfromSessionFinished.
relay/channel/volcengine/tts_v3_http.go: implement HTTP Chunked framesplitter (binary frames over a streaming reader) and HTTP SSE passthrough
(raw
text/event-streamforwarded to the client; usage parsedside-channel for billing).
relay/channel/volcengine/adaptor.go: dispatch onresolveVolcTTSConfig(info)covering all four v3 endpoints + retain v1 default. Per-request override
via
metadata.volc_tts_*.dto/channel_settings.go: addVolcTTSConfig(Protocol / ResourceID /AuthMode / RequireUsage). No DDL — JSON-in-text on the existing
settingscolumn, cross-DB safe.
relay/common/relay_info.go:VolcTTSOverride *dto.VolcTTSConfigforper-request override.
service/log_info_generate.go: logvolc_tts_protocol/volc_resource_idfor audit.Frontend
web/default/(Base UI): Select / Input / Switch controls inchannel-mutate-drawer.tsxfor type=45; schema/defaults/parse/serializein
channel-form.ts; en/zh i18n keys.web/classic/(Semi UI): same four fields wired intoEditChannelModal.jsxtype=45 block.Auth modes
sent verbatim as
X-Api-Key. Single-segment keys are accepted directly;legacy
<app_id>|<access_token>is also accepted (second segment used)for ergonomic continuity.
<app_id>|<access_token>; emitted asX-Api-App-Id+X-Api-Access-Key.Defaults
seed-tts-1.0-concurr, matching the existingOpenAI->Volcengine voice map (
alloy/echo/... ->*_mars_bigtts,v1.0 voices). Users with v2.0 voices (
*_uranus_bigtts,saturn_*) mustoverride to
seed-tts-2.0/seed-icl-2.0accordingly.Tests
relay/channel/volcengine/protocols_v3_test.gocovers:TaskRequest / FinishSession + AudioOnlyServer + ConnectionStarted.
formats, legacy mode rejects single-segment).
writers()/readers()symmetry guard.Cross-DB compatibility (Rule 2)
No schema migration. New fields are nested JSON under the existing
channels.settingstext column, identical on SQLite / MySQL >= 5.7.8 /PostgreSQL >= 9.6.
Conventions
common.Marshal/common.Unmarshal(Rule 1).
omitempty(Rule 6).Test plan
go test ./relay/channel/volcengine/...passes.go build ./...passes.Bidirectional (v3), call
/v1/audio/speechwithstream:true,verify audio frames stream to client.
ws_binarypathstill works for existing flows.
(covered in unit tests; live verification per deploy).
Summary by CodeRabbit