A snapshot is one captured production LLM request — optionally together
with the response your baseline model actually produced. Snapshot files are
what you upload to ModelGenius, and what modelgenius-sdk captures and
validates locally.
- File formats: JSONL (one JSON object per line, blank lines ignored) or
.json(a single object or an array of objects). A directory of such files also works. - Format tag: records may carry
"format": "modelgenius.snapshot/v1". It is optional, but when present it must be exactly that string. The capture SDK always writes it. - Encoding: UTF-8. All timestamps are ISO-8601 strings with a timezone
(trailing
Zor an explicit offset).
ModelGenius answers "is it safe to switch models, and is it worth it?" by
replaying your captured requests against candidate models and comparing. That
comparison is only honest if the replay recreates the conditions your baseline
actually ran under. If production ran with reasoning_effort=high and a
max_tokens=512 cap, but the replay runs default-effort and uncapped, you are
not measuring the same thing — both the quality verdict and the cost
projection drift. So capture the request parameters alongside the prompt; the
SDK wrappers do this for you automatically.
Only two things are strictly required: messages and a resolvable
timestamp.
{"messages": [{"role": "user", "content": "Classify this ticket: ..."}], "timestamp": "2026-06-11T14:32:05Z"}Everything else is optional — but each optional field you include makes the analysis more faithful and unlocks capability (see What each optional field unlocks).
A well-populated snapshot looks like this (one JSONL line, shown expanded):
{
"format": "modelgenius.snapshot/v1",
"snapshot_id": "tkt-84512-triage",
"timestamp": "2026-06-11T14:47:19Z",
"model": "gpt-4o-mini",
"temperature": 0,
"messages": [
{"role": "system", "content": "You triage inbound support tickets..."},
{"role": "user", "content": "The export-to-CSV button does nothing..."}
],
"metadata": {"workflow_name": "support_ticket_triage", "call_name": "classify_ticket"},
"captured_output": {
"output_text": "bug",
"model": "gpt-4o-mini-2024-07-18",
"usage": {"input_tokens": 174, "output_tokens": 2, "thinking_tokens": null, "total_tokens": 176},
"cost_usd": 0.0000274,
"latency_ms": 412.8,
"request_timestamp": "2026-06-11T14:47:19Z",
"response_timestamp": "2026-06-11T14:47:20Z"
}
}The examples/ directory contains complete, validated files
for each major variation.
A record's request portion can be in either of two shapes, auto-detected per line — a single file may freely mix them:
- OpenAI-shaped (canonical) — top-level
messagesin OpenAI chat format, plus the optional parameters listed below. - Anthropic-shaped (Messages API native) — top-level
system(string or block list), content-block messages (tool_use/tool_result/imageblocks),max_tokens,stop_sequences,top_k,thinking, and so on, exactly as your app sent them to the Anthropic API.
Detection rule (structural only — the model name and file name are never
consulted): a record is treated as Anthropic-shaped iff its top-level system
is a string or list, or any message has list-shaped content containing a
block whose type is tool_use or tool_result, or an image block with a
dict source. Otherwise it is OpenAI-shaped.
Anthropic-shaped records are converted to the canonical shape at upload time:
| Anthropic field | Canonical result |
|---|---|
system (string or text-block list) |
First message with role: "system" |
tool_use content blocks |
Assistant tool_calls (OpenAI function-call shape) |
tool_result content blocks |
role: "tool" messages with tool_call_id |
tools[*].input_schema |
{"type": "function", "function": {..., "parameters": ...}} |
tool_choice {auto|any|tool} |
"auto" / "required" / {"type": "function", ...} |
thinking.budget_tokens |
reasoning_effort tier (low/medium/high) |
stop_sequences |
stop |
top_k, raw thinking block |
provider_params (captured, not auto-replayed) |
Write the request the way your app actually made it; do not convert it yourself.
| Field | Required? | Meaning |
|---|---|---|
format |
optional | Self-describing tag; must be "modelgenius.snapshot/v1" when present. |
snapshot_id |
optional | Stable ID for the record. A UUID is generated if absent. |
timestamp |
required (resolvable) | When the production request happened. See Timestamps. |
messages |
required | The conversation, in the request's native shape. |
workflow_id |
optional | Explicit grouping key, used when metadata.workflow_name is absent — see Grouping precedence. |
metadata |
optional | Free-form dict. Recognized keys: workflow_name, call_name, timestamp, request_timestamp. The capture SDK adds capture_sdk: {"name", "version"}. |
captured_output |
optional | The real production response. See captured_output. |
api_surface |
optional | Which request surface produced the snapshot ("chat" for OpenAI chat completions; set server-side for Anthropic records). Provenance for replay-side parameter renaming. |
Unknown top-level fields are permitted (preserved or ignored, never a rejection) — that is part of the v1 compatibility promise.
ModelGenius groups snapshots into workflow/call groups (grouping quality directly drives the quality of the extracted evaluation criteria). The group key is resolved per snapshot, in order:
metadata.workflow_name(paired withmetadata.call_name, defaulting todefault_call) — wins whenever present.workflow_id— used whenworkflow_nameis absent.- Content-derived clustering — the fallback when neither is set. It works, but is fuzzier than telling us explicitly.
The SDK wrappers set metadata.workflow_name / metadata.call_name from
CaptureConfig(workflow_name=..., call_name=...).
All timestamp fields are ISO-8601 strings with timezone information, e.g.
"2026-06-11T14:32:05Z" or "2026-06-11T14:32:05+00:00".
Every snapshot must have a resolvable timestamp. If the top-level
timestamp is absent, it is backfilled from, in order:
metadata.timestampmetadata.request_timestampcaptured_output.request_timestamp
If none of these exist, the record is rejected with:
timestamp is required. Provide top-level timestamp or compatibility metadata.timestamp, metadata.request_timestamp, or captured_output.request_timestamp.
Three distinct timestamps can appear on a record:
timestamp— where the request belongs in the workflow timeline.captured_output.request_timestamp— when the provider request started.captured_output.response_timestamp— when the provider response completed.
request_timestamp and response_timestamp are both required whenever
captured_output is present (real latency is derived from the pair).
| Field | What it is | Replay behavior |
|---|---|---|
model |
Baseline model that served production | Identifies the baseline to compare against |
temperature |
Sampling temperature | Reproduced |
top_p |
Nucleus sampling cutoff | Reproduced |
max_tokens |
Output-token cap (canonical name) | Reproduced |
stop |
Stop sequence(s) | Reproduced |
reasoning_effort |
Reasoning depth (minimal/low/medium/high/…) |
Reproduced, mapped per target model (see below) |
frequency_penalty, presence_penalty, n |
Standard sampling knobs | Reproduced |
tools, tool_choice, response_format |
Tool / structured-output contract | Reproduced; also drives deterministic output checks |
api_surface |
Request surface provenance | Drives replay-side parameter renaming |
provider_params |
Capture-only bag | Not auto-replayed (see below) |
Surface-specific spellings of the token cap are canonicalized for you:
max_completion_tokens and max_output_tokens both map to max_tokens.
Output-shaping parameters (temperature, top_p, max_tokens, stop,
reasoning_effort) are applied to every replayed model. For
reasoning_effort the captured value is run through a per-model capability
registry: it is an identity for the baseline's own model (faithful
reproduction) and is coerced onto a different candidate's supported set
(for example, an OpenAI minimal/low/medium/high tier is translated to
an Anthropic thinking budget or a Gemini thinking level). When a captured
effort cannot be faithfully mapped to a candidate (say, a non-reasoning
model), the parameter is dropped and the candidate runs at its natural
default — a graceful, logged degrade rather than a hard failure.
Some parameters are recorded for provenance but deliberately not applied
on replay: seed, logit_bias, service_tier, parallel_tool_calls,
verbosity, user, top_k, and the raw Anthropic thinking block. You can
send these at the top level of a record (the SDK captures them wherever your
app put them); they are swept into provider_params automatically at upload.
Two reasons they are off by default:
seedis the load-bearing case. ModelGenius measures behavior across stochastic variation — pass rates with confidence intervals only mean something if the dice are actually rolling. Pinning a captured seed would freeze one point of the output distribution and make the measured variance artificially tight. Seed is recorded for a faithful record of the request but is not applied on replay.logit_biasis tokenizer-specific. Token IDs are meaningful only for the baseline's tokenizer; transferring them to a different candidate is at best meaningless and at worst harmful.
captured_output carries the response your baseline model actually produced.
It is optional — but strongly recommended. When present, ModelGenius scores
the baseline's real output directly instead of replaying the baseline model,
which saves replay tokens and preserves the true production latency, cost,
and token usage.
The canonical-shape rule:
captured_outputis always written in the canonical (OpenAI-flavored) shape — even when the request part of the record is Anthropic-shaped. The upload pipeline converts Anthropic-shaped requests but passescaptured_outputthrough verbatim, so a provider-native response body here would be misread. The SDK wrappers do this conversion for you; if you build records by hand from Anthropic responses, apply the same mapping:
output_text= thetextblocks concatenated with"\n"(thinking blocks dropped).- Each
tool_useblock becomes an OpenAI-style tool call:{"id": ..., "type": "function", "function": {"name": ..., "arguments": "<JSON string of input>"}}.
| Field | Type | Meaning |
|---|---|---|
output_text |
string (default "") |
The assistant's text output. |
tool_calls |
list | null | OpenAI function-call shape (see above). |
raw_response |
object (default {}) |
Full provider response, JSON-safe. For streams: {"stream": true, "chunk_count": N, "reconstructed": {...}}. |
model |
string | null | Model name from the response (often more precise than the requested alias). |
usage |
object | null | {input_tokens, output_tokens, thinking_tokens, total_tokens}, each int or null. |
cost_usd |
number | null | Cost of the production call in USD. |
latency_ms |
number | null | Observed production latency. |
api_surface |
string | null | "chat" for OpenAI chat completions, "anthropic" for Anthropic messages. |
provider_metadata |
object (default {}) |
e.g. {"request_id": ..., "stop_reason": ..., "finish_reason": ...}. |
request_timestamp |
required | ISO-8601, when the provider request started. |
response_timestamp |
required | ISO-8601, when the provider response completed. |
| You provide | ModelGenius can then |
|---|---|
captured_output |
Skip the baseline replay entirely — score your real production outputs, keep real latency/cost/usage, save replay tokens. |
captured_output.usage + cost_usd |
Compute an honest savings baseline from what production actually cost, instead of estimating from token counts and price sheets. |
metadata.workflow_name + metadata.call_name, or workflow_id |
Group snapshots into workflows precisely. Precedence: workflow_name (+ call_name) wins; else workflow_id; else grouping is inferred by content clustering — see Grouping precedence. |
tools / response_format |
Run deterministic contract checks (JSON parseability, schema conformance, required fields, type validity) against every candidate's outputs. |
model |
Identify the baseline automatically and anchor the cost/quality comparison. |
temperature, max_tokens, reasoning_effort, … |
Replay under the same conditions production ran under — honest quality verdicts and cost projections. |
modelgenius.snapshot/v1 evolves additively only:
- New optional fields may be added; existing fields will not be removed or change meaning within v1.
- Unknown top-level fields are accepted today and will remain accepted.
- A file that validates cleanly against v1 today will continue to validate against v1.
The machine-readable contract is the JSON Schema shipped inside the package
at modelgenius_sdk/schema/modelgenius.snapshot.v1.schema.json (it describes
the canonical shape; Anthropic-shaped requests are additionally accepted per
the detection rule above).
modelgenius-sdk validate ./modelgenius_snapshotsThe validator applies the same accept/reject semantics as the ModelGenius
upload path, and emits warnings for missing optional fields that would unlock
capability (no captured_output, no grouping keys, no usage/cost, and so on).
Use --strict to treat warnings as failures and --json for
machine-readable output.