Skip to content

Enable streaming when response_format is set for gemini, openai, and …#1162

Open
liukidar wants to merge 1 commit into
mozilla-ai:mainfrom
Zyphra:feat-stream-structured-output
Open

Enable streaming when response_format is set for gemini, openai, and …#1162
liukidar wants to merge 1 commit into
mozilla-ai:mainfrom
Zyphra:feat-stream-structured-output

Conversation

@liukidar

@liukidar liukidar commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Description

Enables streaming chat completions with structured output configuration for providers that support it.

This removes stale stream + response_format rejections for Anthropic and Gemini, and updates OpenAI chat completions so structured output can be requested while streaming since it is now supported by all three providers. For OpenAI Pydantic/dataclass response formats, streaming requests convert the type to a JSON schema response_format before calling chat.completions.create(stream=True), preserving chunk streaming without introducing final-response parsing, similar to the other providers.

This PR does not change the streaming return contract: streaming still returns chunks only, with no partial or final parsing inside any-llm (since there is no explicit delta accumulator within the library code). This means that parsing, both for pydantic and json schema response formats, will have do be carried out externally.

Note that other providers and/or endpoints likely suffer from the same limitation, but i haven't checked those.

PR Type

  • 🐛 Bug Fix

Relevant issues

N/A

Checklist

  • I understand the code I am submitting.
  • I have added unit tests that prove my fix/feature works
  • I have run this code locally and verified it fixes the issue.
  • New and existing tests pass locally
  • Documentation was updated where necessary
  • I have read and followed the contribution guidelines
  • AI Usage:
    • No AI was used.
    • AI was used for drafting/refactoring.
    • This is fully AI-generated.

AI Usage Information

  • AI Model used: GPT-5
  • AI Developer Tool used: Codex
  • Any other info you'd like to share: Used AI assistance to inspect provider behavior, update implementation, and add/adjust unit tests.

When answering questions by the reviewer, please respond yourself, do not copy/paste the reviewer comments into an AI system and paste back its answer. We want to discuss with you, not your AI :)

  • I am an AI Agent filling out this form (check box if true)

Summary by CodeRabbit

  • New Features

    • Streaming requests now support structured response formats across OpenAI, Anthropic, and Gemini integrations.
    • Pydantic-based response schemas are now handled correctly in streamed outputs.
  • Bug Fixes

    • Improved handling of missing or empty tool parameters.
    • Removed previous errors when combining streaming with structured output settings, allowing these requests to proceed normally.

Copilot AI review requested due to automatic review settings July 1, 2026 16:42
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Removes prior restrictions that raised errors when stream and response_format were used together in Anthropic, Gemini, and OpenAI providers. Adjusts related parameter-conversion logic accordingly, and updates unit tests to assert correct streaming behaviour with response_format instead of expecting exceptions.

Changes

Streaming with response_format support

Layer / File(s) Summary
Anthropic parameter conversion and streaming tests
src/any_llm/providers/anthropic/utils.py, tests/unit/providers/test_anthropic_provider.py
_convert_tool_spec normalises tool["parameters"] into a local dict before building input_schema; _convert_params no longer raises UnsupportedParameterError for stream+response_format and now always sets output_config. Tests replace the error-expecting case with one asserting output_config is passed to messages.stream.
Gemini parameter conversion and streaming tests
src/any_llm/providers/gemini/base.py, tests/unit/providers/test_gemini_provider.py
Removes the UnsupportedParameterError branch for stream+response_format in _convert_completion_params. Tests replace the error case with one verifying generation_config.response_mime_type and response_schema are forwarded to generate_content_stream.
OpenAI parameter conversion, streaming control flow, and tests
src/any_llm/providers/openai/base.py, tests/unit/providers/test_openai_base_provider.py
_convert_completion_params now converts BaseModel response_format to json_schema even when streaming; _acompletion no longer raises ValueError for stream+response_format, only stripping stream when response_format is set without streaming. Tests replace the error case with two tests verifying chat.completions.create is used with dict or converted json_schema response_format during streaming.

Related PRs: None identified.

Suggested labels: bug, enhancement, tests

Suggested reviewers: None identified.

Poem
A rabbit once thought streams could break
When schemas and formats overlap and shake,
But now the guard rails quietly fall away,
Streaming and structure can coexist and play,
Hop, hop — tests confirm the payload's true,
No more errors, just the schema shining through.

