Skip to content

Conversation

@5i2urom
Copy link
Contributor

@5i2urom 5i2urom commented Jan 17, 2026

Proposed change

Improves OpenAI tool output rendering when a tool returns a JSON-like result object (e.g. { error: string }, { stdout, stderr, exitCode }).

Instead of embedding a JSON string inside the output: section (double-wrapping), the OpenAI provider now prefers a human-readable multi-line rendering that:

  • preserves real newlines;
  • avoids unreadable blobs like
    output:\n{"error":"..."}.

Implementation details

  • Extends buildToolResponsePayload() with an optional humanize JSON mode for OpenAI tool outputs.
  • Adds humanizeJsonForDisplay() to detect common result shapes and render them as readable multi-line text:
    • prefers text fields like error, message, etc.
    • renders exitCode, stdout, stderr as stable multi-line blocks
    • falls back to pretty-printed JSON for other objects
  • Updates OpenAI tool output formatting paths to enable this mode:
    • OpenAIProvider.formatToolMessage()
    • Responses API buildResponsesRequest() for function_call_output
  • Adds tests to ensure:
    • { error: "multiline..." } does not become output:\n{"error": ...}
    • stdout / stderr / exitCode are rendered as multi-line blocks with stable spacing
  • Updates documentation note in docs/tool-output-format.md clarifying preference for human-readable rendering for structured results

Notes

  • Scope is intentionally limited to OpenAI tool output formatting; other providers keep existing behavior.
  • Tool outputs remain plain multi-line text; this change only affects how non-string results are converted into that text to avoid reintroducing escaping.
  • Preserves real newlines for error text, logs, and diffs.

Related issue

Fixes #1171 — OpenAI tool outputs were still getting JSON-wrapped when the tool result was an object,
reintroducing over-escaping and reducing readability even after #1002.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

Summary by CodeRabbit

Release Notes

  • Documentation

    • Added guidance on structured tool output handling and OpenAI provider's preference for human-readable multi-line formatting to preserve newlines and avoid unnecessary JSON wrappers.
  • Improvements

    • OpenAI provider now displays tool outputs in human-friendly multi-line format instead of JSON-wrapped results.
    • Fixed double-wrapping of tool errors in responses for cleaner error message display.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

This PR adds a "humanizeJson" feature to prevent double-wrapping JSON tool outputs when displaying structured results. A new humanizeJsonForDisplay helper renders JSON-like objects as human-readable multi-line text. The flag is threaded through buildToolResponsePayload and callers in OpenAI providers to format tool responses without extra JSON wrapping. Documentation and tests are updated to reflect this behavior.

Changes

Cohort / File(s) Summary
Documentation
docs/tool-output-format.md
Added note that tools can return structured results and OpenAI providers prefer human-readable multi-line rendering to avoid JSON wrapper and preserve newlines.
Core humanizeJson implementation
packages/core/src/providers/utils/toolResponsePayload.ts
Added humanizeJsonForDisplay() helper function to render JSON objects as multi-line text. Extended coerceToString(), formatToolResult(), and buildToolResponsePayload() signatures with optional humanizeJson boolean parameter to enable human-friendly formatting of both result content and errors.
OpenAI provider integration
packages/core/src/providers/openai/OpenAIProvider.ts, packages/core/src/providers/openai/buildResponsesRequest.ts
Updated calls to buildToolResponsePayload() to pass true for the new humanizeJson parameter, enabling human-readable output formatting for OpenAI tool responses.
Test coverage
packages/core/src/providers/utils/toolResponsePayload.test.ts, packages/core/src/providers/openai/buildResponsesRequest.stripToolCalls.test.ts
Added test suite for humanizeJsonForDisplay() verifying multi-line string conversion of structured results. Added test case verifying tool errors are not double-wrapped as JSON objects.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

maintainer:e2e:ok

Poem

🐰 A JSON too wrapped, a newline too lost,
'Til humanize came and fixed the cost,
Now tool results sing in multi-line grace,
No double-wrapping, just clean readable space! ✨

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: preventing double-wrapping of error objects in OpenAI tool output formatting.
Description check ✅ Passed The PR description provides comprehensive coverage including TLDR, implementation details, testing approach, and related issue reference, though the testing matrix and detailed reviewer test plan sections are not fully completed.
Linked Issues check ✅ Passed Changes fully satisfy issue #1171 objectives: humanizeJsonForDisplay() detects common result shapes and renders them as readable multi-line text, avoiding double-wrapping for {error}, {stdout/stderr/exitCode} objects while preserving newlines.
Out of Scope Changes check ✅ Passed All changes directly address the double-wrapping issue: documentation update, new humanization utility, extended buildToolResponsePayload signature, and test coverage. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a28d09b and df795c9.

