Processamento de documentos (Job API)

Ciclo de vida do 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 — o job foi criado e colocado na fila.
  • JOB_STATUS_FILE_PROCESSING — os arquivos estão sendo baixados, os arquivos compactados descompactados e os formatos normalizados para uma forma processável. Pode transicionar diretamente para JOB_STATUS_FAILED se todos os sources forem inacessíveis ou se um limite de arquivos for excedido (o motivo fica no campo error do job).
  • JOB_STATUS_OCR — reconhecimento de texto de cada arquivo.
  • JOB_STATUS_LLM — o texto reconhecido é enviado ao modelo com seus prompts.
  • JOB_STATUS_COMPLETE — sem erros no estágio de OCR ou de LLM.
  • JOB_STATUS_PARTIAL — ao menos uma resposta bem-sucedida do modelo (LLM), mas também ao menos um erro no estágio de OCR ou de LLM (verifique os erros por arquivo no resultado), ou uma resposta que não está em conformidade com responseSchema (veja “Saída estruturada”).
  • JOB_STATUS_FAILED — erros impediram que qualquer arquivo chegasse a uma resposta bem-sucedida do modelo: ou uma falha durante o download/descompactação dos arquivos (o motivo fica no campo error no nível do job; ocr[]/llm[] ficam vazios), ou nenhum arquivo chegou a um resultado bem-sucedido no estágio de OCR ou de LLM.

O processamento é assíncrono: consulte o status do job via GET /v1/jobs/{id} até o job atingir um status terminal.

Envio de arquivos

Você pode especificar o source de um job de duas formas:

  1. URL pública — passe a URL em sourceUrls ao criar o job. O ChunkChef baixa o arquivo (timeout de download: 30 s, até 3 redirecionamentos).
  2. Upload direto — envie o arquivo e então use a URL retornada em sourceUrls:
    • POST /v1/jobs/upload (multipart/form-data) — envia um único arquivo por HTTP. Este é um endpoint HTTP autônomo (não é grpc-gateway). A resposta é JSON em snake_case: {"url": "…", "name": "…", "size_bytes": 12345}. O corpo de erro deste endpoint é {"code": <int>, "message": "…"}, sem o array details. O limite de tamanho é definido pela config (grpc.maxRecvMsgBytes; 20 MiB no deployment atual), e não por um padrão de código.
    • método gRPC Upload (client-streaming) — um upload em streaming (limite de mensagem única: 20 MiB).

Prompts e extração de dados

A extração de dados é guiada por prompts de texto, não por um schema. No campo prompts você passa um array de instruções. No estágio de LLM, o texto reconhecido de cada arquivo é pego, dividido em chunks se necessário, e enviado ao modelo junto com seu prompt. A resposta do modelo é retornada como texto por arquivo (e por chunk, se o arquivo foi dividido). Opcionalmente, adicione responseSchema — um JSON Schema — para que o ChunkChef valide o formato da resposta após a chamada; veja “Saída estruturada” abaixo.

A divisão em chunks é consciente da estrutura: o texto é cortado ao longo dos limites da estrutura do seu formato (Markdown, HTML, XML ou plain) — as tabelas não são rasgadas no meio de uma linha (e, se uma tabela não couber inteira, a sua linha de cabeçalho é repetida em cada chunk), e o contexto dos títulos de seção é preservado. O tamanho do chunk em tokens é definido por neural.chunkBudgetTokens (veja “Conectando um modelo”); se não for definido, aplica-se o padrão conservador do serviço. Cada chunk é uma chamada separada ao modelo, com uma cópia completa do prompt e uma linha llm[] separada (chunkIndex / chunkTotal). A remontagem da resposta a partir dos chunks na ordem de chunkIndex é feita do seu lado por padrão. Opcionalmente, o ChunkChef pode fazer essa remontagem para você: defina merge.enabled para que o serviço combine as respostas por chunk em um único resultado para o documento inteiro — veja “Mesclagem de resultados em chunks” abaixo.

