An export contains the conversation as the user saw it. A migration needs the conversation as the system produced it, and those are different objects. The gap between them is a specific list of fields, and each absence blocks a specific thing you were planning to do.
Why exports are incomplete by design
It is tempting to read a thin export as an anti-competitive choice. It usually is not. Export features are built to satisfy a data-subject access request and a user’s reasonable expectation of getting their chats back — the right to receive personal data in a structured, commonly used, machine-readable format is written into data protection law, for instance in Article 20 of the EU General Data Protection Regulation. That obligation is about the user’s personal data. It says nothing about the platform’s operational metadata, and operational metadata is exactly what a migration runs on.
The practical consequence: assume the export gives you roles and text, and treat everything else as something you have to establish separately. Below is what “everything else” usually means, grouped by what it stops you doing.
Fields you need to reproduce a turn
Reproduction is the core migration task. You want to send the same conversation to a new provider and compare. You cannot, without these.
- The system prompt, and which version of it. Almost never in the export, because it was not a user message. Two conversations six months apart may have run under materially different instructions, and replaying both against today’s prompt compares nothing. Without a prompt version stamped per turn, older transcripts are not usable as regression cases at all.
- The model identifier, per message. Not per conversation — per message. A long-lived thread can span a model upgrade, and a quality complaint attached to a thread is unattributable if you cannot tell which turns ran on which model.
- Sampling parameters. Temperature, top-p, penalties, any seed. Replaying at defaults when the original ran at temperature 0.2 produces a difference you will attribute to the new provider.
- Tool call arguments and results. The user sees a tool ran; the export often records only the final assistant text. The intermediate steps — the call, its arguments, the raw result fed back — are the part that actually determines the answer, and they are the part that most often goes missing. A tool-using agent whose export has no tool steps cannot be replayed, only re-run, which is a different and much weaker test.
- Reasoning content. Where a reasoning model produced intermediate tokens, these are frequently omitted or summarised. Note that some providers do not return them in the API either, so this one may be unavailable rather than merely unexported.
- Retrieved context. For a RAG conversation, which chunks were retrieved and with what scores. Without it you cannot tell whether a bad answer was a retrieval failure or a generation failure — which is the first question you ask about any bad answer.
- Refusals and moderation outcomes. A turn blocked by a safety filter often appears in an export as an ordinary assistant message, or as nothing. Comparing refusal behaviour across providers is a common migration concern and this is what makes it hard.
Fields you need to reconcile cost
The second reason to want history is to work out what the same traffic will cost somewhere else. That needs token counts, and token counts are almost never in an export.
- Per-turn usage. Input tokens, output tokens, and separately any cached-input and reasoning-token counts, which are billed at their own rates. Reconstructing these by re-tokenising the exported text gets you close on input and nowhere on the rest, because the export does not contain the system prompt, the tool schemas or the reasoning tokens that you were billed for.
- The provider request id. The one field that lets you join a conversation to a line on an invoice. Missing from essentially every export.
- Cache hit status. Whether a turn hit a prompt cache changes its input cost substantially, and a cost model built from exported text assumes every turn was a miss.
Because of all three, a cost estimate derived from an export is a lower bound on input and a guess on output. If the estimate matters, derive it from your own request logs instead — rebaselining cost after a provider switch works from that side.
Content that was never a message
Some absences are not metadata at all. Attachments are the common one: an export typically references a file by name or id and does not include the bytes, or includes them in a separate directory whose links to the transcript you have to rebuild yourself. Images pasted into a conversation may survive as references to URLs that expire.
Edit history is another. When a user edits a message, the product keeps both versions as branches, but an export may serialise only the active path — so the sibling that was regenerated away, which is the most interesting signal in the whole file if you are building preference data, is gone. This is the flip side of the branch handling in normalising a conversation export: the transform there discards siblings deliberately, but you can only discard what the export gave you.
Finally, message-level user feedback — thumbs, ratings, flags — is product telemetry rather than personal data and rarely appears. That is the single highest-value label set in the whole system for evaluating a replacement, and it is the one most likely to be missing.
Capturing it yourself
Every gap above has the same fix, and it is not a better export. It is a record you write at request time, in your own store, keyed by conversation and turn. The platform’s export is then the backup rather than the source of truth.
A sufficient record per turn is small: conversation id, turn index, model identifier and version, prompt version, the full request body you sent including system prompt and tool schemas, the parameters, the provider request id, the usage object exactly as returned, the finish reason, latency, and any feedback the user later attached. That is a few kilobytes per turn and it makes every question on this page answerable.
Storing full request bodies means storing user content, so the same retention policy, access control and redaction that governs your production data governs this store too. A log that is easier to read than the database it mirrors is a security problem, not a feature.
The general point is the one that runs through the whole cluster: what you own is what you wrote down. An export is a convenience, and convenience is not a migration plan — what you actually own when leaving a managed platform makes the same argument for the rest of the stack.
Top comments (0)