Integración Gemini Live con SIP Trunk de netelip
Conecta la API de voz en tiempo real de Google a cualquier número virtual de netelip usando LiveKit, Modal.com y Supabase como infraestructura.

Conecta la API de voz en tiempo real de Google a cualquier número virtual de netelip usando LiveKit, Modal.com y Supabase como infraestructura.
Esta guía explica cómo configurar la integración paso a paso: qué debes preparar en netelip, qué debes configurar en cada plataforma, y cómo verificar que el agente responde antes de ponerlo en producción.
A quién va dirigida: desarrolladores que integran IA conversacional con telefonía VoIP, equipos técnicos de empresas que ya operan con netelip, e integradores que construyen agentes de voz para clientes.
Esta integración conecta Gemini Live API a un número de teléfono real. El audio de cada llamada viaja a través de cuatro componentes que trabajan juntos:
| Componente | Función en la arquitectura |
|---|---|
| netelip | Operador SIP. Proporciona el número virtual y el SIP Trunk que recibe la llamada y la enruta a LiveKit. |
| LiveKit Cloud | Puente SIP ↔ WebRTC. Sin LiveKit, Gemini no puede recibir audio de una llamada telefónica. |
| Modal.com | Infraestructura serverless donde corre el worker Python con la lógica del agente. Debe estar activo 24 horas. |
| Gemini Live API | Motor de IA conversacional. Procesa el audio y genera respuestas de voz en tiempo real vía WebSocket. |
| Supabase | Base de datos donde se almacena la configuración por agente y los transcritos de cada llamada. |
| Plataforma | Qué necesitas |
|---|---|
| netelip | Número virtual activo (geográfico, nacional o internacional). SIP Trunk disponible en tu cuenta. |
| LiveKit Cloud | Cuenta creada en cloud.livekit.io |
| Modal.com | Cuenta creada en modal.com |
| Supabase | Cuenta creada con permisos para crear tablas. |
| Google AI Studio | API key en aistudio.google.com/apikey. Facturación activa en el proyecto de Google Cloud asociado. |
Accede a cloud.livekit.io y crea un proyecto nuevo. Obtendrás tres credenciales que necesitarás en los pasos siguientes:
LIVEKIT_URL → wss://tu-proyecto.livekit.cloud LIVEKIT_API_KEY → tu clave API LIVEKIT_API_SECRET → tu secreto API
Guarda estas credenciales. Las necesitarás al configurar los secrets en Modal.com (Paso 5) y al registrar el SIP Inbound Trunk en LiveKit (Paso 2).
El SIP Inbound Trunk le indica a LiveKit de dónde esperar llamadas entrantes. Apunta al número virtual de netelip que usarás con este agente.
En el panel de LiveKit, ve a SIP → Inbound Trunks → New Trunk e introduce estos datos:
- Name: el nombre que quieras (por ejemplo: Mi trunk netelip).
- Numbers: tu número virtual de netelip en formato E.164.
- Allowed IPs: dejar vacío para aceptar todas las IPs entrantes.
Una vez guardado, LiveKit genera una SIP URI de entrada:
xxxxxxxxxxxxxxx.sip.livekit.cloud
Guarda esta SIP URI. Es el destino que introducirás en la configuración de netelip en el Paso 3.
El número debe incluir el prefijo internacional, sin espacios ni guiones:
+34910000000 → número geográfico de Madrid +34930000000 → número geográfico de Barcelona +34900000000 → número de red inteligente (900) +52550000000 → número virtual de México +56220000000 → número virtual de Chile
Accede a tu panel de cliente de netelip. Localiza la configuración del número virtual o del SIP Trunk y establece como destino la SIP URI de LiveKit con este formato:
+34XXXXXXXXX@xxxxxxxxxxxxxxx.sip.livekit.cloud
- +34XXXXXXXXX → tu número virtual en netelip en formato E.164.
- xxxxxxxxxxxxxxx.sip.livekit.cloud → la SIP URI de tu proyecto en LiveKit (Paso 2).
Desde este momento, cualquier llamada que llegue a ese número viajará automáticamente vía SIP hasta LiveKit. Cuando el worker de Modal esté activo (Paso 5), el agente responderá.
| Tipo de número | Cuándo usarlo |
|---|---|
| Geográfico (91, 93, 96…) | Agente con presencia local. El llamante ve un número fijo de su ciudad. |
| Nacional 900 (gratuito) | Línea de atención centralizada. El 900 elimina la barrera del coste para el llamante. |
| Nacional 901 / 902 | Servicios con coste compartido o total. Uso en declive; valora alternativas. |
| Internacional (México, Chile…) | Agentes que atienden mercados latinoamericanos desde infraestructura en España. |
La Dispatch Rule le indica a LiveKit qué hacer cuando llega una llamada: a qué sala asignarla y qué agente debe gestionarla.
En el panel de LiveKit, ve a SIP → Dispatch Rules → New Rule e introduce esta configuración en JSON:
{
"agent_name": "gemini-sip-agent",
"metadata": "{\"agent_id\": \"TU_UUID_DEL_AGENTE\"}"
}
| Campo | Explicación |
|---|---|
| agent_name | Debe coincidir exactamente con el nombre del worker Python en Modal. Sensible a mayúsculas, espacios y guiones. |
| metadata | JSON en formato string. Transporta el agent_id al worker para cargar la configuración desde Supabase. |
Punto crítico: el valor de agent_name en la Dispatch Rule y el nombre del worker en Modal deben ser idénticos carácter a carácter. Cualquier diferencia —mayúsculas, espacios, guiones— rompe el enrutamiento y la llamada queda sin respuesta.
Modal.com ejecuta el worker Python con la lógica del agente. Es imprescindible mantener siempre una instancia activa para que las llamadas no queden sin respuesta.
Por qué min_containers=1 es imprescindible: sin esta configuración, Modal apaga el worker cuando no hay actividad. El arranque en frío tarda 10-15 segundos: tiempo suficiente para que la llamada ya haya colgado. Con min_containers=1 siempre hay una instancia esperando. Para llamadas simultáneas, aumenta este valor según el volumen esperado. Coste: Modal cobra por tiempo de ejecución continuo — consulta su página de precios antes de desplegar.
image = (
modal.Image.debian_slim(python_version='3.11')
.pip_install(
'livekit-agents[google]>=1.0',
'livekit-plugins-google>=1.0',
'supabase>=2.0',
'google-generativeai>=0.8',
)
)
Carga la configuración del agente desde Supabase, abre la sesión con Gemini y guarda el transcrito al colgar.
async def entrypoint(ctx):
await ctx.connect()
agent_id = json.loads(ctx.job.metadata).get('agent_id')
config = get_agent_config(agent_id) # carga desde Supabase
session = AgentSession(
llm=google.beta.realtime.RealtimeModel(
model='gemini-2.0-flash-live-preview',
api_key=config['gemini_api_key'],
voice=config['voice_name'],
)
)
await session.start(room=ctx.room, agent=VoiceAgent())
await session.wait_for_disconnect()
save_session(...) # guarda transcript en Supabase
@app.function(
secrets=['supabase-credentials', 'livekit-credentials', 'google-api-key'],
timeout=86400,
min_containers=1, # CRÍTICO: siempre hay 1 instancia activa
)
def run_worker():
sys.argv = ['agent', 'start',
'--url', os.environ['LIVEKIT_URL'],
'--api-key', os.environ['LIVEKIT_API_KEY'],
'--api-secret', os.environ['LIVEKIT_API_SECRET'],
]
cli.run_app(WorkerOptions(
entrypoint_fnc=entrypoint,
agent_name='gemini-sip-agent', # idéntico al de la Dispatch Rule
))
Nota — agent_id en ctx.job.metadata: el agent_id se pasa en el metadata de la Dispatch Rule y el worker lo lee desde ctx.job.metadata, no desde ctx.room.metadata. El código debería comprobar ambas ubicaciones por seguridad.
Nota — sys.argv injection: cli.run_app espera argumentos de línea de comandos. Modal no tiene CLI en ejecución, así que los argumentos se inyectan manualmente en sys.argv. Es la forma correcta de inicializar el worker en este entorno.
| Nombre del secret | Variables que debe contener |
|---|---|
| supabase-credentials | SUPABASE_URL, SUPABASE_SERVICE_KEY |
| livekit-credentials | LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET |
| google-api-key | GOOGLE_API_KEY — fallback si el agente no tiene su propia API key de Google |
pip install modal modal setup # autentica con tu cuenta de Modal modal deploy agent.py # despliega el worker; queda corriendo permanentemente
Supabase almacena la configuración individual de cada agente y los transcritos. El worker carga esta configuración al inicio de cada llamada usando el agent_id recibido en el metadata de la Dispatch Rule.
Crea la tabla agents con estos campos:
| Campo | Descripción |
|---|---|
| gemini_api_key | API key de Google AI Studio del propietario del agente. Puede ser por cliente o global. |
| voice_name | Voz de Gemini. Opciones: Puck, Charon, Kore, Fenrir, Aoede, Leda, Orus, Zephyr. |
| system_prompt | Instrucciones del agente: rol, tono, límites, información del negocio, respuestas tipo. |
| greeting | Texto que el agente pronuncia automáticamente al conectar la llamada. |
| user_id | Identificador del propietario del agente en la plataforma. |
Asegúrate de tener al menos un registro con el agent_id que usas en la Dispatch Rule antes de hacer la primera prueba.
API key por agente: cada agente puede tener su propia API key de Google, que el worker carga desde Supabase al inicio de la llamada. Si no existe, usa la key global configurada como secret en Modal. Esto permite que múltiples clientes compartan la plataforma con sus propias credenciales.
Límite de duración de sesión: Gemini Live tiene un límite por conexión WebSocket (actualmente en torno a 15 minutos). Para llamadas largas, implementa lógica de reconexión automática o avisa al llamante antes de que se agote el tiempo.
Lo que ocurre internamente cada vez que alguien llama al número virtual:
agent_name='gemini-sip-agent'.
agent_id.
ctx.connect() y entra en la sala de LiveKit.
El agente atiende llamadas fuera de horario que antes iban a buzón de voz o se perdían. Recoge nombre, empresa, motivo de contacto y disponibilidad horaria, y guarda el lead cualificado en el CRM. Número recomendado: geográfico (91, 93…) vinculado al número habitual. Sin cambios visibles para el llamante.
Un número 900 (gratuito para el llamante) conectado al agente. Resuelve consultas frecuentes de forma autónoma y escala las complejas a un agente humano mediante transferencia. El equipo humano solo recibe las que superan la capacidad del agente.
Una empresa española activa un número virtual de México, Chile o Colombia en netelip. El agente atiende en el idioma y tono adecuado para ese mercado con un system_prompt específico por país. Todo corre desde la misma infraestructura. El llamante ve un número local.
Agente configurado con la documentación técnica del producto como system_prompt. Resuelve incidencias comunes, guía al usuario paso a paso y escala solo las que superan su capacidad. Cada llamada queda transcrita en Supabase para análisis posterior.
Los clientes que ya tienen Centralita Virtual en netelip pueden integrar el agente Gemini en un punto concreto del IVR. El agente atiende fuera de horario mientras que en horario laboral las llamadas van al equipo humano con el flujo habitual. No hay que reemplazar nada de lo que ya funciona.
El mismo agente puede atender llamadas desde un widget web, compartiendo la misma configuración en Supabase: system_prompt, voz y API key.
- Vía teléfono: el llamante marca el número de netelip y la llamada entra por SIP.
- Vía web: el visitante pulsa el botón del widget y habla directamente desde el navegador.
| Síntoma | Causa probable y solución |
|---|---|
| La llamada llega pero nadie responde | El worker no está activo. Verifica que min_containers=1 está configurado y el deploy está running en Modal. |
| El agente responde con retraso de 10-15 s | El worker arrancó en frío. Confirma que min_containers=1 está activo en run_worker. |
| La llamada no llega a LiveKit | Revisa la configuración SIP en netelip. El destino debe ser: número E.164 @ SIP URI de LiveKit. |
| LiveKit no asigna agente a la llamada | El agent_name en la Dispatch Rule no coincide con el del worker en Modal. Deben ser idénticos. |
| El agente no carga su configuración | El agent_id en el metadata de la Dispatch Rule no existe en la tabla agents de Supabase. |
| La llamada se corta a los 15 minutos | Límite de sesión de Gemini Live. Implementa reconexión automática o avisa al llamante. |
| Dos llamadas simultáneas, una sin atender | Solo hay una instancia activa. Aumenta min_containers según el volumen esperado. |
| Las llamadas salientes no funcionan | Esta arquitectura es para llamadas entrantes. Las salientes requieren un SIP Outbound Trunk en LiveKit. |
Para preguntas sobre el SIP Trunk o los números virtuales, abre un ticket desde tu panel privado de cliente.
Para la configuración de LiveKit, Modal y Gemini, consulta la documentación oficial de cada plataforma.

Volver