Saltar al contenido principal
intermediatePart 2

Enrutamiento de complejidad de LiteLLM: el modelo adecuado para cada solicitud y su costo en latencia

· 13 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
LiteLLM+Qwen+
0/5
🎯 Skill path0/5 earned
Self-hosting an LLM gateway

Parte 1 terminó en un número incómodo. La misma respuesta de tres palabras costó 3 tokens de un modelo pequeño y 200 de un modelo de razonamiento — que gastó los 200 pensando y no devolvió nada en absoluto.

Cada solicitud que envían tus aplicaciones elige un modelo, y en su mayoría esa elección se hace una vez, codificada de forma rígida, y nunca se revisita. Esta publicación pone al gateway a cargo de ello en su lugar: clasifica la solicitud, la enruta a un modelo adecuado para el trabajo. Luego mide lo que cuesta esa decisión, porque no es gratis y la mayoría de los informes omiten esa parte.

Agregando el enrutador de complejidad de LiteLLM

El gateway de la Parte 1 ya tiene tres modelos registrados. Un enrutador es solo otra entrada en model_list que asigna niveles de complejidad a ellos:

config.yaml
- model_name: smart-router
litellm_params:
model: auto_router/complexity_router
complexity_router_config:
tiers:
SIMPLE: qwen-small
MEDIUM: qwen-mid
COMPLEX: qwen-large
REASONING: qwen-large
return_raw_model_name: true

return_raw_model_name: true es el importante por ahora. Sin él, la respuesta informa smart-router y no tienes idea de qué modelo te sirvió. Con él, la respuesta nombra el modelo que realmente se ejecutó — así que cada prueba a continuación se verifica por sí misma.

docker compose restart litellm

Observando cómo enruta

Enviarás la misma solicitud muchas veces con solo el prompt cambiando, así que envuélvelo una vez:

ask() { curl -s http://127.0.0.1:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d "{\"model\":\"smart-router\",\"messages\":[{\"role\":\"user\",\"content\":\"$1\"}],\"max_tokens\":10}" | jq -r .model; }

Luego cinco prompts de dificultad creciente:

ask "hi"
ask "what is a vpc"
ask "write a python function that retries an http call with backoff"
ask "our two services deadlock under load, walk me through diagnosing it"
ask "prove that the halting problem is undecidable"

Una línea por prompt, en el orden enviado:

Output
Qwen2.5-3B-Instruct
Qwen2.5-3B-Instruct
Qwen3.5-9B
Qwen2.5-3B-Instruct
Qwen2.5-3B-Instruct

Cinco prompts enrutados por el puntuador heurístico, cuatro aterrizando en el modelo de 3B Figura 1. La solicitud de código subió un nivel. Las dos preguntas más difíciles no.

Un saludo en el modelo pequeño es correcto. Una solicitud de Python en el modelo medio es correcta. Pero un bloqueo de sistemas distribuidos y una de las preguntas más difíciles en ciencias de la computación aterrizaron en un modelo de 3 mil millones de parámetros.

Para entender por qué, debes saber lo que realmente está haciendo el enrutador.

Qué es una heurística y cómo LiteLLM puntúa una

Por defecto, este enrutador no hace llamadas a la API. Puntúa el prompt localmente con coincidencias de patrones — eso es lo que "heurística" significa aquí: una regla de oro barata que aproxima un juicio sin hacerlo.

Puntúa siete dimensiones, cada una produciendo un valor entre −1 y +1, luego multiplica cada una por un peso fijo y las suma:

DimensiónPesoSe activa en
codePresence0.30function, class, api, schema, …
reasoningMarkers0.25"paso a paso", "pensar a través", "analizar"
technicalTerms0.25"arquitectura", "distribuido", "cifrado"
tokenCount0.10−1.0 bajo 15 tokens, +1.0 sobre 400
simpleIndicators0.05"qué es", "definir", saludos — puntúa −1.0
multiStepPatterns0.03"primero… luego", pasos numerados
questionComplexity0.02más de tres signos de interrogación

La suma ponderada se asigna a un nivel en tres límites: por debajo de 0.15 es SIMPLE, por debajo de 0.35 MEDIUM, por debajo de 0.60 COMPLEX, y por encima de eso REASONING. Todos esos valores están documentados y son configurables.

Dos dimensiones pueden empujar la puntuación hacia abajo. Un prompt corto puntúa −1.0 en tokenCount. Un prompt que contiene "qué es" puntúa −1.0 en simpleIndicators.

La aritmética, en un prompt real

El enrutador registra su propio trabajo. Cada solicitud enrutada escribe una decisión en el registro de gastos:

curl -s http://127.0.0.1:4000/spend/logs \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
| jq -r '.[].metadata.routing_decision | select(.) | "\(.tier) \(.score) -> \(.routed_model) \(.signals)"' | head -5

Más reciente primero, así que esto se lee de abajo hacia arriba en contra del orden en que se enviaron los prompts:

Output
SIMPLE -0.1 -> qwen-small ["corto (11 tokens)"]
SIMPLE 0 -> qwen-small null
MEDIUM 0.3 -> qwen-mid ["código (función, python)"]
SIMPLE -0.15000000000000002 -> qwen-small ["corto (3 tokens)","simple (qué es)"]
SIMPLE -0.15000000000000002 -> qwen-small ["corto (0 tokens)","simple (hola)"]

Decisiones de enrutamiento que muestran nivel, puntuación y las señales detrás de cada una Figura 2. No es una caja negra: el enrutador informa la puntuación y las señales que se activaron.

Toma el último. prueba que el problema de parada es indecidible:

tokenCount -1.0 × 0.10 = -0.10 11 tokens, bajo el umbral de 15
codePresence 0.0 × 0.30 = 0.00 sin palabras clave de código
reasoningMarkers 0.0 × 0.25 = 0.00 "prueba" no está en la lista de marcadores
technicalTerms 0.0 × 0.25 = 0.00
simpleIndicators 0.0 × 0.05 = 0.00
multiStepPatterns 0.0 × 0.03 = 0.00
questionComplexity 0.0 × 0.02 = 0.00
------
-0.10 por debajo de 0.15 → SIMPLE → modelo 3B

Cada término es cero excepto una penalización por ser corto. La pregunta se enruta hacia abajo porque es breve.

Ahora el prompt de bloqueo, que es peor: puntuó 0.00 sin señales en absoluto. No es una puntuación baja — nada coincidió. "Nuestros dos servicios se bloquean bajo carga, guíame a través del diagnóstico" no contiene ninguna palabra clave que el puntuador reconozca, por lo que cae en SIMPLE por defecto.

Habilidad desbloqueada 🏅

Puedes leer una decisión de enrutamiento — nivel, puntuación y las señales que la produjeron — y reproducir la aritmética a mano a partir de los pesos de las dimensiones.

El enrutador no está fallando. Está haciendo exactamente lo que dicen sus reglas. Las reglas simplemente no tienen forma de ver la dificultad que no esté escrita en el vocabulario que conoce.

Donde esto duele

El modo de falla es sistemático, no aleatorio: los prompts cortos que necesitan un pensamiento profundo se enrutan hacia abajo. Esos son también los prompts donde un modelo incorrecto es más obvio para el usuario.

Una palabra clave nunca coincide con su propio plural

Las palabras clave de una sola palabra se coinciden en los límites de las palabras, por lo que endpoint no coincide con endpoints. Cada palabra clave de una sola palabra en las listas predeterminadas es singular, y ninguna de ellas coincide con un plural.

Si eso cambia algo depende de cuán cerca esté la puntuación de un límite, porque las dimensiones son escalonadas en lugar de lineales — technicalTerms puntúa 0.5 en dos coincidencias y 1.0 en cuatro, por lo que perder una coincidencia a menudo no cambia nada. A veces cambia el modelo:

Revisa nuestro api endpoint para problemas de autenticación y autorización → COMPLEJO +0.425
Revisa nuestros api endpoints para problemas de autenticación y autorización → MEDIUM +0.275

Una letra, un nivel. La dirección siempre es la misma: un plural puntúa más bajo o igual, nunca más alto, por lo que la deriva es hacia el modelo más barato.

Los plurales que te importan se pueden agregar a la lista técnica con custom_technical_keywords. No hay equivalente para palabras clave de código — la única palanca es code_keywords, que reemplaza la lista incorporada en lugar de extenderla.

Preguntando a un modelo en su lugar

La alternativa es gastar una llamada de modelo en la decisión. Cuatro líneas:

config.yaml
classifier_type: llm
classifier_llm_config:
model: qwen-small
timeout_ms: 3000

Ahora el enrutador envía el prompt a qwen-small con un rubro y un esquema que obliga a devolver exactamente uno de SIMPLE, MEDIUM, COMPLEX, REASONING, y enruta según la respuesta. El clasificador aquí es el modelo más barato que tenemos — el mismo 3B que estaba respondiendo incorrectamente a las preguntas difíciles hace un momento. Resulta ser un mejor juez de dificultad de lo que es un respondedor de ella.

Reinicia, luego vuelve a ejecutar los cinco prompts idénticos:

Output
Qwen2.5-3B-Instruct
Qwen3.5-9B
Qwen3.5-122B
Qwen3.5-122B
Qwen3.5-122B

Los mismos cinco prompts bajo el clasificador LLM, con los difíciles ahora en el modelo de 122B Figura 3. Ambos fallos corregidos — y dos prompts subieron que, argumentablemente, no deberían haberlo hecho.

PromptHeurísticaClasificador LLM
hi3B3B
qué es un vpc3B9B
función de python … retroceso9B122B
bloqueo bajo carga3B ✗122B ✓
problema de parada3B ✗122B ✓

Los dos casos rotos están arreglados. Pero lee de nuevo las filas 2 y 3 — todo subió. "¿Qué es un vpc?" es una búsqueda fáctica que un modelo de 3B responde perfectamente, y ahora se ejecuta en el 9B. Has dejado de subestimar los prompts difíciles y comenzado a sobreestimar los fáciles. Si esa compensación vale la pena depende de tu mezcla de tráfico, y deberías medir la tuya en lugar de confiar en esta tabla.

Lo que cuesta

Esta es la parte que se omite. Agrega un gemelo que omita el enrutador, luego mide ambos:

direct() { curl -s http://127.0.0.1:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d "{\"model\":\"qwen-small\",\"messages\":[{\"role\":\"user\",\"content\":\"$1\"}],\"max_tokens\":10}" | jq -r .model; }
time ask "hi"
time ask "hi"
time direct "hi"
Output
Qwen2.5-3B-Instruct real 0m2.474s ← primera llamada después del reinicio
Qwen2.5-3B-Instruct real 0m1.041s
qwen-small real 0m0.529s

salida de tiempo comparando una llamada enrutada contra una llamada directa Figura 4. Mismo prompt, mismo modelo lo responde, el doble del tiempo de reloj.

El mismo modelo de 3B produjo ambas respuestas. La única diferencia es que una solicitud fue clasificada primero. Ahora descompónlo: el registro de gastos mide cada llamada por separado:

RelojLlamada del clasificadorLlamada de servicio
2.474 s (frío)1449 ms806 ms
1.041 s549 ms456 ms
0.529 s (directo)495 ms

La aritmética cierra: 549 + 456 = 1005 ms contra 1.041 s medidos en la terminal.

Tres cosas se desprenden:

El costo adicional no es constante. A través de las muestras del día, la llamada del clasificador varió de 476 ms a 1449 ms, y la primera solicitud después de un reinicio costó 1449 ms por sí sola. Es una llamada de inferencia completa y hereda lo que sea que esté haciendo el backend. Cualquier número único que cites para ello es el número que te tocó capturar.

El costo de tokens es fijo y mayor de lo que pensarías. Cada clasificación envió 281–293 tokens de entrada — el rubro — y devolvió alrededor de 10. Enrutar hi, un prompt de un token, cuesta ~281 tokens de clasificación antes de que algo responda.

La llamada de servicio también se ralentiza. 456–806 ms cuando se enruta, contra 495 ms directos para la solicitud idéntica. El clasificador parece dejar al backend ocupado para la solicitud en cola detrás de él. Nunca verías esto solo con el tiempo de reloj.

Mide las partes, no el total

El tiempo de reloj emparejado te dice que se volvió más lento, nunca dónde. Descompón en la llamada del clasificador y la llamada de servicio antes de que cites una cifra — el primer número honesto aquí fue casi el doble del que una sola ejecución de time sugirió.

La trampa que vale la pena conocer

Si la llamada de clasificación se agota, devuelve la forma incorrecta o vuelve vacía, el enrutador vuelve a el puntuador heurístico — la cosa cuyas fallas acabas de pagar medio segundo para evitar. Silenciosamente.

La documentación oficial nombra la salida: establece classifier_fallback: default_model y un tiempo de espera enruta a un modelo que elegiste deliberadamente, en lugar de al puntuador que envía preguntas difíciles a un modelo de 3B.

También vale la pena verificar en lugar de asumir: la documentación da a timeout_ms un valor predeterminado de 2000, mientras que el paquete en nuestro contenedor tiene un valor predeterminado de 3000. Lee la versión que realmente instalaste.

Entonces, ¿cuál?

HeurísticaClasificador LLM
Latencia añadidasub-milisegundo476–1449 ms
Llamadas API adicionalesningunauna por solicitud
Tokens de entrada adicionalesninguno~280 por solicitud
Prompts difíciles pero cortosenrutados hacia abajoenrutados correctamente
Prompts fácilesenrutados correctamenteenrutados hacia arriba
Falla porestar confidentemente equivocadoagotarse, luego estar confidentemente equivocado

La heurística es el valor predeterminado correcto para tráfico de alto volumen que se parece, y puedes mejorarlo mucho con custom_technical_keywords para tu propio vocabulario de dominio. El clasificador LLM justifica su costo cuando los prompts son variados, cuando equivocarse de modelo es costoso y cuando medio segundo no importa — trabajo por lotes, agentes, cualquier cosa que ya esté tomando segundos.

Lo que ninguno de ellos es, es gratis.

Solución de problemas

La respuesta dice smart-router en lugar de un nombre de modelo

return_raw_model_name no está configurado. Sin él, el enrutador repite el alias que pediste y no puedes decir qué sirvió la solicitud.

Cada prompt se enruta al mismo nivel

Verifica el campo signals en la decisión de enrutamiento. Si es null, ninguna dimensión coincidió y el prompt puntuó 0.00, lo que cae en SIMPLE. Eso es una señal de que tu tráfico no utiliza el vocabulario que las listas de palabras clave predeterminadas esperan.

Las decisiones de enrutamiento no están en el registro de gastos

Están en la solicitud enrutada, bajo metadata.routing_decision, no en la llamada del clasificador. Filtra con map(select(.)) como arriba — la mitad de las filas son las llamadas del clasificador en sí y no llevan ninguna decisión.

La primera solicitud después de un reinicio es mucho más lenta

Inicio en frío. La primera clasificación aquí tomó 1449 ms contra aproximadamente 550 ms una vez caliente. Descarta la primera muestra al medir.

Finished this tutorial?
Mark it complete to earn Route work to the right model on your skill path.

¿Qué sigue?

El gateway ahora decide qué modelo se ejecuta, y sabes lo que cuesta esa decisión en latencia y tokens. Aún reenvía cada prompt textualmente al modelo que gana — incluidos los que contienen nombres de clientes, correos electrónicos y claves API.

La próxima publicación pone un filtro en ese camino.

Lectura adicional

Comments & questions

Hit an error, spotted a typo, or have a question? Leave a note below.