Submit a batch
Seal many requests into one batch file, submit it, and read every line's result.
A batch is many jobs submitted as one file. Every line is sealed and paid for locally before anything is uploaded, so the file the coordinator stores holds routing terms and ciphertext only.
Write the lines
Each line is an OpenAI-style batch request. Put the model in body:
const requests = prompts.map((prompt, i) => ({
custom_id: `row-${i}`, // optional, 1–64 characters, unique in the batch
url: "/v1/responses", // or "/v1/embeddings"; one endpoint per batch
body: { model: "moonshotai/kimi-k3", input: prompt, max_output_tokens: 512 },
}));body.modelis required.body.rate_in,body.rate_outandbody.units_outare order terms, taken out of the body; everything else inbodyis the model input.- A line with neither rate takes the market. Before sealing, the client asks the coordinator for
a plan: which providers take how many of those lines, and at which ask. A provider is never
given more lines than its on-chain capacity leaves free, and each line is pinned to the
provider it was planned to. If the network cannot take all of a model's unpriced lines in the
window,
submitraisesValidationErrorbefore anything is signed. urldefaults to/v1/responses.methodis ignored.requestsmay also be a JSONL string of such lines.
Submit
const batch = await client.batches.submit(requests, "24h", { providers: [] });
await store.put(batch.id, { jobIds: batch.jobIds }); // keep both before waitingprovidersis required and applies to priced lines only; unpriced lines go where the plan puts them. Priced lines are assigned round-robin across the list, each sealed to its own provider. With an empty list ({ providers: [] }) every priced line is an open order, which needs a client built with averifier.- The window is
"1h"/"24h", or the tier names"async"/"batch"; it defaults to"24h". metadata(at most 16 string pairs) is optional and stored in plaintext on the batch record.- Every distinct model's input is checked against its published schema first, as
submitdoes;{ validateParams: false }turns that off.
batch.jobIds lists each line's job id in input order. Until results are opened, the job id is
the only way to correlate a line: your custom_id travels sealed inside the line and the
coordinator never sees it.
Read the results
import { JobError } from "@vorq-ai/client-sdk";
for (const line of await batch.results()) {
if (line instanceof JobError) console.error(line.jobId, line.type, line.message);
else console.log(line.customId, line.cost);
}results() polls until the batch is terminal (completed, failed, expired or cancelled),
then returns every line: a TextResult, MediaResult or EmbeddingResult for a settled line,
or a JobError for one that produced nothing. Lines come back in file order (all settled
lines, then all failed ones), not input order; match them on .customId or .jobId.
To process lines as they are read instead of collecting them:
await batch.consume(
async (result) => save(result.customId, result),
async (error) => logFailure(error.jobId, error.type),
);A batch whose input file was refused raises BatchFailed. A failed line is never an exception.
Re-attach, list, cancel
const batch = client.batches.get(batchId); // no network call
console.log(await batch.status(), batch.requestCounts);
const page = await client.batches.list({ limit: 20 });
const next = page.hasMore ? await client.batches.list({ after: page.lastId! }) : null;
await batch.cancel(); // lines no provider has claimed are cancelled; claimed ones finishA re-attached handle has jobIds: null, which is why you store them at submit time.
With the openai package
After submission, the stock openai client can read the batch too: batches.retrieve,
batches.list and files.content(output_file_id) all pass through
sealingFetch. Creating the batch must go through
client.batches.submit, which seals each line.