Traitement de documents (Job API)

Cycle de vie d’un job

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 — le job a été créé et mis en file d’attente.
  • JOB_STATUS_FILE_PROCESSING — les fichiers sont téléchargés, les archives décompressées et les formats normalisés vers une forme traitable. Peut passer directement à JOB_STATUS_FAILED si toutes les sources sont inaccessibles ou si une limite de fichiers est dépassée (la raison figure dans le champ error du job).
  • JOB_STATUS_OCR — reconnaissance de texte pour chaque fichier.
  • JOB_STATUS_LLM — le texte reconnu est envoyé au modèle avec vos prompts.
  • JOB_STATUS_COMPLETE — aucune erreur aux étapes OCR ou LLM.
  • JOB_STATUS_PARTIAL — au moins une réponse de modèle (LLM) réussie, mais aussi au moins une erreur à l’étape OCR ou LLM (vérifiez les erreurs au niveau des fichiers dans le résultat), ou une réponse qui ne respecte pas responseSchema (voir « Sortie structurée »).
  • JOB_STATUS_FAILED — des erreurs ont empêché tout fichier d’atteindre une réponse de modèle réussie : soit un échec lors du téléchargement/de la décompression des fichiers (la raison figure dans le champ error au niveau du job ; ocr[]/llm[] sont vides), soit aucun fichier n’a abouti à un résultat réussi à l’étape OCR ou LLM.

Le traitement est asynchrone : interrogez le statut du job via GET /v1/jobs/{id} jusqu’à ce que le job atteigne un statut terminal.

Envoi de fichiers

Vous pouvez indiquer la source d’un job de deux façons :

  1. URL publique — transmettez l’URL dans sourceUrls lors de la création du job. ChunkChef télécharge le fichier (délai de téléchargement : 30 s, jusqu’à 3 redirections).
  2. Envoi direct — envoyez le fichier, puis utilisez l’URL renvoyée dans sourceUrls :
    • POST /v1/jobs/upload (multipart/form-data) — envoie un seul fichier via HTTP. C’est un endpoint HTTP autonome (pas grpc-gateway). La réponse est en JSON au format snake_case : {"url": "…", "name": "…", "size_bytes": 12345}. Le corps d’erreur de cet endpoint est {"code": <int>, "message": "…"}, sans tableau details. La limite de taille est définie par la configuration (grpc.maxRecvMsgBytes ; 20 MiB dans le déploiement actuel), et non par une valeur par défaut du code.
    • méthode gRPC Upload (client-streaming) — un envoi en flux (limite par message : 20 MiB).

Prompts et extraction de données

L’extraction de données repose sur des prompts textuels, et non sur un schéma. Dans le champ prompts, vous transmettez un tableau d’instructions. À l’étape LLM, le texte reconnu de chaque fichier est récupéré, découpé en chunks si nécessaire, et envoyé au modèle avec votre prompt. La réponse du modèle est renvoyée sous forme de texte par fichier (et par chunk, si le fichier a été découpé). Vous pouvez aussi ajouter responseSchema — un JSON Schema — pour que ChunkChef valide la forme de la réponse après l’appel ; voir « Sortie structurée » ci-dessous.

Le découpage en chunks est conscient de la structure : le texte est coupé le long des frontières de la structure de son format (Markdown, HTML, XML ou plain) — les tableaux ne sont pas déchirés au milieu d’une ligne (et si un tableau ne tient pas en entier, sa ligne d’en-tête est répétée dans chaque chunk), et le contexte des titres de section est préservé. La taille des chunks en tokens est définie par neural.chunkBudgetTokens (voir « Connexion d’un modèle ») ; si elle n’est pas définie, la valeur par défaut conservatrice du service s’applique. Chaque chunk est un appel de modèle distinct avec une copie complète du prompt et une ligne llm[] distincte (chunkIndex / chunkTotal). Le réassemblage de la réponse à partir des chunks dans l’ordre de chunkIndex se fait par défaut de votre côté. En option, ChunkChef peut faire ce réassemblage pour vous : définissez merge.enabled pour que le service combine les réponses par chunk en un seul résultat pour tout le document — voir « Fusion des résultats en chunks » ci-dessous.