📒 Files selected for processing (6)
  • docs/tool-output-format.md
  • packages/core/src/providers/openai/OpenAIProvider.ts
  • packages/core/src/providers/openai/buildResponsesRequest.stripToolCalls.test.ts
  • packages/core/src/providers/openai/buildResponsesRequest.ts
  • packages/core/src/providers/utils/toolResponsePayload.test.ts
  • packages/core/src/providers/utils/toolResponsePayload.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: acoliver
Repo: vybestack/llxprt-code PR: 0
File: :0-0
Timestamp: 2026-01-13T19:28:00.789Z
Learning: In the llxprt-code codebase (`packages/core/src/providers/anthropic/AnthropicProvider.ts`), Anthropic's API returns `contentBlock.input` as an already-parsed JavaScript object, not a JSON string. The code was incorrectly calling `JSON.stringify(contentBlock.input)` before passing it to `processToolParameters()`, which was designed for OpenAI-style string parameters. This causes arrays and other complex types to be corrupted into strings (e.g., `paths` array becomes a string `"[\"**/*.toml\"]"` instead of actual array). The fix is to use `contentBlock.input` directly without stringifying for Anthropic provider.
Learnt from: e2720pjk
Repo: vybestack/llxprt-code PR: 583
File: packages/core/src/providers/openai/OpenAIProvider.ts:935-959
Timestamp: 2025-11-16T22:51:26.374Z
Learning: In the llxprt-code codebase (packages/core/src/providers/openai/OpenAIProvider.ts), tools like `run_shell_command` use internal streaming only for real-time UI updates during execution, but each tool execution produces exactly ONE final `ToolResponseBlock` containing the full result. The streaming chunks are never sent to the LLM and are not converted into multiple tool messages. The OpenAI Chat Completions API requires that each tool call (tool_call_id) corresponds to exactly one message with role 'tool', so duplicate tool response detection that removes subsequent tool messages with the same tool_call_id is correct and necessary for API compliance.
📚 Learning: 2025-11-16T22:51:26.374Z
Learnt from: e2720pjk
Repo: vybestack/llxprt-code PR: 583
File: packages/core/src/providers/openai/OpenAIProvider.ts:935-959
Timestamp: 2025-11-16T22:51:26.374Z
Learning: In the llxprt-code codebase (packages/core/src/providers/openai/OpenAIProvider.ts), tools like `run_shell_command` use internal streaming only for real-time UI updates during execution, but each tool execution produces exactly ONE final `ToolResponseBlock` containing the full result. The streaming chunks are never sent to the LLM and are not converted into multiple tool messages. The OpenAI Chat Completions API requires that each tool call (tool_call_id) corresponds to exactly one message with role 'tool', so duplicate tool response detection that removes subsequent tool messages with the same tool_call_id is correct and necessary for API compliance.

Applied to files:

  • packages/core/src/providers/openai/buildResponsesRequest.stripToolCalls.test.ts
  • docs/tool-output-format.md
  • packages/core/src/providers/openai/buildResponsesRequest.ts
  • packages/core/src/providers/utils/toolResponsePayload.test.ts
  • packages/core/src/providers/utils/toolResponsePayload.ts
  • packages/core/src/providers/openai/OpenAIProvider.ts
📚 Learning: 2026-01-13T19:28:00.789Z
Learnt from: acoliver
Repo: vybestack/llxprt-code PR: 0
File: :0-0
Timestamp: 2026-01-13T19:28:00.789Z
Learning: In the llxprt-code codebase (`packages/core/src/providers/anthropic/AnthropicProvider.ts`), Anthropic's API returns `contentBlock.input` as an already-parsed JavaScript object, not a JSON string. The code was incorrectly calling `JSON.stringify(contentBlock.input)` before passing it to `processToolParameters()`, which was designed for OpenAI-style string parameters. This causes arrays and other complex types to be corrupted into strings (e.g., `paths` array becomes a string `"[\"**/*.toml\"]"` instead of actual array). The fix is to use `contentBlock.input` directly without stringifying for Anthropic provider.

Applied to files:

  • packages/core/src/providers/openai/buildResponsesRequest.stripToolCalls.test.ts
  • docs/tool-output-format.md
  • packages/core/src/providers/openai/buildResponsesRequest.ts
  • packages/core/src/providers/utils/toolResponsePayload.test.ts
  • packages/core/src/providers/utils/toolResponsePayload.ts
  • packages/core/src/providers/openai/OpenAIProvider.ts
