Skip to content

[OPIK-7280] [SDK] fix: shape LLM-span output as OpenAI chat completion for pretty mode - #26

Merged
JetoPistola merged 6 commits into
mainfrom
danield/OPIK-7280-llm-span-output-pretty-mode
Jul 10, 2026
Merged

[OPIK-7280] [SDK] fix: shape LLM-span output as OpenAI chat completion for pretty mode#26
JetoPistola merged 6 commits into
mainfrom
danield/OPIK-7280-llm-span-output-pretty-mode

Conversation

@JetoPistola

@JetoPistola JetoPistola commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

User description

Details

image

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 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 → raw JSON. Verified the Output panel routes through prettifyMessage, so no Opik-repo change is required.

Fixes

  1. LLM-span Output → OpenAI chat-completion {"choices":[{"message":{...}}]} shape via a new assistant_output() helper (hooks.py, sanitize/tools.py). Content-only pretty-renders via message.content; tool-call turns get a readable synthesized content, structured tool_calls kept for JSON view.
  2. Trace-level Output → same shape via merge_trace_output / finish_trace (lifecycle.py).
  3. Empty assistant input bubblesserialize_one_message drops assistant turns with no text and no tool calls ({assistant, content:""}).
  4. Reasoning input items → dropped; content is encrypted/opaque so [reasoning] was a misleading empty bubble (real reasoning is on the LLM-span output).
  5. Tool-call input messages → given a readable content summary plus a proper tool_calls entry whose function.arguments is a JSON string (OpenAI wire format). An object triggered the Opik FE's empty-code-block fallback (mapToolCalls requires a string), which showed as an empty assistant bubble.

The readable per-call rendering is a shared tool_call_text() helper in sanitize/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 prettifyConfig to 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 — no trace.update()/end()). Resolved so the finalize upsert re-send carries this PR's choices-shaped output; the merge also removes the "may cause data loss" batching warning, confirmed gone on staging.

Change checklist

  • User facing
  • Documentation updated (if needed)
  • Tests added/updated (if needed)
  • Breaking changes documented (if any)

Issues

  • OPIK-7280

Testing

  • Unit tests across 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.
  • Full suite green: 113 passed (post-merge). ruff check + ruff format --check clean.
  • Verified emitted shapes prettify against the exact Opik FE prettifyOpenAIMessageLogic / mapToolCalls.
  • End-to-end on real staging Opik + real OpenAI (gpt-5), Opik SDK 2.1.21 (PyPI latest) baked into the Hermes image: confirmed via the staging REST API that trace + LLM-span Outputs use the choices shape, span inputs have no empty/[reasoning] bubbles, tool-call function.arguments are 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:13px
Loading

Shape the span output plumbing so sanitize helpers, hooks, and lifecycle emit OpenAI-style choices payloads 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.

TopicDetails
Trace & Input Hygiene Harden trace and input hygiene by letting 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)
  • observability/opik/lifecycle.py
  • observability/opik/sanitize/messages.py
  • tests/test_lifecycle.py
  • tests/test_responses_messages.py
Latest Contributors(1)
UserCommitDate
danield@comet.comrefactor(opik): extrac...July 10, 2026
LLM Output Shape Shape assistant output flows through 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 structured tool_calls for JSON views.
Modified files (4)
  • observability/opik/hooks.py
  • observability/opik/sanitize/__init__.py
  • observability/opik/sanitize/tools.py
  • tests/test_assistant_output.py
Latest Contributors(1)
UserCommitDate
danield@comet.comrefactor(opik): extrac...July 10, 2026
Review this PR on Baz | Customize your next review

…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>
@JetoPistola
JetoPistola marked this pull request as ready for review July 9, 2026 19:04
baz-reviewer[bot]
baz-reviewer Bot previously approved these changes Jul 9, 2026
JetoPistola and others added 3 commits July 10, 2026 08:56
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
baz-reviewer Bot dismissed their stale review July 10, 2026 06:21

Baz dismissed its prior approval because a re-review found new findings.

Comment thread observability/opik/sanitize/messages.py Outdated
JetoPistola and others added 2 commits July 10, 2026 09:27
…-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>
@JetoPistola
JetoPistola merged commit 1940b23 into main Jul 10, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant