[OPIK-7280] [SDK] fix: shape LLM-span output as OpenAI chat completion for pretty mode - #26
Merged
Conversation
…n for pretty mode
Opik's trace-view pretty renderer decides prettifiability via prettifyMessage
(lib/traces.ts): for output it only surfaces keys like answer/output/response,
never `content`, and its OpenAI branch reads `choices[-1].message.content`. The
plugin emitted a flat `{content, reasoning, tool_calls}` dict, which no output
prettifier recognizes, so LLM-span Output fell back to raw JSON while Input
(a `{messages: [...]}` shape) pretty-rendered on the same span.
Emit the OpenAI chat-completion `choices` shape instead. Content-only responses
pretty-render via `message.content`; on tool-call turns with no assistant text
we render the tool calls as readable text (empty content would otherwise fall
back to JSON), keeping the structured `tool_calls` on the message for the JSON
view. Root cause is the plugin output shape, not the Opik FE — no opik change
needed. Input and non-LLM spans are untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Staging verification surfaced the same shape bug one level up: the root
trace Output (finish_trace + merge_trace_output) emitted a flat
{content, tool_calls} dict, which Opik's pretty renderer doesn't recognize
(content is not an output key) -> raw JSON, no "Pretty ✨" toggle.
Route trace output through the same assistant_output() helper as the LLM
spans, so it emits the choices shape and pretty-renders. Verified on real
staging Opik: trace Output + all LLM spans now use the choices shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An assistant turn that produced neither text nor tool calls arrived as a
bare {role: assistant, content: ""} record and rendered as an empty message
bubble in the trace/span Messages view — pure noise with no signal for the
user (surfaced during staging QA of the Output fix).
serialize_one_message now returns None for such records so serialize_messages
skips them. Assistant turns that carry tool_calls, and non-assistant roles
(incl. empty user/tool), are unaffected. The [reasoning] marker stays — it
signals a redacted reasoning step, not an empty message.
Verified on real staging Opik: span inputs no longer contain empty assistant
bubbles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tems
Follow-up from staging QA of the Messages view:
- function_call input items rendered as an empty assistant bubble. The plugin
put the tool call in `content` as a dict (no text body) and emitted
function.arguments as a parsed object. Opik's pretty renderer requires
function.arguments to be a JSON *string* (OpenAI wire format) — an object
triggers an empty-code-block fallback. Now emit a readable content summary
(e.g. `terminal({"command":"42"})`) AND a proper tool_calls entry whose
function.arguments is a JSON string, so the tool-call block renders.
- reasoning input items carried only encrypted/opaque content and rendered as
a bare "[reasoning]" bubble. Drop them on the input side; real reasoning is
still captured on the LLM-span output.
Verified on real staging Opik: no [reasoning] placeholders, no empty assistant
bubbles, tool-call function.arguments are JSON strings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baz-reviewer
Bot
dismissed
their stale review
July 10, 2026 06:21
Baz dismissed its prior approval because a re-review found new findings.
…-span-output-pretty-mode # Conflicts: # observability/opik/lifecycle.py # tests/test_lifecycle.py
Address PR review: _tool_call_text (messages.py) duplicated the per-call rendering in _tool_calls_as_text (tools.py). Extract a single tool_call_text(name, arguments) in tools.py; both call sites use it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
User description
Details
LLM-span Output in the Opik trace view rendered as raw JSON with no "Pretty ✨" toggle — even for plain content-only responses — while Input pretty-rendered. Staging QA surfaced several adjacent input/output rendering issues, all fixed here.
Root cause of the Output bug (plugin, not Opik FE): Opik's pretty toggle decides prettifiability via
prettifyMessage(apps/opik-frontend/src/lib/traces.ts); for output it only surfaces keys likeanswer/output/response(nevercontent) and its OpenAI branch readschoices[-1].message.content. The plugin emitted a flat{content, reasoning, tool_calls}dict, which no output prettifier recognizes → raw JSON. Verified the Output panel routes throughprettifyMessage, so no Opik-repo change is required.Fixes
{"choices":[{"message":{...}}]}shape via a newassistant_output()helper (hooks.py,sanitize/tools.py). Content-only pretty-renders viamessage.content; tool-call turns get a readable synthesized content, structuredtool_callskept for JSON view.merge_trace_output/finish_trace(lifecycle.py).serialize_one_messagedrops assistant turns with no text and no tool calls ({assistant, content:""}).[reasoning]was a misleading empty bubble (real reasoning is on the LLM-span output).contentsummary plus a propertool_callsentry whosefunction.argumentsis a JSON string (OpenAI wire format). An object triggered the Opik FE's empty-code-block fallback (mapToolCallsrequires a string), which showed as an empty assistant bubble.The readable per-call rendering is a shared
tool_call_text()helper insanitize/tools.py, used by both the input serializer and the output tool-call summary (dedup, per review).Note: Metadata, Token-usage, and tool-span panels have no "Pretty ✨" by design — the FE only attaches
prettifyConfigto LLM Input/Output, and Opik highlights JSON/YAML only (no language-aware code highlighting).Merge note
Branch merged
origin/main(OPIK-7279, upsert-only trace lifecycle — notrace.update()/end()). Resolved so the finalize upsert re-send carries this PR'schoices-shaped output; the merge also removes the "may cause data loss" batching warning, confirmed gone on staging.Change checklist
Issues
Testing
tests/test_assistant_output.py,tests/test_lifecycle.py,tests/test_responses_messages.py: Output shapes, trace output, empty-message drop, reasoning drop, tool-call readable content + JSON-string arguments, shared-helper dedup.ruff check+ruff format --checkclean.prettifyOpenAIMessageLogic/mapToolCalls.choicesshape, span inputs have no empty/[reasoning]bubbles, tool-callfunction.argumentsare JSON strings, and the batching warning is gone.Documentation
Behavior documented in code (docstrings/comments on
assistant_output,merge_trace_output,tool_call_text/_json_str, and the drop rules). No user-facing docs changes.Generated description
graph LR serialize_assistant_message_("serialize_assistant_message"):::modified assistant_output_("assistant_output"):::added on_post_llm_call_("on_post_llm_call"):::modified merge_trace_output_("merge_trace_output"):::modified finish_trace_("finish_trace"):::modified serialize_one_message_("serialize_one_message"):::modified tool_call_text_("tool_call_text"):::added json_str_("_json_str"):::added serialize_assistant_message_ -- "Wraps message into OpenAI chat-completion with tool-call pretty content." --> assistant_output_ on_post_llm_call_ -- "Still calls serialize_assistant_message; now additionally extracts tool_calls." --> serialize_assistant_message_ on_post_llm_call_ -- "Formats assistant_response into OpenAI completion shape via assistant_output." --> assistant_output_ merge_trace_output_ -- "Replaces merged dict output with assistant_output chat-completion rendering." --> assistant_output_ finish_trace_ -- "Sends merge_trace_output result directly to client.trace output." --> merge_trace_output_ serialize_one_message_ -- "Renders function_call name/arguments into readable content for bubbles." --> tool_call_text_ serialize_one_message_ -- "_json_str ensures function.arguments JSON-string wire format for renderer." --> json_str_ classDef added stroke:#15AA7A classDef removed stroke:#CD5270 classDef modified stroke:#EDAC4C linkStyle default stroke:#CBD5E1,font-size:13pxShape the span output plumbing so
sanitizehelpers, hooks, and lifecycle emit OpenAI-stylechoicespayloads that Opik’s pretty renderer recognizes while still surfacing tool-call metadata. Normalize trace output handling so lifecycle hooks and traces reuse that shape for both spans and trace-level payloads, keeping readable content and tool-call structure intact.merge_trace_output/finish_trace, message serialization, and the lifecycle/responses tests rely on the new output shape, drop empty or opaque assistant turns, and ensure tool-call arguments become JSON strings so both input and trace panels render cleanly.Modified files (4)
Latest Contributors(1)
hooks.py,sanitize/tools.py, and the assistant-output-focused tests so every LLM span becomes an OpenAI chat-completion with readable summaries when text is missing while preserving structuredtool_callsfor JSON views.Modified files (4)
Latest Contributors(1)