🧬 Code graph analysis (3)
packages/core/src/providers/openai/buildResponsesRequest.stripToolCalls.test.ts (2)
packages/core/src/services/history/IContent.ts (1)
  • IContent (21-40)
packages/core/src/providers/openai/buildResponsesRequest.ts (1)
  • buildResponsesRequest (108-397)
packages/core/src/providers/openai/buildResponsesRequest.ts (1)
packages/core/src/providers/utils/toolResponsePayload.ts (1)
  • buildToolResponsePayload (233-264)
packages/core/src/providers/openai/OpenAIProvider.ts (1)
packages/core/src/providers/utils/toolResponsePayload.ts (1)
  • buildToolResponsePayload (233-264)
🔇 Additional comments (9)
docs/tool-output-format.md (1)

30-30: Docs clearly capture the new OpenAI tool-output preference.
Line 30 aligns the spec with the new human-readable rendering path.

packages/core/src/providers/utils/toolResponsePayload.test.ts (1)

116-141: Nice coverage for humanized stdout/stderr rendering.
The new test at Lines 116-141 locks in the multi-line format and spacing.

packages/core/src/providers/utils/toolResponsePayload.ts (4)

53-115: Humanize helper is clear and targeted.
The preference order and stdout/stderr/exitCode rendering read well and match the intended behavior.


117-137: Humanize-aware coercion is a solid fallback chain.
The pretty-JSON fallback preserves readability without losing the legacy stringify path.


161-186: Humanize flag propagation through formatToolResult looks good.
This keeps the result formatting consistent with the new display mode.


233-261: Payload construction now respects humanized output and error formatting.
The wiring in Lines 233-261 cleanly threads the new option.

packages/core/src/providers/openai/buildResponsesRequest.ts (1)

246-250: Correctly opts OpenAI Responses output into humanized formatting.
The new argument ensures tool output stays readable and avoids JSON double-wrapping.

packages/core/src/providers/openai/OpenAIProvider.ts (1)

1120-1120: OpenAI tool-response content now uses the humanized payload path.
This is the correct call-site to enable the new formatting behavior.

packages/core/src/providers/openai/buildResponsesRequest.stripToolCalls.test.ts (1)

76-124: Great regression test for the double-wrap bug.
Lines 76-124 verify the error text is preserved as plain multi-line output.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@5i2urom 5i2urom marked this pull request as ready for review January 17, 2026 20:23
@5i2urom 5i2urom requested a review from acoliver as a code owner January 17, 2026 20:23
@github-actions
Copy link
Contributor

Issue Summary:: The user wants me to review PR #1172 which fixes OpenAI tool output double-wrapping for error objects. Let me analyze the PR:

The problem this PR solves:
When tool outputs are JSON-stringified repeatedly at different layers, the model receives heavily escaped strings instead of readable text/code. For objects like { error: "..." } or { stdout, stderr, exitCode }, results were being stringified and ended up double-wrapped inside the tool output text, e.g., output:\n{"error":"..."}.

The solution:

  1. Added humanizeJsonForDisplay() function that detects common result shapes and renders them as readable multi-line text
  2. Extended buildToolResponsePayload() with an optional "humanize JSON" mode for OpenAI tool outputs
  3. Updated OpenAI tool output formatting paths to enable this mode
  4. Added tests to verify the behavior

Let me look at the files changed and evaluate the implementation:

  1. docs/tool-output-format.md - Documentation update

  2. packages/core/src/providers/openai/OpenAIProvider.ts - Pass true to buildToolResponsePayload() for humanize mode

  3. packages/core/src/providers/openai/buildResponsesRequest.ts - Pass true to buildToolResponsePayload() for humanize mode

  4. packages/core/src/providers/openai/buildResponsesRequest.stripToolCalls.test.ts - Test added for error double-wrap

  5. packages/core/src/providers/utils/toolResponsePayload.ts - Core implementation of humanizeJsonForDisplay and coerceToString updates

  6. packages/core/src/providers/utils/toolResponsePayload.test.ts - Tests for humanizeJson functionality

I'll examine the test coverage for the new humanize JSON functionality. The tests validate key behaviors:

  1. Error messages render directly without JSON wrapping
  2. Standard output streams appear as clean, readable text blocks
  3. Exit codes get processed correctly
  4. Fallback to pretty-printed JSON for complex objects

The test suite ensures robust handling of different tool output scenarios, verifying the new humanize mode works as expected across various input types.

