Skip to content

[issue-7686] [BE][FE] feat: add OpenInference pretty formatting - #8141

Draft
d8rt8v wants to merge 1 commit into
comet-ml:mainfrom
d8rt8v:d8rt8v/issue-7686-openinference-pretty-formatting
Draft

[issue-7686] [BE][FE] feat: add OpenInference pretty formatting#8141
d8rt8v wants to merge 1 commit into
comet-ml:mainfrom
d8rt8v:d8rt8v/issue-7686-openinference-pretty-formatting

Conversation

@d8rt8v

@d8rt8v d8rt8v commented Sep 3, 2026

Copy link
Copy Markdown

Details

OpenInference spans received through the private OTLP trace endpoint now normalize raw values, chat/completion semantics, tools, multimodal content, model/provider data, usage, cost, sessions, tags, and metadata into the canonical Opik span shape. The Messages tab and shared Pretty formatter now support both that canonical shape and historical flattened OpenInference records, including output attributes that older ingestion stored under input, without a database migration or backfill.

  • Gate backend normalization on the exact per-span openinference.span.kind marker so mixed OTLP batches and unmarked spans keep their existing behavior.
  • Add an isolated backend normalizer with deterministic indexed reconstruction, MIME-aware raw parsing, collision rules, malformed-value fallbacks, and provider resolution.
  • Register the openinference frontend format with a pure canonical/legacy parser, pair-aware message combination, multimodal/tool/reasoning rendering, role normalization, and normalized span usage.
  • Extend table, annotation, trace-message, and export previews to recover historical semantic output without duplicating less precise raw output.

Before

Historical flattened attributes were shown as raw dotted-key data and did not open the Messages tab.

After

The same stored span opens in Messages and renders roles, tool calls/results, finish reason, and usage; newly ingested spans also preserve ordered multimodal content.

Change checklist

  • User facing
  • Documentation update

Issues

AI-WATERMARK

AI-WATERMARK: yes

  • If yes:
    • Tools: OpenAI Codex desktop, GitHub CLI, Maven, Vitest, ESLint, Spotless, Semgrep, local Opik stack and browser automation
    • Model(s): OpenAI Codex (model selected by the Codex app)
    • Scope: OpenInference specification review, backend/frontend implementation, tests, local verification, and PR preparation
    • Human verification: Yes

Testing

  • mvn -q -Dtest=OpenTelemetryMapperTest test
  • mvn -q -Dtest='OpenTelemetryResourceTest$ApiKey#testOpenInferenceSpanNormalization' test
  • npm run test -- src/lib/traces.test.ts src/shared/PrettyLLMMessage/llmMessages/providers/openai/detector.test.ts src/shared/PrettyLLMMessage/llmMessages/providers/openai/mapper.test.ts src/shared/PrettyLLMMessage/llmMessages/providers/langchain/detector.test.ts src/shared/PrettyLLMMessage/llmMessages/providers/langchain/mapper.test.ts src/shared/PrettyLLMMessage/llmMessages/providers/openinference/detector.test.ts src/shared/PrettyLLMMessage/llmMessages/providers/openinference/mapper.test.ts --run (148 tests)
  • npm run typecheck
  • npm run lint
  • mvn -q spotless:check
  • make precommit
  • ./opik.sh --build
  • ./opik.sh --verify
  • temporary Agno app exported a live OpenInference trace through OTLP/HTTP and executed a deterministic function tool
  • strict API verifier checked one AGENT span, one TOOL span, and two LLM spans in one trace, including parsed tool schema, string-preserved JSON arguments, matching tool_call_id, tool result, model/provider fields, and per-call token usage
  • manual container UI check confirmed that Messages opens by default, renders the tool call and tool result as separate blocks, and does not duplicate the raw output

Validated JSON/text/native raw values, invalid JSON and malformed attributes, sparse and shuffled indices, input/output separation, chat/completion/tool schemas and calls, tool results, ordered text/image/audio/reasoning/tool-use content, field priority, mixed marked/unmarked batches, canonical and historical frontend shapes, false-positive detection, preview extraction, and raw-output deduplication.

Documentation

No documentation update is required. This is transparent ingestion and UI compatibility with no public API, schema, configuration, or migration change.

