halfoff.devDocs
Obtén una clave
Referencia de la API

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#

EstadoNombreCausa habitual¿Reintentar?
400Bad requestEl 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
401UnauthenticatedClave ausente, mal escrita, revocada o caducadaNo, revisa la clave
402Payment requiredSaldo en $0, o la clave alcanzó su límite de gastoSolo tras recargar o subir el límite
403ForbiddenEl modelo no está en la lista permitida de la claveNo
404Not foundID de modelo desconocido, modelo no disponible en ese formato de API, endpoint desconocido o método HTTP incorrectoNo
413Payload too largeEl prompt es demasiado largo para el modeloNo, acórtalo
422UnprocessableEl modelo rechazó el contenido de la peticiónNo
429Too many requestsLímite de ritmo o concurrencia de la clave, o capacidad del modelo ocupadaSí, tras retry-after
500Internal errorError inesperado en Half Off DevSí, con backoff
502Bad gatewayEl 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#

Estadoerror.type de Anthropicerror.type y error.code de OpenAIerror.status de Gemini
400invalid_request_errorinvalid_request_errorINVALID_ARGUMENT
401authentication_errorauthentication_errorUNAUTHENTICATED
402billing_errorinsufficient_quotaRESOURCE_EXHAUSTED
403permission_errorpermission_deniedPERMISSION_DENIED
404not_found_error, o not_found para un endpoint desconocidonot_found_error, o not_found para un endpoint desconocidoNOT_FOUND
413, 422invalid_request_errorinvalid_request_errorINVALID_ARGUMENT
429rate_limit_errorrate_limit_exceededRESOURCE_EXHAUSTED
500api_errorapi_errorINTERNAL
502, 503api_errorapi_errorUNAVAILABLE
504api_errorapi_errorDEADLINE_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#

Estadomessage
400Body must be JSON
400Unknown group 'xyz'
400The model provider's content filter refused this request. Rewording it or using another model may help.
400, 413, 422La explicación del propio modelo, por ejemplo prompt is too long
401Missing API key, Invalid or revoked API key, This API key has expired
402Insufficient balance. Top up at the dashboard.
402This API key has reached its spend limit.
403This API key is not allowed to use 'gpt-6.1-sol'.
404Model 'gpt-9' is not available. See GET /v1/models.
404Unknown endpoint /v1/embeddings, No route for GET /v1/messages
429This API key has reached its rate or concurrency limit.
429All upstream capacity for this model is busy. Retry shortly.
502The 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, 504 y errores de red.
  • Espera lo que indique retry-after si 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, 413 o 422. Un 402 solo 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.