Skip to content

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

Open
HumphreySun98 wants to merge 1 commit 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 1 commit 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
    • Enhanced structured output compatibility with OpenAI-compatible providers, including automatic fallback to client-side validation for providers with limited JSON schema support.

…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>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds provider capability checks for native JSON-schema structured output, disables that path for DeepSeek, and updates completion handling to fall back to client-side model validation in sync, async, and streaming flows, with tests covering 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, DeepSeek is marked unsupported for native json_schema output, and registry tests assert the new capability 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
Completion 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.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 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 accurately describes the main fix: adding a fallback mechanism when providers (like DeepSeek) reject the json_schema response_format.
Linked Issues check ✅ Passed The PR directly addresses issue #5990 by implementing the capability gating mechanism to handle providers that don't support json_schema, enabling DeepSeek and similar providers to work with structured output via client-side fallback.
Out of Scope Changes check ✅ Passed All changes are scoped to the json_schema fallback mechanism: OpenAICompletion now checks supports_native_structured_output(), ProviderConfig adds supports_json_schema flag for OpenAI-compatible providers, and tests validate the new fallback behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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 and usage tips.

@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

tool_calls: dict[int, dict[str, Any]] = {}

if response_model:
if response_model and self.supports_native_structured_output():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sync streaming fallback drops structured parsing when native json_schema is disabled.

Line 1914 correctly gates native streaming parse, but in the unsupported-provider path (Line 2039 onward) the code returns _finalize_streaming_response(...) without validating response_model. For stream=True + response_model on DeepSeek-like providers, this returns raw str instead of BaseModel, unlike your non-streaming and async-streaming fallbacks.

Suggested fix (parse fallback in sync streaming finalize path)
-    def _finalize_streaming_response(
+    def _finalize_streaming_response(
         self,
         full_response: str,
         tool_calls: dict[int, dict[str, Any]],
         usage_data: dict[str, Any] | None,
         params: dict[str, Any],
+        response_model: type[BaseModel] | None = None,
         available_functions: dict[str, Any] | None = None,
         from_task: Any | None = None,
         from_agent: Any | None = None,
         finish_reason: str | None = None,
         response_id: str | None = None,
-    ) -> str | list[dict[str, Any]]:
+    ) -> str | list[dict[str, Any]] | BaseModel:
@@
+        if response_model:
+            try:
+                structured_result = self._validate_structured_output(
+                    full_response, response_model
+                )
+                self._emit_call_completed_event(
+                    response=structured_result,
+                    call_type=LLMCallType.LLM_CALL,
+                    from_task=from_task,
+                    from_agent=from_agent,
+                    messages=params["messages"],
+                    usage=usage_data,
+                    finish_reason=finish_reason,
+                    response_id=response_id,
+                )
+                return structured_result
+            except ValueError as e:
+                logging.warning(f"Structured output validation failed: {e}")
+
         full_response = self._apply_stop_words(full_response)
@@
         result = self._finalize_streaming_response(
             full_response=full_response,
             tool_calls=tool_calls,
             usage_data=usage_data,
             params=params,
+            response_model=response_model,
             available_functions=available_functions,
             from_task=from_task,
             from_agent=from_agent,
             finish_reason=stream_finish_reason,
             response_id=stream_response_id,
         )

Also applies to: 2039-2054

🤖 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` at 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.

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