Skip to main content
Build with Inferbase ยท Voice

Voice agent

Speech in, speech out, on one key: transcription, a routed answer kept short and fast, and speech synthesis streamed so the first audio arrives before the sentence is finished. Built as a turn service with a browser client, for turn-based use: an IVR, an in-app helper, a voicemail that answers. Measured at 7 to 13 seconds a turn today, it is not a real-time phone agent, and the guide says what would make it one.

What you build

A service that takes a caller's audio for one turn, transcribes it, answers in one or two spoken sentences from a known domain, and streams the answer back as audio while it is still being synthesised. A browser client records a turn, sends it, and plays PCM as it arrives; a telephony bridge can do the same. The three calls go to Inferbase on one key: transcription and speech take the first model of their type in your policy pool, the answer is routed on latency with thinking off, and every call returns its model and its cost. Optional tools let the answer act on your systems; the session keeps a call on one model across turns.

When you are done

  • A turn service: audio in, transcript, answer, audio out, with every stage timed, so you know what a turn costs before you build the client
  • A browser client that records, sends, and plays the answer as it streams
  • Answers that are short, fast and from your domain, with the model named on every turn
  • A conversation that stays on one model across turns, bounded by a per-call spend cap
  • Three receipts per turn: seconds transcribed, tokens answered, characters spoken, each with its cost

The components

Browser client
Records a turn with the MediaRecorder API, posts it, and plays the streamed PCM through the Web Audio API as chunks arrive.
Turn service
A FastAPI endpoint per turn: transcribe, answer, speak. Streams the answer text and then the audio bytes to the client.
Conversation state
The transcript so far, kept by the service per call id, sent on each answer call. The call id is the routing session.
Domain prompt and tools
The system prompt that keeps answers short and spoken, and optional tools (order lookup, booking) the answer may call.
Inferbase
The key, the routing policy with an ordered pool (one transcription model and one speech model at the top of their types, fast chat models beside them), and the three endpoints.
Receipts
Per turn: the transcription's seconds, the answer's tokens and model, the speech's characters, each with cost and latency, stored with the call.
Evaluation
A set of recorded utterances with expected transcripts and answers; scored on word error, answer correctness, and time to first audio.

The problem

A voice turn is three calls, and the caller hears every millisecond of all of them.

A spoken question has to be transcribed, answered and spoken back, and the person on the line is waiting through all three. A text chat can take four seconds to start; a voice agent that takes four seconds to start answering has lost the caller. The three calls also come from three vendors in most stacks, with three keys, three bills and three places where a request can fail.

The answer itself has a different shape from a text answer. It is read aloud, so it must be short, and it must come from a model that starts fast. A reasoning model that thinks for seven hundred tokens before saying hello is the wrong model for a phone call, however good its answer.

Speech synthesis is the slowest of the three when it is done whole: a sentence is synthesised in full before the first byte returns. The streaming form returns bytes as they are made, and which formats actually stream early depends on the provider.

This guide makes the three calls on one key. Transcription and speech take the first model of their type in your policy pool, in the order you set, because nothing measures audio models against each other and a policy choice is better than a guess. The answer is routed on latency with thinking off and a tight output cap. Speech is requested in the streaming form so the first audio arrives while the rest is still being synthesised. Measured end to end, a turn takes 7 to 13 seconds today, which makes this a turn-based assistant and not a real-time conversation; the scaling section says exactly where the time goes and what would close the gap.

Who has it

  • A phone or in-app voice assistant that answers from a known domain
  • A team with three audio vendors and three bills who wants one
  • Anyone measuring time to first audio and finding it too long

Architecture

System overview

Every component, grouped by who runs it. The product sits at the generation call; everything in your lanes is yours and unchanged by this design.