Critical issues reveal potential implementation weaknesses:

  1. Undefined value handling lacks defensive programming
  2. String concatenation might introduce unexpected formatting
  3. No validation prevents potential type inconsistencies

The review highlights two specific implementation concerns around null and empty value processing. While not overly complex, these edge cases require careful consideration to maintain predictable string conversion behavior.

Documentation updates, though minimal, demonstrate thoughtful attention to clarifying the tool output specification. The approach balances simplicity with precise technical guidance.

Comprehensive test coverage spans multiple functional paths, including error scenarios, stdout/stderr rendering, and message formatting. This systematic testing strategy suggests a robust implementation approach.

The implementation carefully preserves existing module behaviors while introducing targeted enhancements for OpenAI tool interactions. Conditional logic ensures backward compatibility with previous code paths.

Documentation appears comprehensive, addressing key implementation details for the new feature. The note clarifies nuanced tool result handling, explaining human-readable rendering for structured results.

The approach systematically mitigates potential double-wrapping issues, with explicit tests validating error and multi-line output processing. Each implementation step seems thoughtfully designed to maintain existing system integrity while introducing targeted improvements.

The test coverage and documentation strategy demonstrate a methodical approach to feature implementation, ensuring robust and clear communication of technical changes.

toolResponsePayload.ts: Let me analyze this PR #1172 which fixes OpenAI tool output double-wrapping for error objects.

The PR addresses issue #1171, which is about OpenAI tool outputs still getting JSON-wrapped when the tool result is an object (e.g., {error, stdout, stderr}). This reintroduces escaping and breaks real newlines.

Let me examine the changes:

    • Added humanizeJsonForDisplay() function and modified coerceToString(), formatToolResult(), and buildToolResponsePayload() to support humanizing JSON output
  1. OpenAIProvider.ts - Passes true for the new humanizeJson parameter to buildToolResponsePayload()

  2. buildResponsesRequest.ts - Also passes true for humanizeJson when building responses

  3. toolResponsePayload.test.ts - Added test for stdout/stderr/exitCode rendering

  4. buildResponsesRequest.stripToolCalls.test.ts - Added test for error object not being double-wrapped

  5. docs/tool-output-format.md - Added documentation note

Key observations:

  • The fix adds a new optional humanizeJson parameter to buildToolResponsePayload()
  • humanizeJsonForDisplay() prefers common text fields like error, message, etc.
  • For shell-like results (stdout/stderr/exitCode), it renders as multi-line blocks
  • Falls back to pretty-printed JSON if no common patterns detected

Let me check for any issues:

  1. No obvious bugs - The implementation looks correct
  2. Tests added - Yes, two test cases added for the new behavior
  3. Scope limited to OpenAI - Yes, only OpenAI provider paths enable this feature

The PR looks good. Let me finalize the review.

LLxprt PR Review – PR #1172

Issue Alignment

Side Effects

  • [OK] Scope is intentionally limited to OpenAI: only OpenAIProvider.ts and buildResponsesRequest.ts pass true for the new humanizeJson parameter.
  • No config changes or shared module modifications that would affect other providers.

Code Quality

  • Correct: humanizeJsonForDisplay() returns undefined for non-objects, falls back gracefully.
  • Error handling: coerceToString() catches JSON.stringify errors in the humanize fallback path.
  • Validation: Uses typeof value === 'object' guard before casting; checks v.trim() for text fields.
  • Maintainability: Well-structured - dedicated function for the new behavior, clear comments, consistent fallback logic.

Tests & Coverage

  • Coverage impact: increase – new tests added in two files.
  • toolResponsePayload.test.ts:113-140 – Tests stdout/stderr/exitCode multi-line rendering.
  • buildResponsesRequest.stripToolCalls.test.ts:76-126 – Tests {error: string} not becoming output:\n{"error":...}.
  • Tests are behavioral (asserting on output format), not implementation mocks.

Verdict

  • [OK] Ready – The implementation resolves the issue with targeted changes, proper fallback behavior, and adequate test coverage for the new functionality.
    Issue Alignment: Let me analyze this PR fix: prevent OpenAI tool output double-wrapping for error objects #1172 carefully. The user wants me to act as LLxprt code reviewer and provide a review following the specific format.

