Procesamiento de documentos (Job API)
Ciclo de vida de una tarea
JOB_STATUS_NEW → JOB_STATUS_FILE_PROCESSING → JOB_STATUS_OCR → JOB_STATUS_LLM → JOB_STATUS_COMPLETE | JOB_STATUS_PARTIAL | JOB_STATUS_FAILED
- JOB_STATUS_NEW — la tarea se ha creado y puesto en cola.
- JOB_STATUS_FILE_PROCESSING — se descargan los archivos, se descomprimen los comprimidos y se normalizan los formatos a una forma procesable. Puede pasar directamente a
JOB_STATUS_FAILEDsi ninguno de los orígenes es accesible o se supera un límite de archivos (el motivo, en el campoerrorde la tarea). - JOB_STATUS_OCR — reconocimiento de texto de cada archivo.
- JOB_STATUS_LLM — el texto reconocido se envía al modelo con sus prompts.
- JOB_STATUS_COMPLETE — sin errores en la etapa OCR ni LLM.
- JOB_STATUS_PARTIAL — al menos una respuesta del modelo (LLM) correcta, pero también al menos un error en la etapa OCR o LLM (revise los errores a nivel de archivo en el resultado), o una respuesta que no cumple
responseSchema(consulte «Salida estructurada»). - JOB_STATUS_FAILED — los errores impidieron que ningún archivo llegara a una respuesta correcta del modelo: o bien un fallo durante la descarga/descompresión de archivos (el motivo, en el campo
errora nivel de tarea;ocr[]/llm[]quedan vacíos), o bien ningún archivo alcanzó un resultado correcto en la etapa OCR o LLM.
El procesamiento es asíncrono: consulte el estado de la tarea vía GET /v1/jobs/{id} hasta que alcance un estado terminal.
Subida de archivos
Puede especificar el origen de una tarea de dos maneras:
- URL pública — pase la URL en
sourceUrlsal crear la tarea. ChunkChef descarga el archivo (timeout de descarga: 30 s, hasta 3 redirecciones). - Subida directa — suba el archivo y luego use la URL devuelta en
sourceUrls:POST /v1/jobs/upload(multipart/form-data) — sube un único archivo por HTTP. Es un endpoint HTTP independiente (no grpc-gateway). La respuesta es JSON en snake_case:{"url": "…", "name": "…", "size_bytes": 12345}. El cuerpo de error de este endpoint es{"code": <int>, "message": "…"}, sin arraydetails. El límite de tamaño lo fija la configuración (grpc.maxRecvMsgBytes; 20 MiB en el despliegue actual), no un valor por defecto del código.- método gRPC
Upload(client-streaming) — una subida en streaming (límite por mensaje individual: 20 MiB).
Prompts y extracción de datos
La extracción de datos se rige por prompts de texto, no por un esquema. En el campo prompts pasa un array de instrucciones. En la etapa LLM, se toma el texto reconocido de cada archivo, se divide en fragmentos si es necesario y se envía al modelo junto con su prompt. La respuesta del modelo se devuelve como texto por archivo (y por fragmento, si el archivo se dividió). Opcionalmente, añada responseSchema — un JSON Schema — para que ChunkChef valide la forma de la respuesta tras la llamada; consulte «Salida estructurada» más abajo.
La división en fragmentos es consciente de la estructura: el texto se corta por los límites de la estructura de su formato (Markdown, HTML, XML o plain) — las tablas no se rompen a mitad de fila (y, si una tabla no cabe entera, su fila de encabezado se repite en cada fragmento) y se preserva el contexto de los encabezados de sección. El tamaño del fragmento en tokens lo fija neural.chunkBudgetTokens (consulte «Conectar un modelo»); si no se especifica, se aplica el valor por defecto conservador del servicio. Cada fragmento es una llamada al modelo independiente con una copia completa del prompt y una fila llm[] aparte (chunkIndex / chunkTotal). La recomposición de la respuesta a partir de los fragmentos en orden de chunkIndex corre, por defecto, de su lado. Opcionalmente, ChunkChef puede hacer esa recomposición por usted: active merge.enabled para que el servicio combine las respuestas por fragmento en un único resultado para todo el documento — consulte «Fusión de resultados fragmentados» más abajo.
Para obtener datos estructurados, pídalos directamente en el prompt; por ejemplo: «Devuelve el resultado como JSON con los siguientes campos: …». Su prompt y el modelo que elija determinan la validez y la forma del JSON; ChunkChef no impone ni valida ningún esquema, a menos que active responseSchema (consulte «Salida estructurada» más abajo).
Consejos para los prompts:
- enumere de forma explícita e inequívoca los campos que necesita;
- especifique el formato de salida directamente en el texto del prompt;
- tenga en cuenta el límite de tamaño del prompt: 64 KiB (consulte «Límites»).
Nota. Los marcadores <…> de las plantillas siguientes señalan los espacios que debe rellenar: ChunkChef no los sustituye automáticamente, el prompt se envía al modelo exactamente como está escrito. Su prompt y el modelo que elija determinan la estructura de la salida; no hay validación de esquema del lado del servidor, a menos que defina responseSchema (consulte «Salida estructurada»).
La calidad del prompt determina sus resultados. Lo bien que funcione la extracción depende tanto de su prompt como del modelo que elija, a menudo más. Un prompt vago produce una salida vaga incluso en un modelo de primer nivel, mientras que un prompt preciso y bien estructurado obtiene resultados fiables incluso de modelos más pequeños y baratos. Trate la plantilla siguiente como un punto de partida, no como un prompt acabado: tómela, describa su tipo de documento, su tarea y la salida exacta que necesita, y luego pida a un modelo capaz que la convierta en un prompt adaptado a su caso, manteniendo esta estructura pero afinando las reglas, los casos límite y la validación de la salida para sus datos. El meta-prompt para ello está al final de esta sección.
Ejemplo: extracción de campos de factura / pedido / recibo
Texto de prompts[0]:
TASK
Extract structured fields from a single procurement/accounting document and return them strictly as JSON.
IMPORTANT
Your answer must contain ONLY JSON. Do not add any comments, explanations, or surrounding text before or after the JSON.
1. INPUT
The recognized text of a single document follows the "---" marker below (the OCR output is HTML). It is the only source. The document may be an <type: invoice / purchase order / receipt> and may contain stamps, signatures, and multi-row line-item tables. The text may be truncated or split into chunks — work only with the text you are given and never assume content you cannot see.
2. OUTPUT JSON FORMAT
Return a single JSON object matching this schema (the inline comments are explanatory — do not include them in the output):
{
"doc_type": "string", // one of: invoice | purchase_order | receipt | unknown
"number": "string",
"date": "string", // ISO 8601: YYYY-MM-DD
"supplier": {
"name": "string",
"tax_id": "string" // e.g. US EIN or EU VAT ID, as printed
},
"items": [
{ "name": "string", "qty": number, "price": number, "amount": number }
],
"total": number,
"currency": "string" // ISO 4217, e.g. USD, EUR
}
3. EXTRACTION RULES
- doc_type: classify from the title, headers, and content. If it is none of the listed types, set "unknown" and still fill any fields you can.
- number / date: the document's own number and issue date. Convert the date to ISO 8601 (YYYY-MM-DD).
- supplier: the selling/issuing party, not the buyer. tax_id: the supplier's tax identifier, exactly as printed.
- items: one object per line item, in document order. Keep "name" exactly as written, including specifications and units that identify the item.
- qty / price / amount / total: return as JSON numbers — strip thousands separators and currency symbols, use a dot as the decimal separator ("1,200.50" -> 1200.5).
- currency: ISO 4217 code. If only a symbol is present, map it ("$" -> "USD", "€" -> "EUR"). If it cannot be determined, use null.
4. PROCESSING REQUIREMENTS
- Use ONLY the provided document text. Do not add external knowledge or infer values that are not present.
- Do NOT guess, complete, or reformat values beyond the normalization explicitly required above.
- Field not found -> null for scalars (including supplier sub-fields), [] for "items". Never drop a schema key.
- Analyze the entire document, including tables and appendices. A reference to an external attachment is not a line item.
- Be literal and deterministic: the same input must always produce the same output.
5. RESPONSE FORMAT
- Return ONLY the valid JSON object described above.
- No markdown, no code fences, no text before or after the JSON.
REMEMBER
Your answer must start with "{" and end with "}". Nothing else. If the document is not one of the expected types, return the schema with "doc_type": "unknown" and whatever fields you could extract.
Envoltorio JSON (principal — sobre el Xiaomi mimo-v2-flash verificado):
{
"sourceUrls": ["<YOUR_FILE_URL>"],
"title": "Invoice <number>",
"prompts": ["<THE ENTIRE TEMPLATE ABOVE, AS A SINGLE STRING>"],
"ocr": { "provider": "NEURAL_CLIENT_TYPE_MISTRAL", "model": "mistral-ocr-latest", "providerKey": "<YOUR_KEY>" },
"neural": {
"type": "NEURAL_CLIENT_TYPE_XIAOMI",
"model": "mimo-v2-flash",
"apiKey": "<YOUR_PROVIDER_KEY>",
"reasoningEffort": "low"
}
}
Cabecera: Authorization: Bearer <YOUR_CHUNKCHEF_KEY>.
| Campo de la solicitud | Qué es | «Variable» |
|---|---|---|
Authorization | su API key de ChunkChef | clave de acceso a la API |
sourceUrls[] | URL de archivos | enlaces a documentos |
prompts[0] | toda la plantilla como una sola cadena | prompt estructurado |
neural.type / neural.model | proveedor y modelo | modelo |
neural.apiKey | clave del proveedor (BYOK) | clave del modelo |
neural.reasoningEffort | opcional minimal/low/medium/high | profundidad de razonamiento |
ocr.provider | enum del proveedor de OCR (p. ej. NEURAL_CLIENT_TYPE_MISTRAL) | proveedor de OCR |
ocr.model | identificador del modelo de OCR; obligatorio | modelo de OCR |
ocr.providerKey | clave del proveedor para OCR (BYOK); se acepta solo como entrada, nunca se devuelve | clave de OCR |
extractionMode | estrategia de extracción de texto: EXTRACTION_MODE_HYBRID (predeterminado, «Extracción híbrida optimizada») deja que el convertidor decida entre extraer texto directamente u OCR, por archivo; EXTRACTION_MODE_OCR_ALWAYS fuerza el OCR neuronal en todos los archivos; si se omite / EXTRACTION_MODE_UNSPECIFIED, hereda el valor predeterminado de su cuenta | Opcional |
responseSchema | JSON Schema opcional (cadena en bruto) que valida la respuesta JSON del modelo; autocontenida, compatible con el subconjunto estricto (consulte «Salida estructurada») | Opcional |
Más ejemplos (en breve). La misma mecánica: solo cambian el texto del prompt y la forma esperada de la respuesta en llm[].content:
- Clasificación. Prompt: «Determine el tipo de documento: factura / contrato / recibo / carta / otro. Devuelve una sola palabra de la lista, sin explicación». Respuesta: una sola palabra (p. ej.
contract). - Cláusulas del contrato. Prompt: «Extraiga: partes, objeto, importe, plazo y condiciones de rescisión. Devuelve JSON conforme al esquema {parties[], subject, amount, term, termination}. Campo no encontrado → null». Respuesta: JSON conforme al esquema.
- Resumen. Prompt: «Resuma el documento en 3-5 frases. Sin listas de viñetas». Respuesta: texto en prosa.
Construya su propio prompt (meta-prompt)
La forma más rápida de obtener un prompt de alta calidad es que un modelo capaz lo escriba por usted. Entréguele el meta-prompt de abajo: pegue nuestro ejemplo como estructura de referencia, la forma de salida que necesita (JSON/CSV/Markdown) y una descripción de su contexto y su tarea, y obtiene de vuelta un prompt de ChunkChef listo para usar. Rellene los bloques entre corchetes; el modelo se encarga del resto.
You are a senior prompt engineer. Build a production-grade extraction prompt that will be sent to a document-processing model through the ChunkChef API. The model receives the OCR'd text (HTML) of a single document and must return data in a strict, machine-parseable format.
WHAT I'M GIVING YOU
1) REFERENCE PROMPT — the structure and style to follow. Preserve its section anatomy.
<<<REFERENCE_PROMPT
[paste the ChunkChef example prompt here]
REFERENCE_PROMPT
2) TARGET OUTPUT — the exact shape I need back: a JSON schema/sample, CSV columns, or Markdown layout.
<<<TARGET_OUTPUT
[paste your desired JSON / CSV / Markdown here]
TARGET_OUTPUT
3) DOMAIN & CONTEXT — what these documents are, where they come from, and their quirks (languages, layouts, stamps, tables, common OCR errors).
<<<CONTEXT
[describe your documents and domain]
CONTEXT
4) TASK — exactly what to extract or produce, plus the business rules, definitions, and edge cases that matter.
<<<TASK
[describe the task and rules]
TASK
5) OUTPUT FORMAT — one of: JSON | CSV | Markdown. Default: JSON.
<<<FORMAT
JSON
FORMAT
HOW TO BUILD THE PROMPT
1. Study the domain and task deeply before writing. Infer the edge cases a careful human reviewer would catch — ambiguous fields, duplicates, ranges, units, missing data, multi-row tables, appendices — and address each one explicitly.
2. Keep the REFERENCE PROMPT's anatomy: a one-line TASK, an IMPORTANT "only the target format" rule, then numbered sections (INPUT, OUTPUT FORMAT, EXTRACTION/PROCESSING RULES field by field, PROCESSING REQUIREMENTS, RESPONSE FORMAT), and a final REMEMBER reinforcement.
3. Make the output contract unambiguous for the chosen format:
- JSON: give the full schema with types and nullability, mark required vs optional keys, forbid any text/markdown/code fences outside the JSON, and require the answer to start with "{" (or "[") and end with "}" (or "]").
- CSV: fix the exact column order and header row, the delimiter, the quoting/escaping rule, and how empty values are written; one record per row, no prose.
- Markdown: fix the exact headings/table columns and forbid any content outside that layout.
4. Pin the data discipline: use only the provided document text; never invent, guess, or reformat beyond the normalization you explicitly define; specify number, date, and unit normalization; define how "not found" is represented (null / empty / skipped) and how duplicates are handled; preserve source values verbatim where identity matters.
5. Account for ChunkChef specifics: the model sees one document's OCR'd HTML, possibly truncated or split into chunks; do not rely on any temperature setting — enforce determinism through wording ("be literal and deterministic"); the prompt is sent verbatim, so resolve every "<placeholder>" yourself.
6. Self-check before finishing: re-read the TARGET OUTPUT and confirm the prompt forces exactly that shape, that every field has a rule, and that a small, cheap model could follow it without guessing.
OUTPUT
Return ONLY the finished prompt, ready to paste into ChunkChef's "prompts" array — no explanation, no preamble, no code fences.
Conectar un modelo (BYOK)
La etapa LLM se ejecuta con la clave de su proveedor. La configuración se pasa en el objeto neural al crear una tarea:
| Campo | Obligatorio | Descripción |
|---|---|---|
type | sí | proveedor (consulte la lista de abajo) |
model | sí | identificador del modelo; se pasa al proveedor tal cual |
apiKey | sí | la clave de su proveedor; se acepta solo como entrada, nunca se devuelve en las respuestas |
reasoningEffort | no | indicación de profundidad de razonamiento; valores permitidos: minimal, low, medium, high (vacío = desactivado). Un valor inválido → error 400. Que se respete o no depende del proveedor/modelo. |
chunkBudgetTokens | no | presupuesto de tokens para una sola llamada al modelo: cubre tanto el prompt como el texto del documento en un único fragmento. 0/sin especificar → el valor por defecto conservador del servicio. Rango: 16000–2000000; un valor fuera de rango → 400. Ajústelo a la ventana de contexto de su modelo, que solo usted conoce. La respuesta siempre devuelve el presupuesto efectivo real (incluso cuando depende del valor por defecto): el servicio reserva un pequeño margen para los tokens de la plantilla de chat, por lo que el valor devuelto es algo menor que el que fijó. |
Proveedores admitidos:
| Proveedor | valor de neural.type |
|---|---|
| OpenAI | NEURAL_CLIENT_TYPE_OPENAI |
| Anthropic (Claude) | NEURAL_CLIENT_TYPE_CLAUDE |
| xAI (Grok) | NEURAL_CLIENT_TYPE_GROK |
| Together | NEURAL_CLIENT_TYPE_TOGETHER |
| DeepSeek | NEURAL_CLIENT_TYPE_DEEPSEEK |
| Xiaomi | NEURAL_CLIENT_TYPE_XIAOMI |
| Mistral | NEURAL_CLIENT_TYPE_MISTRAL |
| OpenRouter | NEURAL_CLIENT_TYPE_OPENROUTER |
A través de NEURAL_CLIENT_TYPE_OPENROUTER obtiene modelos de muchos proveedores que no tienen una integración directa.
Paga directamente al proveedor a su tarifa: ChunkChef no añade ningún recargo sobre los tokens ni el OCR.
Modo de extracción (Extracción híbrida optimizada)
Por defecto (EXTRACTION_MODE_HYBRID), el convertidor elige por archivo entre extraer el texto directamente o usar OCR — la estrategia de «Extracción híbrida optimizada». Establezca extractionMode en EXTRACTION_MODE_OCR_ALWAYS en una tarea para forzar el OCR neuronal en todos los archivos, incluso si ya tienen una capa de texto extraíble. Tenga en cuenta que, en ese caso, cada archivo pasa a generar una llamada de OCR al proveedor — un aumento de coste frente al modo híbrido. El extractionMode propio de una tarea anula el valor predeterminado de su cuenta, que se define en el panel bajo Configuración.
Salida estructurada (responseSchema)
Pase un responseSchema opcional — un JSON Schema, como cadena JSON en bruto — al crear la tarea para que ChunkChef valide la forma de la respuesta JSON del modelo, además de (no en lugar de) describir esa forma en su prompt.
El esquema debe ser autocontenido: solo se permiten referencias internas #/$defs — nada de $ref externos ni $schema/$id remotos. Límites: máximo 128 KiB, profundidad de anidamiento 64, 10000 nodos en total. Un esquema inválido o que exceda los límites se rechaza con 400 al crear la tarea, antes de procesar cualquier archivo. Para OpenAI y Grok, las salidas estructuradas se ejecutan de forma nativa y requieren el subconjunto estricto de ChunkChef: cada propiedad listada en required (exprese la opcionalidad con type: [..., "null"], no omitiéndola) y additionalProperties: false en cada objeto. Escribir su esquema en ese subconjunto estricto lo hace portable: el mismo esquema funciona sin cambios en todos los proveedores admitidos. Un esquema que supera las comprobaciones de ChunkChef al crear la tarea pero no es compatible con el subconjunto estricto para OpenAI/Grok no falla en la creación ni pasa por el flujo schemaValid/schemaErrors — en su lugar, la fila llm[] correspondiente termina como JOB_LLM_STATUS_FAILED con un error del proveedor, ya que la conformidad con el subconjunto estricto la exige el proveedor, no la comprobación de creación de ChunkChef.
Dos campos en cada fila llm[] informan el resultado (consulte «Referencia de la API»):
schemaValid— si elcontentde la fila cumple conresponseSchema. Presente solo cuando el archivo no se dividió en fragmentos (chunkTotal= 1); se omite en archivos con varios fragmentos — el mismo esquema igualmente se envía en cada llamada por fragmento, pero los veredictos por fragmento aún no se combinan en uno solo para todo el documento.schemaErrors— las infracciones de conformidad cuandoschemaValidesfalse(hasta 10, cada una de hasta 512 bytes).
Si la respuesta de alguna fila no cumple, la tarea termina como JOB_STATUS_PARTIAL en lugar de JOB_STATUS_COMPLETE — content igual se devuelve tal cual, solo que marcado. Para los proveedores en los que ChunkChef emula salidas estructuradas (DeepSeek, Xiaomi), una primera respuesta no conforme recibe un reintento de reparación automático antes de evaluarse.
Fusión de resultados fragmentados (merge)
Cuando un documento se divide en fragmentos (consulte «Prompts y extracción de datos» más arriba), cada fragmento recibe su propia llamada al modelo y su propia fila llm[] — combinar esas piezas en una sola respuesta corre, por defecto, de su lado. Active merge al crear la tarea y ChunkChef hará esa combinación por usted, en el servidor: el resultado gana un array merged[] con una respuesta por todo el documento (o por archivo), construida a partir de las respuestas por fragmento.
Cuándo activarlo. Si extrae JSON estructurado de un documento lo bastante largo como para dividirse en varios fragmentos —por ejemplo, una factura de varias páginas cuyas líneas de artículos se reparten entre la página 1 y la página 2—, merge le ahorra escribir usted mismo el código de recomposición: ChunkChef une los campos JSON, concatena y deduplica los arrays, y resuelve cualquier campo que difiera entre fragmentos.
Actívelo:
{ "merge": { "enabled": true } }
El resto de campos merge.* son opcionales y tienen valores por defecto seguros (consulte «Opciones de fusión» en la referencia de la API para la lista completa).
Ejemplo: una factura de dos páginas
Imagine una factura de 2 páginas, reconocida por OCR en un texto lo bastante largo como para dividirse en 2 fragmentos — la página 1 cae en el fragmento 0, la página 2 en el fragmento 1. Su prompt pide JSON: { "supplier": "...", "items": [...], "total": "..." }. Sin merge, obtiene dos respuestas llm[] separadas, una por fragmento, y las combina usted mismo. Con merge activo, ChunkChef las combina en un único objeto JSON: los arrays items de ambos fragmentos se concatenan en uno solo, y supplier/total se toman del fragmento que realmente los reporta. Si el fragmento 0 y el fragmento 1 reportan un total distinto para el mismo documento (por ejemplo, el OCR duplicó una línea de subtotal, o el modelo leyó mal un número), eso es un conflicto real — consulte «Resolución de valores en conflicto» más abajo.
Alcance: una respuesta por archivo o una para toda la tarea (merge.scope)
merge.scope controla cuánto se combina en una única entrada de merged[]:
MERGE_SCOPE_FILE(por defecto) — los fragmentos se fusionan por separado dentro de cada archivo: una entradamerged[]por archivo, conmerged[].fileigual a la URL de ese archivo.MERGE_SCOPE_JOB— los fragmentos se fusionan entre todos los archivos de la tarea en una sola respuesta: una entradamerged[]conmerged[].filevacío (""). Use este modo cuando los archivos que subió son páginas o partes de un mismo documento lógico (p. ej. una orden de compra dividida en varios archivos de origen) y quiere un único resultado combinado en lugar de uno por archivo.
Resolución de valores en conflicto (merge.conflictPolicy)
Al fusionar JSON, dos fragmentos pueden discrepar sobre el mismo campo — uno reporta total: 1500, otro total: 1520 para la misma factura. merge.conflictPolicy decide qué valor gana:
MERGE_CONFLICT_POLICY_FIRST_NON_NULL(por defecto) — gana el primer valor no nulo, en el orden de los fragmentos.MERGE_CONFLICT_POLICY_MAJORITY— gana el valor que aparece con más frecuencia; con menos de 3 fragmentos, o cuando ningún valor tiene mayoría estricta, ChunkChef recurre aFIRST_NON_NULL.
En cualquier caso, cada discrepancia de este tipo se registra en merged[].conflicts[] —el field, los values en competencia y de qué archivo/fragmento proviene cada uno— de modo que un total discordante no se oculta sin más. Consulte conflicts[] cuando necesite saber si el valor ganador es fiable o si conviene revisarlo a mano.
Dos tipos de deduplicación
Merge elimina contenido duplicado de dos formas distintas:
- Deduplicación técnica — siempre activa, sin configuración. Los fragmentos de un mismo archivo suelen solaparse un poco en su límite (la misma fila de tabla aparece al final de un fragmento y al principio del siguiente); merge detecta y elimina ese solapamiento automáticamente. Es el comportamiento seguro por defecto: nunca toca contenido más allá de ese solapamiento en el límite.
- Deduplicación de contenido — opcional, vía
merge.dedupeBy. Conscope=job, el mismo registro lógico puede aparecer legítimamente en más de un archivo (p. ej. una línea de artículo repetida en dos documentos relacionados), y esa repetición puede ser intencional — así que ChunkChef no la toca a menos que usted lo pida. Indique enmerge.dedupeBylos nombres de campo JSON que identifican un registro único (p. ej.["invoiceNumber", "lineNo"]) y ChunkChef colapsará los registros que coincidan en esos campos, conservando la primera aparición. Dejemerge.dedupeByvacío —el valor por defecto— para conservar solo la deduplicación técnica, nada más.
merge.dedupeBy solo funciona con salida JSON y requiere que responseSchema esté definido — los nombres de campo se buscan como claves simples en el objeto JSON ya analizado. Como máximo 32 nombres de campo; más se rechazan con 400 al crear la tarea.
Ambos tipos de eliminación se cuentan por separado en el resultado, para que pueda distinguirlos:
merged[].dedupeRemovedTechnical— registros eliminados por la deduplicación automática de solapamiento en el límite.merged[].dedupeRemovedContent— registros eliminados por sus camposdedupeBy.
Cómo leer el resultado: merged[] frente a llm[] en bruto
Cuando merge está activo, el resultado gana merged[] junto al llm[] existente:
merged[]— la respuesta combinada por el servidor, para todo el documento (o el archivo). Léala como su resultado principal.llm[]— sin cambios: sigue habiendo una fila por fragmento. Permanece disponible para que pueda inspeccionar exactamente qué devolvió la llamada al modelo de cada fragmento — útil al depurar un conflicto.
Si definió responseSchema, el objeto JSON fusionado se valida contra ella y merged[].schemaValid informa el resultado — igual que hoy hace llm[].schemaValid para un archivo de un único fragmento.
Cuando un fragmento no pudo fusionarse (merged[].mergeIncomplete)
La fusión es best-effort: si la respuesta de un fragmento no se puede analizar, o la estructura del documento se reparte entre fragmentos de un modo que merge no puede combinar con seguridad, el contenido en bruto de ese fragmento se incluye igualmente —añadido, no fusionado— y merged[].mergeIncomplete se establece en true, con los fragmentos afectados listados en merged[].incompleteChunks. El resto de la fusión se completa con normalidad; consulte este indicador cuando necesite saber si el resultado fusionado está totalmente limpio o se ensambló parcialmente.
Ejecuciones de consenso (consensus)
En una tarea con esquema (responseSchema definido), puede pedirle a ChunkChef que ejecute el mismo fragmento en el modelo varias veces y vote la respuesta campo por campo, en lugar de confiar en una sola ejecución. Actívelo con consensus.mode al crear la tarea: ChunkChef ejecuta el fragmento k veces, toma el valor mayoritario (por pluralidad) para cada campo y reporta tanto la respuesta acordada como cada campo en el que el modelo dudó — una señal de confianza sobre su llamada BYOK habitual, al coste de k veces los tokens en su propia clave. Como las ejecuciones son secuenciales, el consenso también multiplica el tiempo de procesamiento — una tarea puede tardar hasta k veces más.
Requiere responseSchema. El consenso necesita un esquema para saber qué es un «campo» — sin uno no hay nada que votar. Definir consensus.mode sin responseSchema se rechaza con 400 al crear la tarea.
Actívelo:
{ "responseSchema": "...", "consensus": { "mode": "CONSENSUS_MODE_THREE_RUNS" } }
Modos
El nombre del modo lleva incorporado el multiplicador de tokens — no hay que fijar un número de «ejecuciones» aparte:
| Modo | Ejecuciones | Notas |
|---|---|---|
CONSENSUS_MODE_TWO_RUNS | 2 | el más barato — un sondeo de inestabilidad, no una votación fiable (vea las advertencias más abajo) |
CONSENSUS_MODE_THREE_RUNS | 3 | predeterminado recomendado — una votación por pluralidad real |
CONSENSUS_MODE_FIVE_RUNS | 5 | máximo escrutinio, coste más alto |
Omitir consensus (o dejar CONSENSUS_MODE_UNSPECIFIED) lo desactiva — el comportamiento es idéntico byte a byte al de una tarea sin consenso.
Cómo leer el resultado: minAgreement y disagreements
Cada fila llm[] gana un objeto consensus (la lista completa de campos está en «Referencia de la API»). Dos valores a mirar primero:
minAgreement— el acuerdo por campo más bajo de la fila, como fracción (1.0= todos los campos votados coincidieron en todas las ejecuciones). Un valor de0es un centinela reservado que significa que no hubo una votación real (menos de 2 ejecuciones votables, o la votación se degradó) — léalo como «consenso no disponible para este fragmento», no como «0 % de acuerdo».disagreements[]— una entrada por campo (o array) en el que las ejecuciones no coincidieron por completo, con el menor acuerdo primero. Cada entrada lista lasvariants[]en competencia: el valor, cuántas ejecuciones lo produjeron y siincluded— ganó la votación y está presente en la respuesta acordada. Una variante puede tenerincluded: trueincluso cuando su valor esnull: unnullganador significa que la clave del campo se omite del JSON acordado, no que aparezca unnullliteral.
Advertencias honestas
CONSENSUS_MODE_TWO_RUNSes un sondeo, no una votación. Conk=2, una discrepancia escalar siempre se divide 1 contra 1 (acuerdo0.5), y en los arrays nunca se filtra nada — cada elemento reportado por cualquiera de las dos ejecuciones termina en la respuesta acordada. Dos ejecuciones le dicen que el modelo dudó; solo con tres ejecuciones o más se filtra realmente un valor minoritario.- La votación por pluralidad puede descartar una entidad que todas las ejecuciones coinciden en que existe. Si un elemento de un array está presente en todas las ejecuciones pero con una celda distinta (p. ej. una línea de artículo que todas extrajeron, pero en la que una ejecución leyó mal un número), la versión de cada ejecución cuenta como un valor canónico distinto — con
k≥3cada una recibe 1 de k, por debajo del umbral de mayoría, y el elemento puede quedar fuera del array acordado aunque todas las ejecuciones coincidieran en que pertenece ahí.disagreements[]sigue exponiendo cada variante, así que la evidencia bruta sobrevive aunque la respuesta acordada no la incluya — revíselo cuando un array se vea más corto de lo esperado. - Una respuesta demasiado grande cuenta como no votable, no como un error. La respuesta de una ejecución que se analiza como JSON válido pero excede los límites internos de análisis de ChunkChef queda excluida de la votación; la votación continúa con las ejecuciones restantes — el mismo tratamiento que una ejecución que devuelve texto no JSON.
- El consenso y
mergese componen como dos votaciones independientes. Con ambos activos, cada fragmento primero se vota internamente (kejecuciones → una respuesta acordada por fragmento), y luego, simerge.conflictPolicyesMERGE_CONFLICT_POLICY_MAJORITY, el paso de fusión vuelve a votar otra vez entre las respuestas acordadas de los fragmentos. Nada se traslada entre las dos votaciones — un campo que el consenso por fragmento ya resolvió se presenta a la fusión como un valor asentado, igual que la respuesta de cualquier otro fragmento.
Reintentos idempotentes
Para reintentar la creación de una tarea de forma segura, genere un único idempotencyKey (un UUID) y reutilícelo
en los reintentos de la misma solicitud:
POST /v1/jobs
{ "sourceUrls": ["..."], "ocr": { ... }, "neural": { ... }, "idempotencyKey": "3f1c…" }
Reintentar con la misma clave y parámetros idénticos devuelve la tarea original;
cambiar cualquier parámetro con la misma clave devuelve 400. Use una clave nueva para una
tarea genuinamente nueva.
Webhooks
Los webhooks le permiten evitar el sondeo en bucle: el servicio enviará un POST a su endpoint en cuanto la tarea termine. Es totalmente opcional: si no define los campos, el comportamiento de la API no cambia.
Configuración
Al crear una tarea (POST /v1/jobs), pase uno o ambos campos opcionales:
| Campo | Tipo | Descripción |
|---|---|---|
webhookUrl | string | URL absoluta de su endpoint (http:// o https://). Se acepta solo como entrada, nunca se devuelve en las respuestas. |
webhookSecret | string | Secreto opcional para la firma HMAC. Se acepta solo como entrada, nunca se devuelve en las respuestas; se almacena cifrado y se elimina junto con la tarea (consulte «Seguridad y datos»). |
Ejemplo:
{
"sourceUrls": ["https://example.com/invoice.pdf"],
"prompts": ["Extract the total."],
"ocr": { "provider": "NEURAL_CLIENT_TYPE_MISTRAL", "model": "mistral-ocr-latest", "providerKey": "..." },
"neural": { "type": "NEURAL_CLIENT_TYPE_XIAOMI", "model": "mimo-v2-flash", "apiKey": "..." },
"webhookUrl": "https://your-service.example.com/hooks/ChunkChef",
"webhookSecret": "my-secret-value"
}
Cuándo se dispara el webhook
Una vez por tarea, en la primera transición a un estado terminal (JOB_STATUS_COMPLETE, JOB_STATUS_PARTIAL o JOB_STATUS_FAILED). La entrega en sí es at-least-once (es posible que se dupliquen en caso de fallo); consulte «Garantías de entrega». El webhook no transporta el resultado del procesamiento; solo señala la finalización. Obtenga el resultado completo con la habitual solicitud GET /v1/jobs/{id}/result.
Cuerpo de la solicitud
El servicio envía un POST a su webhookUrl con Content-Type: application/json y un cuerpo JSON:
{
"job_id": "15b07304-...",
"account_id": "a1b2c3d4-...",
"status": "complete",
"finished_at": "2026-06-24T12:34:56Z"
}
| Campo | Tipo | Descripción |
|---|---|---|
job_id | string | UUID de la tarea |
account_id | string (identificador de cuenta) | Identificador de cuenta |
status | string | Uno de: complete, partial, failed |
finished_at | string | Hora de finalización de la tarea en formato RFC3339 (UTC) |
Verificación de la firma
Cuando webhookSecret está definido, cada solicitud incluye dos cabeceras adicionales:
| Cabecera | Valor de ejemplo | Descripción |
|---|---|---|
X-Chunkchef-Timestamp | 1750765200 | Hora Unix de la entrega (segundos) |
X-Chunkchef-Signature | sha256=a3f4... | Firma HMAC-SHA256 |
Algoritmo de firma:
signature = "sha256=" + hex( hmac_sha256(secret, "<timestamp>.<body>") )
donde <timestamp> es la representación en cadena de la hora Unix de X-Chunkchef-Timestamp, <body> es el cuerpo de la solicitud en bruto (bytes tal como se reciben) y . es el separador. hex va en minúsculas; secret se usa como bytes UTF-8.
Cómo verificarla de su lado:
- Extraiga el valor de
X-Chunkchef-Timestamp. - Calcule
hmac_sha256(secret, "<X-Chunkchef-Timestamp value from step 1>.<raw request body>"), codifíquelo como hex en minúsculas y antepongasha256=. - Compárelo con
X-Chunkchef-Signaturemediante una comparación de tiempo constante (hmac.Equal/crypto/subtle.ConstantTimeCompareo equivalente). - Rechace la solicitud si la marca de tiempo es demasiado antigua (tolerancia recomendada: 5 minutos).
Si webhookSecret no está definido, las cabeceras X-Chunkchef-Timestamp y X-Chunkchef-Signature no se envían.
Política de reintentos
Si su endpoint no es accesible o devuelve un error, el servicio reintenta la entrega según el siguiente calendario:
| Intento | Retraso antes del siguiente |
|---|---|
| 1 → 2 | 1 minuto |
| 2 → 3 | 5 minutos |
| 3 → 4 | 15 minutos |
| 4 → 5 | 30 minutos |
| 5 → 6 | 30 minutos |
| 6 | — (final; tras él la entrega se marca como fallida) |
Total: hasta 6 intentos.
Los errores permanentes (4xx salvo 408/429, URL inutilizable, bloqueo SSRF) no se reintentan: la entrega se marca como fallida de inmediato. Una respuesta 2xx se considera un éxito.
Garantías de entrega
La entrega es at-least-once: en la mayoría de los casos su endpoint recibe exactamente una llamada, pero los reintentos pueden provocar entregas duplicadas en caso de fallo. Deduplique los eventos de su lado usando job_id.