Pour obtenir des données structurées, demandez-le directement dans le prompt — par exemple : « Return the result as JSON with the following fields: … ». Votre prompt et le modèle que vous avez choisi déterminent la validité et la forme du JSON ; ChunkChef n’impose ni ne valide aucun schéma, sauf si vous activez responseSchema (voir « Sortie structurée » ci-dessous).

Conseils pour les prompts :

  • listez explicitement et sans ambiguïté les champs dont vous avez besoin ;
  • précisez le format de sortie directement dans le texte du prompt ;
  • gardez à l’esprit la limite de taille du prompt — 64 KiB (voir « Limites »).

Remarque. Les marqueurs <…> dans les modèles ci-dessous indiquent les emplacements à compléter : ChunkChef ne les remplace pas automatiquement — le prompt est envoyé au modèle exactement tel qu’il est écrit. Votre prompt et le modèle que vous choisissez déterminent la structure de sortie ; il n’y a aucune validation de schéma côté serveur, sauf si vous définissez responseSchema (voir « Sortie structurée »).

La qualité de vos prompts détermine vos résultats. La qualité de l’extraction dépend autant de votre prompt que du modèle choisi — souvent davantage. Un prompt vague produit une sortie vague même sur un modèle haut de gamme, tandis qu’un prompt précis et bien structuré donne des résultats fiables même avec des modèles plus petits et moins coûteux. Traitez le modèle ci-dessous comme un point de départ, et non comme un prompt fini : prenez-le, décrivez votre type de document, votre tâche et la sortie exacte dont vous avez besoin, puis demandez à un modèle performant de le transformer en un prompt adapté à votre cas — en conservant cette structure tout en affinant les règles, les cas limites et la validation de la sortie pour vos données. Le méta-prompt prévu à cet effet se trouve à la fin de cette section.

Exemple : extraction des champs d’une facture / commande / reçu

Texte 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.

Enveloppe JSON (principale — sur le modèle Xiaomi mimo-v2-flash vérifié) :

{
  "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"
  }
}

En-tête : Authorization: Bearer <YOUR_CHUNKCHEF_KEY>.

Champ de requêteDe quoi il s’agit« Variable »
Authorizationvotre clé API ChunkChefclé d’accès API
sourceUrls[]URL des fichiersliens des documents
prompts[0]le modèle entier sous forme d’une seule chaîneprompt structuré
neural.type / neural.modelfournisseur et modèlemodèle
neural.apiKeyclé du fournisseur (BYOK)clé du modèle
neural.reasoningEffortfacultatif minimal/low/medium/highprofondeur de raisonnement
ocr.providerenum du fournisseur OCR (par ex. NEURAL_CLIENT_TYPE_MISTRAL)fournisseur OCR
ocr.modelidentifiant du modèle OCR ; requismodèle OCR
ocr.providerKeyclé du fournisseur pour l’OCR (BYOK) ; acceptée uniquement en entrée, jamais renvoyéeclé OCR
extractionModestratégie d’extraction de texte : EXTRACTION_MODE_HYBRID (par défaut, « Extraction hybride optimisée ») laisse le convertisseur choisir par fichier entre l’extraction directe du texte et l’OCR ; EXTRACTION_MODE_OCR_ALWAYS force l’OCR neuronal sur tous les fichiers ; omis / EXTRACTION_MODE_UNSPECIFIED hérite de la valeur par défaut de votre compteFacultatif
responseSchemaJSON Schema optionnel (chaîne brute) qui valide la réponse JSON du modèle ; autonome, compatible avec le sous-ensemble strict (voir « Sortie structurée »)Facultatif

