Audio
llm.buzz serves speech to text and text to speech with OpenRouter's request and answer shapes, so the OpenAI and OpenRouter SDKs reach them with the base URL alone. Each audio model serves one of the two. Chat Completions, Responses and Messages refuse an audio model with a 400 that names its route.
Every audio route takes your key as a bearer token, as on every other route (Authentication).
Find an audio model
GET https://api.llm.buzz/api/v1/models lists audio models as OpenRouter does. Its default, output_modalities=text, leaves them out: with output_modalities=transcription it lists the speech-to-text models, with output_modalities=speech the text-to-speech models, and with output_modalities=all every model.
curl "https://api.llm.buzz/api/v1/models?output_modalities=speech" \
-H "Authorization: Bearer $LLM_BUZZ_KEY"A speech-to-text model's architecture is audio->transcription, and a text-to-speech model's is text->speech, with its voices in supported_voices; the model's page on the Models page lists them too, under Voices. pricing.prompt is the price in USD per second of audio or per character of input text, at your rate, and pricing.completion is "0".
Three more keys say what a model takes. A speech-to-text model's verbose_json says whether it answers response_format: "verbose_json". A text-to-speech model's speech_speed says whether it takes speed, and its default_voice names the voice that speaks when you send none, or is null when the model needs a voice.
The Models page shows each audio model's price per million seconds of audio or per million characters, such as "$3.33/M seconds", and its Output filter picks out transcription and speech models.
Speech to text
POST https://api.llm.buzz/api/v1/audio/transcriptions transcribes one audio file. Send it as OpenRouter's JSON body, the file in base64 in input_audio, or as an OpenAI multipart upload in file, as the OpenAI SDK does.
from openai import OpenAI
import os
client = OpenAI(
base_url="https://api.llm.buzz/api/v1",
api_key=os.environ["LLM_BUZZ_KEY"],
)
with open("call.mp3", "rb") as audio:
transcript = client.audio.transcriptions.create(
model="openai/whisper-large-v3-turbo",
file=audio,
)
print(transcript.text)curl https://api.llm.buzz/api/v1/audio/transcriptions \
-H "Authorization: Bearer $LLM_BUZZ_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "openai/whisper-large-v3-turbo",
"input_audio": {"data": "'"$(base64 -w0 call.mp3)"'", "format": "mp3"}}'curl https://api.llm.buzz/api/v1/audio/transcriptions \
-H "Authorization: Bearer $LLM_BUZZ_KEY" \
-F model=openai/whisper-large-v3-turbo \
-F file=@call.mp3The fields are OpenRouter's. A multipart upload sends each one as a text part, provider and trace as JSON text, and timestamp_granularities[] once per value.
| Field | Rule |
|---|---|
model | Required: a speech-to-text model. Another model is a 400 on model. |
input_audio | The JSON form: data, the file in base64, and format. A url is a 400: send the bytes. |
file | The multipart form: the audio file. A source_url is a 400. |
response_format | json, the default, answers text and usage. verbose_json, on a model whose verbose_json is true, adds task, language, duration, segments, and words when the provider sends them; on another model it is a 400. text, srt, vtt and any other value are a 400. |
language | An ISO-639-1 code such as en. Omitted, the provider detects the language. |
temperature | 0 to 1. |
timestamp_granularities | segment and word, with verbose_json only. |
diarize, keyterms | diarize: true and any keyterms are a 400: no model here diarizes or takes key terms. |
provider | Only zdr and data_collection apply, as described in Data policy. order, only, ignore and options are accepted and not applied, as on OpenRouter for audio. |
user, session_id, trace, prompt | Accepted. user is recorded as the end-user label; the others are not sent to the provider. |
The file is at most 25 MiB: a larger file is a 413, and so is a JSON body over 39,234,221 bytes. It is one of wav, mp3, flac, ogg (Opus or Vorbis), webm, m4a, aac, or pcm (headerless signed 16-bit little-endian mono at 16 kHz). The format comes from input_audio.format, or from a multipart file's extension, else its content type.
llm.buzz reads the file and measures its duration before it calls a provider. A file it cannot read to the end, an empty file, or one too small to hold its duration (under 1,000 bits per second) is a 400 on input_audio.data or file that names the formats it takes. Such a file is never sent to a provider and never billed.
The answer:
{"text": "Thanks for calling.", "usage": {"seconds": 12.48, "cost": 0.000041558}}usage.seconds is what the call billed, and usage.cost its price in USD at your rate. With verbose_json, the provider's task, language, duration, segments and words come with it unchanged.
Text to speech
POST https://api.llm.buzz/api/v1/audio/speech speaks one text and answers with the audio.
These samples send no voice, so the model's default voice speaks. To choose a voice, send voice with one of the IDs listed under Voices on the model's page, or as supported_voices by GET https://api.llm.buzz/api/v1/models?output_modalities=speech. The Python sample posts over plain HTTP with httpx, which the OpenAI Python SDK installs, because the OpenAI SDKs require voice.
import os
import httpx
reply = httpx.post(
"https://api.llm.buzz/api/v1/audio/speech",
headers={"Authorization": f"Bearer {os.environ['LLM_BUZZ_KEY']}"},
json={"model": "hexgrad/kokoro-82m", "input": "Your order has shipped.", "response_format": "mp3"},
timeout=60,
)
reply.raise_for_status()
with open("reply.mp3", "wb") as file:
file.write(reply.content)curl https://api.llm.buzz/api/v1/audio/speech \
-H "Authorization: Bearer $LLM_BUZZ_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "hexgrad/kokoro-82m", "input": "Your order has shipped.", "response_format": "mp3"}' \
-o reply.mp3| Field | Rule |
|---|---|
model | Required: a text-to-speech model. Another model is a 400 on model. |
input | Required: one string of 1 to 4,096 characters, counted as Unicode code points. An empty or longer string, or an array of turns, is a 400 on input. |
voice | One of the model's voices, listed as supported_voices by GET https://api.llm.buzz/api/v1/models?output_modalities=speech; another voice is a 400 on voice. Omitted, the model's default_voice speaks, and a model whose default_voice is null answers a 400 on voice. With the OpenAI SDKs, which require voice, pass one of those IDs. |
response_format | pcm, the default, or mp3. Any other value is a 400. |
speed | 0.25 to 4.0, on a model whose speech_speed is true. A speed other than 1 on another model is a 400 on speed. |
input_references | Voice cloning: a 400 unless empty. |
instructions | Accepted and ignored, as on OpenRouter for these models. |
provider, user, session_id, trace | As on speech to text. |
The answer is the whole audio, not streamed: Content-Type: audio/mpeg for mp3, or audio/pcm for pcm (signed 16-bit little-endian mono at the provider's sample rate, 24 kHz for hexgrad/kokoro-82m), with Cache-Control: no-store. A provider answer that is not valid audio in the format you asked for moves the call to the next provider and is not billed; from the last provider, the call bills the characters, as Billing says.
Cost and the generation record
Every audio answer carries X-Generation-Id: gen- and 32 hex digits. A text-to-speech answer has no JSON body for its cost, so read it from the generation record. The record covers any finished request of your organization, not only audio: any request's generation ID is the gen- form of its x-request-id.
curl "https://api.llm.buzz/api/v1/generation?id=gen-..." \
-H "Authorization: Bearer $LLM_BUZZ_KEY"The answer is OpenRouter's {"data": {...}}, read in your key's organization:
| Key | Value |
|---|---|
total_cost, usage | What you were charged, in USD at your rate. |
api_type | stt for speech to text, tts for text to speech, completions for Chat Completions, Responses and Messages, embeddings, image and decisions. |
model | The model's name. |
provider_name | The provider that served the call. |
latency, generation_time | In milliseconds. |
data_region | europe for a provider in the EU, else global. |
A generation ID of another organization, a malformed one, and one whose request has not finished all answer 404. A request without id answers 400.
Billing
Speech to text is billed per second of audio and text to speech per character of input text, at the serving provider's price and your rate. Neither has an output price. GET https://api.llm.buzz/api/v1/models/{author}/{slug}/endpoints lists each provider's price.
- Speech to text bills the seconds of audio llm.buzz measures from the file, or the provider's own count when it is longer, up to the measured duration rounded up to a whole multiple of 30 seconds. Some providers count a minimum or whole chunks; such a count is billed up to that bound, never more.
- Text to speech bills the characters of
input. - Before any provider is called, llm.buzz reserves the most the call can cost. A budget, a key limit or credit that cannot cover it answers 402 or 403, and nothing is sent to a provider. Errors lists the codes.
- An attempt that fails and moves to the next provider bills nothing. When the last provider fails after it received the request, the call bills the measured seconds or the characters, never the 30-second rounding: a 1.5-second file whose only provider answers 500 bills 1.5 seconds. When the last attempt never reaches its provider (the connection is refused, or DNS or TLS fails), the call bills nothing.
- Neither route reads an
Idempotency-Key: a retry is a new request, served and billed again.
Privacy
No audio or text of an audio call is kept in llm.buzz's prompt logs.
Not supported
Streaming synthesis, URL inputs, diarization, key terms, voice cloning, multi-speaker input, the text, srt and vtt transcription formats, and audio output from chat models (modalities: ["text", "audio"]). Audio input to a chat model (input_audio content parts on Chat Completions) is a chat capability of its own, apart from these routes.