Para obter dados estruturados, peça-os diretamente no prompt — por exemplo: “Return the result as JSON with the following fields: …”. Seu prompt e o modelo escolhido determinam a validade e o formato do JSON; o ChunkChef não impõe nem valida nenhum schema, a menos que você opte por responseSchema (veja “Saída estruturada” abaixo).

Dicas de prompt:

  • liste de forma explícita e inequívoca os campos de que você precisa;
  • especifique o formato de saída diretamente no texto do prompt;
  • tenha em mente o limite de tamanho do prompt — 64 KiB (veja “Limites”).

Nota. Os marcadores <…> nos templates abaixo indicam os pontos para você preencher: o ChunkChef não os substitui automaticamente — o prompt é enviado ao modelo exatamente como está escrito. Seu prompt e o modelo escolhido determinam a estrutura da saída; não há validação de schema no lado do servidor, a menos que você defina responseSchema (veja “Saída estruturada”).

A qualidade do prompt determina seus resultados. O quão bem a extração funciona depende tanto do seu prompt quanto do modelo escolhido — muitas vezes mais. Um prompt vago produz uma saída vaga mesmo em um modelo de ponta, enquanto um prompt preciso e bem estruturado obtém resultados confiáveis até de modelos menores e mais baratos. Trate o template abaixo como um ponto de partida, não como um prompt finalizado: pegue-o, descreva o tipo do seu documento, sua tarefa e a saída exata de que precisa, e então peça a um modelo capaz que o transforme em um prompt sob medida para o seu caso — mantendo esta estrutura e, ao mesmo tempo, refinando as regras, os casos extremos e a validação da saída para os seus dados. O meta-prompt para isso está no fim desta seção.

Exemplo: extrair campos de nota fiscal / 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.

Envelope JSON (principal — no 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"
  }
}

Cabeçalho: Authorization: Bearer <YOUR_CHUNKCHEF_KEY>.

Campo da requisiçãoO que é”Variável”
Authorizationsua chave de API do ChunkChefchave de acesso à API
sourceUrls[]URLs dos arquivoslinks dos documentos
prompts[0]o template inteiro como uma única stringprompt estruturado
neural.type / neural.modelprovedor e modelomodelo
neural.apiKeychave do provedor (BYOK)chave do modelo
neural.reasoningEffortopcional minimal/low/medium/highprofundidade de raciocínio
ocr.providerenum do provedor de OCR (por exemplo, NEURAL_CLIENT_TYPE_MISTRAL)provedor de OCR
ocr.modelidentificador do modelo de OCR; obrigatóriomodelo de OCR
ocr.providerKeychave do provedor para OCR (BYOK); aceita apenas como entrada, nunca retornadachave de OCR
extractionModeestratégia de extração de texto: EXTRACTION_MODE_HYBRID (padrão, “Extração híbrida otimizada”) deixa o conversor decidir entre extrair texto diretamente ou usar OCR, por arquivo; EXTRACTION_MODE_OCR_ALWAYS força OCR neural em todos os arquivos; se omitido / EXTRACTION_MODE_UNSPECIFIED, herda o padrão da sua contaOpcional
responseSchemaJSON Schema opcional (string bruta) que valida a resposta JSON do modelo; autocontida, compatível com o subconjunto estrito (veja “Saída estruturada”)Opcional

Mais exemplos (em resumo). Mesma mecânica — só mudam o texto do prompt e o formato esperado da resposta em llm[].content:

  • Classificação. Prompt: “Determine the document type: invoice / contract / receipt / letter / other. Return a single word from the list, with no explanation.” Resposta: uma única palavra (por exemplo, contract).
  • Termos de contrato. Prompt: “Extract: parties, subject, amount, term, and termination conditions. Return JSON matching the schema {parties[], subject, amount, term, termination}. Field not found → null.” Resposta: JSON conforme o schema.
  • Resumo. Prompt: “Summarize the document in 3–5 sentences. No bullet lists.” Resposta: texto corrido.

Crie seu próprio prompt (meta-prompt)

