Migrar desde OpenAI u OpenRouter
Conecta tu código de OpenAI a SpicyAPI. Cambios de URL base, clave e ID de modelo, campos con otro comportamiento y ajustes que debes quitar de OpenRouter.
Si tu código ya llama a un modelo de chat mediante las bibliotecas oficiales de OpenAI o una herramienta con la opción «OpenAI-compatible», migrar a SpicyAPI requiere tres cambios: la URL base, la clave de API y el ID del modelo. Esta página enumera esos cambios y los comportamientos diferentes para evitar que la migración termine con un 400 inesperado. Para programas completos de ejemplo, consulta Ejemplos prácticos de chat.
Los tres cambios
| Ajuste | Valor en SpicyAPI |
|---|---|
| URL base | https://api.spicyapi.ai/v1 |
| Clave de API | Una clave de SpicyAPI (comienza con sk-spicy-), enviada como Authorization: Bearer |
| Modelo | Un ID exacto de modelo de texto del catálogo en vivo, por ejemplo en SPICY_MODEL (consulta ID de modelos) |
import os
from openai import OpenAI
# Before: client = OpenAI() (reads OPENAI_API_KEY, calls api.openai.com)
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
reply = client.chat.completions.create(
model=os.environ["SPICY_MODEL"],
messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
print(reply.choices[0].message.content)Las bibliotecas de OpenAI también leen OPENAI_BASE_URL y OPENAI_API_KEY del entorno. Definir ambas permite ejecutar el código existente sin cambios, pero también redirige cualquier otra llamada a OpenAI del mismo proceso. Por eso, es preferible pasar los valores explícitamente, como en los ejemplos anteriores.
ID de modelos
Los ID de modelos de texto tienen el formato publisher/model/chat. Cópialos del catálogo en vivo en lugar de adaptar un nombre que conozcas de otro servicio:
GET /api/v1/models?modality=textenumera solo modelos de texto y sus precios, y añade elinputSchemade cada uno conincludeSchema=1. Consulta Catálogo de modelos y esquemas.GET /v1/modelses la lista en formato OpenAI que consultan las herramientas. Sin un filtro, incluye modelos de imagen, video y audio que no pueden llamarse desde/v1/chat/completions; añade?modality=textpara obtener solo modelos de texto. Además de los cuatro campos de OpenAI, cada entrada contienemodalityytask, y los modelos de chat también tienencontext_length,max_output_tokensysupported_parameters(los campos de solicitud que acepta el modelo). Las bibliotecas de OpenAI ignoran los campos adicionales.
Un ID de modelo que no exista devuelve 404 con code: "model_not_found" y param: "model"; el mensaje muestra el formato del ID y, cuando existe uno, el modelo de chat disponible más cercano. Llamar a un modelo de imagen o video desde un endpoint de chat devuelve 400 e indica que no es un modelo de chat. Los ID de otros servicios suelen parecer similares, pero no son intercambiables: los nombres de los creadores se escriben de otra manera, aquí no existen sufijos como :free y los modelos de texto terminan en /chat.
Campos de solicitud
Cada modelo publica los campos que acepta en su inputSchema y, por nombre, en supported_parameters de /v1/models.
- Los campos de muestreo que el modelo no enumera se ignoran.
temperature,top_p,top_k,min_p,top_a,frequency_penalty,presence_penaltyyrepetition_penaltysolo modifican la aleatoriedad o repetición de una respuesta. Como muchos clientes los envían de forma predeterminada, un modelo que no los admite simplemente genera sin ellos. Lo mismo ocurre conn: 1, unstopvacío,stopen un modelo sin secuencias de detención yreasoning_efforten un modelo sin niveles de razonamiento. La respuesta indica lo ignorado en el encabezadoX-Spicy-Ignored-Params, por ejemplofrequency_penalty, presence_penalty. - Los valores que el modelo no admite se ajustan al valor aceptado más cercano. Un valor de muestreo fuera del rango del modelo se limita al extremo correspondiente; una lista
stopmás larga de lo permitido conserva sus primeras entradas; un nivel dereasoning_effortno disponible se convierte en el nivel admitido más cercano, o en el inferior si hay empate. La respuesta enumera cada cambio comoname=valueen el encabezadoX-Spicy-Adjusted-Params, por ejemploreasoning_effort=low, temperature=1. - Cualquier otro campo no enumerado por el modelo se rechaza con
400,code: "unsupported_parameter",paramcon el nombre del campo y un mensaje que lo identifica, por ejemplologprobs: is not a supported field. Estos campos cambian lo que recibes, por lo que nunca pagas por una respuesta generada sin una opción que creías haber enviado.
| Campo | Comportamiento en SpicyAPI |
|---|---|
messages | Misma forma. Roles system, developer, user, assistant y tool. El contenido de texto funciona en todos los modelos; las partes image_url, solo donde el esquema las enumera |
max_tokens, max_completion_tokens | Ambos se aceptan y significan lo mismo. Los tokens de razonamiento cuentan dentro del límite. Cada modelo tiene su propio valor predeterminado y máximo |
stream, stream_options | Compatibles, con las reglas de uso de OpenAI: un stream solo incluye uso si include_usage es true (consulta Respuestas y streams) |
tools, tool_choice | Compatibles con todos los modelos de texto. Algunos solo aceptan auto y none en tool_choice; forzar una herramienta concreta devuelve 400 en esos modelos |
parallel_tool_calls | Solo en los modelos que lo enumeran |
reasoning_effort | En la mayoría de los modelos, pero los valores permitidos varían entre ellos: algunos añaden none, minimal, xhigh o max, y otros no tienen medium. Un valor no disponible se convierte en el más cercano admitido; consulta el enum del modelo si necesitas un nivel exacto |
temperature, top_p, top_k, frequency_penalty, presence_penalty | Se utilizan en los modelos que los enumeran, se limitan a su rango y se ignoran en los demás (con sus nombres en X-Spicy-Ignored-Params). Varios modelos no admiten ningún campo de muestreo |
seed | Solo en los modelos que lo enumeran |
stop | Se utiliza donde el modelo lo enumera y se recorta a su cantidad máxima; se ignora si el modelo no tiene secuencias de detención o si está vacío |
response_format | json_object en los modelos que enumeran el campo; no está disponible en los demás |
user, metadata, store | Se aceptan y se ignoran |
n | 1 se ignora; cualquier otro valor se rechaza con 400 en los modelos que no lo enumeran |
logprobs, top_logprobs, logit_bias y cualquier otro campo no enumerado por el modelo | Se rechazan con 400 |
Para consultar los campos de un modelo y sus valores permitidos antes de enviar nada, ejecuta el comando de Ejemplos de chat: antes de empezar.
Respuestas y streams
Una respuesta sin streaming es un objeto chat.completion normal, de modo que reply.choices[0].message.content y reply.usage funcionan como antes. Las diferencias son pequeñas:
reasoning_content. Los modelos que razonan antes de responder devuelven ese texto enmessage.reasoning_content, o endelta.reasoning_contentdurante el streaming, junto acontent. Ya está incluido enusage.completion_tokens.- El ID es el ID de la solicitud.
idtiene el formatochatcmpl-req_..., y el encabezado de respuestax-request-idcontiene el mismo valorreq_.... Inclúyelo al contactar con soporte. Otro encabezado,X-Spicy-Task-Id, identifica la tarea donde se registra el cargo (consulta Costo y uso). - Sin
system_fingerprintnilogprobs.
Los streams utilizan los mismos eventos chat.completion.chunk, terminan con data: [DONE] y siguen las reglas de uso de OpenAI:
- El uso solo se incluye si se solicita. Sin
stream_options.include_usage, ningún fragmento contieneusagey todos tienen una listachoicesno vacía. Con"include_usage": true, llega un fragmento adicional con una listachoicesvacía y elusagefinal justo antes de[DONE]; todos los demás tienenusage: null. El código basado en los ejemplos de streaming de OpenAI funciona sin cambios; comprobarchoicesantes de acceder achoices[0]evita errores en ese último fragmento. - Un
finish_reasonpor alternativa. Cada alternativa termina con exactamente un fragmento que contiene sufinish_reason, independientemente del modelo utilizado. - Líneas de mantenimiento de conexión. Mientras un modelo sigue razonando, el stream envía comentarios
: keep-alivepara mantener abierta la conexión. Las bibliotecas de OpenAI los omiten; un analizador propio también debe hacerlo.
Para respuestas largas o un reasoning_effort alto, utiliza streaming. Una solicitud sin streaming que supera unos 30 segundos se mantiene abierta con espacios antes del cuerpo JSON y el encabezado X-Spicy-Keepalive: 1, por lo que las bibliotecas de OpenAI siguen interpretándola. Sin embargo, si falla después de ese punto, el objeto de error llega en el cuerpo de una respuesta 200, que las bibliotecas no tratan como excepción. Consulta Tiempos de espera y streaming.
Errores
Los errores utilizan el formato OpenAI, {"error": {"message", "type", "param", "code"}}, con los códigos HTTP habituales, de modo que las bibliotecas de OpenAI lanzan sus excepciones normales. param identifica el campo relacionado con el error mediante una ruta con puntos, como temperature o messages.1.content.0, y code distingue errores que comparten estado: unsupported_parameter (un campo o parte de mensaje no admitido), invalid_request (un campo ausente o un valor no admitido), content_rejected (rechazo por política de contenido, también 400) y model_not_found (404). Una diferencia con OpenAI: se utiliza 402 cuando el saldo o un límite de gasto no cubre la solicitud. La tabla completa y las acciones para cada código están en Ejemplos de chat: cuando algo falla.
Las bibliotecas de OpenAI reintentan automáticamente las respuestas 429 y 5xx. Aquí es seguro: una solicitud fallida no se factura. Si envías un Idempotency-Key, pásalo en cada llamada y lee primero Claves de idempotencia en endpoints de texto.
Costo y uso
Las respuestas indican cantidades de tokens en usage, no un precio. Los precios de entrada, entrada en caché y salida se expresan por millón de tokens y aparecen en la página de cada modelo y en el catálogo. Para conocer el costo real de una llamada, lee el encabezado de respuesta X-Spicy-Task-Id (job_…), presente en todas las solicitudes aceptadas, con streaming o sin él, y consulta la tarea mediante GET /api/v1/jobs/recordInfo?taskId=<job_…>: cost es definitivo cuando settled es true. Las entradas de GET /api/v1/jobs también incluyen el requestId de la llamada que las creó (el encabezado X-Request-Id), y Consola → Logs muestra las mismas tareas. Ejemplos de chat: conocer el costo de una llamada explica cómo leer el encabezado desde las bibliotecas de OpenAI; consulta también Cotizaciones y facturación.
Antes de ejecutar una solicitud, se retiene del saldo un importe que cubre tus mensajes y todo el max_tokens de salida, o el valor predeterminado del modelo si lo omites. Solo pagas el uso real, pero primero debe haber saldo suficiente para la retención; por eso, un saldo pequeño con un max_tokens grande devuelve 402. Define un max_tokens realista o configura límites de gasto en la clave; consulta Facturación.
Endpoints que no encontrarás aquí
La generación de texto está disponible como OpenAI Chat Completions, OpenAI Responses, Anthropic Messages y Google Gemini; los detalles aparecen en Texto y streaming. Estos endpoints de OpenAI no existen y devuelven 404: /v1/embeddings, el antiguo /v1/completions, /v1/images/*, /v1/audio/*, /v1/moderations, /v1/files, /v1/batches, /v1/assistants y fine-tuning. Los modelos de imagen, video y audio se llaman mediante la API de tareas; los videos también pueden llamarse mediante /v1/videos.
Si vienes de OpenRouter
OpenRouter también utiliza el formato OpenAI, así que se aplican los tres cambios anteriores: sustituye la URL base https://openrouter.ai/api/v1 por https://api.spicyapi.ai/v1 y cambia la clave y los ID de modelos. Después elimina los campos exclusivos de OpenRouter; aquí devuelven 400:
| Campo de OpenRouter | Qué hacer |
|---|---|
models (una lista de alternativas) y route | Elimínalos. Envía un ID de modelo; SpicyAPI decide cómo ejecutarlo |
provider | Elimínalo. No hay selección del servicio de ejecución por solicitud |
reasoning (un objeto como {"effort": "high"}) | Se convierte en el campo raíz reasoning_effort (enabled: false pasa a none si el modelo lo admite); las demás claves internas se ignoran. Enviar reasoning_effort directamente resulta más claro |
usage ({"include": true}) | Elimínalo. Las respuestas sin streaming siempre incluyen usage; para streams, envía stream_options: {"include_usage": true} |
transforms, plugins | Elimínalos |
Los encabezados HTTP-Referer y X-Title no causan problemas y se ignoran.
Lista de comprobación
- URL base
https://api.spicyapi.ai/v1y una clave de SpicyAPI. - ID del modelo copiado del catálogo en vivo y terminado en
/chat. - Campos de solicitud comprobados con el
inputSchemadel modelo; campos ajenos al muestreo no admitidos eliminados; encabezados de respuestaX-Spicy-Ignored-ParamsyX-Spicy-Adjusted-Paramsrevisados una vez para saber qué cambió. - Si envías
stream_options.include_usage, los bucles de streaming omiten el fragmento final cuya listachoicesestá vacía. max_tokenscon un valor realista o con el predeterminado del modelo, y límites de gasto en la clave.- Las solicitudes largas o con mucho razonamiento utilizan
stream: true.
Sigue leyendo
Ejemplos prácticos de chat · Texto y streaming · Catálogo de modelos y esquemas · Autenticación · Facturación
Ejemplos de chat
Programas completos para conversaciones, rol de personajes, streaming en tu interfaz y llamadas a herramientas con Python, Node.js y curl.
Tutorial de imagen a video
Empieza con una imagen de tu equipo y termina con un archivo de video descargado. Cada paso muestra cómo hacerlo con la CLI, con el SDK de TypeScript y con HTTP directo.

