Texto y streaming
Llama a modelos de texto con el formato oficial de OpenAI, Anthropic o Google Gemini y gestiona bien conversaciones, herramientas, streaming y costos.
Elegir un modelo de texto disponible
Consulta /api/v1/models?modality=text&includeSchema=1 con autenticación y envía solicitudes solo a modelos con enabled y available en true. Las herramientas, los mensajes multimodales, los campos de razonamiento y las salidas estructuradas deben estar admitidos también por el inputSchema de ese modelo. Que un protocolo sea compatible no significa que todos los modelos ofrezcan todas las funciones.
Puedes llamar a un mismo ID de modelo con cualquiera de los protocolos siguientes, sin importar qué empresa lo haya desarrollado: el formato de Anthropic sirve para modelos de otras compañías y el de Gemini, para modelos que no son de Google.
Protocolos admitidos
Los cuatro protocolos de texto comparten la misma clave API, el mismo catálogo de modelos y los mismos límites de solicitudes, validación de parámetros y facturación; solo cambia el formato de las solicitudes y las respuestas. Elige el que ya usen tu código o tu SDK.
| Protocolo | Endpoint | Cabecera de la clave |
|---|---|---|
| OpenAI Chat Completions | POST /v1/chat/completions | Authorization: Bearer |
| OpenAI Responses | POST /v1/responses | Authorization: Bearer |
| Anthropic Messages | POST /v1/messages | x-api-key o Authorization: Bearer |
| Google Gemini | POST /v1beta/models/{model}:generateContent | x-goog-api-key o Authorization: Bearer |
| Google Gemini en streaming | POST /v1beta/models/{model}:streamGenerateContent | x-goog-api-key o Authorization: Bearer |
| Streaming nativo | POST /api/v1/jobs/stream | Authorization: Bearer |
Las respuestas y los errores de /v1 y /v1beta siguen cada uno su propio protocolo, sin la envoltura nativa {code,msg,data,request_id}; no los interpretes con un cliente que solo lea body.data. En el formato Gemini, el ID del modelo va en la ruta de la URL: sus barras se mantienen tal cual y no hace falta codificarlas.
Envía la clave únicamente en las cabeceras de la solicitud. x-api-key y x-goog-api-key solo funcionan en /v1 y /v1beta; la API nativa /api/v1 solo reconoce Authorization: Bearer. El parámetro de consulta ?key= no se acepta en ningún caso: una clave que aparece en una URL queda guardada en el historial del navegador, en los servidores proxy y en los registros de acceso. Los SDK oficiales envían la clave en las cabeceras, así que no les afecta.
Usar los SDK oficiales
Cambia solo la URL base y la clave; todo lo demás se usa como siempre en cada SDK.
| SDK | URL base |
|---|---|
OpenAI (openai) | https://api.spicyapi.ai/v1 |
Anthropic (anthropic) | https://api.spicyapi.ai |
Google GenAI (google-genai) | https://api.spicyapi.ai |
Los SDK de Anthropic y Google añaden /v1 o /v1beta por su cuenta, por eso su URL base no lleva ruta de versión; la del SDK de OpenAI, en cambio, debe incluir /v1. Los tres ejemplos de Python siguientes leen de las variables de entorno SPICY_API_KEY y SPICY_MODEL, este último elegido en el catálogo actual.
import os
from openai import OpenAI
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": "Explain a rainbow in one sentence."}],
)
print(reply.choices[0].message.content)import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai")
message = client.messages.create(
model=os.environ["SPICY_MODEL"],
max_tokens=256,
messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
)
print(message.content[0].text)import os
from google import genai
from google.genai import types
client = genai.Client(
api_key=os.environ["SPICY_API_KEY"],
http_options=types.HttpOptions(base_url="https://api.spicyapi.ai"),
)
response = client.models.generate_content(
model=os.environ["SPICY_MODEL"],
contents="Explain a rainbow in one sentence.",
)
print(response.text)Para que un reintento de red no genere cargos duplicados, crea un Idempotency-Key por cada acción del usuario, guárdalo de forma persistente y envíalo con la opción de cabeceras personalizadas del SDK (en los SDK de OpenAI y Anthropic es extra_headers).
Correspondencia de parámetros entre protocolos
Los campos de cada protocolo se traducen primero a los nombres de parámetro unificados de la plataforma y después se validan con el inputSchema del modelo elegido. Un parámetro que el modelo no admite devuelve 400: no se ignora en silencio para luego cobrar como si nada. La tabla solo muestra las equivalencias; «—» indica que ese protocolo no tiene un campo equivalente. Si un modelo acepta un parámetro concreto, y con qué rango de valores, lo determina su inputSchema.
| Parámetro unificado | Chat Completions | Responses | Messages | Gemini |
|---|---|---|---|---|
messages | messages | instructions, input | system, messages | systemInstruction, contents |
max_tokens | max_tokens, max_completion_tokens | max_output_tokens | max_tokens | generationConfig.maxOutputTokens |
temperature | temperature | temperature | temperature | generationConfig.temperature |
top_p | top_p | top_p | top_p | generationConfig.topP |
tools | tools | tools (tipo function) | tools (herramientas personalizadas) | tools[].functionDeclarations |
tool_choice | tool_choice | tool_choice | tool_choice | toolConfig.functionCallingConfig |
response_format | response_format | text.format | — | generationConfig.responseMimeType más responseSchema o responseJsonSchema |
reasoning_effort | reasoning_effort | reasoning.effort | — | generationConfig.thinkingConfig |
Los campos que no figuran en la tabla también se traducen a parámetros unificados y los valida el modelo; si no están en el inputSchema, devuelven 400. Por ejemplo, si el modelo no acepta stop, devuelven 400 tanto stop en Chat como stop_sequences en Messages y generationConfig.stopSequences en Gemini. Hoy ningún modelo de texto incluye seed en su inputSchema, así que seed en Chat y generationConfig.seed en Gemini también devuelven 400.
Algunas excepciones:
- Se ignoran, sin afectar a la generación, los campos que solo tienen sentido en la plataforma de origen de cada protocolo:
user,metadataystoreen Chat;user,metadata,storeeincludeen Responses; ymetadataen Messages. - Responses no admite
previous_response_id: incluye el historial completo eninput. - En Gemini,
safetySettingsse acepta pero no tiene efecto;cachedContenty las herramientas que se ejecutan en el servidor, comogoogleSearchycodeExecution, no se admiten y devuelven 400. Las imágenes pueden enviarse coninlineData(base64) ofileData.fileUri(URL https); si el modelo acepta imágenes lo determina su inputSchema.
Enviar un flujo Chat
Configura en tu servidor SPICY_API_KEY, SPICY_MODEL obtenido del catálogo y un SPICY_IDEMPOTENCY_KEY guardado para esta operación. El ejemplo necesita jq. Puedes cambiar el prompt, pero los campos deben cumplir el esquema vigente.
jq -n --arg model "$SPICY_MODEL" '{
model: $model,
messages: [{role: "user", content: "Explain a rainbow in one sentence."}],
stream: true
}' > chat-request.json
curl --fail-with-body --no-buffer --max-time 120 \
https://api.spicyapi.ai/v1/chat/completions \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data-binary @chat-request.jsonEnviar una solicitud Gemini
Las variables de entorno son las mismas de la sección anterior. El ID del modelo va en la URL y el cuerpo de la solicitud no incluye model. Pon la URL entre comillas dobles y escribe ${SPICY_MODEL} para que la shell no interprete los dos puntos que siguen como un modificador de variable.
jq -n '{
systemInstruction: {parts: [{text: "Answer in one sentence."}]},
contents: [{role: "user", parts: [{text: "Explain a rainbow."}]}],
generationConfig: {temperature: 0.7, maxOutputTokens: 256}
}' > gemini-request.json
curl --fail-with-body --no-buffer --max-time 120 \
"https://api.spicyapi.ai/v1beta/models/${SPICY_MODEL}:streamGenerateContent?alt=sse" \
-H "x-goog-api-key: $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data-binary @gemini-request.jsonSi no necesitas streaming, cambia :streamGenerateContent?alt=sse por :generateContent y lee el texto de la respuesta en candidates[0].content.parts[].text; el consumo está en usageMetadata. Si una solicitud de streaming no lleva alt=sse, la respuesta es un array JSON que se va escribiendo progresivamente; los SDK oficiales añaden alt=sse por su cuenta.
Conversaciones y herramientas
En Chat sin streaming lee choices[0].message; con streaming, lee choices[].delta en cada evento SSE completo. No interpretes cada fragmento TCP como JSON. Los argumentos de las herramientas pueden llegar repartidos en varios fragmentos: une los tool_calls por index y valida los argumentos completos antes de ejecutarlos. Conserva el mensaje assistant y devuelve cada resultado con su tool_call_id correspondiente. Responses requiere el historial completo en input y no acepta previous_response_id; devuelve los resultados de funciones con function_call_output y el call_id correspondiente. Messages trabaja con bloques text, tool_use y tool_result dentro de content; no le sirve tal cual el formato de mensajes de Chat.
En Gemini, contents alterna dos roles: user y model. Cuando el modelo quiere llamar a una herramienta, devuelve una parte functionCall; después de ejecutarla, convierte el resultado en una parte functionResponse y envíala en el siguiente contenido user, con el mismo name que en la llamada.
Finalización, errores y desconexiones
| Protocolo | Fin normal | Consumo |
|---|---|---|
| Chat Completions | data: [DONE] | usage del último chunk |
| Responses | Evento response.completed | response.usage dentro del evento |
| Messages | message_stop después de message_delta | usage de message_delta |
| Gemini | Último evento con finishReason; no hay [DONE] | usageMetadata |
Antes de leer el flujo, comprueba el estado HTTP y el Content-Type; los errores llegan en el JSON propio de cada protocolo:
| Protocolo | Cuerpo del error |
|---|---|
| Chat Completions, Responses | {"error": {"message", "type", "param", "code"}} |
| Messages | {"type": "error", "error": {"type", "message"}} |
| Gemini | {"error": {"code", "message", "status"}} |
Los códigos de estado HTTP son los mismos en los cuatro protocolos. El campo status de Gemini usa los nombres de estado de Google: 400 es INVALID_ARGUMENT, 401 es UNAUTHENTICATED, 402 es FAILED_PRECONDITION, 403 es PERMISSION_DENIED, 404 es NOT_FOUND, 429 es RESOURCE_EXHAUSTED y 503 es UNAVAILABLE. En tu código, decide según el código de estado y el tipo de error; no compares el texto de message.
Si el error se produce con el flujo ya iniciado, Chat y Gemini envían un evento con un objeto error, Responses envía un evento error y Messages envía event: error; el cliente debe manejar estos eventos de error, los timeouts y los cortes prematuros. Desactiva el buffering de respuestas en el proxy. Cerrar el navegador o usar AbortSignal solo detiene la recepción; no equivale a cancelar ni garantiza un reembolso. Si el servidor dispone de un consumo fiable, una desconexión iniciada por el cliente se liquida igualmente según el consumo real ya generado; sin datos de consumo, cerrar la conexión tampoco puede darse por una finalización gratuita.
Cotización y cobro
Para que el usuario confirme antes la cotización exacta y el límite de cargo, puedes usar la vía nativa jobs/quote → jobs/stream con el mismo model/input, además de quoteId y expectedCost. /v1 y /v1beta no aceptan estos datos de cotización nativos y aplican el precio vigente en el momento de aceptar la solicitud. El importe final lo determinan el registro de la tarea y la facturación; la marca de fin del texto no acredita el cobro. reasoning_tokens ya está incluido en completion_tokens y no se suma otra vez.
Otras solicitudes
Estos ejemplos muestran la estructura del protocolo. Sustituye MODEL_ID_FROM_CATALOG por un modelo vigente que admita los campos; no envíes el marcador.
{
"model": "MODEL_ID_FROM_CATALOG",
"input": "Explain a rainbow in one sentence.",
"stream": false
}{
"model": "MODEL_ID_FROM_CATALOG",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Explain a rainbow in one sentence."}],
"stream": false
}Más información
Cotizaciones y compatibilidad · Facturación · Integración en producción · Diagnóstico y recuperación