A forma mais rápida de obter um prompt de alta qualidade é fazer um modelo capaz escrevê-lo para você. Entregue-lhe o meta-prompt abaixo: cole nosso exemplo como estrutura de referência, o formato de saída de que você precisa (JSON/CSV/Markdown) e uma descrição do seu contexto e tarefa — e você recebe de volta um prompt do ChunkChef pronto para uso. Preencha os blocos entre colchetes; o modelo cuida do 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.

Conectando um modelo (BYOK)

O estágio de LLM roda com a sua chave de provedor. A configuração é passada no objeto neural ao criar um job:

CampoObrigatórioDescrição
typesimprovedor (veja a lista abaixo)
modelsimidentificador do modelo; passado ao provedor exatamente como está
apiKeysimsua chave de provedor; aceita apenas como entrada, nunca retornada nas respostas
reasoningEffortnãodica de profundidade de raciocínio; valores permitidos: minimal, low, medium, high (vazio = desligado). Um valor inválido → erro 400. Se será respeitado depende do provedor/modelo.
chunkBudgetTokensnãoorçamento de tokens para uma única chamada ao modelo: cobre tanto o prompt quanto o texto do documento em um chunk. 0/não definido → o padrão conservador do serviço. Faixa: 160002000000; um valor fora da faixa → 400. Defina-o como a janela de contexto do seu modelo — só você a conhece. A resposta sempre retorna o orçamento efetivo real (inclusive quando você confia no padrão): o serviço reserva uma pequena margem para os tokens do chat-template, então o valor retornado é ligeiramente menor do que o que você definiu.

Provedores suportados:

Provedorvalor de 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

Por meio de NEURAL_CLIENT_TYPE_OPENROUTER você acessa modelos de muitos fornecedores que não têm integração direta.

Você paga o provedor diretamente pela tarifa dele — o ChunkChef não adiciona markup sobre tokens ou OCR.

Modo de extração (Extração híbrida otimizada)

Por padrão (EXTRACTION_MODE_HYBRID), o conversor escolhe por arquivo entre extrair o texto diretamente ou usar OCR — a estratégia de “Extração híbrida otimizada”. Defina extractionMode como EXTRACTION_MODE_OCR_ALWAYS em um job para forçar o OCR neural em todos os arquivos, mesmo que já tenham uma camada de texto extraível. Atenção: nesse caso, cada arquivo passa a gerar uma chamada de OCR ao provedor — um aumento de custo em relação ao modo híbrido. O extractionMode do próprio job sobrepõe o padrão da conta, definido no painel em Configurações.

Saída estruturada (responseSchema)

Passe um responseSchema opcional — um JSON Schema, como string JSON bruta — na criação do job para que o ChunkChef valide o formato da resposta JSON do modelo, além de (não em vez de) descrever esse formato no seu prompt.

O schema precisa ser autocontido: apenas referências internas #/$defs são permitidas — nada de $ref externo, nem $schema/$id remoto. Limites: no máximo 128 KiB, profundidade de aninhamento 64, 10000 nós no total. Um schema inválido ou fora dos limites é rejeitado com 400 na criação do job, antes de qualquer arquivo ser processado. Para OpenAI e Grok, as saídas estruturadas rodam de forma nativa e exigem o subconjunto estrito do ChunkChef: toda propriedade listada em required (expresse opcionalidade via type: [..., "null"], não por omissão) e additionalProperties: false em todo objeto. Escrever seu schema nesse subconjunto estrito o torna portátil — o mesmo schema funciona sem alterações em todos os provedores suportados. Um schema que passa pelas verificações do ChunkChef na criação, mas não é compatível com o subconjunto estrito para OpenAI/Grok, não falha na criação nem passa pelo fluxo schemaValid/schemaErrors — em vez disso, a linha llm[] correspondente termina como JOB_LLM_STATUS_FAILED com um erro do provedor, já que a conformidade com o subconjunto estrito é aplicada pelo provedor, não pela verificação de criação do ChunkChef.

