Capture, at request time, the metadata that lets you reconstruct a model-by-task picture later.
| Field | Why it goes in the log |
|---|
task_type | The unit you route on, from a fixed list of about a dozen values |
model | The model that actually produced the output |
passed | True or false, or null when the check has not run yet |
input_tokens | Provider-reported, not estimated |
output_tokens | Provider-reported, including reasoning tokens where billed |
cost_usd | Computed at log time from the rate table in force |
retry_of | Links a retry to the attempt that failed |
Tag at the call site rather than parsing logs afterward, because post-hoc parsing reconstructs context the application has already discarded, and a routed request shows up in raw logs as an opaque model call with only a provider, model and token count (Braintrust).
Take token numbers from the provider's usage object. On OpenAI endpoints, Chat Completions reports prompt_tokens, completion_tokens and total_tokens, while the Responses API reports input_tokens, output_tokens and total_tokens, and reasoning tokens are billed as output even though they never appear in the answer (OpenAI Help Center). A reasoning model can look cheap on visible output and expensive on the invoice.
import uuid
def call_model(client, model, task_type, prompt, max_out=800):
resp = client.responses.create(model=model, input=prompt, max_output_tokens=max_out)
return {
"request_id": str(uuid.uuid4()),
"task_type": task_type,
"model": model,
"input_tokens": resp.usage.input_tokens,
"output_tokens": resp.usage.output_tokens,
"passed": None, # set by the checker, inline or in a batch job
}
Write null for passed unless your checker runs inline. For schema-validated output the check can run in the same request. For anything a human reviews, backfill in a batch. The row is cheap; the gap is expensive. Also record the prompt template version next to the model, since a template change moves input tokens across every request in a feature.