Autres exemples (en bref). Même mécanique — seuls le texte du prompt et la forme attendue de la réponse dans llm[].content diffèrent :

  • Classification. Prompt : « Determine the document type: invoice / contract / receipt / letter / other. Return a single word from the list, with no explanation. » Réponse : un seul mot (par ex. contract).
  • Conditions contractuelles. Prompt : « Extract: parties, subject, amount, term, and termination conditions. Return JSON matching the schema {parties[], subject, amount, term, termination}. Field not found → null. » Réponse : JSON conforme au schéma.
  • Résumé. Prompt : « Summarize the document in 3–5 sentences. No bullet lists. » Réponse : texte en prose.

Construisez votre propre prompt (méta-prompt)

Le moyen le plus rapide d’obtenir un prompt de haute qualité est de le faire rédiger par un modèle performant. Confiez-lui le méta-prompt ci-dessous : collez-y notre exemple comme structure de référence, la forme de sortie dont vous avez besoin (JSON/CSV/Markdown) et une description de votre contexte et de votre tâche — et vous récupérez un prompt ChunkChef prêt à l’emploi. Remplissez les blocs entre crochets ; le modèle s’occupe du reste.

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.

Connexion d’un modèle (BYOK)

L’étape LLM s’exécute sur votre clé de fournisseur. La configuration est transmise dans l’objet neural lors de la création d’un job :

ChampRequisDescription
typeouifournisseur (voir la liste ci-dessous)
modelouiidentifiant du modèle ; transmis tel quel au fournisseur
apiKeyouivotre clé de fournisseur ; acceptée uniquement en entrée, jamais renvoyée dans les réponses
reasoningEffortnonindication de profondeur de raisonnement ; valeurs autorisées : minimal, low, medium, high (vide = désactivé). Une valeur invalide → erreur 400. Sa prise en compte dépend du fournisseur/modèle.
chunkBudgetTokensnonbudget de tokens pour un seul appel de modèle : couvre à la fois le prompt et le texte du document dans un même chunk. 0/non défini → la valeur par défaut conservatrice du service. Plage : 160002000000 ; une valeur hors plage → 400. Réglez-le sur la fenêtre de contexte de votre modèle — vous seul la connaissez. La réponse renvoie toujours le budget effectif réel (y compris lorsque vous vous reposez sur la valeur par défaut) : le service réserve une petite marge pour les tokens du chat-template, de sorte que la valeur renvoyée est légèrement inférieure à celle que vous avez définie.

Fournisseurs pris en charge :

FournisseurValeur neural.type
OpenAINEURAL_CLIENT_TYPE_OPENAI
Anthropic (Claude)NEURAL_CLIENT_TYPE_CLAUDE
xAI (Grok)NEURAL_CLIENT_TYPE_GROK
TogetherNEURAL_CLIENT_TYPE_TOGETHER
DeepSeekNEURAL_CLIENT_TYPE_DEEPSEEK
XiaomiNEURAL_CLIENT_TYPE_XIAOMI
MistralNEURAL_CLIENT_TYPE_MISTRAL
OpenRouterNEURAL_CLIENT_TYPE_OPENROUTER

Via NEURAL_CLIENT_TYPE_OPENROUTER, vous accédez à des modèles de nombreux fournisseurs qui ne disposent pas d’une intégration directe.

Vous payez directement le fournisseur à son tarif — ChunkChef n’ajoute aucune marge sur les tokens ni sur l’OCR.

Mode d’extraction (Extraction hybride optimisée)

Par défaut (EXTRACTION_MODE_HYBRID), le convertisseur choisit par fichier entre extraire le texte directement ou utiliser l’OCR — la stratégie « Extraction hybride optimisée ». Définissez extractionMode sur EXTRACTION_MODE_OCR_ALWAYS pour un job afin de forcer l’OCR neuronal sur tous les fichiers, même s’ils disposent déjà d’une couche de texte extractible. Remarque : chaque fichier devient alors un appel OCR facturé par le fournisseur — un surcoût par rapport au mode hybride. Le extractionMode propre à un job prime sur la valeur par défaut du compte, définie dans le tableau de bord sous Paramètres.

