Los errores de la API de embeddings son más difíciles de detectar que los de una interfaz conversacional. Cuando falla una solicitud de chat, el usuario lo ve al momento; los embeddings suelen ejecutarse en tuberías de ingesta offline, donde un 400 o un 429 puede hacer que a un lote de documentos le falten unas filas y nadie lo note hasta que, días después, baja la calidad de la recuperación.

El diagnóstico no debe empezar por "probar otro modelo". El orden es: primero el código de estado y el tipo de error, después el tamaño del cuerpo y el lote, y por último el vector en sí. A continuación se desglosan los casos 401, 413, 429, dimensión incompatible y vector vacío, con código reutilizable para cada uno.

Nota: OpenAI, DeepSeek y otros nombres son marcas de sus respectivos titulares. Los límites citados toman como ejemplo la documentación pública de la interfaz embeddings de OpenAI: cada entrada no supera los 8192 tokens, una solicitud admite como máximo 2048 elementos y el total por solicitud no excede los 300 000 tokens. Los límites de otros proveedores y pasarelas pueden variar; prevalece la documentación real.

1. Agrupar primero por tipo de error

Los errores de embeddings se pueden clasificar en siete grupos. Primero se identifica la categoría y después se decide si hay que cambiar el código, el tamaño del lote o los datos.

Síntoma Código habitual Causa típica Qué revisar primero
Fallo de autenticación 401 o 403 Clave incorrecta, encabezado ausente, clave desactivada, permiso o región equivocados Encabezado Authorization y origen de la clave
Entrada demasiado larga 400 o 413 Una entrada supera el límite del modelo o el cuerpo supera el de la pasarela Tokens por entrada y bytes del cuerpo
Lote demasiado grande 400 o 413 Número de elementos o total de tokens por encima del límite Número de elementos y tokens totales
Límite de tráfico 429 Aumento brusco de concurrencia, clave compartida, tormenta de reintentos Velocidad, concurrencia y Retry-After
Fallo del upstream 5xx Sobrecarga o fallo temporal del servidor ID de solicitud y hora del incidente
Dimensión incompatible 400 o excepción del cliente Cambio de modelo, cambio de dimensión, índice con dimensión fija Nombre del modelo, dimensión y configuración del índice
Vector vacío o NaN 200 con datos anómalos Texto vacío, error de análisis, división por cero en la normalización Longitud, valores finitos y norma del vector

Los registros deben incluir al menos hora, código de estado, tipo de error, nombre del modelo, tamaño del lote, duración e ID de solicitud. No se debe registrar la clave completa ni imprimir párrafos enteros del texto original.

2. 401 y 403: revisar primero los encabezados

El 401 indica que la identidad no se ha validado; el 403, que la identidad es válida pero no tiene permiso de acceso. En ninguno de los dos casos conviene culpar primero a la capacidad del modelo.

Causas habituales: el encabezado no usa Authorization: Bearer <key>; la clave tiene espacios o comillas al principio o al final; la variable de entorno no se cargó; la clave fue eliminada o desactivada; la URL base y la clave pertenecen a plataformas distintas; el proxy inverso descarta el encabezado de autenticación. En el caso del 403 también hay que revisar permisos de modelo, permisos de proyecto, región y lista blanca de IP.

Conviene empezar con una solicitud mínima por curl, con una entrada corta, confirmar que devuelve 200 y luego ir añadiendo lote, concurrencia y proxy. Si sigue devolviendo 401, se prueba otra clave para distinguir si el problema está en la clave o en la configuración de la pasarela.

3. 413 y entrada demasiado larga: distinguir de quién es el límite

Un 413 suele significar que el cuerpo de la solicitud es demasiado grande; puede ser un límite de la pasarela o del proxy inverso, no necesariamente un rechazo del modelo. El límite de entrada del modelo puede devolver 400. Son dos casos distintos.

Tomando como ejemplo embeddings de OpenAI: cada entrada no supera los 8192 tokens, la matriz admite como máximo 2048 elementos y el total por solicitud no excede los 300 000 tokens. Superar el total suele devolver 400, aunque ninguna entrada individual se pase. La pasarela también puede imponer un límite al tamaño del cuerpo y devolver 413 antes de que la solicitud llegue al modelo.

Para tratar una entrada demasiado larga se recomienda este orden:

  1. Calcular los tokens con el tokenizador del modelo correspondiente, no estimar por número de caracteres.
  2. Fragmentar por títulos, párrafos y límites semánticos, con un pequeño solapamiento entre fragmentos.
  3. Truncar como último recurso los fragmentos excesivos, registrando la longitud original y la posición del corte.
  4. En los lotes, limitar a la vez el número de elementos y el total de tokens, dejando entre un 10 % y un 20 % de margen.
  5. Ante un 413, dividir primero el cuerpo; ante un 400, revisar primero el contenido de cada entrada y los parámetros.

También hay que vigilar las cadenas vacías. La interfaz embeddings de OpenAI no acepta entradas vacías, así que los párrafos que quedan en blanco tras la limpieza deben filtrarse antes del envío.

