SuperPenguin Docs
SDKsTypeScript

Batch content capture

Prompt samples from batch and flex jobs with trackBatchRequest and trackBatchResult.

wrap() samples prompts and completions from live calls. Batch and flex jobs have no live call, and submit() and the track*Batch helpers record usage only, so batch work produces no prompt samples. If you loop over a job's results yourself, use these two functions instead.

They need @superpenguin/js 0.15.0 or later (trackBatchResult with request is available from 0.13.0). Capture still requires the org opt-in in Settings; see Content capture and privacy.

When to use it

Use it for batch and flex jobs whose output files hold only responses. The prompt is in the input file you submitted, and the result arrives hours later without it. Instead of keeping every prompt until its result returns, send each half when you already have it. The server joins the two halves on the line's idempotency key.

import { flush, init, trackBatchRequest, trackBatchResult } from "@superpenguin/js";

init({ apiKey: process.env.SP_API_KEY });

// At submit time, once the batch id is known: the prompt half.
for (const line of inputLines) {
  await trackBatchRequest({
    key: `${batch.id}:${line.custom_id}`,      // the key the usage event will carry
    request: line,                             // the input line (its body is read)
    metadata: { feature: "ai_extractor" },
    provider: "sail_research",
  });
}

// On completion: usage, plus the output half. No request needed.
for (const line of outputLines) {
  const event = buildUsageEvent(line);         // idempotency_key = `${batch.id}:${custom_id}`
  await trackBatchResult({ usageEvent: event, response: line.response.body });
}
await flush();

The key passed to trackBatchRequest must equal the usage event's idempotency_key, for example `${batchId}:${customId}`. This is the key trackOpenAIBatch uses. If the batch id is not known until submit returns, call trackBatchRequest after submit.

The server pairs a result with a prompt half sent up to 3 days earlier. Batch jobs finish within 24 hours, so a normal job always pairs.

trackBatchRequest

trackBatchRequest(input: BatchRequestInput): Promise<boolean>
FieldDescription
keyRequired. The line's idempotency key, the same one its usage event carries.
requestRequired. The input line's request body, or the whole input line (its body is used).
metadataAttribution such as feature, customer_id and prompt_key. It outranks an active withMetadata scope. Where the result's usage event also sets a field, the usage event wins.
provider, baseUrlThe provider, as on the usage event.
clientEvent client; defaults to the process-level client.

Resolves to true when the prompt half was queued for upload, and false when the line is not sampled or capture is off. It never throws.

One call, if you still hold the prompts

If you keep the request body until the result returns, pass it as request and one call sends the usage event and one complete sample:

for (const line of outputLines) {              // the job's output JSONL, parsed
  const customId = line.custom_id;
  const event = buildUsageEvent(line);         // the object you pass to submit() today
  await trackBatchResult({
    request: requestLines[customId].body,      // the matching input line's body
    response: line.response.body,
    usageEvent: event,
  });
}
await flush();

This path samples at the org's adaptive rate, the same rate wrap() uses. Reporting the same result twice stores one sample.

trackBatchResult

trackBatchResult(input: BatchResultInput): Promise<boolean>
FieldDescription
responseRequired. The result's response body, or the whole output line (its response.body is used).
requestThe input line's request body, or the whole input line. Omit it after trackBatchRequest to send only the output half.
usageEventThe usage event you already pass to submit(). It is submitted unchanged.
keyJoin key of the output half when request is omitted. Defaults to the usage event's idempotency_key.
provider, batchId, customIdWithout usageEvent, the SDK builds the event the way trackOpenAIBatch does and needs all three. customId is taken from a whole input or output line when not given.
completedAtThe job's completion time (Date, unix seconds or ISO string). Backdates a built event; also stamps the sample when the usage event has no created_at.
metadataAttribution for a built event. With usageEvent, it only fills sample attribution the event does not carry.
baseUrl, region, deploymentType, serviceTierPricing fields for a built event.
clientEvent client; defaults to the process-level client.

Resolves to true when a usage event was submitted. A built event is skipped for a non-200 result line, matching trackOpenAIBatch. Without usageEvent, and without provider, batchId and customId, it throws.

Chat (messages, choices) and Responses (instructions, input, output) bodies are both read, including the system prompt. A withMetadata scope cannot override the usage event's attribution on the sample.

Guarantees

  • Spend never depends on capture. trackBatchResult submits the usage event first and unchanged, exactly as submit() does. Capture runs afterwards, and a capture error never affects the usage event.
  • Only sampled lines send anything. In the split pattern, both calls hash the key against your organization's stable batch rate, so they make the same decision without shared state. An unsampled line reads no text and makes no network call. The SDK fetches your organization's capture settings once and caches them.
  • The same privacy rules apply. The org opt-in, redaction and encryption at rest apply as for every sample. configureContentCapture() can still narrow capture: allow: false vetoes both halves, recordPrompt: false drops the prompt half, recordOutcome: false drops the output half, and sampleRateMultiplier scales the batch rate.
  • Unpaired halves expire. A half without its partner, such as a prompt half whose result never arrives, is discarded after 14 days.
  • Capture is optional. Skipping trackBatchRequest just means no samples for those lines. Usage and cost are recorded the same way.