Let me analyze:

  1. : The PR addresses issue OpenAI tool outputs still get JSON-wrapped when tool result is an object (e.g. {error, stdout, stderr}) #1171 which states that OpenAI tool outputs are still getting JSON-wrapped when the tool result is an object (e.g., {error: "..."} or {stdout, stderr, exitCode}), reintroducing over-escaping.

  2. What the PR does:

    • Extends buildToolResponsePayload() with an optional humanize JSON mode for OpenAI tool outputs
    • Adds humanizeJsonForDisplay() to detect common result shapes and render them as readable multi-line text
    • Updates OpenAI tool output formatting paths to enable this mode
    • Adds tests to verify the behavior
  3. Key Changes:

    • toolResponsePayload.ts: Added humanizeJsonForDisplay() function that:
      • Checks for common text fields like error, error_text, message, llmContent, returnDisplay
      • Handles shell-like result shapes with stdout, stderr, exitCode
      • Falls back to pretty-printed JSON for other objects
    • Modified coerceToString(), formatToolResult(), and buildToolResponsePayload() to accept optional humanizeJson parameter
    • Updated OpenAIProvider.ts and buildResponsesRequest.ts to pass true for humanizeJson
  4. Tests:

    • Added test in buildResponsesRequest.stripToolCalls.test.ts for {error: string} case
    • Added humanizeJson test suite in toolResponsePayload.test.ts for stdout/stderr/exitCode rendering
  5. Side Effects:

    • Documentation update in docs/tool-output-format.md
    • Scope is intentionally limited to OpenAI providers
    • Fallback exists if humanization doesn't apply

I confirm the implementation precisely targets the core problem. The solution introduces a flexible mechanism for transforming complex tool output objects into more readable formats, specifically for OpenAI providers. By adding a configurable humanization mode, the PR addresses JSON-wrapping issues while maintaining backward compatibility and providing sensible defaults.

LLxprt PR Review – PR #1172

Issue Alignment

  • [OK] Fully addresses OpenAI tool outputs still get JSON-wrapped when tool result is an object (e.g. {error, stdout, stderr}) #1171: The PR explicitly fixes the issue where tool results as objects like {error: "..."} or {stdout, stderr, exitCode} were being JSON-stringified again inside the output: section, causing double-wrapping.
  • Evidence: humanizeJsonForDisplay() (line 55-117 in toolResponsePayload.ts) extracts text from error, message, llmContent, returnDisplay fields directly rather than JSON-stringifying the object. For shell results, it renders exitCode/stdout/stderr as multi-line blocks.
  • Key call sites updated: OpenAIProvider.ts:1120 and buildResponsesRequest.ts:246 now pass true for the new humanizeJson parameter.

Side Effects

  • Minimal and scoped: Changes are isolated to OpenAI provider tool output paths only (humanizeJson defaults to undefined/false elsewhere).
  • Backward compatible: Existing humanizeJson parameter is optional; other providers retain original behavior.
  • Performance: No significant impact—only additional type checks when humanizeJson=true, and fallbacks are deterministic.
  • Documentation updated: docs/tool-output-format.md now notes the preference for human-readable rendering for structured results.

Code Quality

  • Correctness: humanizeJsonForDisplay() safely handles edge cases (null/undefined, non-objects, empty strings). The fallback to pretty-printed JSON (via JSON.stringify(value, null, 2)) ensures unknown shapes still render readably.
  • Error handling: Empty catch blocks in stringify fallbacks are acceptable since they revert to original behavior.
  • Maintainability: Clear logic separation—text field preference → shell shape detection → pretty JSON fallback.
  • Naming: humanizeJsonForDisplay is appropriately descriptive; parameter humanizeJson is clear.

Tests & Coverage

  • Coverage impact: Increase – new test cases explicitly added for the new behavior.
  • Test quality: Tests are substantive, not "mock theater":
    • buildResponsesRequest.stripToolCalls.test.ts:76-119: Validates {error: string} renders without {"error": wrapper and preserves multiline content.
    • toolResponsePayload.test.ts:116-144: Verifies stdout/stderr/exitCode render with stable multi-line formatting and correct content preservation.
  • Gaps: None identified—key paths (error field, shell results) have dedicated tests; fallback to pretty JSON is implicitly covered by the existing tests hitting unknown object shapes.

Verdict

  • [OK] Ready: The implementation directly solves the issue with minimal scope, proper fallbacks, and adequate test coverage. Changes are well-isolated to OpenAI provider output formatting without affecting other providers or core logic.

@acoliver acoliver merged commit ab47948 into vybestack:main Jan 17, 2026
19 of 20 checks passed
@5i2urom 5i2urom deleted the fix/openai-tool-output-no-doublewrap branch January 18, 2026 04:33
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.

OpenAI tool outputs still get JSON-wrapped when tool result is an object (e.g. {error, stdout, stderr})

2 participants