Skip to content

fix(openai): fallback to json_object when json_schema structured output fails#914

Open
hsy5571615 wants to merge 1 commit into
plastic-labs:mainfrom
hsy5571615:fix/openai-compatible-structured-output-fallback
Open

fix(openai): fallback to json_object when json_schema structured output fails#914
hsy5571615 wants to merge 1 commit into
plastic-labs:mainfrom
hsy5571615:fix/openai-compatible-structured-output-fallback

Conversation

@hsy5571615

@hsy5571615 hsy5571615 commented Jul 18, 2026

Copy link
Copy Markdown

OpenAI-compatible providers (e.g. Kimi, MiMo) often accept chat.completions.parse() / json_schema but return empty or unparseable content.\n\nThis change retries the request with structured_output_mode=json_object when parse() produces no parseable content. json_object injects the schema into the prompt and lets json_repair recover loose provider JSON.\n\nIf json_object mode also fails, the original exception is re-raised so the retry/fallback chain can still engage.\n\n- Added fallback logic in OpenAIBackend.complete()\n- Added tests for empty content, repairable malformed JSON, and unrepairable content\n\nRelates to: issues where providers return key-must-be-a-string or empty JSON from structured outputs.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of incomplete or invalid structured responses from OpenAI.
    • Automatically retries using JSON mode when structured output cannot be parsed.
    • Supports recovery from malformed but repairable JSON without an additional request.
    • Preserves the original error when fallback attempts also fail.

…ut fails\n\nOpenAI-compatible providers (e.g. Kimi, MiMo) often accept\nchat.completions.parse() / json_schema but return empty or unparseable\ncontent. Instead of immediately raising or returning empty, retry the\nrequest with structured_output_mode=json_object, which injects the schema\ninto the prompt and lets json_repair recover loose provider JSON.\n\nIf json_object mode also fails, the original exception is re-raised so\nthe retry/fallback chain can still engage as before.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

OpenAI structured-output handling now retries with json_object mode when parse() returns unpopulated content that fails validation or repair. Tests cover empty, repairable, and unrepairable response paths.

Changes

OpenAI structured-output fallback

Layer / File(s) Summary
Structured-output retry implementation
src/llm/backends/openai.py
Guards structured parsing, retries failed validation or repair through json_object mode, and re-raises the original error if the retry fails.
Fallback behavior tests
tests/llm/test_backends/test_openai.py
Covers empty, repairable, and unrepairable parsed-none responses, including fallback call counts and returned structured values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: vvoruganti

Poem

A rabbit found JSON, malformed and bare,
And sent it through fallback with careful repair.
Parse hopped once, create hopped twice,
Structured answers came out nice.
“No content?” the rabbit says—“Retry!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: falling back to json_object when json_schema structured output fails.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/llm/backends/openai.py`:
- Around line 271-274: Update the exception re-raise in the fallback handler to
use explicit exception chaining suppression, changing the raise of
structured_exc to raise it from None. Preserve the existing exception types and
retry/fallback behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 22225f30-bb52-4000-a668-5681727155d1

📥 Commits

Reviewing files that changed from the base of the PR and between 055d73b and e457e79.

📒 Files selected for processing (2)
  • src/llm/backends/openai.py
  • tests/llm/test_backends/test_openai.py

Comment on lines +271 to +274
except (ValidationException, StructuredOutputError, ValidationError):
# json_object mode also failed; raise the original exception
# so the retry/fallback chain can engage as before.
raise structured_exc

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/llm/backends/openai.py"

echo "== Outline =="
ast-grep outline "$file" --view expanded || true

echo
echo "== Relevant lines =="
sed -n '230,285p' "$file" | cat -n

echo
echo "== Search for similar raise patterns =="
rg -n "raise structured_exc|from None|B904|json_object" "$file" src/llm -g '!**/__pycache__/**' || true

Repository: plastic-labs/honcho

Length of output: 10361


Suppress the fallback exception context

raise structured_exc keeps the JSON-object parsing error in the traceback. Use raise structured_exc from None so the original failure stays cleanly preserved and Ruff B904 is avoided.

Proposed fix
-                    raise structured_exc
+                    raise structured_exc from None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except (ValidationException, StructuredOutputError, ValidationError):
# json_object mode also failed; raise the original exception
# so the retry/fallback chain can engage as before.
raise structured_exc
except (ValidationException, StructuredOutputError, ValidationError):
# json_object mode also failed; raise the original exception
# so the retry/fallback chain can engage as before.
raise structured_exc from None
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 274-274: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/llm/backends/openai.py` around lines 271 - 274, Update the exception
re-raise in the fallback handler to use explicit exception chaining suppression,
changing the raise of structured_exc to raise it from None. Preserve the
existing exception types and retry/fallback behavior.

Source: Linters/SAST tools

@karaokedjodua

Copy link
Copy Markdown

Finn-loop review of e457e79

CI: no required checks configured — escalate to human
Mergeability: BLOCKED (stale branch protection) / MERGEABLE (structurally clean)

Review

Summary: Adds a fallback from json_schema to json_object mode when OpenAI-compatible providers (Kimi, MiMo) return unparseable structured output. Well-tested with 3 new test cases covering empty, malformed-repairable, and unrepairable content.

1. Must fix before merge

[DEFECT] Missing method dependency — _apply_json_object_mode undefined on upstream
The PR calls self._apply_json_object_mode(json_object_params, response_format) at the new line 260 in complete(), but this method only exists on the fork's branch (hsy5571615/honcho), not on plastic-labs/honcho main. Merging as-is will produce an AttributeError at runtime:

AttributeError: 'OpenAIBackend' object has no attribute '_apply_json_object_mode'

The PR must either:

  • Include the definitions for _apply_json_object_mode, _with_json_schema_instructions, _json_object_instruction, _structured_output_mode, _json_schema_response_format, and _convert_tool_choice (all present on the fork's main but absent from upstream), or
  • Be rebased on top of (or merged after) a separate PR that introduces these methods upstream.

[CI] No required CI configured + stale branch protection
mergeStateStatus: BLOCKED with mergeable: MERGEABLE and no required checks reported — this is the stale branch-protection pattern. An admin must update or remove the stale branch-protection rule. The only running check (CodeRabbit) passes.

2. Should fix soon

Dependency on unreviewed fork infrastructure
The fork's main branch carries significant unreviewed changes (_convert_tool_choice, apply_sdk_passthroughs, thinking_budget_tokens support, validate-then-repair pattern, tool-loop json_schema handling) that the PR depends on. A reviewer should verify that all of these are intentional and correct before merge.

No linked issue
The PR body doesn't contain Closes KAR-NNN. Consider linking a Paperclip issue for traceability.

3. Safe to merge

No — the missing method dependency is a hard blocker. Code quality of the actual change is good: clean error handling, clear test coverage, and sensible re-raise of the original exception on double-failure.


⚠️ Labels to apply: loop-changes-requested, needs-human-review
(Label operations skipped — Finn lacks admin rights on this repo)

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.

3 participants