El bloqueo de proveedor en las aplicaciones de IA normalmente no ocurre de golpe. Se va filtrando — un import openai directo por aquí, un nombre de modelo codificado de forma rígida por allá, un campo de respuesta que analizas sin comprobar si otros proveedores devuelven lo mismo. Seis meses después, cambiar de proveedor significa reescribir la mitad de tu backend.
Las cuatro formas en que ocurre el bloqueo
La mayoría de los desarrolladores piensa que el bloqueo significa «Estoy usando el SDK de OpenAI». Ese es el tipo menos peligroso. Las trampas reales son más sutiles:
| Tipo de bloqueo | Cómo sucede | Consecuencia |
|---|---|---|
| Bloqueo por SDK | from openai import OpenAI en todas partes | Cambiar de SDK implica tocar cada archivo |
| Bloqueo por nombre de modelo | model="gpt-4o" fijo en la lógica de negocio | Cada cambio de modelo es un cambio de código |
| Bloqueo por parámetros | Usar logprobs, n>1, o reasoning_effort | Esto no existe en Claude ni en Gemini |
| Bloqueo por formato de respuesta | Analizar campos de respuesta específicos del proveedor | Distintos proveedores devuelven estructuras diferentes |
El objetivo no es eliminar todos estos — algunos son compromisos aceptables. El objetivo es saber cuáles estás asumiendo.
Usa un endpoint compatible con OpenAI como capa de abstracción
La forma más limpia de evitar el bloqueo por SDK es usar un único endpoint compatible con OpenAI que enrute a varios proveedores. Mantienes el SDK de OpenAI, pero el backend puede ser cualquier proveedor.
CometAPI hace esto: un endpoint, una clave, más de 500 modelos entre OpenAI, Anthropic, Google, DeepSeek, xAI y otros:
import osfrom openai import OpenAIfrom dotenv import load_dotenvload_dotenv()api_key = os.environ.get("AI_API_KEY")if not api_key: raise ValueError("AI_API_KEY environment variable is not set")client = OpenAI( base_url=os.environ.get("AI_BASE_URL", "https://api.cometapi.com/v1"), api_key=api_key,)
Cambiar de GPT a Claude o a Gemini es un cambio de una sola línea:
# Beforeresponse = client.chat.completions.create(model="gpt-5.4", messages=[...])# After — same code, different modelresponse = client.chat.completions.create(model="claude-sonnet-4-6", messages=[...])
Nota: Los nombres de modelo como gpt-5.4 y claude-sonnet-4-6 son identificadores de plataforma de CometAPI: solo funcionan a través de https://api.cometapi.com/v1, no directamente con las API de OpenAI o Anthropic. Consulta la lista completa de modelos para ver el catálogo completo y los precios.
Mantén los nombres de modelo fuera de tu lógica de negocio
Tener nombres de modelo dispersos por tu código es la forma más común de bloqueo. La solución es una configuración central que lea de variables de entorno:
# config.py — one place to change model assignmentsimport osMODEL_CONFIG = { "summarize": os.environ.get("MODEL_SUMMARIZE", "claude-opus-4-7"), "code": os.environ.get("MODEL_CODE", "gpt-5.4"), "classify": os.environ.get("MODEL_CLASSIFY", "claude-haiku-4-5"), "chat": os.environ.get("MODEL_CHAT", "gpt-5.4-mini"),}# Validate at startup — fail fast rather than getting mysterious API errorsfor task, model in MODEL_CONFIG.items(): if not model: raise ValueError(f"Model config for '{task}' is not set")
Tu lógica de negocio nunca hace referencia a un nombre de modelo directamente:
from config import MODEL_CONFIGdef summarize(text: str) -> str: response = client.chat.completions.create( model=MODEL_CONFIG["summarize"], messages=[{"role": "user", "content": f"Summarize: {text}"}], max_tokens=300 # move to config in production ) return response.choices[0].message.content
Para cambiar el modelo de resumen en toda tu aplicación, cambia una variable de entorno. Sin grep, sin buscar y reemplazar.
Encapsula la respuesta para que tu código no dependa de campos específicos del proveedor
Distintos proveedores devuelven formas de respuesta ligeramente diferentes. Si analizas respuestas crudas de la API por todo tu código, quedas atado al formato de ese proveedor.
Encápsulalo en un dataclass normalizado:
from dataclasses import dataclassfrom typing import Optionalfrom openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutErrorfrom openai.types.chat import ChatCompletionimport logging@dataclassclass AIResponse: content: str model: str input_tokens: int output_tokens: intdef call_model(task: str, messages: list, **kwargs) -> AIResponse: """ Single entry point for all LLM calls. Returns a normalized AIResponse regardless of which model handled it. Raises on 4xx (client errors). Logs and re-raises on 5xx/network errors. """ model = MODEL_CONFIG.get(task, "gpt-5.4-mini") if not model: raise ValueError(f"No model configured for task '{task}'") try: response: ChatCompletion = client.chat.completions.create( model=model, messages=messages, **kwargs ) except APIStatusError as e: logging.error(f"API error for task={task} model={model}: {e.status_code} {e.message}") raise except (APIConnectionError, APITimeoutError) as e: logging.error(f"Network error for task={task} model={model}: {e}") raise # content is None when the model triggers a tool call instead of returning text content = response.choices[0].message.content or "" # usage is None in streaming mode — default to 0 if not available usage = response.usage input_tokens = usage.prompt_tokens if usage else 0 output_tokens = usage.completion_tokens if usage else 0 logging.info( f"task={task} model={model} " f"input_tokens={input_tokens} output_tokens={output_tokens}" ) return AIResponse( content=content, model=response.model, input_tokens=input_tokens, output_tokens=output_tokens, )
Ahora tu lógica de negocio trabaja con objetos AIResponse, no con respuestas crudas de la API. Si un proveedor cambia su formato de respuesta, lo corriges en un solo lugar.
Añade compatibilidad con streaming al wrapper
Para interfaces de chat, querrás streaming. El wrapper lo gestiona como una ruta separada:
from typing import Iteratordef stream_model(task: str, messages: list, **kwargs) -> Iterator[str]: """ Stream tokens from the routed model. Note: streaming doesn't return usage data. Fallback is not supported in streaming mode — you've already started yielding tokens before you know if the full request succeeds. """ model = MODEL_CONFIG.get(task, "gpt-5.4-mini") if not model: raise ValueError(f"No model configured for task '{task}'") stream = client.chat.completions.create( model=model, messages=messages, stream=True, **kwargs ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: yield delta# Usagefor token in stream_model("chat", [{"role": "user", "content": "Hello"}]): print(token, end="", flush=True)
Conoce qué parámetros generan bloqueo
Algunos parámetros solo existen en proveedores específicos. Usarlos está bien; solo ten presente que estás tomando una decisión deliberada:
| Parámetro | Funciona en | Riesgo de bloqueo |
|---|---|---|
| logprobs | Solo GPT | Alto — no hay equivalente en Claude o Gemini |
| n > 1 | GPT, Gemini (no Claude) | Medio — Claude requiere iterar |
| reasoning_effort | Solo la serie o de GPT | Alto — no hay equivalente en otros |
| temperature > 1.0 | GPT, Gemini (no Claude) | Bajo — Claude limita a 1.0 |
| tools | Todos los proveedores principales | Ninguno — seguro de usar |
| response_format | Todos los proveedores principales | Bajo — diferencias menores de esquema |
Si estás usando logprobs para puntuar la confianza, quedas atado a GPT para esa función. Es un compromiso razonable; solo documéntalo para que el próximo desarrollador sepa por qué.
Haz configurable el endpoint del proveedor
Fijar en código base_url="https://api.cometapi.com/v1" sigue siendo una forma de bloqueo. Conviértelo en una variable de entorno:
# .env — using CometAPIAI_BASE_URL=https://api.cometapi.com/v1AI_API_KEY=your_cometapi_key# To switch to OpenAI directly, change two lines:# AI_BASE_URL=https://api.openai.com/v1# AI_API_KEY=your_openai_key
La inicialización del cliente del Paso 1 ya lee estas variables. Cambiar entre CometAPI y una conexión directa a un proveedor ahora es un cambio de configuración, no de código.
Versión para Node.js
import OpenAI from 'openai';const apiKey = process.env.AI_API_KEY;if (!apiKey) throw new Error('AI_API_KEY is not set');const client = new OpenAI({ baseURL: process.env.AI_BASE_URL ?? 'https://api.cometapi.com/v1', apiKey,});// Model IDs are CometAPI platform identifiers — see cometapi.com/modelsconst MODEL_CONFIG = { summarize: process.env.MODEL_SUMMARIZE ?? 'claude-opus-4-7', code: process.env.MODEL_CODE ?? 'gpt-5.4', classify: process.env.MODEL_CLASSIFY ?? 'claude-haiku-4-5', chat: process.env.MODEL_CHAT ?? 'gpt-5.4-mini',};// Validate at startupfor (const [task, model] of Object.entries(MODEL_CONFIG)) { if (!model) throw new Error(`Model config for '${task}' is not set`);}/** * Single entry point for all LLM calls. * Returns normalized response. Raises on 4xx, logs and re-raises on 5xx/network. */async function callModel(task, messages, options = {}) { const model = MODEL_CONFIG[task] ?? 'gpt-5.4-mini'; let response; try { response = await client.chat.completions.create({ model, messages, ...options, }); } catch (err) { // Don't swallow errors — log and re-raise console.error(`API error task=${task} model=${model}:`, err.message); throw err; } // content is null when model triggers a tool call const content = response.choices[0].message.content ?? ''; // usage may be absent in some configurations const inputTokens = response.usage?.prompt_tokens ?? 0; const outputTokens = response.usage?.completion_tokens ?? 0; console.log(`task=${task} model=${model} input=${inputTokens} output=${outputTokens}`); return { content, model: response.model, inputTokens, outputTokens };}/** * Stream tokens from the routed model. * Usage data is not available in streaming mode. */async function* streamModel(task, messages, options = {}) { const model = MODEL_CONFIG[task] ?? 'gpt-5.4-mini'; const stream = await client.chat.completions.create({ model, messages, stream: true, ...options, }); for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content; if (delta) yield delta; }}// Usage — blockingconst result = await callModel('classify', [ { role: 'user', content: 'Positive or negative? "Loved it!"' }]);console.log(result.content);// Usage — streamingfor await (const token of streamModel('chat', [ { role: 'user', content: 'Hello' }])) { process.stdout.write(token);}
Qué bloqueo es aceptable
No todo bloqueo merece ser combatido. Algunos compromisos tienen sentido:
- Usar el OpenAI SDK — Es el estándar de facto. La mayoría de los proveedores lo admiten. Bloqueo de bajo riesgo.
- Funciones específicas del proveedor que realmente necesitas — Si necesitas
logprobs, úsalas. Aísla ese código para que sea fácil de encontrar y reemplazar después. - Modelos afinados — Un modelo afinado está intrínsecamente ligado a un proveedor. Es lo esperado.
El bloqueo que conviene evitar es el accidental: nombres de modelo en la lógica de negocio, análisis de respuestas crudas disperso por archivos, claves de API incrustadas en el código fuente.
Qué sigue
Ahora tienes una capa de abstracción que mantiene los detalles del proveedor fuera de tu lógica de negocio. El último artículo de esta serie cubre qué ocurre cuando las cosas salen mal: cómo depurar generaciones fallidas, interpretar códigos de error y crear un manejo de errores que realmente te diga qué se rompió.
Sigue: Cómo depurar generaciones fallidas de la API de IA
Preguntas frecuentes
P: ¿Cuál es la diferencia entre el bloqueo por SDK y el bloqueo por modelo?
El bloqueo por SDK significa que tu código importa una biblioteca específica y tendría que cambiar si cambias de SDK. El bloqueo por modelo significa que los nombres de modelo están dispersos en tu lógica de negocio. El bloqueo por SDK es menos peligroso porque la mayoría de los proveedores ahora admiten el formato del SDK de OpenAI. El bloqueo por modelo es más insidioso porque es más difícil de encontrar y corregir.
P: Si uso CometAPI, ¿solo estoy cambiando el bloqueo de OpenAI por el bloqueo de CometAPI?
Parcialmente. Estás cambiando el bloqueo a un proveedor directo por una capa proxy. La ventaja: una clave, un endpoint, cambio de modelo sencillo. El riesgo: si CometAPI sufre una caída, todos tus proveedores caen a la vez. La mitigación ya está en el código de arriba: AI_BASE_URL es una variable de entorno. Si necesitas omitir CometAPI y llamar a un proveedor directamente, es un cambio de configuración, no de código.
P: ¿Puedo usar el extended thinking de Claude o el reasoning_effort de OpenAI con este patrón?
Sí, pásalos como **kwargs a call_model. Solo ten en cuenta que, si enrutas esa tarea a un modelo diferente, esos parámetros se ignorarán o provocarán un error. Documenta qué tareas usan funciones específicas del proveedor para que el próximo desarrollador sepa por qué.
P: ¿Cómo gestiono el límite de temperature de Claude en 1.0 al enrutar entre Claude y GPT**?**
Mantén temperature en 1.0 o por debajo para permanecer en el rango seguro de ambos. Si necesitas una temperatura más alta para tareas creativas en GPT específicamente, enruta esas tareas explícitamente a GPT en MODEL_CONFIG en lugar de dejarlas pasar por el enrutador genérico.
P: ¿Debería abstraer las API de generación de imágenes y video del mismo modo?
Se aplican los mismos principios: configuración central, wrapper de respuesta normalizada, sin campos específicos del proveedor en la lógica de negocio. Las API de imagen y video tienen más diferencias estructurales (async vs. sync, conjuntos de parámetros distintos), por lo que la capa de abstracción requiere más trabajo. Empieza con texto y luego extiende el patrón cuando la estructura esté probada.
P: ¿Qué hay de las diferencias de ventana de contexto entre modelos?
Este es un riesgo real al enrutar. GPT-5.5 tiene una ventana de contexto de 1 M de tokens, los modelos de Claude admiten hasta 200 K y Gemini 3.5 Flash admite hasta 1 M. Si enrutas una tarea de documento largo a un modelo con una ventana de contexto más corta, la entrada se trunca silenciosamente. Agrega una comprobación de longitud de contexto antes de enrutar si tus tareas implican entradas largas, o enruta siempre las tareas de largo contexto a un modelo específico en MODEL_CONFIG en lugar de dejarlas pasar a un valor predeterminado.
