Trace Formats: OpenAI Messages vs Langfuse
openai_messages is the default and the one to use unless Langfuse is already your observability layer. Both are set through trace_processing.observation_format in your config, both are JSONL with one trace per line, and both feed the identical downstream pipeline. The choice is about export convenience, not capability.
How do the two formats compare?
openai_messages (default) |
langfuse |
|
|---|---|---|
| Line shape | {"messages": [...]} |
A Langfuse observation object |
| Conversation lives in | messages |
input |
| Tool schemas live in | Top-level tools |
metadata.tools, JSON-encoded |
| Tool results live in | tool role messages |
output |
Requires an id |
No | Yes |
| Best when | You are exporting from anything else | Langfuse is already in your stack |
Neither format is preferred by the pipeline. Both are deduplicated, split into train and test seeds, scored for relevance and coherence, relabelled by a teacher committee, and validated in exactly the same way. Pick whichever means less transformation code on your side.
What does the OpenAI messages format expect?
An object per line with a messages array following the OpenAI chat completion format, plus two optional siblings.
| Field | Type | Required | Description |
|---|---|---|---|
messages |
array | Yes | Array of chat completion messages |
tools |
array | No | Tool definitions in OpenAI function-calling format |
response_format |
object | No | Response format specification for structured output |
Each message carries role (one of system, user, assistant, tool), an optional content string, and an optional tool_calls array.
{"messages": [{"role": "system", "content": "You are a helpful banking assistant."}, {"role": "user", "content": "What is my account balance?"}, {"role": "assistant", "content": "Your current account balance is $1,234.56."}]}
The reason this is the default is that it is the lingua franca. Almost every LLM gateway, SDK and logging wrapper can emit it, and if you are storing raw request bodies you may already have it on disk with no transformation at all.
What does the Langfuse format expect?
A Langfuse observation object per line, with observation_format: langfuse set in your config.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique observation identifier |
input |
array | Yes | Input messages, same structure as OpenAI messages |
output |
array | No | Tool call outputs with toolCallId, toolName and input |
metadata |
object | No | May contain a tools array of JSON-encoded tool definitions |
Note the two structural differences that catch people. Tool definitions live inside metadata as JSON-encoded strings rather than as a top-level array of objects. And output holds tool call outputs rather than the assistant’s reply — the assistant turns are inside input along with everything else.
The id requirement is the practical reason to use this format when you already have Langfuse: it lets you trace a training example back to the exact observation it came from, which is genuinely useful when you are inspecting what the relabelling committee changed.
Which one should you use?
| Your situation | Format |
|---|---|
| Langfuse is your observability tool | langfuse — export directly |
| You log raw OpenAI-compatible request bodies | openai_messages |
| You use a different tracing tool | openai_messages, transformed |
| Your traces include images | openai_messages_with_images |
| You have domain documents alongside conversations | unstructured_with_openai_messages |
If you are transforming from a third tool, target openai_messages. Converting an arbitrary tracing schema into a messages array is a smaller job than reproducing a Langfuse observation object, and there is no downstream benefit to the latter unless the id linkage matters to you.
The OpenTelemetry GenAI semantic conventions are worth knowing about if you are choosing an observability stack now rather than exporting from an existing one — several tools emit spans that map cleanly onto a messages array.
What are the other two format options?
Two variants extend openai_messages rather than replacing it, both set through the same observation_format parameter documented in the config reference:
openai_messages_with_images— OpenAI messages that may include images.unstructured_with_openai_messages— unstructured data alongside OpenAI messages, for when you have domain documents to supply as generation context in the same file as your conversations.
The second is the more commonly useful one. Unstructured context keeps synthetic generation on-domain, and traces beyond the seed budget already become unstructured context automatically — this format lets you add documents that were never conversations at all.
What is the same regardless of format?
Everything after parsing. One gotcha is worth flagging: remove_system_prompt_from_traces defaults to true, so leading system messages are stripped from both formats. The reasoning is documented — the system prompt is typically captured by the job description, and keeping it inside the conversation breaks the single-turn [user, assistant] shape expected at the training boundary.
There is also a format asymmetry worth knowing if your task is tool calling. In traces, tool_calls[].function.arguments is a JSON-encoded string, matching OpenAI’s wire format. In a hand-written train.jsonl for the tool-calling tasks, arguments is a real JSON object — HuggingFace format. The tool schemas use the OpenAI function format in both cases. See the tool calling data preparation guide before writing examples by hand.
For the end-to-end procedure once your format is chosen, see turn production traces into training data, and for whether your traces are worth uploading at all, what makes a good production trace.