La API de MKA1 proporciona una interfaz de voz en tiempo real a través de LiveKit. Esta guía cubre cómo obtener un token de sala, conectarse a una sesión de voz, enviar audio y texto como entrada, y capturar las respuestas del agente.
Descripción general
La integración de voz consta de tres componentes principales:
- Token de Sala: Un JWT que otorga acceso a una sala de LiveKit
- Conexión LiveKit: Comunicación en tiempo real basada en WebRTC
- Agente de Voz: Procesa entradas de audio/texto y genera respuestas habladas
El flujo de trabajo del agente es el siguiente:
- STT (Speech-to-Text): El audio se transmite por WebSocket a 16kHz y se transcribe
- LLM: El texto transcrito es procesado por la API de Respuestas de MKA1
- TTS (Text-to-Speech): La salida del LLM se sintetiza a audio a 24kHz
Cada solicitud que el agente de voz envía a la API de Respuestas incluye automáticamente "voice_mode": "true" en el metadata de la solicitud. Esto te permite distinguir respuestas originadas por voz de las basadas en texto al revisar el uso o el historial de respuestas.
Obtener un token de sala
Para iniciar una sesión de voz, primero solicita un token de sala a la API de MKA1. El endpoint de token requiere una clave API y opcionalmente acepta X-On-Behalf-Of para identificar usuarios finales. Consulta Autenticación para más detalles.
import { SDK } from '@meetkai/mka1';
const mka1 = new SDK({
bearerAuth: `Bearer ${YOUR_API_KEY}`,
});
const session = await mka1.llm.speech.livekitToken({
llm: {
model: 'gpt-5',
reasoning: { effort: 'none' }
}
}, { headers: { 'X-On-Behalf-Of': 'user-123' } }); // Opcional
console.log(session.token); // Token JWT
console.log(session.url); // URL de WebSocket
console.log(session.roomName); // Nombre de la sala
Parámetros
El cuerpo de la solicitud tiene dos objetos de nivel superior:
{
"llm": { ... }, // Requerido — configuración del LLM
"stt": { ... } // Opcional — ajuste de speech-to-text
}
llm — Configuración del LLM (requerido)
El objeto llm acepta los mismos campos que el cuerpo de la solicitud de la API de Respuestas, menos los campos gestionados por el agente de voz (input, stream, store, background).
| Campo | Requerido | Descripción |
|---|
model | Sí | Modelo LLM a usar (por ejemplo, gpt-5) |
instructions | No | Instrucciones personalizadas del sistema para el agente |
previous_response_id | No | Encadena esta sesión a una respuesta específica de una sesión anterior |
conversation | No | Continúa una conversación existente — pasa { "id": "conv_abc123..." } o el ID de la conversación como string |
tools | No | Array de definiciones de herramientas (function, web_search, file_search, etc.) |
tool_choice | No | Cómo el modelo selecciona herramientas ("auto", "none", "required", o una herramienta específica) |
parallel_tool_calls | No | Permitir ejecución paralela de herramientas |
max_tool_calls | No | Máximo de llamadas a herramientas por respuesta (por defecto: 30) |
temperature | No | Temperatura de muestreo (por ejemplo, 0.7) |
max_output_tokens | No | Máximo de tokens en la respuesta |
reasoning | No | Configuración de razonamiento (por ejemplo, { "effort": "high" }). Establece { "effort": "none" } para sesiones de voz para minimizar la latencia — ver nota abajo. |
top_p | No | Parámetro de muestreo nucleus |
presence_penalty | No | Penalización de presencia para repetición de tokens |
frequency_penalty | No | Penalización de frecuencia para repetición de tokens |
truncation | No | "auto" o "disabled" — controla el truncamiento de contexto |
context_management | No | Estrategias de gestión de contexto para truncamiento de conversación |
service_tier | No | "auto", "default", "flex", o "priority" |
prompt | No | Referencia a una plantilla de prompt y sus variables |
text | No | Configuración de salida de texto (formato, verbosidad) |
metadata | No | Metadatos clave-valor enviados a la API de Respuestas |
No puedes especificar ambos previous_response_id y conversation.
Los metadatos del token se incrustan en un JWT, que se pasa como cabecera HTTP. Mantén el payload total de llm por debajo de ~8 KB — los arrays grandes de tools pueden necesitar ser recortados.
Para sesiones de voz, desactiva el razonamiento configurando "reasoning": { "effort": "none" }. El razonamiento añade tiempo de pensamiento antes de que el modelo responda, lo que incrementa la latencia y crea pausas notables en la conversación. Desactivarlo mantiene las respuestas rápidas y naturales.
stt — Configuración de speech-to-text (opcional)
Controla la detección de actividad de voz (VAD) y el comportamiento de endpointing en el servidor.
| Campo | Requerido | Descripción |
|---|
silence_timeout_ms | No | Milisegundos de silencio antes de finalizar el habla (100–5000) |
initial_silence_timeout_ms | No | Tiempo de espera antes de detectar cualquier habla (1000–30000) |
Configuración avanzada
Puedes pasar herramientas, instrucciones personalizadas y ajuste de STT en una sola solicitud de token:
const session = await mka1.llm.speech.livekitToken({
llm: {
model: 'gpt-5',
instructions: 'Eres un asistente de viajes útil. Sé conciso en las respuestas de voz.',
temperature: 0.7,
tools: [
{
type: 'web_search',
user_location: { country: 'US' }
},
{
type: 'function',
name: 'book_flight',
description: 'Reservar un vuelo para el usuario',
parameters: {
type: 'object',
properties: {
origin: { type: 'string' },
destination: { type: 'string' },
date: { type: 'string' }
},
required: ['origin', 'destination', 'date']
}
}
],
tool_choice: 'auto'
},
stt: {
silence_timeout_ms: 500,
initial_silence_timeout_ms: 10000
}
});
Respuesta
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"url": "wss://apigw.mka1.com/api/v1/livekit",
"roomName": "550e8400-e29b-41d4-a716-446655440000"
}
| Campo | Descripción |
|---|
token | Token de acceso JWT (TTL de 5 minutos) con permisos para unirse, publicar y suscribirse a la sala |
url | URL de WebSocket de LiveKit para conectarse |
roomName | UUID autogenerado para esta sesión |
El token incluye metadatos que el agente de voz utiliza para configurar la sesión.
Continuar una sesión
Para continuar desde una respuesta anterior:
const mka1 = new SDK({
bearerAuth: `Bearer ${YOUR_API_KEY}`,
});
const session = await mka1.llm.speech.livekitToken({
llm: {
model: 'gpt-5',
previousResponseId: 'resp_abc123...'
}
}, { headers: { 'X-On-Behalf-Of': 'user-123' } }); // Opcional, pero debe coincidir si la sesión original lo usó
Para continuar una conversación existente:
const mka1 = new SDK({
bearerAuth: `Bearer ${YOUR_API_KEY}`,
});
const session = await mka1.llm.speech.livekitToken({
llm: {
model: 'gpt-5',
conversation: { id: 'conv_abc123...' }
}
}, { headers: { 'X-On-Behalf-Of': 'user-123' } }); // Opcional, pero debe coincidir si la sesión original lo usó
Al continuar una sesión, la clave API y la cabecera X-On-Behalf-Of (si se usa) deben coincidir con la sesión original. El agente de voz cifra ambos en el token de sala y los pasa a todos los servicios MKA1 aguas abajo. Si no coinciden, el agente no tendrá acceso al contexto anterior.
Conectarse a una sala
Una vez que tengas un token, usa el SDK de LiveKit para conectarte a la sala.
import { Room, RoomEvent, Track } from 'livekit-client';
const room = new Room();
// Conéctate a la sala
await room.connect(session.url, session.token);
console.log('Conectado a la sala:', room.name);
Enviar entrada de audio
El agente acepta entrada de audio a través de la pista de audio de la sala de LiveKit. El audio se procesa a una frecuencia de muestreo de 16kHz.
import { createLocalAudioTrack } from 'livekit-client';
// Crea una pista de audio local desde el micrófono
const audioTrack = await createLocalAudioTrack({
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
});
// Publica la pista en la sala
await room.localParticipant.publishTrack(audioTrack);
Comportamiento del audio
- Detección de Actividad de Voz (VAD): El VAD se maneja en el servidor por el agente MKA1, no localmente. El agente detecta automáticamente cuando dejas de hablar y comienza el procesamiento.
- Frecuencia de muestreo: El audio se transmite a 16kHz al servicio STT.
- Endpointing: El agente utiliza endpointing del lado del servidor para determinar cuándo termina el habla. No hay retardo de endpointing local.
Enviar entrada de texto
También puedes enviar mensajes de texto directamente al agente sin hablar.
// Envía un mensaje de texto al agente
const message = JSON.stringify({
type: 'user_message',
content: '¿Cuál es la capital de Francia?'
});
await room.localParticipant.publishData(
new TextEncoder().encode(message),
{ reliable: true, topic: 'lk.chat' }
);
Recibir respuestas del agente
El agente responde de tres maneras:
- Salida de audio: Voz sintetizada a través de una pista de audio
- Transcripción: Texto de lo que el agente está diciendo (para subtítulos)
- Metadatos de respuesta: ID de respuesta e ID de conversación a través del canal de datos
Suscribirse a la salida de audio
import { RoomEvent, Track } from 'livekit-client';
room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
if (track.kind === Track.Kind.Audio && participant.identity !== room.localParticipant.identity) {
// Esta es la salida de audio del agente
const audioElement = track.attach();
document.body.appendChild(audioElement);
}
});
Recibir transcripciones
El agente publica transcripciones de su habla. Puedes usarlas para subtítulos o registros.
room.on(RoomEvent.TranscriptionReceived, (segments, participant) => {
for (const segment of segments) {
console.log(`El agente dijo: ${segment.text}`);
}
});
El agente publica el response_id y conversation_id (si aplica) cuando comienza a generar una respuesta. Guarda el response_id para encadenar futuras sesiones usando previous_response_id.
room.on(RoomEvent.DataReceived, (payload, participant) => {
if (participant.identity !== room.localParticipant.identity) {
const data = JSON.parse(new TextDecoder().decode(payload));
if (data.response_id) {
console.log('ID de respuesta:', data.response_id);
console.log('ID de conversación:', data.conversation_id); // presente si se usa una conversación
// Guarda response_id para encadenar futuras sesiones con previous_response_id
}
}
});
Continuidad de la conversación
El agente soporta conversaciones de varios turnos con memoria persistente. Cada respuesta recibe automáticamente un response_id, mientras que las conversaciones deben ser creadas y gestionadas explícitamente a través de la API de Conversaciones.
Hay dos formas de continuar una conversación:
llm.previous_response_id encadena una nueva sesión a una respuesta específica. El agente recibe el contexto de esa respuesta y todas las respuestas previas en la cadena. Úsalo cuando:
- Quieres continuar desde un punto específico en una conversación
- Estás construyendo un flujo de conversación lineal
- Quieres ramificar desde una respuesta específica
llm.conversation hace referencia a una conversación creada mediante la API de Conversaciones. Úsalo cuando:
- Necesitas gestionar metadatos de la conversación (títulos, etiquetas, etc.)
- Quieres listar o buscar conversaciones pasadas
- Estás construyendo una interfaz de chat con historial de conversación persistente
- Múltiples clientes necesitan acceder a la misma conversación
Iniciar una nueva sesión
import { Room, RoomEvent } from 'livekit-client';
import { SDK } from '@meetkai/mka1';
const mka1 = new SDK({
bearerAuth: `Bearer ${YOUR_API_KEY}`,
});
// 1. Obtén un token para una nueva sesión
const session = await mka1.llm.speech.livekitToken({
llm: { model: 'gpt-5' }
}, { headers: { 'X-On-Behalf-Of': 'user-123' } }); // Opcional
// 2. Conéctate a la sala
const room = new Room();
await room.connect(session.url, session.token);
// 3. Rastrea el ID de respuesta cuando el agente responda
let lastResponseId: string;
room.on(RoomEvent.DataReceived, (payload, participant) => {
const data = JSON.parse(new TextDecoder().decode(payload));
if (data.response_id) {
lastResponseId = data.response_id;
}
});
// 4. Conversa...
// 5. Desconéctate cuando termines
room.disconnect();
Continuar desde una respuesta anterior
Usa previous_response_id para encadenar una nueva sesión a la última respuesta, preservando el contexto de la conversación:
// Usa la misma clave API y X-On-Behalf-Of que la sesión original
const mka1 = new SDK({
bearerAuth: `Bearer ${YOUR_API_KEY}`,
});
// 1. Obtén un nuevo token encadenado a la respuesta anterior
const session = await mka1.llm.speech.livekitToken({
llm: {
model: 'gpt-5',
previousResponseId: lastResponseId
}
}, { headers: { 'X-On-Behalf-Of': 'user-123' } }); // Opcional, pero debe coincidir si la sesión original lo usó
// 2. Conéctate a la nueva sala
const room = new Room();
await room.connect(session.url, session.token);
// 3. El agente ahora tiene contexto de la sesión anterior
// Usuario: "¿Qué te pregunté antes?"
// Agente: "Me preguntaste sobre la capital de Francia..."
Continuar desde una conversación
Usa conversation_id para continuar una conversación existente creada mediante la API de Conversaciones:
// Usa la misma clave API y X-On-Behalf-Of que la sesión original
const mka1 = new SDK({
bearerAuth: `Bearer ${YOUR_API_KEY}`,
});
// 1. Obtén un nuevo token con el ID de la conversación
const session = await mka1.llm.speech.livekitToken({
llm: {
model: 'gpt-5',
conversation: { id: conversationId }
}
}, { headers: { 'X-On-Behalf-Of': 'user-123' } }); // Opcional, pero debe coincidir si la sesión original lo usó
// 2. Conéctate a la nueva sala
const room = new Room();
await room.connect(session.url, session.token);
// 3. El agente ahora tiene contexto de todo el historial de la conversación
Al continuar una conversación, la clave API y la cabecera X-On-Behalf-Of (si se usa) deben coincidir con la sesión original. El contexto está limitado a la identidad autenticada.
Manejo de desconexiones
Los tokens expiran después de 5 minutos. Si necesitas sesiones más largas, implementa lógica de reconexión:
room.on(RoomEvent.Disconnected, async () => {
console.log('Desconectado de la sala');
// Obtén un nuevo token (continuando desde la última respuesta)
const newSession = await mka1.llm.speech.livekitToken({
llm: {
model: 'gpt-5',
previousResponseId: savedResponseId
}
});
// Reconéctate
await room.connect(newSession.url, newSession.token);
});
Ejemplo completo
Aquí tienes un ejemplo completo integrando todo:
import { Room, RoomEvent, Track, createLocalAudioTrack } from 'livekit-client';
import { SDK } from '@meetkai/mka1';
async function startVoiceSession(model: string = 'gpt-5') {
const mka1 = new SDK({ bearerAuth: `Bearer ${YOUR_API_KEY}` });
// Obtén credenciales de sala
const session = await mka1.llm.speech.livekitToken({ llm: { model } });
// Crea y conéctate a la sala
const room = new Room();
let lastResponseId: string | undefined;
// Maneja la salida de audio del agente
room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
if (track.kind === Track.Kind.Audio) {
const audio = track.attach();
document.body.appendChild(audio);
}
});
// Maneja transcripciones
room.on(RoomEvent.TranscriptionReceived, (segments) => {
for (const segment of segments) {
console.log('Agente:', segment.text);
}
});
// Maneja metadatos de respuesta
room.on(RoomEvent.DataReceived, (payload, participant) => {
const data = JSON.parse(new TextDecoder().decode(payload));
if (data.response_id) {
lastResponseId = data.response_id;
}
});
// Conéctate a la sala
await room.connect(session.url, session.token);
// Captura y publica el micrófono
const audioTrack = await createLocalAudioTrack({
echoCancellation: true,
noiseSuppression: true
});
await room.localParticipant.publishTrack(audioTrack);
// El agente te saludará automáticamente
// ¡Comienza a hablar para interactuar!
return { room, getLastResponseId: () => lastResponseId };
}
Manejo de errores
Errores del endpoint de token
Estos se devuelven como respuestas HTTP al solicitar un token de sala:
| Error | Causa | Solución |
|---|
| 400 Bad Request | Falta el parámetro requerido llm.model | Incluye model dentro del objeto llm |
| 400 Bad Request | Se especificaron ambos previous_response_id y conversation | Usa solo uno, no ambos |
| 401 Unauthorized | Clave API inválida o ausente | Verifica que tu clave API sea válida |
Errores en sesión
Durante una sesión de voz activa, el agente publica errores a través del canal de datos de LiveKit. Escúchalos junto con los metadatos de respuesta:
room.on(RoomEvent.DataReceived, (payload, participant) => {
if (participant.identity === room.localParticipant.identity) return;
const data = JSON.parse(new TextDecoder().decode(payload));
if (data.error) {
console.error(`[${data.error.service}] ${data.error.code}: ${data.error.message}`);
// data.error.details puede contener información adicional para depuración
}
if (data.response_id) {
lastResponseId = data.response_id;
}
});
La estructura del payload de error:
{
"error": {
"code": "rate_limited",
"message": "HTTP 429",
"service": "llm",
"details": "..."
}
}
| Campo | Descripción |
|---|
code | Código de error (ver tabla abajo) |
message | Descripción corta del error |
service | Qué parte del pipeline falló: llm, stt, o tts |
details | Contexto adicional para depuración (opcional) |
Códigos de error:
| Código | Servicio | Causa |
|---|
invalid_session | — | Faltan campos de metadatos requeridos (sub, llm) |
auth_error | — | Fallo al descifrar credenciales del token de sala |
session_error | — | El agente no pudo iniciar la sesión de voz |
invalid_request | llm | Solicitud incorrecta a la API de Respuestas (HTTP 400) |
auth_error | llm | Clave API inválida (HTTP 401) |
access_denied | llm | Permisos insuficientes (HTTP 403) |
rate_limited | llm | Límite de tasa excedido (HTTP 429) |
service_error | llm | Error interno del servidor (HTTP 500) |
service_unavailable | llm | Servicio upstream no disponible (HTTP 502/503) |
timeout | llm | Tiempo de espera excedido (HTTP 504) |
connection_error | llm | Fallo al conectar con la API de Respuestas |
transcription_error | stt | Fallo en el procesamiento de speech-to-text |
speech_error | tts | Fallo en la síntesis de text-to-speech |
Errores de conexión
| Problema | Causa | Solución |
|---|
| Tiempo de espera de conexión | Problemas de red o token inválido | Obtén un token nuevo y reintenta |
| Token expirado | Sesión excedió los 5 minutos | Obtén un nuevo token con previous_response_id para continuar |
Próximos pasos