Sortie structurée (responseSchema)

Transmettez un responseSchema optionnel — un JSON Schema, sous forme de chaîne JSON brute — à la création du job pour que ChunkChef valide la forme de la réponse JSON du modèle, en plus de (et non à la place de) décrire cette forme dans votre prompt.

Le schéma doit être autonome : seules les références internes #/$defs sont autorisées — aucun $ref externe, ni $schema/$id distant. Limites : au plus 128 KiB, profondeur d’imbrication 64, 10000 nœuds au total. Un schéma invalide ou dépassant ces limites est rejeté avec 400 à la création du job, avant tout traitement de fichier. Pour OpenAI et Grok, les sorties structurées s’exécutent nativement et exigent le sous-ensemble strict de ChunkChef : chaque propriété listée dans required (exprimez l’optionnalité via type: [..., "null"], pas par omission) et additionalProperties: false sur chaque objet. Écrire votre schéma dans ce sous-ensemble strict le rend portable — le même schéma fonctionne sans modification chez tous les fournisseurs pris en charge. Un schéma qui passe les contrôles de ChunkChef à la création mais n’est pas compatible avec le sous-ensemble strict pour OpenAI/Grok n’échoue pas à la création et ne passe jamais par le flux schemaValid/schemaErrors — à la place, la ligne llm[] correspondante se termine en JOB_LLM_STATUS_FAILED avec une erreur du fournisseur, la conformité au sous-ensemble strict étant appliquée par le fournisseur, et non par le contrôle de création de ChunkChef.

Deux champs de chaque ligne llm[] rapportent le résultat (voir « Référence de l’API ») :

  • schemaValid — indique si le content de la ligne respecte responseSchema. Présent uniquement si le fichier n’a pas été découpé en chunks (chunkTotal = 1) ; absent pour les fichiers à plusieurs chunks — le même schéma est tout de même envoyé à chaque appel de chunk, mais les verdicts par chunk ne sont pas encore agrégés en un verdict pour tout le document.
  • schemaErrors — les violations de conformité lorsque schemaValid vaut false (jusqu’à 10, chacune jusqu’à 512 octets).

Si la réponse d’au moins une ligne ne respecte pas le schéma, le job se termine avec le statut JOB_STATUS_PARTIAL au lieu de JOB_STATUS_COMPLETEcontent est tout de même renvoyé tel quel, simplement signalé. Pour les fournisseurs pour lesquels ChunkChef émule les sorties structurées (DeepSeek, Xiaomi), une première réponse non conforme bénéficie d’une tentative de réparation automatique avant d’être évaluée.

Fusion des résultats en chunks (merge)

Lorsqu’un document est découpé en chunks (voir « Prompts et extraction de données » ci-dessus), chaque chunk reçoit son propre appel de modèle et sa propre ligne llm[] — combiner ces morceaux en une seule réponse reste, par défaut, à votre charge. Activez merge à la création du job et ChunkChef effectue cette combinaison pour vous, côté serveur : le résultat gagne un tableau merged[] contenant une réponse pour tout le document (ou tout le fichier), construite à partir des réponses par chunk.

Quand l’activer. Si vous extrayez du JSON structuré d’un document assez long pour être découpé en plusieurs chunks — par exemple, une facture de plusieurs pages dont les lignes d’articles sont réparties entre la page 1 et la page 2 —, merge vous évite d’écrire vous-même le code de réassemblage : ChunkChef fusionne les champs JSON, concatène et déduplique les tableaux, et résout tout champ qui diverge entre les chunks.

Activez-le :

{ "merge": { "enabled": true } }

Tous les autres champs merge.* sont facultatifs et ont des valeurs par défaut sûres (voir « Options de fusion » dans la référence de l’API pour la liste complète).

