[Bugfix] Fix /v1/audio/speech usage token accounting for Qwen3-TTS (#4646) (#4673)

Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: Sy03 <1370724210@qq.com>
This commit is contained in:
Yueqian LinandSy03 authored and GitHub committed 2026-06-27 06:13:31 +08:00
1 parent c582805e7a
commit b66cd024b9
7 files changed
+697 -8

No files matched your search

@@ -421,12 +421,65 @@ class BatchSpeechRequest(BaseModel):
non_streaming_mode: bool | None = None
class SpeechInputTokenDetails(BaseModel):
"""Per-modality breakdown of the speech request's *input* tokens.
The aggregate ``input_tokens`` on :class:`SpeechTokenUsage` is the sum of
these. We surface the split (rather than one opaque number) for the same
reason OpenAI's realtime/chat usage does: text and audio inputs are billed
and reasoned about differently, and folding them together is misleading.
Fields:
text_tokens: Tokens of the text to synthesize. This is ``input`` plus
``instructions`` (style/emotion prompt), because both are tokenized
into the model prefill. This is the number that should scale with
how much text the caller asked to speak.
audio_tokens: Reference-audio codec frames used as voice-cloning
conditioning. NON-ZERO only when in-context voice cloning is
actually active (Qwen3-TTS ``task_type='Base'`` ICL). It is 0 for
CustomVoice/VoiceDesign and for x-vector-only cloning, because those
paths put no reference codec frames into the prefill. See issue
#4646: this is the value that previously leaked into ``prompt_tokens``
and made Base usage look independent of the input text.
"""
text_tokens: int = 0
audio_tokens: int = 0
class SpeechTokenUsage(BaseModel):
"""Token usage for a speech (TTS) request.
Field naming follows OpenAI's documented ``speech.audio.done`` event
(``input_tokens``/``output_tokens``/``total_tokens``), NOT chat's
``prompt_tokens``/``completion_tokens``.
input_tokens = text_tokens + audio_tokens (see SpeechInputTokenDetails)
output_tokens = generated codec/audio tokens (stage-0 decode steps)
total_tokens = input_tokens + output_tokens
IMPORTANT: ``input_tokens`` is computed from the *semantic* request inputs
(tokenized text + reference-audio frames), NOT from ``len(prompt_token_ids)``.
For staged TTS models that engine prompt is a ``[1] * prefill_len``
placeholder whose length mirrors the model prefill, so it is not a faithful
input-token count (issue #4646).
"""
input_tokens: int = 0
output_tokens: int = 0
total_tokens: int = 0
input_token_details: SpeechInputTokenDetails = Field(default_factory=SpeechInputTokenDetails)
class SpeechBatchItemResult(BaseModel):
index: int
status: Literal["success", "error"]
audio_data: str | None = None
media_type: str | None = None
error: str | None = None
# Per-item token usage (input text + reference-audio conditioning, and
# generated audio tokens). None when the item errored before generation.
usage: SpeechTokenUsage | None = None
class BatchSpeechResponse(BaseModel):