SuperPenguin Docs
SDKsPython

Batch content capture

Prompt samples from batch and flex jobs with track_batch_request and track_batch_result.

sp.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 Python SDK 0.19.0 or later (track_batch_result with request= is available from 0.17.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 superpenguin as sp

sp.init(api_key="sp-...")

# At submit time, once the batch id is known: the prompt half.
for line in input_lines:
    sp.track_batch_request(
        f"{batch.id}:{line['custom_id']}",    # the key the usage event will carry
        line,                                  # the input line (its body is read)
        {"feature": "ai_extractor"},
        provider="sail_research",
    )

# On completion: usage, plus the output half. No request needed.
for line in output_lines:
    event = build_usage_event(line)            # idempotency_key = f"{batch.id}:{custom_id}"
    sp.track_batch_result(usage_event=event, response=line["response"]["body"])
sp.flush()

The key passed to track_batch_request must equal the usage event's idempotency_key, for example f"{batch_id}:{custom_id}". This is the key track_openai_batch uses. If the batch id is not known until submit returns, call track_batch_request 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.

track_batch_request

sp.track_batch_request(
    key: str,
    request: Any,
    metadata: dict | None = None,
    *,
    provider: str | None = None,
    base_url: str | None = None,
) -> bool
ParameterDescription
keyThe line's idempotency key, the same one its usage event carries.
requestThe 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 sp.metadata() or @sp.trace scope. Where the result's usage event also sets a field, the usage event wins.
provider, base_urlThe provider, as on the usage event.

Returns True when the prompt half was queued for upload, and False when the line is not sampled or capture is off.

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 line in output_lines:                     # the job's output JSONL, parsed
    custom_id = line["custom_id"]
    event = build_usage_event(line)           # the dict you pass to submit() today
    sp.track_batch_result(
        request=request_lines[custom_id]["body"],  # the matching input line's body
        response=line["response"]["body"],
        usage_event=event,
    )
sp.flush()

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

track_batch_result

sp.track_batch_result(
    *,
    response: Any,
    request: Any = None,
    usage_event: dict | None = None,
    key: str | None = None,
    provider: str | None = None,
    base_url: str | None = None,
    batch_id: str | None = None,
    custom_id: str | None = None,
    completed_at: Any = None,
    metadata: dict | None = None,
    region: str | None = None,
    deployment_type: str | None = None,
    service_tier: str | None = None,
) -> bool
ParameterDescription
responseThe 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 track_batch_request to send only the output half.
usage_eventThe usage dict 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, batch_id, custom_idWithout usage_event, the SDK builds the event the way track_openai_batch does and needs all three. custom_id is taken from a whole input or output line when not given.
completed_atThe job's completion time (datetime, 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 usage_event, it only fills sample attribution the event does not carry.
base_url, region, deployment_type, service_tierPricing fields for a built event.

Returns True when a usage event was submitted. A built event is skipped for a non-200 result line, matching track_openai_batch. Without usage_event, and without provider, batch_id and custom_id, it raises ValueError.

Chat (messages, choices) and Responses (instructions, input, output) bodies are both read, including the system prompt.

Guarantees

  • Spend never depends on capture. track_batch_result 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. sp.configure_content_capture() can still narrow capture: allow=False vetoes both halves, record_prompt=False drops the prompt half, record_outcome=False drops the output half, and sample_rate_multiplier 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 track_batch_request just means no samples for those lines. Usage and cost are recorded the same way.