Dois campos em cada linha llm[] reportam o resultado (veja “Referência da API”):

  • schemaValid — se o content da linha está em conformidade com responseSchema. Presente apenas quando o arquivo não foi dividido em chunks (chunkTotal = 1); omitido para arquivos com múltiplos chunks — o mesmo schema ainda é enviado em cada chamada de chunk, mas os veredictos por chunk ainda não são combinados em um único veredicto para o documento inteiro.
  • schemaErrors — as violações de conformidade quando schemaValid é false (até 10, cada uma com até 512 bytes).

Se a resposta de qualquer linha não estiver em conformidade, o job termina como JOB_STATUS_PARTIAL em vez de JOB_STATUS_COMPLETEcontent ainda é retornado como está, apenas sinalizado. Para os provedores em que o ChunkChef emula saídas estruturadas (DeepSeek, Xiaomi), uma primeira resposta não conforme recebe uma tentativa automática de reparo antes de ser avaliada.

Mesclagem de resultados em chunks (merge)

Quando um documento é dividido em chunks (veja “Prompts e extração de dados” acima), cada chunk recebe sua própria chamada ao modelo e sua própria linha llm[] — combinar esses pedaços em uma única resposta fica, por padrão, a seu cargo. Ative merge na criação do job e o ChunkChef faz essa combinação por você, no servidor: o resultado ganha um array merged[] com uma resposta para o documento inteiro (ou para o arquivo inteiro), construída a partir das respostas por chunk.

Quando ativar. Se você está extraindo JSON estruturado de um documento longo o bastante para ser dividido em vários chunks — por exemplo, uma nota fiscal de várias páginas cujos itens de linha se espalham entre a página 1 e a página 2 — o merge evita que você escreva o código de remontagem: o ChunkChef une os campos JSON, concatena e deduplica arrays, e resolve qualquer campo que divirja entre chunks.

Ative assim:

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

Todos os demais campos merge.* são opcionais e têm padrões seguros (veja “Opções de mesclagem” na referência da API para a lista completa).

Exemplo: uma nota fiscal de duas páginas

Imagine uma nota fiscal de 2 páginas, reconhecida por OCR em um texto longo o bastante para ser dividido em 2 chunks — a página 1 cai no chunk 0, a página 2 no chunk 1. Seu prompt pede JSON: { "supplier": "...", "items": [...], "total": "..." }. Sem merge, você recebe duas respostas llm[] separadas, uma por chunk, e as combina você mesmo. Com merge ativado, o ChunkChef as combina em um único objeto JSON: os arrays items dos dois chunks são concatenados em um só, e supplier/total são obtidos do chunk que realmente os reporta. Se o chunk 0 e o chunk 1 reportarem um total diferente para o mesmo documento (por exemplo, o OCR duplicou uma linha de subtotal, ou o modelo leu um número errado), isso é um conflito genuíno — veja “Resolvendo valores conflitantes” abaixo.

Escopo: uma resposta por arquivo, ou uma para o job inteiro (merge.scope)

merge.scope controla quanto é combinado em uma única entrada de merged[]:

  • MERGE_SCOPE_FILE (padrão) — os chunks são mesclados separadamente dentro de cada arquivo: uma entrada merged[] por arquivo, com merged[].file igual à URL desse arquivo.
  • MERGE_SCOPE_JOB — os chunks são mesclados entre todos os arquivos do job em uma única resposta: uma entrada merged[], com merged[].file vazio (""). Use este modo quando os arquivos que você enviou são páginas ou partes de um mesmo documento lógico (por exemplo, um pedido de compra dividido em vários arquivos de origem) e você quer um único resultado combinado, em vez de um por arquivo.

Resolvendo valores conflitantes (merge.conflictPolicy)

Ao mesclar JSON, dois chunks podem divergir sobre o mesmo campo — um reporta total: 1500, outro total: 1520 para a mesma nota fiscal. merge.conflictPolicy decide qual valor prevalece:

  • MERGE_CONFLICT_POLICY_FIRST_NON_NULL (padrão) — prevalece o primeiro valor não nulo, na ordem dos chunks.
  • MERGE_CONFLICT_POLICY_MAJORITY — prevalece o valor que aparece com mais frequência; com menos de 3 chunks, ou quando nenhum valor tem maioria estrita, o ChunkChef recorre a FIRST_NON_NULL.

