← All learn articles

Trace Formats: OpenAI Messages vs Langfuse

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.

Sources

Related

All Training data articles →