🚥 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 matches the main change: enabling streaming with response_format for the supported providers.
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.

Copilot AI 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.

Pull request overview

Enables streaming chat completions when response_format is configured for OpenAI, Gemini, and Anthropic by removing previous provider-side rejections and ensuring OpenAI streaming uses chat.completions.create(stream=True) with a JSON schema response_format when needed.

Changes:

  • OpenAI: allow streaming with response_format, converting structured output types to JSON schema for streaming requests so .parse() is not used.
  • Gemini and Anthropic: remove stream + response_format rejection paths and ensure streaming calls receive the correct provider-specific structured output configuration.
  • Add and update unit tests to validate the new streaming behavior across the three providers.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/any_llm/providers/openai/base.py Updates response_format conversion and streaming behavior to permit structured output while streaming.
src/any_llm/providers/gemini/base.py Removes the unsupported-parameter guard that blocked streaming with response_format.
src/any_llm/providers/anthropic/utils.py Allows output_config generation even when streaming and hardens tool parameter conversion.
tests/unit/providers/test_openai_base_provider.py Adds coverage asserting streaming uses create() (not parse()) for dict and BaseModel response formats.
tests/unit/providers/test_gemini_provider.py Adds coverage asserting streaming passes generation config for JSON schema response formats.
tests/unit/providers/test_anthropic_provider.py Adds coverage asserting streaming passes output_config through to the Anthropic streaming call.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 82 to 84
Plain dataclasses are converted to JSON schema dicts since the
OpenAI SDK's ``.parse()`` only supports Pydantic BaseModel types.
"""

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Happy to change this if considered important. let me know

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/any_llm/providers/openai/base.py (1)

85-94: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not rewrite CompletionParams.response_format in place.

This now mutates a caller owned CompletionParams when stream=True. If the same params object is reused for a later non streaming call, the original BaseModel type has already been replaced with a dict, so _acompletion() will skip .parse() and silently change behaviour.

Proposed fix
-        if is_structured_output_type(params.response_format) and (
-            params.stream or not issubclass(params.response_format, BaseModel)
-        ):
-            params.response_format = {
+        response_format = params.response_format
+        if is_structured_output_type(response_format) and (
+            params.stream or not issubclass(response_format, BaseModel)
+        ):
+            response_format = {
                 "type": "json_schema",
                 "json_schema": {
-                    "name": params.response_format.__name__,
-                    "schema": get_json_schema(params.response_format),
+                    "name": response_format.__name__,
+                    "schema": get_json_schema(response_format),
                 },
             }
         converted_params = params.model_dump(exclude_none=True, exclude={"model_id", "messages"})
+        if response_format is not None:
+            converted_params["response_format"] = 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 `@src/any_llm/providers/openai/base.py` around lines 85 - 94, The structured
output handling in the OpenAI base provider is mutating caller-owned
CompletionParams.response_format in place, which can change later calls when the
same params object is reused. Update the logic in the response_format handling
path to avoid assigning back onto params.response_format; instead build a local
normalized schema payload and pass that through the streaming request path. Keep
the original BaseModel type intact so _acompletion() can still detect it and
apply .parse() for non-streaming calls.
🤖 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.

Outside diff comments:
In `@src/any_llm/providers/openai/base.py`:
- Around line 85-94: The structured output handling in the OpenAI base provider
is mutating caller-owned CompletionParams.response_format in place, which can
change later calls when the same params object is reused. Update the logic in
the response_format handling path to avoid assigning back onto
params.response_format; instead build a local normalized schema payload and pass
that through the streaming request path. Keep the original BaseModel type intact
so _acompletion() can still detect it and apply .parse() for non-streaming
calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 13fff04e-4461-4281-98f5-d0f8ef24bedb

📥 Commits

Reviewing files that changed from the base of the PR and between b4e00c1 and 46696da.

📒 Files selected for processing (6)
  • src/any_llm/providers/anthropic/utils.py
  • src/any_llm/providers/gemini/base.py
  • src/any_llm/providers/openai/base.py
  • tests/unit/providers/test_anthropic_provider.py
  • tests/unit/providers/test_gemini_provider.py
  • tests/unit/providers/test_openai_base_provider.py
💤 Files with no reviewable changes (1)
  • src/any_llm/providers/gemini/base.py

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.

2 participants