CALLERMicrophoneBrowser MediaRecorder, or atelephony bridgeSpeakerWeb Audio plays PCM as it arrivesTURN SERVICEPOST /turnOne request per utteranceConversation stateTranscript per call idDomain prompt + toolsShort spoken answers; optionalactionsReceiptsThree per turn, stored with thecallINFERBASE/audio/transcriptionsFirst transcription model in thepool/chat/completionsmodel auto, latency, thinking off/audio/speechFirst speech model; stream_formataudioPROVIDERSTranscription modele.g. Whisper, Parakeet, Qwen3-ASRFast chat modelsWinner per turnSpeech modele.g. Qwen3-TTS; PCM streamsaudio blobmultipart fileservetranscript so farhistory + text, sessionserveanswer text, pcm, streamedservePCM chunksthree per turnFigure 1. Three endpoints, one key. The transcription and speech models are whichever sit first of their type in the pool; the chat model is a decisionper turn. The service streams audio to the speaker as the speech endpoint streams it to the service.inferbase.ai

One request, end to end

One key, three endpoints. The audio models are a policy decision, the answer is a routing decision, and every call returns its model and its cost.

01 Calleryour stackAudio from a phone line, abrowser microphone, or a file.02 Your appyour stackCaptures audio, keeps theconversation, plays the answer asit arrives.- segment the audio- one session id per call- play PCM as it streams03 InferbaseInferbaseThree calls on one key:transcribe, answer, speak.- transcription: first model of its type in the pool- answer routed on latency, thinking off- speech streamed before synthesis ends- the receipt on every call04 Calleryour stackHears the first audio while thesentence is still being made.Figure 2. The same request as a flow: what each stage decides and hands on.inferbase.ai
One turn, from the caller's last word to the first sound of the answer
  1. 01BrowserBrowserDetects the end of the utterance (silence for 700 ms) and stops the recorder.Turn-taking is the client's job; the service sees whole utterances.
  2. 02BrowserTurn servicePOST /turn with the audio blob and the call id.WebM or WAV; the extension tells the provider the container.
  3. 03Turn serviceInferbase /audio/transcriptionsMultipart: the file, model auto, response_format verbose_json.verbose_json returns the duration the receipt meters. 25 MB limit per file.
  4. 04InferbaseTurn serviceThe transcript, the model that served, and the receipt with seconds of audio.No routing: the first transcription model in the pool.
  5. 05Turn serviceInferbase /chat/completionsThe transcript so far plus the new turn; model auto; routing optimize latency and the call id as session; extended_thinking false; max_tokens 120.Streamed, so the text can be shown as it arrives.
  6. 06InferbaseInferbaseClassifies the turn, keeps fast models in the pool, keeps the session on its model when it still qualifies, serves.Time to decide: p50 18 to 31 ms.
  7. 07InferbaseTurn serviceRouting event, the answer in chunks, usage.Measured TTFT p50 1.4 s at 24 in flight.
  8. 08Turn serviceInferbase /audio/speechThe answer text, voice, response_format pcm, stream_format audio.PCM streams on the measured provider; other formats are synthesised whole first.
  9. 09InferbaseTurn servicePCM bytes as they are made; the model in a header.First audio at about 1.2 s with PCM against about 4.7 s whole, 97 characters, measured.
  10. 10Turn serviceBrowserStreams the text then the PCM chunks; the browser plays each chunk as it lands.24 kHz, 16-bit, mono on the measured model; read the header to be sure.
  11. 11Turn serviceReceiptsStores the three receipts with the call id.Seconds, tokens and characters, each with cost and latency.
Figure 3. The caller hears the answer start about a second after the answer text starts, instead of after the whole sentence. The three receipts are what the turn cost.

Deployment

Where each part runs, and what talks to what over the network.

CALLERSBrowser clientServed as static filesTelephony bridgeOptional: SIP or a CPaaS streamTURN SERVICEFastAPI serviceLong-lived; streams; scales byinstanceRedisConversation state per call idINFERBASEapi.inferbase.aitranscriptions, chat, speech;your policyPROVIDERSAudio and chat providersManaged, or your own keysPOST /turn, streamed responseaudio framestranscriptHTTPS, bearer keyserveFigure 4. A long-lived service, because every turn streams. State is a transcript in Redis keyed by call id, so any instance can take the next turn. Placethe service in the same region as the callers; the three provider hops are what they are.inferbase.ai

What stays in your stack

  • Audio capture and playback
  • Turn-taking and interruption handling
  • The conversation state between turns
  • Telephony or WebRTC

