Voxial LLM — Voice-Pipeline LLM Routing
Audiences: developer, internal
Every voice-pipeline LLM call (FlowAgent step, Vivoxx avatar, TUCAN Operator) routes through the Dudoxx LiteLLM gateway with
thinkingBudgetTokens: 0, an OpenAI-compatible client wrapper, and tight latency budgets — so voice agents never hit OpenAI / Anthropic directly, never burn tokens on internal reasoning, and never leak the model choice into transport code.
Business Purpose
Voice is the most latency-sensitive surface in HMS — a 10 s "thinking" pause kills the conversation. It is also the most cost-sensitive (every silent re-prompt is a paid LLM round-trip) and the most policy-sensitive (the Mastra agent has access to PHI tools). Centralising every voice-side LLM call behind LiteLLM gives us:
- One place to change models without redeploying ddx-livekit-flowagent-ts / ddx-vivoxx
- Per-tenant quota + per-model budget enforcement
- Forced
enable_thinking: false(reasoning tokens MUST be 0 for voice — see Cardinal "default LLM" rule in rootCLAUDE.md) - Strict provider whitelist (the Vivoxx LLM factory throws if anything but
LITELLM/dudoxxis passed)
This page describes only the voice-side LLM contract. Text-side TUCAN dispatcher LLM routing is documented in tucan-dispatcher.md; the LiteLLM gateway infra (port + auth) lives at infra-litellm.md.
Audiences
- Investor: Model substitution is a config change, not a deploy. Lets us race upstream model releases without code risk.
- Clinical buyer: Voice answers stay under the latency floor; no foreign cloud sees PHI; clinic admin can cap monthly LLM spend in LiteLLM dashboard.
- Developer/partner: Single
createDudoxxMastraModel({ baseURL, apiKey, model })factory; everything else (timeouts, retries, thinking-off injection) is handled bylitellmFetch. - Internal (ops/support): One env var (
DUDOXX_BASE_URL) controls every voice-side LLM call. One token (DUDOXX_API_KEY) gates them all. One model name (DUDOXX_MODEL_NAME) is the default.
Architecture
┌──────────────────────────────────┐
│ voice consumers │
│ - ddx-livekit-flowagent-ts │
│ - ddx-vivoxx │
│ - ddx-api TUCAN Operator │
└────────────────┬─────────────────┘
│ OpenAI-compatible /v1/chat/completions
│ (apiKey: DUDOXX_API_KEY)
│ + body injection: enable_thinking:false
│
▼
┌──────────────────────────────────────────────────────┐
│ LiteLLM Gateway (DUDOXX_BASE_URL, default :4000/v1) │
│ - model alias → upstream resolution │
│ - per-tenant budgets / quotas │
│ - logging + observability │
│ - thinking-off enforcement (canonical default) │
└──────────────────────────────────────────────────────┘
│
▼
upstream model providers (dudoxx-gemma,
cloud OpenAI, Anthropic, local vLLM, …)
The voice-side caller never knows whether the upstream model is local vLLM or cloud. It just picks an alias (dudoxx-gemma, openai/gpt-4o, etc.) and trusts the gateway to route.
Tech Stack & Choices
- Client SDK:
@ai-sdk/openaiv5 with a customcreateLitellmFetchmiddleware (ddx-livekit-flowagent-ts/src/mastra/model.ts:18-103) that intercepts request bodies and injectsenable_thinking:false,temperature,thinkingBudgetTokens,maxOutputTokens. - Endpoint policy: forces
provider.chat(model)→/v1/chat/completions(Mastra default would hit/v1/responseswhich LiteLLM does not support —model.ts:135-138). - Vivoxx side: same provider whitelist (
ddx-vivoxx/src/providers/llm.provider.tsaccepted = ['dudoxx','DUDOXX','LITELLM','litellm','DUDOXX_LITELLM','DUDOXX_LLM']— anything else throws); uses@livekit/agents-plugin-openaiLLM with the same baseURL. - TUCAN side: same gateway, different factory (
ddx-api/src/ai/tucan/registry/llm-config.factory.ts:316documentsDUDOXX_BASE_URLas the LiteLLM proxy URL). Upstream probe (dudoxx-upstream-probe.service.ts:108-113) discovers each alias' real model name; falls back to regex if probe is disabled. - Default model:
dudoxx-gemma($1 / M tokens — INTERNAL_COST tier per rootCLAUDE.md). All other tiers are ZERO_COST. - Thinking budget: 0 for voice (
MEMORY_LIVEKIT.md"thinkingBudgetTokens: 0 for voice"). Reasoning tokens MUST be 0 — verified at observability level. - Why not
model: inherit? It IS the rule for MastraAgent()calls (root project memoryfeedback_agent_model_inheritance.md) — voice agents declare the model once at workflow-compile time and pass it into every Agent factory call (workflow-compiler.ts:101,:126,:139). - Why not call providers directly? Three reasons: (1) latency monitoring, (2) per-tenant budget, (3) compliance log of every PHI-adjacent prompt. Direct OpenAI / Anthropic / vLLM is rejected at provider whitelist time.
Data Flow
- FlowAgent voice step starts:
workflow-compiler.tsproduces a step that calls a MastraAgentwithmodel: createDudoxxMastraModel({ baseURL: config.llm.baseUrl, apiKey: config.llm.apiKey, model: config.llm.model, thinkingBudgetTokens: 0 }). - The Mastra agent receives
TranscriptionFinal.text(from STT) plus prior memory and the step's tool set (≤7 voice-essential tools — seeMEMORY_LIVEKIT.md"7 essential tools"). - The
@ai-sdk/openaiclient serialises the chat request;litellmFetch(mastra/model.ts:18-103) intercepts, parses the JSON body, injectstemperature,thinkingBudgetTokens,maxOutputTokens, andenable_thinking: false, then forwards viaglobalThis.fetchtoDUDOXX_BASE_URL. - LiteLLM resolves the alias, calls the upstream provider, returns the OpenAI-compatible response.
- Mastra parses tool calls; voice-allowed tools execute (
fill_field,skip_field,batch_fill, …); the assistant text is sanitised (strip<think>, "section is complete" hallucinations, markdown) before being handed to the TTS provider (Dudoxx CUDA Live TTS; Kokoro is retired). - Observability: LiteLLM logs
wall_ms,input_tokens,output_tokens,reasoning_tokens(must be 0),cost_usd. Samemetricsblock format asrag_investigate(rootCLAUDE.md).
Implicated Code
MANDATORY: ≥3 file:line citations.
ddx-livekit-flowagent-ts/src/config.ts:21-25— single source of truth for voice-pipeline LLM config (baseUrl,apiKey,model); defaults tohttp://localhost:4000/v1(LiteLLM dev) andopenai/gpt-4o.ddx-livekit-flowagent-ts/src/mastra/model.ts:18—createOpenAIimport (AI SDK v5 OpenAI provider).ddx-livekit-flowagent-ts/src/mastra/model.ts:106-113—DudoxxMastraModelOptionsshape (baseURL,apiKey,model,temperature?,thinkingBudgetTokens?,maxOutputTokens?).ddx-livekit-flowagent-ts/src/mastra/model.ts:122-139—createDudoxxMastraModelfactory; forcesprovider.chat(model)to hit/v1/chat/completions(LiteLLM does not implement/v1/responses).ddx-livekit-flowagent-ts/src/mastra/model.ts:18-103—createLitellmFetchmiddleware: parses request body, injectsenable_thinking:false/ temperature / thinking budget, passes through toglobalThis.fetch.ddx-vivoxx/src/providers/llm.provider.ts:1-20— Vivoxx LLM provider preamble: "Direct OpenAI cloud is rejected — ALL LLM calls route through LiteLLM."ddx-vivoxx/src/providers/llm.provider.ts:36-55— provider whitelist +openaiPlugin.LLMconstruction withbaseURL: config.llm.baseUrl.ddx-api/src/ai/tucan/registry/llm-config.factory.ts:316—DUDOXX_BASE_URLdocumented as LiteLLM proxy URL.ddx-api/src/ai/tucan/registry/dudoxx-upstream-probe.service.ts:108-113— probe disabled whenDUDOXX_BASE_URL/DUDOXX_API_KEYmissing → regex fallback.ddx-livekit-flowagent-ts/src/mastra/workflow-compiler.ts:101,:126,:139— step model parameter wiring.
Operational Notes
- Gateway port: see infra-litellm.md for the production port + auth. The voice services use
DUDOXX_BASE_URL(defaulthttp://localhost:4000/v1in dev); the W4 brief mentioned port 15800 — verify against currentdocker-compose.ymlbefore assuming that number is canonical. - Provider whitelist: anything other than the accepted set in
ddx-vivoxx/src/providers/llm.provider.tsthrows. Updating must update the whitelist AND the LiteLLM config. enable_thinking: falserule: enforced at three layers — gateway default (rootCLAUDE.mdM-USER 2026-05-17),litellmFetchbody injection (model.ts), and voice-FSM rule (MEMORY_LIVEKIT.md). Ifreasoning_tokens > 0shows up in LiteLLM analytics for a voice session, it is a regression in the middleware or the gateway default.- Latency target: <1 s first-token for voice. Increasing
thinkingBudgetTokensblows this budget — measured 10-21 s for 1024 tokens (MEMORY_LIVEKIT.md). - Model substitution: change
DUDOXX_MODEL_NAMEenv or per-call override; do not edit the consumer code. - Pitfall — Mastra default endpoint: AI SDK v5
provider()calls/v1/responses(OpenAI Responses API), which LiteLLM does not support. ALWAYS useprovider.chat(model)(model.ts:135-138). This is the most common cause of "LiteLLM 404" voice regressions. - Pitfall — Groq 503 with
reply_to_operator: TUCAN Operator-side defect (MEMORY_LIVEKIT.mdKnown Open Bugs) — when a voice tool is not inrequest.tools, some upstream providers 503. Mitigation: always inject the voice tool set explicitly.
Related Topics
- LiteLLM Gateway Infra — the gateway this page sits on top of
- LiveKit Infra — voice transport
- Live STT — what produces the text the LLM consumes
- Live TTS — what consumes the LLM's text
- FlowAgent Scenarios — primary voice-side LLM consumer
- Vivoxx Voice — alternative voice-side LLM consumer (LiveKit Agents plugin)
- LiveKit FlowAgent TS Bridge — owns the
litellmFetchmiddleware