Exemple : une facture de deux pages

Imaginez une facture de 2 pages, reconnue en un texte assez long pour être découpé en 2 chunks — la page 1 atterrit dans le chunk 0, la page 2 dans le chunk 1. Votre prompt demande du JSON : { "supplier": "...", "items": [...], "total": "..." }. Sans merge, vous obtenez deux réponses llm[] distinctes, une par chunk, et vous les combinez vous-même. Avec merge activé, ChunkChef les combine en un seul objet JSON : les tableaux items des deux chunks sont concaténés en un seul, et supplier/total sont pris dans le chunk qui les rapporte réellement. Si le chunk 0 et le chunk 1 rapportent un total différent pour le même document (par exemple l’OCR a dupliqué une ligne de sous-total, ou le modèle a mal lu un nombre), c’est un vrai conflit — voir « Résolution des valeurs en conflit » ci-dessous.

Portée : une réponse par fichier, ou une pour tout le job (merge.scope)

merge.scope détermine ce qui est combiné en une seule entrée merged[] :

  • MERGE_SCOPE_FILE (par défaut) — les chunks sont fusionnés séparément au sein de chaque fichier : une entrée merged[] par fichier, avec merged[].file égal à l’URL de ce fichier.
  • MERGE_SCOPE_JOB — les chunks sont fusionnés entre tous les fichiers du job en une seule réponse : une entrée merged[], avec merged[].file vide (""). Utilisez ce mode lorsque les fichiers que vous avez envoyés sont des pages ou des parties d’un même document logique (par ex. un bon de commande découpé en plusieurs fichiers source) et que vous voulez un seul résultat combiné plutôt qu’un par fichier.

Résolution des valeurs en conflit (merge.conflictPolicy)

Lors de la fusion JSON, deux chunks peuvent diverger sur le même champ — l’un rapporte total: 1500, l’autre total: 1520 pour la même facture. merge.conflictPolicy détermine quelle valeur l’emporte :

  • MERGE_CONFLICT_POLICY_FIRST_NON_NULL (par défaut) — la première valeur non nulle, dans l’ordre des chunks, l’emporte.
  • MERGE_CONFLICT_POLICY_MAJORITY — la valeur la plus fréquente l’emporte ; avec moins de 3 chunks, ou lorsqu’aucune valeur n’a de majorité stricte, ChunkChef revient à FIRST_NON_NULL.

Dans tous les cas, chaque divergence de ce type est enregistrée dans merged[].conflicts[] — le field, les values en compétition, et de quel fichier/chunk provient chacune — afin qu’un total discordant ne soit pas silencieusement masqué. Consultez conflicts[] lorsque vous devez savoir si la valeur retenue est fiable ou si un humain devrait vérifier.

Deux types de déduplication

Merge supprime le contenu dupliqué de deux façons différentes :

  1. Déduplication technique — toujours active, aucune configuration nécessaire. Les chunks d’un même fichier se chevauchent généralement un peu à leur frontière (la même ligne de tableau apparaît à la fin d’un chunk et au début du suivant) ; merge détecte et supprime automatiquement ce chevauchement. C’est le comportement sûr par défaut — il ne touche jamais à du contenu au-delà de ce chevauchement de frontière.
  2. Déduplication de contenu — facultative, via merge.dedupeBy. À scope=job, le même enregistrement logique peut légitimement apparaître dans plusieurs fichiers (par ex. une ligne d’article répétée dans deux documents liés), et cette répétition peut être intentionnelle — donc ChunkChef n’y touche pas sauf si vous le demandez. Définissez dans merge.dedupeBy les noms de champs JSON qui identifient un enregistrement unique (par ex. ["invoiceNumber", "lineNo"]) et ChunkChef fusionnera les enregistrements correspondant sur ces champs, en conservant la première occurrence. Laissez merge.dedupeBy vide — la valeur par défaut — pour ne garder que la déduplication technique, rien de plus.