Technology stack

One choice per layer, the alternative that was considered, and the reason. Swap any row; the Inferbase call does not change.

LayerChoiceAlternativesWhy
ClientBrowser: MediaRecorder for capture, Web Audio for streamed playbackA telephony bridge (Twilio media streams, SIP) feeding the same serviceA browser proves the loop end to end with nothing to provision; the service does not care where the audio came from.
ServicePython 3.12 with FastAPI, streaming responsesNode with the same SDKLong-lived, streams a body, and the SDK's streaming speech helper writes bytes as they arrive.
API clientopenai 3.x with base_url set to InferbasehttpxTranscription is one multipart call, speech one streaming call, chat one streamed call, all on the same client.
TranscriptionThe first transcription model in the policy pool, through /audio/transcriptionsA separate ASR vendorSame key and bill; verbose_json returns the duration the receipt meters.
Answermodel auto, routing optimize latency, extended_thinking false, max_tokens 120One pinned fast modelThe fastest model that clears the bar for the turn, and a cap that keeps answers spoken-length.
SpeechThe first speech model in the pool, response_format pcm, stream_format audioA separate TTS vendor; whole-file synthesisPCM streams early on the measured provider; the service relays bytes as they arrive.
StateRedis: the transcript per call idIn memory for one instanceAny instance can take the next turn; the call id is also the routing session.
Turn-takingClient-side silence detection, 700 msServer-side VAD on a continuous streamSimplest first version. A continuous stream with server VAD is the telephony upgrade.
EvaluationRecorded utterances with expected transcripts and answers; time to first audio measured by the clientListening testsWord error rate and answer checks are scripts; time to first audio is the number the caller feels.

What carries it

The Inferbase features this design rests on. Each exists in the product today and is linked to its documentation.

Transcription on the same key
A multipart request in the OpenAI shape. Auto takes the first transcription model in your pool; verbose_json returns the duration and segments the provider stated. Documentation
The answer routed on latency
The chat call sends optimize latency, extended_thinking false and a small output cap. A reasoning model is not asked to reason before it speaks. Documentation
Speech streamed as it is made
stream_format audio with response_format pcm returns bytes before the whole sentence is synthesised. Which formats stream early depends on the provider, and the documentation says what was measured. Documentation
The pool decides the audio models
No endpoint routes audio models, because nothing measures them against each other. Your policy pool, in your order, says which transcription and which speech model auto takes, and the editor names them. Documentation
Session affinity across turns
One session identifier per call keeps the answer model steady across turns unless it cannot serve or something beats it by the objective margin, and groups the turns under one run. Documentation
A receipt on every call
Transcription records seconds of audio, speech records characters, the answer records tokens; each with the model that served and the cost. Speech carries its receipt in a header, because its body is the audio. Documentation
Tools on the answer
The answer call may declare tools, so a spoken request can look an order up or book a slot; routing then keeps tool-capable models and the loop stays on one model. Documentation
Fail-over before the first byte
A speech stream can fail over to another provider only before its first byte; a provider that refuses outright falls through to the next in the pool. Documentation

Build it step by step

Seven steps from an empty service to a browser you can talk to. The code is Python for the service and JavaScript for the client; the three-call turn alone is also given in Python and TypeScript after the policy, and both were run against the API on a spoken question. Each step ends with the thing it turns on.

Before you start

  • An Inferbase account, a project, and a key with a spend cap for this service.
  • Python 3.12 with openai and FastAPI; Redis; a browser with microphone access for the client.
  • A policy pool that holds one transcription model and one speech model; the editor names which auto will use.

Step 1: Create the routing policy with an ordered pool

Choose the pool by hand and order it: the transcription model you want first among transcription models, the speech model you want first among speech models, and two or three fast chat models. Set the objective to Fastest, eligibility Standard, two attempts with the model allowed to switch, a spend cap per session sized to one call, and the injection check in record mode. Attach to the key. The editor shows which model each auto will use; that is the check that the order is right.

Speech is charged per character sent, all of which are in the request, so hanging up mid-sentence records the same usage as hearing it out. The per-session cap is what bounds a long call; short answers are what make it rarely matter.

Step 2: The turn endpoint: transcribe

One POST per utterance with the audio file and the call id. Transcribe through the SDK with model auto and verbose_json; the response carries the text, the model, and a receipt with the seconds of audio when the provider states a duration. Keep the file extension the browser gave you; most providers detect the container from it.

service/turn.py
python
from fastapi import FastAPI, UploadFile, Form
from fastapi.responses import StreamingResponse
from openai import OpenAI

client = OpenAI(api_key="inf_your_api_key", base_url="https://api.inferbase.ai/api/v1/inference")
app = FastAPI()


def transcribe(upload: UploadFile) -> tuple[str, dict]:
    transcript = client.audio.transcriptions.create(
        model="auto",  # the first transcription model in the policy pool
        file=(upload.filename, upload.file, upload.content_type),  # keep the extension: the provider reads the container from it
        response_format="verbose_json",
    )
    receipt = transcript.model_extra.get("usage", {})
    return transcript.text, {"model": receipt.get("model"), "seconds": getattr(transcript, "duration", None), "cost_usd": receipt.get("cost_micros", 0) / 1e6}

Step 3: Answer: short, fast, no thinking, streamed

Load the transcript so far for the call id, append the new turn, and stream a chat completion with model auto, routing optimize latency and the call id as session, extended_thinking false, and max_tokens 120. The system prompt says words only, one or two spoken sentences, and what to do when the agent does not know. Collect the text as it streams so the client can show it and the speech step can start on the full sentence.

service/answer.py
python
SYSTEM = (
    "You are a phone agent for a store. Answer in one or two short spoken sentences, in words only, never JSON or code. "
    "If you do not know the status of an order, say so and offer to look it up."
)


def answer(call_id: str, history: list[dict], text: str):
    """Yields text chunks; the caller collects them and reads the receipt from the closing event."""
    stream = client.chat.completions.create(
        model="auto",
        stream=True,
        max_tokens=120,
        messages=[{"role": "system", "content": SYSTEM}, *history, {"role": "user", "content": text}],
        extra_body={"routing": {"optimize": "latency", "session": call_id}, "extended_thinking": False},
    )
    for chunk in stream:
        raw = chunk.model_dump()
        if raw.get("object") == "routing":
            yield {"type": "routing", "model": raw.get("model")}
        elif raw.get("object") == "usage":
            u = raw.get("usage") or raw
            yield {"type": "usage", "cost_usd": u.get("cost_micros", 0) / 1e6, "latency_ms": u.get("latency_ms"), "ttft_ms": u.get("ttft_ms")}
        elif raw.get("choices") and raw["choices"][0].get("delta", {}).get("content"):
            yield {"type": "delta", "text": raw["choices"][0]["delta"]["content"]}

The verified run answered as JSON once on MiniMax M3 until the prompt said "in words only, never JSON or code". A spoken agent's prompt has to say what a text prompt can leave implicit.

Step 4: Speak: PCM, streamed, relayed as it arrives

Synthesise the answer with model auto, a voice, response_format pcm and stream_format audio, through the SDK's streaming response so bytes are read as they come. Read the model from the X-Inferbase-Model header; with the streaming form there is no usage header, because headers are sent before the first byte, so the speech receipt is read from the usage API afterwards or by requesting sse instead of audio. Relay each chunk to the client as it lands.

service/speak.py
python
def speak(text: str, voice: str = "alloy"):
    """Yields PCM chunks as the provider makes them."""
    with client.audio.speech.with_streaming_response.create(
        model="auto",  # the first speech model in the policy pool
        input=text,
        voice=voice,
        response_format="pcm",  # streams early on the measured provider; other formats are synthesised whole
        extra_body={"stream_format": "audio"},
    ) as speech:
        model = speech.headers.get("x-inferbase-model")
        yield {"type": "speech_model", "model": model}
        for chunk in speech.iter_bytes(chunk_size=4096):
            yield chunk

Measured on DeepInfra Qwen3-TTS, 97 characters, five runs: first audio at about 1.2 s with PCM, about 4.7 s without. Other providers and formats differ; measure your pool before relying on it.

Step 5: Wire the turn and stream it to the client

The endpoint runs the three steps in order and streams one response: JSON lines for the transcript, the routing event, the text chunks and the usage, then a boundary, then raw PCM. The client reads lines until the boundary and bytes after it. Store the transcript and the three receipts for the call id when the turn ends.

service/turn.py
python
import json

BOUNDARY = b"\n--audio--\n"


@app.post("/turn")
async def turn(call_id: str = Form(...), audio: UploadFile = ...):
    text, asr_receipt = transcribe(audio)
    history = load_history(call_id)  # Redis, keyed by call id

    def body():
        yield json.dumps({"type": "transcript", "text": text, **asr_receipt}).encode() + b"\n"
        answer_text = ""
        for event in answer(call_id, history, text):
            if event["type"] == "delta":
                answer_text += event["text"]
            yield json.dumps(event).encode() + b"\n"
        yield BOUNDARY
        for item in speak(answer_text):
            if isinstance(item, dict):
                continue  # the speech model name; log it with the receipts
            yield item
        save_history(call_id, history + [{"role": "user", "content": text}, {"role": "assistant", "content": answer_text}])

    return StreamingResponse(body(), media_type="application/octet-stream")

Step 6: The browser client

Record with MediaRecorder, stop on 700 ms of silence, post the blob with the call id, and read the response: lines until the boundary, then PCM. Play PCM through the Web Audio API by scheduling each chunk as a buffer at the next start time, so playback is gapless and begins on the first chunk. The sample rate and format come from the speech model; the measured model returns 24 kHz, 16-bit, mono.

client/turn.js (abridged)
javascript
const ctx = new AudioContext({ sampleRate: 24000 })
let nextStart = 0

function playPcm16(bytes) {
  const samples = new Int16Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 2))
  const buffer = ctx.createBuffer(1, samples.length, 24000)
  const channel = buffer.getChannelData(0)
  for (let i = 0; i < samples.length; i++) channel[i] = samples[i] / 32768
  const source = ctx.createBufferSource()
  source.buffer = buffer
  source.connect(ctx.destination)
  nextStart = Math.max(nextStart, ctx.currentTime)
  source.start(nextStart) // gapless: each chunk starts where the last one ends
  nextStart += buffer.duration
}

async function sendTurn(blob, callId) {
  const form = new FormData()
  form.append('call_id', callId)
  form.append('audio', blob, 'turn.webm')
  const response = await fetch('/turn', { method: 'POST', body: form })
  const reader = response.body.getReader()
  const boundary = new TextEncoder().encode('\n--audio--\n')
  let head = new Uint8Array(0), inAudio = false
  for (;;) {
    const { value, done } = await reader.read()
    if (done) break
    if (inAudio) { playPcm16(value); continue }
    head = concat(head, value)
    const at = indexOf(head, boundary)
    if (at >= 0) {
      for (const line of new TextDecoder().decode(head.slice(0, at)).split('\n').filter(Boolean)) showEvent(JSON.parse(line))
      inAudio = true
      playPcm16(head.slice(at + boundary.length))
    }
  }
}

Step 7: Tools on the answer, and the call as a session

A spoken request often wants an action: look an order up, book a slot. Declare the tools on the answer call and run the loop as the agent guides do; routing then keeps tool-capable models, and the loop stays on one model. Keep the loop to two steps for a phone call. The call id as session keeps the whole call on one model across turns and groups it under one run in Usage, where the three receipts per turn show what the call cost.

