Skip to content

fix(llm): fall back when a provider rejects json_schema response_format#6191

Open
HumphreySun98 wants to merge 3 commits into
crewAIInc:mainfrom
HumphreySun98:fix/openai-compatible-structured-output-fallback
Open

fix(llm): fall back when a provider rejects json_schema response_format#6191
HumphreySun98 wants to merge 3 commits into
crewAIInc:mainfrom
HumphreySun98:fix/openai-compatible-structured-output-fallback

Conversation

@HumphreySun98

@HumphreySun98 HumphreySun98 commented Jun 16, 2026

Copy link
Copy Markdown

Problem

DeepSeek and some other OpenAI-compatible endpoints reject OpenAI's json_schema structured-output response_format:

Error code: 400 - {'error': {'message': 'This response_format type is unavailable now', ...}}

The native OpenAI provider (OpenAICompatibleCompletion extends OpenAICompletion) unconditionally sent it via beta.chat.completions.parse / .stream and via a Pydantic response_format, so any crew using structured output against those endpoints failed.

Fix

Add a capability gate:

  • OpenAICompletion.supports_native_structured_output()True.
  • A per-provider supports_json_schema flag on the OpenAI-compatible ProviderConfig (DeepSeek = False); the subclass override reads it.

When unsupported, the three json_schema-sending branches are skipped and the call falls back to a plain completion. The non-streaming path then validates the result against the requested model client-side, and the Task converter reconciles it otherwise. OpenAI behavior is unchanged. (The async-streaming path already used a plain completion + client-side validation.)

Tests

Added TestStructuredOutputFallback in test_openai_compatible.py: capability flags, json_schema suppression in _prepare_completion_params, and a mock asserting DeepSeek skips the native parse and still returns the validated model. Verified failing-without/passing-with the fix; 137 OpenAI/compatible provider tests still pass; ruff and mypy clean.

Fixes #5990


This PR was authored with Claude Code. Per CONTRIBUTING.md, AI-generated contributions require the llm-generated label — I don't have triage permission to set it, so could a maintainer please add it? 🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Improved structured-output handling for OpenAI and OpenAI-compatible providers, with native JSON-schema usage now enabled only when supported by the endpoint/provider.
    • Added provider capability detection and fallback to client-side validation when native structured output isn’t available (or is rejected).
    • Introduced configurable provider support for JSON-schema structured output, with DeepSeek configured to disable it.
  • Tests

    • Added structured-output fallback tests covering both non-streaming and streaming completions across OpenAI, OpenRouter, and DeepSeek.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ecdfa66-083a-4070-a5f2-5f26888ffa08

📥 Commits

Reviewing files that changed from the base of the PR and between 3a61232 and fbda756.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/llms/providers/openai/completion.py
  • lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py
  • lib/crewai/src/crewai/llms/providers/openai/completion.py

📝 Walkthrough

Walkthrough

The change adds provider capability checks for native JSON-schema structured output, disables that path for DeepSeek, and updates sync, async, and streaming completion handling to fall back to client-side validation. Tests cover provider flags and fallback behavior.

Changes

Structured output fallback for OpenAI-compatible providers

Layer / File(s) Summary
Capability flags and provider wiring
lib/crewai/src/crewai/llms/providers/openai/completion.py, lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py, lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py
OpenAICompletion adds supports_native_structured_output(), ProviderConfig adds supports_json_schema, OpenAI-compatible providers map that capability, and registry tests assert the provider values.
Completion fallback execution and validation
lib/crewai/src/crewai/llms/providers/openai/completion.py, lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py
Parameter preparation and sync, async, and streaming parsing branches now require native structured-output support before using parse/json_schema paths, and fallback validation uses response_model or self.response_format; tests verify DeepSeek omits native response formatting and still returns validated structured output.
🚥 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 behavioral fix: falling back when a provider rejects json_schema structured output.
Linked Issues check ✅ Passed The changes gate native structured output, disable it for DeepSeek, and fall back to client-side validation as requested.
Out of Scope Changes check ✅ Passed The added provider capability flag, fallback logic, and tests all support the DeepSeek structured-output fix.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

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

✨ 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

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py (1)

321-386: ⚡ Quick win

Add a DeepSeek streaming fallback regression test (stream=True + response_model).

Current coverage validates the non-streaming fallback well, but it does not exercise the sync streaming fallback path. A targeted test here would lock in the intended contract and catch the current mismatch early.

🤖 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 `@lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py` around
lines 321 - 386, Add a new test method to the TestStructuredOutputFallback class
that validates DeepSeek's streaming fallback behavior when using response_model.
Create a test similar to
test_deepseek_completion_skips_native_parse_and_validates_client_side but pass
stream=True in the _handle_completion parameters to exercise the sync streaming
fallback path. Mock the streaming response appropriately (using an iterator or
stream chunks) and verify that the client.chat.completions.create is called with
stream=True, that client.beta.chat.completions.parse is not called, and that the
final result is still a validated _Answer instance.
🤖 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 `@lib/crewai/src/crewai/llms/providers/openai/completion.py`:
- Line 1914: The sync streaming fallback path for unsupported providers drops
structured parsing validation, causing stream=True with response_model to return
raw str instead of BaseModel. At
lib/crewai/src/crewai/llms/providers/openai/completion.py line 1914, the native
streaming parse is correctly gated by the response_model check, which is the
anchor pattern. The issue is in the sibling fallback path at lines 2039-2054
where _finalize_streaming_response(...) is called without validating
response_model. Add response_model parsing validation in the
unsupported-provider path (2039-2054) to ensure that when response_model is
provided with stream=True on DeepSeek-like providers, the result is properly
parsed into a BaseModel instance, matching the behavior of non-streaming and
async-streaming fallbacks.

---

Nitpick comments:
In `@lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py`:
- Around line 321-386: Add a new test method to the TestStructuredOutputFallback
class that validates DeepSeek's streaming fallback behavior when using
response_model. Create a test similar to
test_deepseek_completion_skips_native_parse_and_validates_client_side but pass
stream=True in the _handle_completion parameters to exercise the sync streaming
fallback path. Mock the streaming response appropriately (using an iterator or
stream chunks) and verify that the client.chat.completions.create is called with
stream=True, that client.beta.chat.completions.parse is not called, and that the
final result is still a validated _Answer instance.
🪄 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 Plus

Run ID: 92196d58-34cc-4d5b-b435-b3b948a52c64

📥 Commits

Reviewing files that changed from the base of the PR and between e9d568d and aaf93c2.

📒 Files selected for processing (3)
  • lib/crewai/src/crewai/llms/providers/openai/completion.py
  • lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py
  • lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py

Comment thread lib/crewai/src/crewai/llms/providers/openai/completion.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/crewai/src/crewai/llms/providers/openai/completion.py (1)

1572-1582: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate dict-based json_schema formats too.

Line 1581 still forwards any dict response_format, so DeepSeek-like providers will still 400 if callers pass {"type": "json_schema", ...} directly instead of a Pydantic model. Preserve non-native formats like json_object, but suppress json_schema when supports_native_structured_output() is false.

Proposed fix
             elif isinstance(self.response_format, dict):
-                params["response_format"] = self.response_format
+                if (
+                    self.response_format.get("type") != "json_schema"
+                    or self.supports_native_structured_output()
+                ):
+                    params["response_format"] = self.response_format
🤖 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 `@lib/crewai/src/crewai/llms/providers/openai/completion.py` around lines 1572
- 1582, The dict-based response_format handling in completion.py still forwards
native json_schema payloads even when the provider does not support structured
output. Update the response_format branch in the completion path (around
self.response_format / supports_native_structured_output) so dict inputs with
type json_schema are only passed through when
supports_native_structured_output() is true, while still allowing non-native
formats like json_object. Keep the existing BaseModel path using
generate_model_description and preserve other dict formats unchanged.
🤖 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 `@lib/crewai/src/crewai/llms/providers/openai/completion.py`:
- Around line 1808-1809: The sync streaming fallback in the OpenAI completion
flow is ignoring the configured response format, so
`_finalize_streaming_response` only sees `response_model` and skips
validation/parsing when `self.response_format` is set. Update the streaming path
in the completion logic to pass or resolve `response_model or
self.response_format` consistently, matching the non-streaming behavior and the
related sync/async finalize calls, so `_Answer`-style configured formats are
parsed instead of returning raw JSON text.

---

Outside diff comments:
In `@lib/crewai/src/crewai/llms/providers/openai/completion.py`:
- Around line 1572-1582: The dict-based response_format handling in
completion.py still forwards native json_schema payloads even when the provider
does not support structured output. Update the response_format branch in the
completion path (around self.response_format /
supports_native_structured_output) so dict inputs with type json_schema are only
passed through when supports_native_structured_output() is true, while still
allowing non-native formats like json_object. Keep the existing BaseModel path
using generate_model_description and preserve other dict formats unchanged.
🪄 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 Plus

Run ID: 3518cfdb-3364-42ac-a581-379a36c739aa

📥 Commits

Reviewing files that changed from the base of the PR and between aaf93c2 and 3a61232.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/llms/providers/openai/completion.py
  • lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py

Comment thread lib/crewai/src/crewai/llms/providers/openai/completion.py
HumphreySun98 and others added 3 commits June 24, 2026 15:15
…hat reject it

DeepSeek and some other OpenAI-compatible endpoints reject OpenAI's
json_schema structured-output response_format with "This response_format
type is unavailable now". The native OpenAI provider unconditionally sent it
via beta.chat.completions.parse/stream and via a Pydantic response_format,
breaking any crew using structured output against those endpoints.

Add a supports_native_structured_output() capability (True for OpenAI) and a
per-provider supports_json_schema flag on the OpenAI-compatible config
(DeepSeek = False). When unsupported, skip the native json_schema paths and
fall back to a plain completion; the non-streaming path then validates the
result against the requested model client-side, and the Task converter
reconciles it otherwise. OpenAI behavior is unchanged.

Fixes crewAIInc#5990

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When native json_schema streaming is skipped for a provider that rejects it
(e.g. DeepSeek), the sync streaming path fell through to a plain stream and
returned the raw text, while the async streaming and non-streaming paths
returned the parsed model. Validate the accumulated text against
response_model in _finalize_streaming_response so all paths behave
consistently. Adds a sync-streaming fallback regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The streaming fallback validated only against a per-call response_model, so
stream=True with a configured self.response_format on a provider that rejects
native json_schema returned raw text instead of the parsed model. Validate
against `response_model or self.response_format`, matching the non-streaming
fallback. Adds a regression test for the configured-response_format case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@HumphreySun98 HumphreySun98 force-pushed the fix/openai-compatible-structured-output-fallback branch from fbda756 to cb13afe Compare June 24, 2026 20:15
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.

[BUG] OpenAI API call fails with "response_format type is unavailable now" when using Deepseek

1 participant