# Batching — Bulk prompt processing

> **Scope:** stacks=["dotnet"]
> **Layer:** 2 (on-keyword)
> **Keywords:** batching, batch api, bulk prompts, batch processing, async batch
> **Read by Claude in:** plan/implement

**Verified against:** OpenAI 2.13.0 (ai-pin 2026-09-08) + Microsoft.Extensions.AI 10.9.0. **Verificação documental** do fluxo da Batch API (platform.openai.com/docs/guides/batch, lido 2026-09-08), sem cláusula `provado por`: o ai-kit **referencia** o pacote `OpenAI` no `.csproj` — contrato de versão, régua = restore (`NU1102`) — mas **não o exercita**; não existe um único `using OpenAI` em `src/` (medido 2026-09-08), logo um rename de API não reprova a PR. Ver a tabela "Dois níveis de garantia" no README do kit. **Todos os nomes de tipo e assinaturas .NET foram medidos por reflexão sobre a assembly `OpenAI` 2.13.0 em 2026-09-08**, e os 9 marcadores `// VERIFY` desta página foram RESOLVIDOS por essa medição. Last-verified: 2026-09-08.

---

## Quando usar batch

Use a Batch API quando você tem **N prompts independentes sem necessidade de resposta imediata**:

- Relatório mensal que agrega análise de N documentos/itens
- Reprocessamento de dados históricos com LLM (re-classificação, re-extração)
- Geração em massa de sumários, traduções ou avaliações offline
- Pipeline noturno de enriquecimento de dados

**Vantagens:**
- **Desconto de custo significativo** — OpenAI Batch API custa 50% menos que completions síncronas
- **SLA relaxado** — resultados em até 24 h; adequado para trabalho assíncrono
- **Rate limits separados** — não compete com a cota síncrona da aplicação

> Regra prática: se o usuário não está esperando o resultado na tela, considere batch.

---

## A API

> **AVISO:** Batch API é **OpenAI-specific**. O `IChatClient` do MAF não expõe batch.
> Obtenha o `OpenAIClient` direto via configuração (mesma chave/modelo que o Model Registry usa)
> em vez de `_modelRegistry.GetChatClient(alias)`.

O fluxo tem 4 etapas: **upload do arquivo JSONL → submissão do batch → poll de status → download dos resultados**.

### A superfície real do SDK 2.13.0 — medida, não suposta

A versão anterior deste standard marcava sete nomes como "a confirmar". Foram conferidos por reflexão sobre a assembly do pin em 2026-09-08. O resultado corrige mais do que confirma:

| O que estava escrito | O que a assembly 2.13.0 diz |
|---|---|
| `openAIClient.GetBatchClient()` (talvez `GetOpenAIBatchClient()`) | **`GetBatchClient()`** ✔ — devolve `OpenAI.Batch.BatchClient` |
| namespace `OpenAI.Batch` | ✔ existe (`BatchClient`, `BatchJob`, `CreateBatchOperation`, `BatchCollectionOptions`) |
| `FileUploadPurpose.Batch` | ✔ existe (ao lado de `Assistants`, `FineTune`, `Vision`, `UserData`, `Evaluations`) |
| `fileClient.UploadFileAsync(stream, filename, purpose)` | ✔ existe, com essa exata assinatura |
| `fileClient.DownloadFileAsync(fileId)` | ✔ existe — devolve `ClientResult<BinaryData>` |
| `CreateBatchAsync(fileId, endpoint, completionWindow)` | ✘ **NÃO EXISTE.** A assinatura real é `CreateBatchAsync(BinaryContent content, bool waitUntilCompleted, RequestOptions options)` — método de **protocolo**: o corpo do request é montado por você |
| tipo de retorno `OpenAIBatch` | ✘ **não existe.** O modelo é **`BatchJob`**; e `CreateBatchAsync` devolve **`CreateBatchOperation`** |
| `GetBatchAsync(id)` com `Status` tipado | ✘ **`GetBatchAsync(string batchId, RequestOptions options)` devolve `ClientResult` cru.** Além disso `BatchJob.Status` é do tipo **`InternalBatchStatus`**, que **não é exportado**: mesmo desserializando o `BatchJob`, o consumidor não consegue nomear o tipo do status |