@github-actions github-actions Bot added java Pull requests that update Java code Frontend Backend tests Including test files, or tests related like configuration. typescript *.ts *.tsx 🔴 size/XL labels Sep 3, 2026
@d8rt8v
d8rt8v force-pushed the d8rt8v/issue-7686-openinference-pretty-formatting branch from df8eec5 to 82398bd Compare September 3, 2026 21:03
Comment on lines +117 to +122
const url = content.image?.url;
if (url) {
blocks.push({
blockType: "image",
component: PrettyLLMMessage.ImageBlock,
props: { images: [{ url, name: mediaName(url, "Image", index) }] },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untrusted media URL enables unsafe navigation

The OpenInference mapper copies unvalidated content.image.url and audio URLs into shared descriptors, so AttachmentThumbnail uses them as <a href> values and the audio component as media sources, allowing javascript:, data:text/html, or custom-scheme URLs to reach unsafe browser destinations — should we validate schemes before building the descriptors or enforce the policy at the shared destination boundary?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-frontend/src/shared/PrettyLLMMessage/llmMessages/providers/openinference/mapper.ts
around lines 117-134, update the `contentBlocks` image/audio handling so untrusted
`content.image.url` and `content.audio.url` values are not passed directly into
`ImageBlock` or `AudioPlayerBlock`. Validate URLs with the existing media safety policy
or a shared helper that permits only approved passive schemes (such as `http`/`https`
and explicitly supported placeholders), and skip or safely replace rejected URLs before
constructing the descriptors.

Comment on lines +497 to +500
JsonNode parsed = JsonUtils.getJsonNodeFromString(value.getStringValue());
if (!parsed.isObject()) {
metadata.set("metadata", parsed);
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty metadata aborts OTLP ingestion

The OpenInference metadata branch passes empty or whitespace-only JSON to mergeMetadata, where readTree returns null and parsed.isObject() is called unconditionally, so toOpikSpan throws during OTLP normalization and aborts the batch — should we guard the no-document result before checking its shape or use the fallback parser?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-backend/src/main/java/com/comet/opik/domain/mapping/otel/OpenInferenceSpanNormalizer.java`
around lines 491-510, fix the `State.mergeMetadata` method so empty or whitespace-only
metadata strings do not cause a null dereference at `parsed.isObject()`. Guard a null
parse result (or switch to the fallback parser) and preserve the original metadata
string or an explicit null value in the metadata object, ensuring marked spans continue
ingesting without aborting the OTLP batch.

Comment on lines +31 to +34
if (
inputDetection.format === "openinference" ||
outputDetection.format === "openinference"
) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixed-format output is silently dropped

A mixed pair with a legacy OpenInference side is routed to combineOpenInferenceMessages, whose item.text/item["completion.text"] parsing ignores OpenAI choices[].message, so the assistant output is dropped — should we restrict the combiner to compatible pairs or retain the sibling mapper result?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-frontend/src/shared/PrettyLLMMessage/llmMessages/mapAndCombineMessages.ts`
around lines 31-38, update `mapAndCombineMessages` so the OpenInference combiner is not
used merely because one side detected as OpenInference. For mixed pairs such as legacy
OpenInference input and OpenAI `choices[].message` output, preserve the sibling mapper
result by restricting the combiner to compatible inputs or combining its result with the
independently mapped result.

Comment on lines +640 to +644
let processedMessage = extractOpenInferencePrettyText(message, config.type);

if (!isString(processedMessage)) {
processedMessage = prettifyOpenAIMessageLogic(message, config);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unmarked messages bypass provider formatting

extractOpenInferencePrettyText runs before provider-specific formatters and accepts any role-bearing messages without an OpenInference marker, so unmarked inputs are rendered as OpenInference text instead of their existing representation — should we gate it on the marker/legacy attributes or run provider-specific formatting first at apps/opik-frontend/src/lib/traces.ts:640-644?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-frontend/src/lib/traces.ts around lines 640-644, update the `prettifyMessage`
formatter chain so `extractOpenInferencePrettyText` does not process arbitrary
role-bearing `messages` arrays before provider-specific logic. Gate it on an explicit
OpenInference marker or legacy attribute, or run the existing OpenAI/provider formatters
first and use OpenInference extraction only as an appropriate fallback. Preserve the
current behavior for unmarked system, assistant, developer, and tool messages.

Comment on lines +68 to +72
const DISPLAYABLE_LEGACY_KEYS = new Set([
OPENINFERENCE_SPAN_KIND,
"llm.finish_reason",
"llm.function_call",
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metadata-only records show empty tab

detectOpenInferenceFormat treats openinference.span.kind, llm.finish_reason, and malformed indexed prefixes as renderable data, so TraceDataViewer exposes an empty Messages tab when mapParsed produces no messages — should we base support on parsed renderable messages, prompts, tools, choices, or function calls while keeping metadata in Details?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-frontend/src/lib/openinference.ts around lines 68-72 and 483-490, refactor the
OpenInference support predicates so `openinference.span.kind`, `llm.finish_reason`, and
malformed indexed prefixes do not by themselves mark a payload as renderable. Base
Messages-tab support on successfully parsed messages, prompts, tools, choices, or
function calls that the mapper can display, while retaining marker and finish-reason
metadata for Details. Add or update visibility tests covering marker-only, finish-only,
and malformed-index-only records so detection and rendering stay consistent.

Comment on lines +186 to +189
// OpenInference semantic attributes have already been normalized as one coherent shape.
// Skipping them here prevents generic prefix rules from processing the same key again.
if (openInference != null && openInference.consumes(key)) {
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaks existing online scoring paths

Marked spans now route semantic llm.output_messages.* keys to span.output.messages instead of span.input, so OnlineScoringEngine mappings such as input.llm.output_messages... silently produce no replacement for newly ingested spans — should we retain a compatibility alias or explicitly migrate/document this released consumer contract?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/main/java/com/comet/opik/domain/OpenTelemetryMapper.java around
lines 186-189, update enrichSpanWithAttributes so consumed OpenInference
llm.output_messages.* attributes remain available under the legacy span.input path used
by OnlineScoringEngine mappings, while preserving their canonical span.output.messages
representation. Add a compatibility alias during OpenInference input/output composition
(or an equivalent explicit migration path) and add regression coverage proving
input.llm.output_messages mappings still resolve for newly ingested marked spans.

@d8rt8v

d8rt8v commented Sep 3, 2026

Copy link
Copy Markdown
Author

Here are some examples

image image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend Frontend java Pull requests that update Java code 🔴 size/XL tests Including test files, or tests related like configuration. typescript *.ts *.tsx

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR]: Add Pretty ✨ formatting support for OpenInference spans

1 participant