merge.dedupeBy ne fonctionne qu’avec une sortie JSON et nécessite que responseSchema soit défini — les noms de champs sont recherchés comme de simples clés dans l’objet JSON analysé. 32 noms de champs au maximum ; au-delà, la requête est rejetée avec 400 à la création du job.

Les deux types de suppression sont comptabilisés séparément dans le résultat, afin que vous puissiez les distinguer :

  • merged[].dedupeRemovedTechnical — enregistrements supprimés par la déduplication automatique de chevauchement de frontière.
  • merged[].dedupeRemovedContent — enregistrements supprimés grâce à vos champs dedupeBy.

Lire le résultat : merged[] vs llm[] brut

Lorsque merge est activé, le résultat gagne merged[] en plus du llm[] existant :

  • merged[] — la réponse combinée côté serveur, pour tout le document (ou le fichier). Considérez-la comme votre résultat principal.
  • llm[] — inchangé : toujours une ligne par chunk. Il reste disponible pour que vous puissiez inspecter exactement ce que l’appel de modèle de chaque chunk a renvoyé — utile pour déboguer un conflit.

Si vous avez défini responseSchema, l’objet JSON fusionné est validé par rapport à celui-ci et merged[].schemaValid rapporte le résultat — de la même façon que llm[].schemaValid le fait aujourd’hui pour un fichier à chunk unique.

Quand un chunk n’a pas pu être fusionné (merged[].mergeIncomplete)

La fusion fonctionne en best-effort : si la réponse d’un chunk ne peut pas être analysée, ou si la structure du document se répartit entre chunks d’une façon que merge ne peut pas combiner en toute sécurité, le contenu brut de ce chunk est tout de même inclus — ajouté, non fusionné — et merged[].mergeIncomplete est mis à true, avec le ou les chunks concernés listés dans merged[].incompleteChunks. Le reste de la fusion se termine normalement ; consultez cet indicateur lorsque vous devez savoir si le résultat fusionné est entièrement propre ou partiellement assemblé.

Exécutions de consensus (consensus)

Pour un job avec schéma (responseSchema défini), vous pouvez demander à ChunkChef d’exécuter plusieurs fois le même chunk dans le modèle et de voter sur la réponse champ par champ, plutôt que de faire confiance à une seule exécution. Activez-le avec consensus.mode à la création du job : ChunkChef exécute le chunk k fois, retient la valeur majoritaire (à la pluralité) pour chaque champ, et rapporte à la fois la réponse retenue et chaque champ sur lequel le modèle a hésité — un signal de confiance en plus de votre appel BYOK habituel, au prix de k fois le coût en tokens sur votre propre clé. Comme les exécutions sont séquentielles, le consensus multiplie aussi le temps de traitement — un job peut prendre jusqu’à k fois plus de temps.

Nécessite responseSchema. Le consensus a besoin d’un schéma pour savoir ce qu’est un « champ » — sans cela, il n’y a rien sur quoi voter. Définir consensus.mode sans responseSchema est rejeté avec 400 à la création du job.

Activez-le :

{ "responseSchema": "...", "consensus": { "mode": "CONSENSUS_MODE_THREE_RUNS" } }

Modes

Le nom du mode porte en lui le multiplicateur de tokens — il n’y a pas de nombre d’« exécutions » séparé à définir :

ModeExécutionsRemarques
CONSENSUS_MODE_TWO_RUNS2le moins cher — un sondage d’instabilité, pas un vote fiable (voir les limites ci-dessous)
CONSENSUS_MODE_THREE_RUNS3valeur par défaut recommandée — un véritable vote à la pluralité
CONSENSUS_MODE_FIVE_RUNS5rigueur maximale, coût le plus élevé

Omettre consensus (ou laisser CONSENSUS_MODE_UNSPECIFIED) le désactive — le comportement est identique bit à bit à celui d’un job sans consensus.

Lire le résultat : minAgreement et disagreements