**A conclusão que muda o código:** no `OpenAI` 2.13.0, **Batch é uma API de protocolo, não de modelo**. Você monta o corpo do request e lê o status do **JSON da resposta**. Um exemplo escrito com opções fortemente tipadas não compila — e essa era exatamente a dúvida que os `// VERIFY` marcavam.

`CreateBatchOperation` traz `BatchId`, `GetBatchAsync()`, `CancelAsync()` e `RehydrationToken` — este último é o que permite **retomar o acompanhamento depois de um restart** sem guardar estado próprio: guarde o token, reidrate com `CreateBatchOperation.RehydrateAsync(client, token)`.

---

## Exemplo C# completo

```csharp
using OpenAI;
using OpenAI.Batch;
using OpenAI.Files;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Text;
using System.Text.Json;

// ── 1. Construir o cliente OpenAI direto (mesma config que o Model Registry usa) ──
var apiKey = configuration["OpenAI:ApiKey"]!;
var openAIClient = new OpenAIClient(new ApiKeyCredential(apiKey));
OpenAIFileClient fileClient  = openAIClient.GetOpenAIFileClient();
BatchClient      batchClient = openAIClient.GetBatchClient();

// ── 2. Montar o arquivo JSONL com N requests independentes ──
var items = new[] { "Item A", "Item B", "Item C" }; // substitua pela sua coleção
var jsonlBuilder = new StringBuilder();
foreach (var item in items)
{
    var request = new
    {
        custom_id = $"req-{Guid.CreateVersion7()}",
        method = "POST",
        url = "/v1/chat/completions",
        body = new
        {
            model = configuration["OpenAI:Model"] ?? "gpt-4o-mini",
            messages = new[]
            {
                new { role = "user", content = $"Analyze this item and return a JSON summary: {item}" }
            },
            max_tokens = 256
        }
    };
    jsonlBuilder.AppendLine(JsonSerializer.Serialize(request));
}

// ── 3. Upload do arquivo JSONL ──
using var jsonlStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonlBuilder.ToString()));
OpenAIFile batchFile = await fileClient.UploadFileAsync(
    jsonlStream, "batch_requests.jsonl", FileUploadPurpose.Batch);

// ── 4. Submeter o batch ──
// CreateBatchAsync é método de PROTOCOLO: o corpo vai como BinaryContent.
// waitUntilCompleted: false → devolve a operação imediatamente (o poll é seu, no §5).
using var createBody = BinaryContent.Create(BinaryData.FromObjectAsJson(new
{
    input_file_id    = batchFile.Id,
    endpoint         = "/v1/chat/completions",
    completion_window = "24h",
}));

CreateBatchOperation operation =
    await batchClient.CreateBatchAsync(createBody, waitUntilCompleted: false);

string batchId = operation.BatchId;
// Guarde operation.RehydrationToken se o poll roda em OUTRO processo/depois de um restart.

// ── 5. Poll de status (em background job / Hangfire / Worker) ──
// GetBatchAsync devolve ClientResult cru, e BatchJob.Status é de um tipo NÃO EXPORTADO
// (InternalBatchStatus). O status se lê do JSON — não há caminho tipado no 2.13.0.
string status;
JsonElement batchDoc;
do
{
    await Task.Delay(TimeSpan.FromSeconds(30)); // poll interval para batch — não interativo
    ClientResult raw = await batchClient.GetBatchAsync(batchId, options: null);
    batchDoc = JsonDocument.Parse(raw.GetRawResponse().Content.ToMemory()).RootElement;
    status = batchDoc.GetProperty("status").GetString()!;
} while (status is not ("completed" or "failed" or "cancelled" or "expired"));

// ── 6. Recuperar e processar resultados ──
if (batchDoc.TryGetProperty("output_file_id", out var outEl) && outEl.GetString() is { } outputFileId)
{
    BinaryData resultFile = await fileClient.DownloadFileAsync(outputFileId);
    using var reader = new StreamReader(resultFile.ToStream());
    string? line;
    while ((line = await reader.ReadLineAsync()) is not null)
    {
        using var doc = JsonDocument.Parse(line);
        var customId = doc.RootElement.GetProperty("custom_id").GetString();
        var content = doc.RootElement
            .GetProperty("response")
            .GetProperty("body")
            .GetProperty("choices")[0]
            .GetProperty("message")
            .GetProperty("content")
            .GetString();
        // processe content...
    }
}

// ── 7. Tratar falhas de itens individuais ──
if (batchDoc.TryGetProperty("error_file_id", out var errEl) && errEl.GetString() is { } errorFileId)
{
    BinaryData errorFile = await fileClient.DownloadFileAsync(errorFileId);
    // log itens que falharam; retente individualmente se necessário
}
```

