Errores
Códigos HTTP de Half Off Dev, cuerpos de error en formato Anthropic, OpenAI y Gemini, errores dentro de un stream y código de reintento listo para copiar.
Los tokens a mitad de precio solo salen baratos si tu código gestiona bien los fallos. Half Off Dev lo pone fácil: los errores usan códigos de estado HTTP normales y el cuerpo viene en el formato de la API que llamaste, así que los SDK de Anthropic, OpenAI y Google lanzan las mismas excepciones que contra el proveedor original. Como regla general, un 4xx significa que hay que cambiar la petición (o recargar saldo), mientras que 429 y 5xx son temporales y vale la pena reintentarlos.
Códigos de estado de un vistazo#
| Estado | Nombre | Causa habitual | ¿Reintentar? |
|---|---|---|---|
400 | Bad request | El cuerpo no es JSON válido. El modelo rechazó un parámetro, como un max_tokens por encima de su límite. x-halfoff-group desconocido. El filtro de contenido del modelo rechazó la petición. | No, corrige la petición |
401 | Unauthenticated | Clave ausente, mal escrita, revocada o caducada | No, revisa la clave |
402 | Payment required | Saldo en $0, o la clave alcanzó su límite de gasto | Solo tras recargar o subir el límite |
403 | Forbidden | El modelo no está en la lista permitida de la clave | No |
404 | Not found | ID de modelo desconocido, modelo no disponible en ese formato de API, endpoint desconocido o método HTTP incorrecto | No |
413 | Payload too large | El prompt es demasiado largo para el modelo | No, acórtalo |
422 | Unprocessable | El modelo rechazó el contenido de la petición | No |
429 | Too many requests | Límite de ritmo o concurrencia de la clave, o capacidad del modelo ocupada | Sí, tras retry-after |
500 | Internal error | Error inesperado en Half Off Dev | Sí, con backoff |
502 | Bad gateway | El modelo falló en todos los canales que Half Off Dev probó | Sí, con backoff |
Trata cualquier otro 5xx como un 502. Las peticiones rechazadas antes de llegar al modelo nunca se cobran: 400 por JSON no válido o grupo desconocido, 401, 402, 403, 404 por modelo o endpoint desconocido, y 429 por un límite de la clave.
Formatos del cuerpo de error#
El mismo 401 en cada uno de los tres formatos:
{"type": "error", "error": {"type": "authentication_error", "message": "Invalid or revoked API key"}}Tipo y estado de cada código#
| Estado | error.type de Anthropic | error.type y error.code de OpenAI | error.status de Gemini |
|---|---|---|---|
400 | invalid_request_error | invalid_request_error | INVALID_ARGUMENT |
401 | authentication_error | authentication_error | UNAUTHENTICATED |
402 | billing_error | insufficient_quota | RESOURCE_EXHAUSTED |
403 | permission_error | permission_denied | PERMISSION_DENIED |
404 | not_found_error, o not_found para un endpoint desconocido | not_found_error, o not_found para un endpoint desconocido | NOT_FOUND |
413, 422 | invalid_request_error | invalid_request_error | INVALID_ARGUMENT |
429 | rate_limit_error | rate_limit_exceeded | RESOURCE_EXHAUSTED |
500 | api_error | api_error | INTERNAL |
502, 503 | api_error | api_error | UNAVAILABLE |
504 | api_error | api_error | DEADLINE_EXCEEDED |
En un cuerpo de Gemini, error.code es siempre el estado HTTP, y tanto 402 como 429 se traducen en RESOURCE_EXHAUSTED, así que decide según el número, no según el texto. Incluso los errores que se producen antes de leer el cuerpo, como una ruta desconocida, usan el formato que corresponde a la ruta que llamaste.
Mensajes que puedes ver#
| Estado | message |
|---|---|
400 | Body must be JSON |
400 | Unknown group 'xyz' |
400 | The model provider's content filter refused this request. Rewording it or using another model may help. |
400, 413, 422 | La explicación del propio modelo, por ejemplo prompt is too long |
401 | Missing API key, Invalid or revoked API key, This API key has expired |
402 | Insufficient balance. Top up at the dashboard. |
402 | This API key has reached its spend limit. |
403 | This API key is not allowed to use 'gpt-6.1-sol'. |
404 | Model 'gpt-9' is not available. See GET /v1/models. |
404 | Unknown endpoint /v1/embeddings, No route for GET /v1/messages |
429 | This API key has reached its rate or concurrency limit. |
429 | All upstream capacity for this model is busy. Retry shortly. |
502 | The model provider returned an error. Please retry. |
Toda respuesta de error lleva la cabecera x-halfoff-request-id, incluso si la petición nunca llegó a un modelo (clave incorrecta, saldo vacío, modelo desconocido). Inclúyela cuando escribas a soporte.
Errores dentro de un stream#
Una vez abierto el stream, el estado 200 ya se envió, así que un problema posterior llega como un evento dentro del stream. Los fallos anteriores al primer token se reintentan en otro canal sin que lo notes, por lo que lo habitual es que un stream funcione o falle con un error HTTP normal antes de empezar.
Si el modelo deja de enviar datos durante 3 minutos a mitad de un stream, Half Off Dev lo cierra con un evento de error en el formato de ese stream:
event: error
data: {"type": "error", "error": {"type": "api_error", "message": "Upstream stopped responding mid-stream"}}Los errores que un canal informa a mitad de un stream se sustituyen por un evento genérico en el mismo formato. La capacidad ocupada se convierte en overloaded_error de Anthropic, rate_limit_exceeded de OpenAI o RESOURCE_EXHAUSTED de Gemini, con el mensaje Upstream capacity for this model is busy. Retry shortly. Todo lo demás pasa a ser api_error (UNAVAILABLE en Gemini) con The model provider returned an error. Please retry. En la API Responses llega un evento error, como {"type": "error", "code": "server_error", "message": "…"}. Repite la petición desde el principio; lo que ya se transmitió antes del error se cobra.
Estrategia de reintentos#
Half Off Dev ya pasa una petición fallida a otros canales antes de devolver un error. Añade tus propios reintentos para lo que queda:
- Reintenta ante
429,500,502,503,504y errores de red. - Espera lo que indique
retry-aftersi viene la cabecera. Si no, aplica backoff exponencial: 1, 2, 4, 8 segundos y así hasta 30, con hasta un segundo de variación aleatoria para que tus procesos no reintenten todos a la vez. - Abandona tras unos cinco intentos y muestra el error.
- No repitas sin cambios un
400,401,403,404,413o422. Un402solo tiene sentido reintentarlo después de recargar. - Cuidado con los timeouts. Una petición que agotó el tiempo en tu lado puede haber terminado y haberse cobrado. Consulta Reintentos e idempotencia.
Los SDK oficiales de Anthropic y OpenAI ya reintentan 429 y 5xx dos veces con backoff; ajusta max_retries (Python) o maxRetries (Node.js) en el cliente para cambiarlo. Para llamadas HTTP directas, te sirve una función como esta:
import os
import random
import time
import requests
RETRY_ON = {429, 500, 502, 503, 504}
def wait_time(attempt, retry_after=None):
if retry_after:
return float(retry_after)
return min(30, 2 ** (attempt - 1)) + random.random()
def call_with_retries(url, payload, attempts=5):
headers = {"Authorization": f"Bearer {os.environ['HALFOFF_API_KEY']}"}
for attempt in range(1, attempts + 1):
try:
resp = requests.post(url, json=payload, headers=headers, timeout=300)
except (requests.ConnectionError, requests.Timeout):
if attempt == attempts:
raise
time.sleep(wait_time(attempt))
continue
if resp.status_code in RETRY_ON and attempt < attempts:
time.sleep(wait_time(attempt, resp.headers.get("retry-after")))
continue
if not resp.ok: # un 4xx que no conviene repetir, o falló el último intento
raise RuntimeError(f"{resp.status_code}: {resp.text[:500]}")
return resp.json()
answer = call_with_retries(
"https://halfoff.dev/v1/chat/completions",
{
"model": "gpt-6.1-sol",
"messages": [{"role": "user", "content": "Dime tres usos curiosos de un clip."}],
},
)
print(answer["choices"][0]["message"]["content"])Sea cual sea el formato, error.message está escrito para personas. Regístralo junto con el código de estado y la cabecera x-halfoff-request-id.