Chaque ligne llm[] gagne un objet consensus (liste complète des champs dans la « Référence API »). Deux valeurs à regarder en premier :

  • minAgreement — l’accord par champ le plus faible de la ligne, sous forme de fraction (1.0 = tous les champs votés ont concordé sur toutes les exécutions). Une valeur de 0 est une valeur sentinelle réservée signifiant qu’aucun vote réel n’a eu lieu (moins de 2 exécutions votables, ou le vote a dégradé) — lisez-la comme « consensus indisponible pour ce chunk », pas comme « 0 % d’accord ».
  • disagreements[] — une entrée par champ (ou tableau) sur lequel les exécutions ne se sont pas pleinement accordées, la plus faible concordance en premier. Chaque entrée liste les variants[] concurrentes : la valeur, le nombre d’exécutions qui l’ont produite, et si elle est included — a gagné le vote et figure dans la réponse retenue. Une variante peut avoir included: true même quand sa valeur est null : un null gagnant signifie que la clé du champ est omise du JSON retenu, pas qu’un null littéral apparaît.

Limites, honnêtement

  • CONSENSUS_MODE_TWO_RUNS est un sondage, pas un vote. À k=2, une divergence scalaire se répartit toujours 1 contre 1 (accord 0.5), et sur les tableaux rien n’est jamais filtré — chaque élément rapporté par l’une ou l’autre des deux exécutions se retrouve dans la réponse retenue. Deux exécutions vous indiquent que le modèle a hésité ; seules trois exécutions ou plus filtrent réellement une valeur minoritaire.
  • Le vote à la pluralité peut écarter une entité dont toutes les exécutions s’accordent à dire qu’elle existe. Si un élément de tableau est présent dans chaque exécution mais avec une cellule qui diffère (par ex. une ligne d’article que chaque exécution a extraite, mais dont une exécution a mal lu un chiffre), la version de chaque exécution compte comme une valeur canonique distincte — à k≥3, chacune obtient 1 voix sur k, en dessous du seuil de majorité, et l’élément peut être écarté du tableau retenu alors même que chaque exécution s’accordait sur son appartenance. disagreements[] continue d’exposer chaque variante, si bien que la preuve brute survit même quand la réponse retenue ne l’inclut pas — vérifiez-le lorsqu’un tableau semble plus court que prévu.
  • Une réponse trop volumineuse compte comme non votable, pas comme une erreur. La réponse d’une exécution qui s’analyse comme du JSON valide mais dépasse les limites internes d’analyse de ChunkChef est exclue du vote ; le vote se poursuit sur les exécutions restantes — le même traitement qu’une exécution renvoyant du texte non JSON.
  • Consensus et merge se combinent comme deux votes indépendants. Avec les deux activés, chaque chunk est d’abord voté en interne (k exécutions → une réponse retenue par chunk), puis, si merge.conflictPolicy vaut MERGE_CONFLICT_POLICY_MAJORITY, l’étape de fusion vote à nouveau — cette fois entre les réponses retenues des chunks. Rien ne se reporte entre les deux votes — un champ déjà résolu par le consensus par chunk est présenté à la fusion comme une valeur acquise, au même titre que la réponse de tout autre chunk.

Réessais idempotents

Pour réessayer la création d’un job en toute sécurité, générez un seul idempotencyKey (un UUID) et réutilisez-le sur tous les réessais de la même requête :

POST /v1/jobs
{ "sourceUrls": ["..."], "ocr": { ... }, "neural": { ... }, "idempotencyKey": "3f1c…" }

Réessayer avec la même clé et des paramètres identiques renvoie le job d’origine ; modifier un paramètre quelconque sous la même clé renvoie 400. Utilisez une nouvelle clé pour un job véritablement nouveau.

Webhooks

Les webhooks vous évitent l’interrogation (polling) : le service enverra un POST à votre endpoint une fois le job terminé. C’est entièrement facultatif — si vous ne renseignez pas les champs, le comportement de l’API reste inchangé.