> O poll de status (`do...while`) deve rodar num **background job** (Hangfire, Worker Service),
> não em uma requisição HTTP. O usuário não espera; o resultado é entregue via event/webhook/DB.
> Se o job pode reiniciar, persista `operation.RehydrationToken` e retome com
> `CreateBatchOperation.RehydrateAsync(batchClient, token)` em vez de guardar só o `batchId`.

---

## Quando NÃO usar

| Situação | Por quê não usar batch |
|----------|------------------------|
| Resposta interativa — usuário aguarda na tela | Latência de batch (minutos a horas) é inaceitável |
| Volume baixo (< ~20 requests/dia) | Overhead de JSONL + poll não compensa |
| Requests com dependência entre si | Batch é para prompts **independentes** |
| Provider não-OpenAI (Anthropic, Google) | Batch API é OpenAI-specific; use completions síncronas |

---

## Anti-patterns

| Anti-pattern | Problema |
|-------------|----------|
| Usar batch para requests síncronos de usuário | Alta latência — batch pode demorar horas |
| Não tratar `error_file_id` | Itens que falharam são silenciados; reprocessamento nunca acontece |
| Poll com `Task.Delay(1s)` em loop | Abuso de rate limits; use intervalo ≥ 30 s para batch |
| Assumir que todos os itens do batch concluem | Batch pode ter falhas parciais; itere o arquivo de erros |
| Escrever `CreateBatchAsync(fileId, endpoint, completionWindow)` | Essa sobrecarga não existe no `OpenAI` 2.13.0 — o método é de protocolo e recebe `BinaryContent` |
| Tipar o status como `OpenAIBatch.Status` | `OpenAIBatch` não existe; `BatchJob.Status` é de um tipo não exportado. Leia o status do JSON |
| Guardar só o `batchId` num job que pode reiniciar | Perde o acompanhamento da operação | 
| Incluir prompt com dados do usuário sem sanitização | Prompt injection em batch; aplique `ai-agents-middleware-patterns` |

---

## Checklist (verifiable by morph-eval)

- [ ] Batch é usado somente para prompts independentes, sem usuário aguardando resposta
- [ ] O poll de status roda em background job (Worker/Hangfire), não em request HTTP
- [ ] `error_file_id` é verificado e erros de itens individuais são logados/retentados
- [ ] Nenhum `Guid.NewGuid()` — custom_id usa `Guid.CreateVersion7()`
- [ ] Provider é OpenAI, com `PackageReference Include="OpenAI" Version="2.13.0"` (versão exata)
- [ ] O status do batch é lido do JSON da resposta — nenhum código tenta tipar `Status` ou usar `OpenAIBatch`
- [ ] `waitUntilCompleted: false` + poll próprio, ou `RehydrationToken` persistido se o poll pode reiniciar

---

## References

- `ai-agents-setup` — setup do agente MAF e packages
- `ai-agents-service-tier-flex` — otimização de custo para jobs async
- `ai-agents-providers-model-registry` — configuração provider-agnostic
- OpenAI Batch API: https://platform.openai.com/docs/guides/batch
- OpenAI .NET SDK `OpenAI.Batch`: https://github.com/openai/openai-dotnet (superfície medida na assembly 2.13.0 em 2026-09-08)

---

*MORPH-SPEC by Polymorphism Tech — ai-agents/batching.md v2.0 (2026-09-08)*