4. 429: límite de tráfico, concurrencia y tormenta de reintentos

Un 429 indica que se superó un límite de velocidad, concurrencia o cuota; no significa saldo insuficiente. Los desencadenantes habituales son: varias aplicaciones comparten la misma clave; las tareas por lotes arrancan a la misma hora en punto; los reintentos no aplican retroceso; las solicitudes largas consumen más cuota.

Primero se mira si la respuesta incluye Retry-After; si está, se respeta. Si no, se aplica retroceso exponencial con fluctuación aleatoria y un límite de reintentos. El reintento solo sirve para errores transitorios: los 401, 403, 400 y la mayoría de los 413 no se resuelven por muchas veces que se repitan.

A nivel de ingeniería también se puede: separar claves por proyecto; añadir cola y límite de concurrencia a las tareas por lotes; escalonar las tareas grandes; enviar los fallos a una cola de mensajes muertos. Los reintentos deben limitarse a tres o cinco; al superarlos se emite una alerta, sin bucles infinitos.

5. Dimensión incompatible: el error que más se pasa por alto

Una dimensión incompatible puede aparecer como un error de la base de datos, por ejemplo "expected dimension 1536, got 1024"; también como un error de forma en numpy, un error de difusión al calcular la similitud coseno o, simplemente, como resultados de búsqueda que de repente pierden toda relación.

Causas habituales: se cambió de modelo pero los vectores antiguos siguen en el índice; se ajustó el parámetro dimensions; un modelo con el mismo nombre tiene dimensiones distintas según el proveedor o la versión; el índice se creó con una dimensión fija; alguien truncó o rellenó con ceros manualmente. Que la dimensión coincida tampoco garantiza que se puedan mezclar: los espacios vectoriales de modelos distintos no son compatibles.

La solución no es rellenar con ceros, sino reconstruir. Cada vector debe registrar nombre del modelo, dimensión, método de normalización, métrica de distancia y versión de fragmentación; un índice solo debe contener vectores del mismo modelo y la misma versión; al cambiar de modelo se crea una colección nueva, se recalcula todo y se valida con un desvío gradual de tráfico. Hay que comprobar la dimensión tanto en la escritura como en la consulta.

6. Vectores vacíos, vectores cero y NaN

Que la interfaz devuelva 200 no significa que los datos sean utilizables. Un vector vacío, un vector de ceros o un NaN invalidan el cálculo de similitud y, a menudo, no lanzan ningún error.

Orígenes habituales: texto que tras la limpieza solo contiene espacios; una matriz de entrada vacía; un error de análisis de la respuesta que deja embedding como lista vacía; un modelo local que devuelve un vector de ceros para una entrada vacía; una normalización que divide por cero y produce NaN; una decodificación base64 incorrecta.

Antes de la ingesta conviene aplicar tres comprobaciones: que la longitud sea igual a la dimensión esperada; que todos los elementos sean números finitos; que la norma del vector supere un umbral muy pequeño. Los registros que no pasan la validación se envían a una tabla de cuarentena con el ID del texto original y el motivo, se corrigen y se reprocesan; no se descartan en silencio.

7. Código reutilizable para el manejo de errores

El ejemplo siguiente se dirige a una interfaz compatible con OpenAI y cubre retroceso por límite de tráfico, división de lotes y validación de vectores. En uso real hay que sustituir el nombre del modelo, la dimensión y la URL base por la configuración propia.

import logging
import math
import random
import time

from openai import OpenAI, APIStatusError, APITimeoutError, RateLimitError

logger = logging.getLogger("embedding")
client = OpenAI(
    api_key="sk-tu-clave",
    base_url="https://api.zhongshangqihang.com/v1",
    max_retries=0,  # Desactiva los reintentos automáticos del SDK para no superponerlos
)

MODEL = "text-embedding-3-small"
EXPECTED_DIM = 1536


def vector_is_valid(vec: list[float]) -> bool:
    if len(vec) != EXPECTED_DIM:
        return False
    if not all(math.isfinite(x) for x in vec):
        return False
    norm = math.sqrt(sum(x * x for x in vec))
    return norm > 1e-6


def embed_batch(texts: list[str], attempt: int = 0) -> list[list[float] | None]:
    # No se envían textos vacíos; se usa None para mantener la alineación de índices
    pairs = [(i, t) for i, t in enumerate(texts) if t and t.strip()]
    if not pairs:
        return [None] * len(texts)
    indices = [i for i, _ in pairs]
    payload = [t for _, t in pairs]

    try:
        resp = client.embeddings.create(model=MODEL, input=payload, timeout=60)
        items = sorted(resp.data, key=lambda item: item.index)
        vectors = [item.embedding for item in items]
        if len(vectors) != len(payload):
            raise ValueError(f"Cantidad devuelta incoherente: {len(vectors)} != {len(payload)}")
        for index, vec in enumerate(vectors):
            if not vector_is_valid(vec):
                raise ValueError(f"Vector anómalo: index={index}, dim={len(vec)}")
        result: list[list[float] | None] = [None] * len(texts)
        for source_index, vec in zip(indices, vectors):
            result[source_index] = vec
        return result
    except (RateLimitError, APITimeoutError) as exc:
        if attempt >= 5:
            raise
        wait = min(2 ** attempt, 30) + random.random()
        logger.warning("Límite de tráfico o tiempo de espera; reintento en %.1f s: %s", wait, exc)
        time.sleep(wait)
        return embed_batch(texts, attempt + 1)
    except APIStatusError as exc:
        if exc.status_code in (400, 413) and len(texts) > 1:
            mid = len(texts) // 2
            logger.warning("Lote demasiado grande; se divide en %d + %d", mid, len(texts) - mid)
            return embed_batch(texts[:mid], attempt) + embed_batch(texts[mid:], attempt)
        if exc.status_code >= 500 and attempt < 5:
            wait = min(2 ** attempt, 30) + random.random()
            logger.warning("Error del upstream; reintento en %.1f s: %s", wait, exc)
            time.sleep(wait)
            return embed_batch(texts, attempt + 1)
        raise