Configuration

Lors de la création d’un job (POST /v1/jobs), transmettez l’un de ces champs facultatifs, ou les deux :

ChampTypeDescription
webhookUrlstringURL absolue de votre endpoint (http:// ou https://). Acceptée uniquement en entrée — jamais renvoyée dans les réponses.
webhookSecretstringSecret HMAC de signature facultatif. Accepté uniquement en entrée — jamais renvoyé dans les réponses ; stocké chiffré au repos et supprimé avec le job (voir « Sécurité et données »).

Exemple :

{
  "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"
}

Quand le webhook se déclenche

Une fois par job — au premier passage à un statut terminal (JOB_STATUS_COMPLETE, JOB_STATUS_PARTIAL ou JOB_STATUS_FAILED). La livraison elle-même est at-least-once (des doublons sont possibles en cas d’échec) ; voir « Garanties de livraison ». Le webhook ne transporte pas le résultat du traitement ; il signale uniquement la fin. Récupérez le résultat complet avec le GET /v1/jobs/{id}/result habituel.

Corps de la requête

Le service envoie un POST à votre webhookUrl avec Content-Type: application/json et un corps JSON :

{
  "job_id":      "15b07304-...",
  "account_id":  "a1b2c3d4-...",
  "status":      "complete",
  "finished_at": "2026-06-24T12:34:56Z"
}
ChampTypeDescription
job_idstringUUID du job
account_idstring (identifiant de compte)Identifiant de compte
statusstringL’un de : complete, partial, failed
finished_atstringHeure de fin du job au format RFC3339 (UTC)

Vérification de la signature

Lorsque webhookSecret est défini, chaque requête inclut deux en-têtes supplémentaires :

En-têteExemple de valeurDescription
X-Chunkchef-Timestamp1750765200Heure Unix de la livraison (secondes)
X-Chunkchef-Signaturesha256=a3f4...Signature HMAC-SHA256

Algorithme de signature :

signature = "sha256=" + hex( hmac_sha256(secret, "<timestamp>.<body>") )

<timestamp> est la forme chaîne de l’heure Unix issue de X-Chunkchef-Timestamp, <body> est le corps brut de la requête (les octets tels que reçus), et . est le séparateur. hex est en minuscules ; secret est utilisé comme octets UTF-8.

Comment vérifier de votre côté :

  1. Extrayez la valeur de X-Chunkchef-Timestamp.
  2. Calculez hmac_sha256(secret, "<X-Chunkchef-Timestamp value from step 1>.<raw request body>"), encodez en hex minuscule et préfixez par sha256=.
  3. Comparez-la à X-Chunkchef-Signature à l’aide d’une comparaison à temps constant (hmac.Equal / crypto/subtle.ConstantTimeCompare ou équivalent).
  4. Rejetez la requête si le timestamp est trop ancien (tolérance recommandée : 5 minutes).

Si webhookSecret n’est pas défini, les en-têtes X-Chunkchef-Timestamp et X-Chunkchef-Signature ne sont pas envoyés.

Politique de réessai

Si votre endpoint est injoignable ou renvoie une erreur, le service réessaie la livraison selon le calendrier suivant :

TentativeDélai avant la suivante
1 → 21 minute
2 → 35 minutes
3 → 415 minutes
4 → 530 minutes
5 → 630 minutes
6— (finale ; la livraison est ensuite marquée comme échouée)

Total : jusqu’à 6 tentatives.

Les erreurs permanentes (4xx autres que 408/429, URL inutilisable, blocage SSRF) ne sont pas réessayées : la livraison est immédiatement marquée comme échouée. Une réponse 2xx est considérée comme un succès.

Garanties de livraison

La livraison est at-least-once : dans la plupart des cas votre endpoint reçoit exactement un appel, mais les réessais peuvent provoquer une livraison en double en cas d’échec. Dédupliquez les événements de votre côté à l’aide de job_id.