service/answer.py (tools)
python
LOOKUP = {"type": "function", "function": {"name": "lookup_order", "description": "Fetch an order by id.", "parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}}}


def answer_with_tools(call_id: str, history: list[dict], text: str) -> str:
    messages = [{"role": "system", "content": SYSTEM}, *history, {"role": "user", "content": text}]
    for _ in range(2):  # a phone call cannot wait for a long loop
        step = client.chat.completions.create(
            model="auto", max_tokens=120, messages=messages, tools=[LOOKUP],
            extra_body={"routing": {"optimize": "latency", "session": call_id}, "extended_thinking": False},
        )
        choice = step.choices[0]
        messages.append(choice.message)
        if choice.finish_reason != "tool_calls":
            return choice.message.content or ""
        for call in choice.message.tool_calls:
            messages.append({"role": "tool", "tool_call_id": call.id, "content": orders.lookup(**json.loads(call.function.arguments))})
    return "Let me check that and call you back."

The routing policy

A chosen pool with one transcription model and one speech model at the top of their types, fast chat models beside them, latency as the objective, and a session cap sized to a call.

StageSettingValueWhy
PoolHow the pool growsModels I choose, orderedThe first transcription model and the first speech model in the list are the ones auto takes. Order them deliberately; the editor names which each auto will use.
RoutingOptimization modeFastestTime to first token is the figure a caller hears. The chat call can also say so per request.
RoutingEligibilityStandardThe default evidence bar for short spoken answers in a known domain.
FallbackRetries2 attempts, model may switchA busy provider should not leave silence on the line. Speech can only fail over before its first byte; a provider that refuses outright still falls through.
SessionMaximum spend per sessionSized to one callSpeech is charged per character sent whether or not the caller hears it out, so the cap is what bounds a long call.
GuardrailsGuardrail policyPrompt injection: recordSpoken input is rarely an injection vector; record first and see.

Create it under Policies in the portal and attach it to the key the application uses; the steps are in Create a routing policy.

The call

One turn: transcribe the caller audio, answer in one or two spoken sentences, and synthesise the answer with streaming so playback can start on the first bytes. The answer file is raw PCM, which is what a telephony stack plays. The build above wraps this in a service and a browser client.

python
from openai import OpenAI

client = OpenAI(api_key="inf_your_api_key", base_url="https://api.inferbase.ai/api/v1/inference")

CALL = "call-5521"  # one session id per call: the answer model stays steady across turns

# 1. Transcribe the caller's turn. Auto takes the first transcription model in your pool.
with open("question.wav", "rb") as audio:
    transcript = client.audio.transcriptions.create(model="auto", file=audio, response_format="verbose_json")
print("heard:", transcript.text)

# 2. Answer: short, fast, no thinking.
completion = client.chat.completions.create(
    model="auto",
    max_tokens=120,
    messages=[
        {"role": "system", "content": "You are a phone agent for a store. Answer in one or two short spoken sentences, in words only, never JSON or code. If you do not know the status of an order, say so and offer to look it up."},
        {"role": "user", "content": transcript.text},
    ],
    extra_body={"routing": {"optimize": "latency", "session": CALL}, "extended_thinking": False},
)
answer = completion.choices[0].message.content or ""
print("answer:", answer, f'({completion.model}, {completion.usage.model_extra["latency_ms"]} ms)')

# 3. Speak it, streamed: PCM bytes arrive before the whole sentence is synthesised.
with client.audio.speech.with_streaming_response.create(
    model="auto",
    input=answer,
    voice="alloy",
    response_format="pcm",
    extra_body={"stream_format": "audio"},
) as speech:
    print("speech model:", speech.headers.get("x-inferbase-model"))
    speech.stream_to_file("answer.pcm")  # play as it arrives in production

Both snippets were run against api.inferbase.ai on 2026-10-13 on a spoken question synthesised through the same API: the transcript came back word for word, the answer was served by MiniMax M3 in 2 to 4 seconds, and the streamed PCM landed in a file while it was still being made.

Open in PlaygroundAPI reference

Security and privacy

Voice carries the caller's voice itself, which is personal data, and the conversation. The controls are about what is kept and what a call may spend.

Audio is not kept by the service
The service transcribes the blob and discards it; the transcript is the state. If audio must be kept, that is your store and your retention policy.
Request content at Inferbase
The transcript and the answer are request content, kept for up to 30 days unless zero retention is on for the key; audio sent for transcription and synthesised speech are not stored as request content. The decision record is kept without content.
The key
One key for the service, with a spend cap and a rate limit, never in the browser. The client talks to your service only.
Spend per call
The policy session cap bounds a call; the key's daily cap bounds the day. Speech is charged per character sent, so short answers are the cheapest control.
Tools with consequences
A spoken agent that can act should confirm aloud before a write: "Shall I change the address to this?" The tools guide's approval gate applies, with the caller as the approver.
Injection by voice
Rare, but a caller can read an instruction aloud. The guardrail injection check in record mode shows whether it happens; refuse it once you have seen the verdicts.

Evaluation

Three numbers per utterance, measured by scripts: how well it was heard, whether the answer was right, and how long the caller waited for the first sound. A set of recorded utterances with expected transcripts and expected answers drives all three.

Word error rate
The transcript against the expected transcript, per utterance and by accent or noise condition. It tells you whether the first transcription model in your pool is the right one for your callers.
Answer correctness
The answer against expected content, with a string check or a judge call. Short answers are easy to check; a wrong refusal is the usual failure.
Time to first audio
Measured by the client from the end of the utterance to the first PCM chunk. It is the sum of the three calls' first-byte times; the receipts break it down.
Cost per turn
From the three receipts: seconds, tokens, characters. Compare speech models by characters per second of audio produced as well as price.
Auto against pinned
The answer call pinned to one model against auto on latency: correctness and TTFT. The audio models are a pool choice; compare them by reordering the pool and rerunning.
eval/turns.py
python
import time

for case in utterances:  # {"file": "q-017.wav", "expect_text": "...", "expect_any": ["shipped", "look it up"]}
    t0 = time.perf_counter()
    with open(case["file"], "rb") as f:
        heard = client.audio.transcriptions.create(model="auto", file=f, response_format="verbose_json").text
    answer = answer_text(heard)
    first_audio = None
    with client.audio.speech.with_streaming_response.create(model="auto", input=answer, voice="alloy", response_format="pcm", extra_body={"stream_format": "audio"}) as speech:
        for _ in speech.iter_bytes(chunk_size=4096):
            first_audio = time.perf_counter() - t0
            break
    print(case["file"], f"wer={wer(heard, case['expect_text']):.2f}", "ok" if any(s in answer.lower() for s in case["expect_any"]) else "WRONG", f"first audio {first_audio:.2f}s")

Operations

A call is a session, so Usage shows it as one run with three receipts per turn. Watch the three first-byte times, because the caller does.

Time to first audio by component
Transcription latency, answer TTFT, speech first byte, from the receipts and your client. The slowest component is where to spend effort; on the measured pool it is speech, which the streaming form halves.
Which audio models serve
The receipt names the model on every call. A reordered pool changes it on the next call; watch the names after a policy change.
Cost per call
Sum the three receipts per session. Speech characters dominate when answers run long; the output cap and the prompt keep them short.
Fail-over on speech
A stream can fail over only before its first byte. A speech provider refusing outright shows as a fallback in the receipt's attempts; one failing mid-stream shows as a short answer, and your client should play what it has.
Session hops
The routing event on the answer says whether the call stayed on its model. Hops on a phone call are audible as a change of style; a task rule steadies them.
Policy changes
The pool order and the objective have a revision history. Reorder with the evaluation in hand.

Scaling and limits

Calls are concurrent and each turn streams, so the service is long-lived and scales by instance. The limits are the key, file sizes, and the speech provider's streaming behaviour.

Real time, measured: not yet a conversation
A natural conversation needs the first sound inside about 1.5 s of the caller's last word. Measured on 2026-10-14, three turns on a 9 s spoken question: transcription 1.5 to 3.3 s, the answer's first token 2.7 to 5.0 s, its first sentence 3.3 to 5.9 s, the first audio of the reply 1.8 to 3.7 s; the turn, pipelined on the first sentence, 6.7 to 13 s. Routing is 20 to 50 ms of that; the rest is three provider hops. So this design is a turn-based voice assistant today, an IVR, an in-app helper, a voicemail that answers, and not a real-time phone agent. Four things close the gap, in order of payoff: streaming transcription so the answer starts before the caller finishes; a chat route with a sub-second first token winning the latency objective; sentence-level pipelining into speech, which step 5 sketches; and a speech model whose first byte is reliably near the 1.2 s the documentation measured. With all four, expect 2 to 3 s, acceptable for an assistant, still not a conversation.
Concurrent calls
Each turn is three requests over a few seconds. A key at 600 requests a minute carries dozens of concurrent calls; raise the limit before launch.
Utterance size
Transcription takes up to 25 MB per file; a turn is far below it. For a continuous stream, cut utterances at silence on the client or with server-side VAD.
State
The transcript per call id in Redis lets any instance take the next turn. Expire it when the call ends.
Speech formats
PCM streams on the measured provider; a container format may be synthesised whole first. Measure each speech model in the pool before choosing it.
Regions
The three hops are provider latency the service cannot remove. Place the service near the callers, and keep answers short so the speech hop is short.
Telephony
A bridge that sends audio frames needs server-side VAD and a continuous session; the three calls and the receipts do not change.

Troubleshooting

What goes wrong when this is built, what it means, and what to do. Each one was met while building or running it.

SymptomCauseFix
The first audio arrives only after several secondsThe speech request was not in the streaming form, or the format does not stream on the provider.Send stream_format audio with response_format pcm; measure the pool's speech model; the documentation names what was measured.
The answer is JSON or codeA fast model treated the request as a tool-shaped task.Say "in words only, never JSON or code" in the system prompt; the verified run needed it.
403 on transcription or speech saying the pool holds no model of that typeThe policy pool has no transcription or speech model, or auto found none.Add one to the pool and put it first of its type; the editor names which auto will use.
400 saying the model is served on another endpointA transcription model was named on /audio/speech, or the reverse.Use the endpoint the message names.
503 saying the model runs only on a route with no rate for this unitA speech or transcription model whose Inferbase route carries no per-character or per-second rate.Pick a model the catalog marks inference_available for that surface, or connect your own provider key for it.
Transcription returns no durationresponse_format was not verbose_json.Ask for verbose_json; the receipt meters seconds only when the provider states a duration.
Playback stuttersChunks were played as they arrived without scheduling, so gaps appear between them.Schedule each buffer at the previous end time through the Web Audio API, as the client does.
The agent takes 30 seconds to answerThinking was on and the model reasoned before answering.Send extended_thinking false on the answer call; keep max_tokens small.

Extending

The service sees utterances and returns audio; everything around it can change.

Telephony
A SIP or CPaaS bridge that sends audio frames and plays PCM. Add server-side voice activity detection to cut utterances, and keep the call id as the session.
Interruptions
Stop playback when the caller speaks and start a new turn; the speech request is already paid for, so keep answers short.
A knowledge base
Retrieve passages before the answer call, as the RAG guide does, and keep the answer to two spoken sentences with a citation the agent can read out.
Actions with approval
Confirm aloud before a write, then execute; the tools guide's gate applies with the caller as the approver.
Voices
A voice-design model takes a description instead of a name; keep the voice per deployment, not per turn, so the agent sounds the same across a call.
Your own audio vendors
Connect a provider key under Connections and put its models first of their type in the pool; the receipt records a zero cost and the provider bills you.

What was measured

A full voice turn on the public API, stage by stage, from the caller's last word to the first sound of the answer. The last row is the speech endpoint's own documented measurement on a shorter text; the three runs above are what a turn sees end to end.

MeasurementValue
Transcription of a 9 s question (3 runs)1.5 to 3.3 s
Answer, first token, latency objective (3 runs)2.7 to 5.0 s
Answer, first sentence complete (3 runs)3.3 to 5.9 s
Speech, first audio, PCM streamed (3 runs)1.8 to 3.7 s
One turn, pipelined on the first sentence (3 runs)6.7 to 13 s
Speech, first audio, the documentation's measurement, 97 characters, 5 runsabout 1.2 s streamed, 4.7 s whole

Measured from our own infrastructure in India against api.inferbase.ai on 2026-10-14, three runs each, client-side wall time including the network hop (about 0.25 s each way), thinking off. Every response returns its own figures.

Further reading

Frequently asked questions

Put intelligence in the middle.

One gateway in front of every model, with your policies applied and every decision on record. Start on the free plan with 5,000 requests a month, no card required.