De qualquer forma, cada divergência desse tipo é registrada em merged[].conflicts[] — o field, os values concorrentes e de qual arquivo/chunk cada um veio — assim um total divergente não é encoberto silenciosamente. Verifique conflicts[] quando precisar saber se o valor vencedor é confiável ou se vale a pena checá-lo manualmente.

Dois tipos de deduplicação

O merge remove conteúdo duplicado de duas formas diferentes:

  1. Deduplicação técnica — sempre ativa, sem configuração necessária. Chunks de um mesmo arquivo costumam se sobrepor um pouco na fronteira (a mesma linha de tabela aparece no fim de um chunk e no início do seguinte); o merge detecta e remove essa sobreposição automaticamente. Esse é o comportamento seguro por padrão — ele nunca remove conteúdo além dessa sobreposição de fronteira.
  2. Deduplicação de conteúdo — opcional, via merge.dedupeBy. Em scope=job, o mesmo registro lógico pode aparecer legitimamente em mais de um arquivo (por exemplo, um item de linha repetido em dois documentos relacionados), e essa repetição pode ser intencional — então o ChunkChef não mexe nela a menos que você peça. Defina em merge.dedupeBy os nomes de campo JSON que identificam um registro único (por exemplo, ["invoiceNumber", "lineNo"]) e o ChunkChef colapsa os registros que coincidirem nesses campos, mantendo a primeira ocorrência. Deixe merge.dedupeBy vazio — o padrão — para manter apenas a deduplicação técnica, nada além disso.

merge.dedupeBy funciona apenas com saída JSON e exige que responseSchema esteja definido — os nomes de campo são procurados como chaves simples no objeto JSON já decodificado. No máximo 32 nomes de campo; mais são rejeitados com 400 na criação do job.

Os dois tipos de remoção são contados separadamente no resultado, para que você consiga distingui-los:

  • merged[].dedupeRemovedTechnical — registros removidos pela deduplicação automática de sobreposição de fronteira.
  • merged[].dedupeRemovedContent — registros removidos pelos seus campos dedupeBy.

Lendo o resultado: merged[] vs llm[] bruto

Quando o merge está ativado, o resultado ganha merged[] junto com o llm[] existente:

  • merged[] — a resposta combinada no servidor, para o documento inteiro (ou o arquivo). Leia isso como seu resultado principal.
  • llm[] — inalterado: continua uma linha por chunk. Permanece disponível para que você possa inspecionar exatamente o que a chamada ao modelo de cada chunk retornou — útil ao depurar um conflito.

Se você definiu responseSchema, o objeto JSON mesclado é validado contra ela e merged[].schemaValid reporta o resultado — da mesma forma que llm[].schemaValid já faz hoje para um arquivo de chunk único.

Quando um chunk não pôde ser mesclado (merged[].mergeIncomplete)

A mesclagem é best-effort: se a resposta de um chunk não puder ser decodificada, ou a estrutura do documento se espalhar entre chunks de um jeito que o merge não consiga combinar com segurança, o conteúdo bruto desse chunk ainda é incluído — anexado, não mesclado — e merged[].mergeIncomplete é definido como true, com os chunks afetados listados em merged[].incompleteChunks. O restante da mesclagem ainda é concluído normalmente; verifique essa flag quando precisar saber se o resultado mesclado está totalmente limpo ou foi montado parcialmente.

Execuções de consenso (consensus)

Para um job com schema (responseSchema definido), você pode pedir ao ChunkChef para executar o mesmo chunk no modelo várias vezes e votar na resposta campo a campo, em vez de confiar em uma única execução. Ative com consensus.mode na criação do job: o ChunkChef executa o chunk k vezes, adota o valor majoritário (por pluralidade) para cada campo, e reporta tanto a resposta acordada quanto cada campo em que o modelo hesitou — um sinal de confiança sobre a sua chamada BYOK habitual, ao custo de k vezes os tokens na sua própria chave. Como as execuções são sequenciais, o consenso também multiplica o tempo de processamento — um job pode levar até k vezes mais.