El código asume dos compromisos. Primero, el 400 también participa en la división del lote, porque algunas pasarelas informan el exceso de tokens totales como 400; si una sola entrada sigue fallando, la excepción se propaga. Segundo, la validación del vector se hace antes de escribir, de modo que los datos defectuosos no entran en el índice. En producción también conviene registrar el ID de solicitud, el número de reintentos y el lote fallido.

8. Orden de diagnóstico y lista de monitoreo

Ante un error de embeddings, este orden suele ser más rápido que cambiar de modelo repetidamente:

  1. Registrar código de estado, tipo de error, ID de solicitud, nombre del modelo, tamaño del lote y duración.
  2. Ante 401 o 403, revisar encabezados, clave, variables de entorno y plataforma.
  3. Ante 400, revisar tokens por entrada, tokens totales, cadenas vacías y formato de parámetros.
  4. Ante 413, revisar bytes del cuerpo y límites de la pasarela; dividir la solicitud y reintentar.
  5. Ante 429, mirar Retry-After, aplicar retroceso, reducir concurrencia y comprobar si la clave se comparte.
  6. Ante 5xx, reintentar con retroceso un número limitado de veces; si persiste, contactar al proveedor con el ID de solicitud.
  7. Si devuelve 200 pero el resultado es anómalo, revisar dimensión, norma, NaN y normalización.
  8. Integrar estas métricas en el monitoreo y ejecutar regresiones periódicas con un conjunto de evaluación fijo.

El monitoreo debe contar por hora los 401, 400, 413, 429 y 5xx, así como los vectores vacíos y las anomalías de dimensión. Si la tasa de error se dispara, primero hay que ver si se concentra en un proyecto, una clave, un modelo o un lote de documentos, y después decidir si se reduce la concurrencia, se divide el lote, se revierte o se corrigen los datos.

Preguntas frecuentes

413 y 400 indican ambos que la entrada es demasiado larga; ¿cuál es la diferencia?

El 413 suele provenir del límite de tamaño del cuerpo impuesto por la pasarela o el proxy inverso, y la solicitud puede ser rechazada antes de llegar al modelo; el 400 proviene más bien de parámetros de la interfaz o del límite de tokens. Hay que revisar el cuerpo de la respuesta y el ID de solicitud para decidir si se divide la solicitud o se fragmenta el texto.

Si ninguna entrada supera los 8192 tokens, ¿por qué sigue apareciendo 400?

Puede que el total de tokens por solicitud supere el límite, que la matriz de entrada tenga demasiados elementos, que haya cadenas vacías, que el formato de los parámetros sea incorrecto o que el nombre del modelo no exista. Conviene dividir el lote, filtrar textos vacíos y revisar el cuerpo punto por punto.

¿Cuántos reintentos conviene hacer ante un 429?

Lo habitual es de tres a cinco, con retroceso exponencial y fluctuación aleatoria, respetando primero Retry-After. Al superar el límite se pasa a una cola o se emite una alerta, sin reintentos infinitos. Cuando el límite es severo, reducir la concurrencia es más eficaz que aumentar los reintentos.

¿Se puede resolver una dimensión incompatible rellenando con ceros?

No. Rellenar con ceros altera la dirección del vector y la similitud, y los resultados de recuperación dejan de ser fiables. Lo correcto es unificar modelo y dimensión, crear un índice nuevo, recalcular todo y validar con un desvío gradual de tráfico.

¿Por qué la interfaz devuelve 200 y el vector llega vacío?

Puede que la entrada estuviera vacía o que tras la limpieza solo quedaran espacios, o que haya un problema de análisis de la respuesta, decodificación base64 o inferencia local. Antes de la ingesta hay que validar longitud, valores finitos y norma, y aislar los registros anómalos para investigarlos.

Siguiente paso

Conviene crear primero una clave de prueba en ZSQH AI Gateway, ejecutar con un lote pequeño de textos las pruebas de autenticación, lotes, límite de tráfico y validación de vectores, y después conectar la tubería de ingesta definitiva. La disponibilidad de modelos y los precios figuran en los precios de los modelos.

Lecturas relacionadas: