Cuando necesitas extraer datos públicos de más de veinte plataformas, la primera reacción suele ser diseñar una abstracción común: un Post, un User y un mapeo para encajar todas las respuestas ahí. Al fin y al cabo, un vídeo de bilibili, uno de tiktok, una respuesta de zhihu o una publicación de linkedin parecen ser lo mismo: contenido con un autor. Esa idea funciona de maravilla con las tres primeras plataformas. Para cuando llegas a la vigésima, te ha enterrado bajo su propio peso.
Al final no construí ese modelo unificado. Con más de veinte plataformas y más de doscientos comandos, sobrevivió una división que parece más simple: cada plataforma se ocupa de lo suyo.
Por qué el modelo unificado termina rompiéndose
El colapso es gradual. Al integrar la octava plataforma, tu Post ya arrastra una docena de campos opcionales: unas tienen contador de danmaku y otras no; la «fecha de publicación» puede ser una marca de tiempo con precisión de segundos o un texto como «hace 3 días». Al superar la veintena, el modelo ya no aporta nada. No falla al compilar: simplemente deja de ahorrarte trabajo. Todo el código aguas abajo debe preguntarse antes si esa plataforma llegó a rellenar tal campo; la lógica para decidirlo acaba siendo más larga que leer la respuesta original, y la capa unificada pasa de ayudar a convertirse en un obstáculo que hay que esquivar. Integrar una plataforma nueva también paga el mismo peaje: toca volver al modelo común y forzar sus datos en huecos diseñados para las anteriores.
Cada plataforma debe ser un contexto delimitado
La estructura que sí se sostiene es la contraria: no abstraigas un modelo único; deja que cada plataforma gestione su propio dominio. En el catálogo, cada plataforma vive en un contexto <platform>_reverse/ y posee cuatro responsabilidades que no comparte:
Validación de entrada. Solo esa plataforma sabe qué formato tienen sus IDs y qué combinaciones de parámetros son válidas.
Protocolo. HTTP directo o un fragmento de firma en JS local, dominios, cabeceras: todo es privado de la plataforma.
Firma. Los mecanismos de firma varían enormemente entre plataformas. Meterlos en un firmador compartido solo crea un monstruo de condiciones if-else.
Normalización de respuestas. La respuesta en bruto se ordena en una estructura propiedad de esa plataforma, no en un esquema global.
El cuarto punto es el que más se malinterpreta. «No hay modelo unificado» no significa «no hay normalización». Por supuesto que cada plataforma normaliza; lo que cambia es que define su propia estructura objetivo, en lugar de imponerla a un modelo compartido. La unificación sigue siendo válida cuando realmente hablamos de lo mismo: si dos endpoints de una misma plataforma comparten una estructura de publicación, tiene sentido reutilizarla porque es el mismo objeto de dominio. El error es extender esa unificación interna a plataformas distintas.
La capa compartida solo debe contener lo que de verdad es común
¿Qué entra entonces en la capa compartida? Capacidades que se comportan exactamente igual en todas las plataformas, no cosas que solo se parecen a primera vista. Mi capa compartida contiene únicamente tres elementos:
El modelo de lectura de la interfaz. A partir de las declaraciones argparse de cada plataforma, se deriva un catálogo unificado de capacidades. Lo que se unifica es cómo se descubren y describen los comandos, no lo que devuelven. Lo primero sí es transversal; lo segundo pertenece a cada plataforma. (La idea de que «la declaración es la interfaz» se desarrolla en el artículo sobre interfaz como código).
Transporte local de loopback. Las solicitudes autenticadas pasan por un servicio local de sesión WebSocket que trata todas las plataformas por igual, sin tocar ninguno de sus campos de negocio.
Punto de entrada de despacho. Descubre la plataforma, entrega el comando a su contexto y no hace nada más.
La prueba es sencilla: para entrar en la capa compartida, algo debe comportarse realmente igual en todas las plataformas. El transporte, el despacho y la generación de descripciones de interfaz cumplen esa condición. En cambio, «una pieza de contenido» no se comporta igual en bilibili y linkedin, así que no debe entrar. «Se parecen» es la mayor trampa de la abstracción: dos vídeos parecen iguales y dan ganas de unificarlos, pero la similitud superficial no equivale a identidad de comportamiento. Confundir ambas cosas es la raíz del colapso del modelo unificado.
La distribución de comandos indica dónde compensa abstraer
Si aún dudas sobre el modelo unificado, basta con mirar la distribución real de comandos. Son 22 plataformas y 241 comandos, repartidos de forma muy desigual:
Plataforma | Comandos |
|---|---|
tiktok | 34 |
bilibili | 26 |
18 | |
zhihu | 18 |
douyin | 17 |
xiaohongshu | 16 |
Las otras 16 plataformas | De 1 a 13 cada una |
Las seis primeras reúnen 129 comandos, más de la mitad del total. La otra mitad se reparte entre 16 plataformas de cola larga, muchas con apenas dos o tres comandos y algunas con uno solo.
Esa distribución determina la economía de la abstracción: el coste de un modelo unificado es fijo —cada integración debe rellenar campos, comprobar nulos y sortear sus limitaciones—, mientras que el beneficio se reparte por plataforma. En una plataforma de cola larga con dos o tres comandos, la abstracción tiene un beneficio negativo: el adaptador necesario para hacerla encajar en el modelo unificado acaba siendo más largo que todo su código de negocio.
No anticipes abstracciones para una sola implementación
De esta distribución sale otra regla: no reserves una abstracción para una implementación única. Si una plataforma solo tiene una implementación, no añadas un repositorio, una fábrica ni una capa de interfaces por si «algún día aparece otra». Incorporar una plataforma debe consistir en añadir un contexto <platform>_reverse/, sin tener que modificar antes una clase base compartida.
Una capa de interfaces sirve para intercambiar varias implementaciones. Con una sola, su valor es cero y su coste de mantenimiento es positivo. Reservar espacio para una segunda implementación inexistente y reservarlo para una similitud entre plataformas que no existe son el mismo error. La migración entre lenguajes volvió a demostrarlo: varios cientos de comandos del registro antiguo dejaron explícitamente un lote sin migrar, sin stubs ni proxies de compatibilidad, porque una carcasa vacía cuesta más que un hueco: hace pensar a la siguiente persona que hay algo funcionando. Una abstracción reservada provoca el mismo efecto.
Para normalizar entre plataformas, también hay que dar contexto por plataforma al modelo
Esta idea de «separar por plataforma y evitar un modelo unificado» se mantiene al usar un modelo para normalizar datos. Para transformar respuestas en bruto de más de veinte plataformas en una estructura analizable, es natural recurrir a un modelo. Aquí el error más fácil es repetir el de la capa de código: definir un esquema único, pasarle el JSON en bruto de cada plataforma y pedirle que lo mapee. No funciona, porque el modelo no sabe si el campo de reproducciones de bilibili significa exactamente lo mismo que el de tiktok. Al forzarlo hacia un esquema de mínimo común denominador, acabará descartando un campo importante para la plataforma o rellenándolo a medias.
Lo correcto es aportar contexto específico: «esto es bilibili, estos campos significan esto y quiero esta estructura para esta plataforma». Normaliza una plataforma cada vez y deja la combinación entre plataformas para la capa de análisis. El proceso se divide en varios pasos, y cada uno exige una capacidad distinta al modelo:
Paso | Capacidad necesaria | Elección | model id |
|---|---|---|---|
Leer la estructura completa de la respuesta en bruto de una plataforma | Contexto largo, capaz de absorber de una vez la respuesta completa y las notas sobre campos | Kimi K3 |
|
Definir el límite de la normalización: qué campos son realmente transversales y cuáles son específicos | Razonamiento sólido, resistente a unificar de más | Claude Opus 5 |
|
Extraer campos en masa para cada plataforma y mapear elemento a elemento | Económico, con cientos o miles de llamadas en alta concurrencia | Claude Sonnet 5 |
|
Explicar por qué campos con el mismo nombre en dos plataformas no coinciden | Razonamiento intermedio, capaz de explicar las diferencias a partir de los campos | GPT-5.6 Sol |
|
El segundo paso es el único en el que cambiar de modelo modifica de forma visible el resultado. Lo que se evalúa es si el modelo admite que dos campos no son realmente lo mismo, igual que en la sección de contraevidencia en la identificación de familias de algoritmos: un modelo débil sigue tu indicación de «unificar»; uno fuerte señala el límite.
El coste de cambiar de modelo es el obstáculo real
Los cuatro niveles proceden de tres proveedores, tres SDK, tres esquemas de autenticación y tres formatos de error. Reescribir el cliente tres veces para cambiar de modelo entre pasos no compensa, así que la mayoría utiliza el mismo nivel de principio a fin. A menudo escogen uno que se aplana justo al «definir el límite», y terminan creando un esquema que vuelve a colapsar al llegar a más de veinte plataformas.
AIReiter elimina esa capa de fricción: una clave, una interfaz compatible con OpenAI y los cuatro niveles detrás. Basta cambiar el campo model en el cuerpo de la solicitud.
# Set the normalization boundary: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "<one platform response sample + have it mark which fields are platform-specific>"}]
}'
# Extract fields per platform in bulk: change the model field, leave the rest
# "model": "claude-sonnet-5"
# Field-difference attribution:
# "model": "gpt-5.6-sol"
Si ya utilizas el SDK de OpenAI, apunta base_url a https://aireiter.com/api/v1 y no cambies nada más. Con el SDK de Anthropic, usa POST /api/v1/messages con la misma clave.
En precios, los modelos Claude tienen un 30% de descuento sobre tarifa, los modelos GPT cuestan la mitad y Kimi K3 se puede invocar con la misma clave. El descuento incide justo en el principal coste: la extracción masiva de campos por plataforma es la fase con mayor densidad de llamadas, con más de veinte plataformas y cientos o miles de registros por cada una, una llamada por registro, ejecutada en el Sonnet más económico con un 30% adicional de descuento. Leer una respuesta larga completa con Kimi K3 consume unos cientos de miles de tokens por entrada, otro bloque de coste. El nivel de razonamiento para definir límites requiere pocas llamadas, así que su coste apenas pesa.
Pruébalo sin registrarte: primero procesa manualmente la respuesta de una plataforma y comprueba si el modelo señala honestamente las diferencias o se apresura a homogeneizarlas; después decide si merece la pena integrarlo.
Conclusión
El primer impulso al recopilar datos de varias plataformas —abstraer un único Post/User— resulta muy cómodo a pequeña escala, pero termina cayéndose al superar la veintena de plataformas: coste fijo, beneficio por plataforma y una distribución de comandos con una cola larguísima. La división que funciona es un contexto delimitado por plataforma, responsable de validar entradas, gestionar protocolo, firmar y normalizar respuestas. La capa compartida debe guardar solo lo que se comporta igual en todas las plataformas —transporte, despacho y generación de descripciones de interfaz—, no un modelo de dominio basado en parecidos superficiales. Tampoco debe reservar abstracciones para una única implementación ni para una similitud inexistente. Con los modelos ocurre lo mismo: normaliza con contexto por plataforma, no mediante un esquema unificado, y deja la fusión entre plataformas exclusivamente para la capa de análisis. El flujo completo de cuatro etapas explica con más detalle la división en cuatro niveles; al conectarlos mediante una interfaz unificada, el coste de cambiar entre ellos deja de ser un motivo para no utilizarlos.