Requer responseSchema. O consenso precisa de um schema para saber o que é um “campo” — sem ele não há nada para votar. Definir consensus.mode sem responseSchema é rejeitado com 400 na criação do job.

Ative assim:

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

Modos

O nome do modo carrega o multiplicador de tokens embutido — não há uma contagem separada de “execuções” para definir:

ModoExecuçõesNotas
CONSENSUS_MODE_TWO_RUNS2o mais barato — uma sondagem de instabilidade, não uma votação confiável (veja as ressalvas abaixo)
CONSENSUS_MODE_THREE_RUNS3padrão recomendado — uma votação por pluralidade real
CONSENSUS_MODE_FIVE_RUNS5escrutínio máximo, custo mais alto

Omitir consensus (ou deixar CONSENSUS_MODE_UNSPECIFIED) o desativa — o comportamento é idêntico byte a byte ao de um job sem consenso.

Lendo o resultado: minAgreement e disagreements

Cada linha llm[] ganha um objeto consensus (a lista completa de campos está na “Referência da API”). Dois valores para olhar primeiro:

  • minAgreement — a menor concordância por campo da linha, como fração (1.0 = todo campo votado concordou em todas as execuções). Um valor 0 é um sentinela reservado que significa que nenhuma votação real ocorreu (menos de 2 execuções votáveis, ou a votação degradou) — leia-o como “consenso indisponível para este chunk”, não como “0% de concordância”.
  • disagreements[] — uma entrada por campo (ou array) em que as execuções não concordaram totalmente, com a menor concordância primeiro. Cada entrada lista as variants[] concorrentes: o valor, quantas execuções o produziram e se ela included — venceu a votação e está presente na resposta acordada. Uma variante pode ter included: true mesmo quando seu valor é null: um null vencedor significa que a chave do campo é omitida do JSON acordado, não que um null literal apareça.

Ressalvas honestas

  • CONSENSUS_MODE_TWO_RUNS é uma sondagem, não uma votação. Em k=2, uma divergência escalar sempre se divide 1 a 1 (concordância 0.5), e em arrays nada é filtrado — todo elemento reportado por qualquer uma das duas execuções acaba na resposta acordada. Duas execuções mostram que o modelo hesitou; só a partir de três execuções um valor minoritário é de fato filtrado.
  • A votação por pluralidade pode descartar uma entidade que todas as execuções concordam que existe. Se um elemento de array está presente em toda execução, mas com uma célula divergente (por exemplo, um item de linha que toda execução extraiu, mas em que uma execução leu um número errado), a versão de cada execução conta como um valor canônico distinto — em k≥3 cada uma recebe 1 de k, abaixo do limiar de maioria, e o elemento pode ser descartado do array acordado mesmo que toda execução concordasse que ele pertence ali. disagreements[] continua expondo cada variante, então a evidência bruta sobrevive mesmo quando a resposta acordada não a inclui — verifique isso quando um array parecer mais curto do que o esperado.
  • Uma resposta grande demais conta como não votável, não como um erro. A resposta de uma execução que se decodifica como JSON válido, mas excede os limites internos de parsing do ChunkChef, é excluída da votação; a votação prossegue com as execuções restantes — o mesmo tratamento de uma execução que retorna texto não JSON.
  • Consenso e merge se compõem como duas votações independentes. Com ambos ativados, cada chunk é primeiro votado internamente (k execuções → uma resposta acordada por chunk), e então, se merge.conflictPolicy for MERGE_CONFLICT_POLICY_MAJORITY, a etapa de merge vota novamente — dessa vez entre as respostas acordadas dos chunks. Nada é transportado entre as duas votações — um campo que o consenso por chunk já resolveu é apresentado ao merge como um valor já definido, igual à resposta de qualquer outro chunk.

Retentativas idempotentes

Para repetir a criação de um job com segurança, gere um único idempotencyKey (um UUID) e reutilize-o nas repetições da mesma requisição:

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

Repetir com a mesma chave e parâmetros idênticos retorna o job original; alterar qualquer parâmetro sob a mesma chave retorna 400. Use uma nova chave para um job genuinamente novo.

Webhooks

Os webhooks permitem que você evite o polling: o serviço fará um POST no seu endpoint assim que o job terminar. Isso é totalmente opcional — se você não definir os campos, o comportamento da API não muda.

Configuração

Ao criar um job (POST /v1/jobs), passe um ou ambos os campos opcionais:

CampoTipoDescrição
webhookUrlstringURL absoluta do seu endpoint (http:// ou https://). Aceita apenas como entrada — nunca retornada nas respostas.
webhookSecretstringSegredo opcional de assinatura HMAC. Aceito apenas como entrada — nunca retornado nas respostas; armazenado criptografado em repouso e excluído junto com o job (veja “Segurança e dados”).

Exemplo:

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

Quando o webhook dispara

Uma vez por job — na primeira transição para um status terminal (JOB_STATUS_COMPLETE, JOB_STATUS_PARTIAL ou JOB_STATUS_FAILED). A entrega em si é at-least-once (duplicatas são possíveis em caso de falha); veja “Garantias de entrega”. O webhook não carrega o resultado do processamento; ele apenas sinaliza a conclusão. Busque o resultado completo com o habitual GET /v1/jobs/{id}/result.

Corpo da requisição

O serviço envia um POST para o seu webhookUrl com Content-Type: application/json e um corpo JSON:

{
  "job_id":      "15b07304-...",
  "account_id":  "a1b2c3d4-...",
  "status":      "complete",
  "finished_at": "2026-06-24T12:34:56Z"
}
CampoTipoDescrição
job_idstringUUID do job
account_idstring (identificador da conta)Identificador da conta
statusstringUm de: complete, partial, failed
finished_atstringHora de conclusão do job no formato RFC3339 (UTC)

Verificação da assinatura

Quando webhookSecret está definido, toda requisição inclui dois cabeçalhos adicionais:

CabeçalhoValor de exemploDescrição
X-Chunkchef-Timestamp1750765200Hora Unix da entrega (segundos)
X-Chunkchef-Signaturesha256=a3f4...Assinatura HMAC-SHA256

Algoritmo de assinatura:

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

onde <timestamp> é a forma em string da hora Unix do X-Chunkchef-Timestamp, <body> é o corpo bruto da requisição (bytes como recebidos) e . é o separador. hex é em minúsculas; secret é usado como bytes UTF-8.

Como verificar do seu lado:

  1. Extraia o valor de X-Chunkchef-Timestamp.
  2. Calcule hmac_sha256(secret, "<X-Chunkchef-Timestamp value from step 1>.<raw request body>"), codifique como hex em minúsculas e prefixe com sha256=.
  3. Compare-o com X-Chunkchef-Signature usando uma comparação de tempo constante (hmac.Equal / crypto/subtle.ConstantTimeCompare ou equivalente).
  4. Rejeite a requisição se o timestamp for antigo demais (tolerância recomendada: 5 minutos).

Se webhookSecret não estiver definido, os cabeçalhos X-Chunkchef-Timestamp e X-Chunkchef-Signature não são enviados.

Política de retentativas

Se o seu endpoint estiver inacessível ou retornar um erro, o serviço tenta entregar novamente conforme o seguinte cronograma:

TentativaAtraso até a próxima
1 → 21 minuto
2 → 35 minutos
3 → 415 minutos
4 → 530 minutos
5 → 630 minutos
6— (final; a entrega é marcada como falha depois disso)

Total: até 6 tentativas.

Erros permanentes (4xx que não sejam 408/429, URL inutilizável, bloqueio de SSRF) não são repetidos: a entrega é imediatamente marcada como falha. Uma resposta 2xx é considerada um sucesso.

Garantias de entrega

A entrega é at-least-once: na maioria dos casos o seu endpoint recebe exatamente uma chamada, mas as retentativas podem causar entrega duplicada em caso de falhas. Deduplique os eventos do seu lado usando job_id.