Skip to content

Conversation

@notgitika
Copy link
Contributor

@notgitika notgitika commented Oct 3, 2025

Description

This PR implements OpenAI Responses API as a separate model provider supporting streaming, structured output and tool calling.

Note: this commit does not include the extra capabilities that the Responses API supports such as the built-in tools and the stateful conversation runs.

Related Issues

#253

Documentation PR

TODO; coming next: will be adding a section for the Responses API within the existing openai model page

Type of Change

New feature

Testing

Added a unit test file similar to the existing test_openai.py model provider. Reusing the integ tests in the same file test_model_openai.py using pytest.parameterize with OpenAIResponses model so that I could test that the funcitonality is the same between the two models.

I ran everything in the CONTRIBUTING.md file.

hatch run integ-test
================== 81 passed, 68 skipped, 49 warnings in 106.56s (0:01:46) ==========

hatch run test-integ tests_integ/models/test_model_openai.py -v

============= 18 passed, 2 skipped in 13.44s =============

pre-commit run --all-files

  • I ran hatch run prepare --> yes, all tests pass

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

"""
match event["chunk_type"]:
case "message_start":
return {"messageStart": {"role": "assistant"}}
Copy link
Member

Choose a reason for hiding this comment

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

reach out to @zastrowm , but I believe we can make this more readable by now returning the typed StreamEvents rather than the dictionaries

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was looking into this briefly but did not implement this. I will talk to him about this and maybe we can implement this in the next iteration.

@fsajjad
Copy link

fsajjad commented Oct 24, 2025

Hey Team, this is one of the feature my customer request and concerned as blocker from adopting Strands Agent. They mentioned Strands currently only support deprecated chatcompletion API and not responses API. When can we expect this to be merged? and will this also enable streaming structured output?

Copy link
Member

@dbschmigelski dbschmigelski left a comment

Choose a reason for hiding this comment

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

A major version bump is proposed in #1370.

Adding a comment here that we would need to consider when the user has v1 installed compared to v2.

For example

import pydantic
from packaging import version

# Detect the version once at the module level
PYDANTIC_V2 = version.parse(pydantic.VERSION) >= version.parse("2.0.0")

if PYDANTIC_V2:
    from pydantic import ConfigDict
    def get_model_fields(model):
        return model.model_fields
else:
    def get_model_fields(model):
        return model.__fields__

@notgitika notgitika force-pushed the gitikavj/add-openai-responses-model branch from 7c05e84 to 8eea5d9 Compare January 21, 2026 06:16
@codecov
Copy link

codecov bot commented Jan 21, 2026

Codecov Report

❌ Patch coverage is 91.41631% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/strands/models/openai_responses.py 91.41% 4 Missing and 16 partials ⚠️

📢 Thoughts on this report? Let us know!

# Validate OpenAI SDK version - Responses API requires v2.0.0+
openai_version = Version(get_package_version("openai"))
if openai_version < _MIN_OPENAI_VERSION:
raise ImportError(
Copy link
Member

Choose a reason for hiding this comment

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

We should be able to do this on the import rather than in the constructor, no?


# Yield tool calls if any
for call_info in tool_calls.values():
mock_tool_call = type(
Copy link
Member

Choose a reason for hiding this comment

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

mock_tool_call?

_MIN_OPENAI_VERSION = Version("2.0.0")

# Maximum file size for media content in tool results (20MB)
MAX_MEDIA_SIZE_BYTES = 20 * 1024 * 1024
Copy link
Member

Choose a reason for hiding this comment

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

why public?

async with openai.AsyncOpenAI(**self.client_args) as client:
try:
response = await client.responses.create(**request)
except openai.BadRequestError as e:
Copy link
Member

Choose a reason for hiding this comment

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

it looks like we can just move to one big try catch to avoid duplicating these lines

            except openai.BadRequestError as e:
                if hasattr(e, "code") and e.code == "context_length_exceeded":
                    logger.warning("OpenAI Responses API threw context window overflow error")
                    raise ContextWindowOverflowException(str(e)) from e
                raise
            except openai.RateLimitError as e:
                logger.warning("OpenAI Responses API threw rate limit error")
                raise ModelThrottledException(str(e)) from e

if event.type == "response.output_text.delta":
# Text content streaming
if not has_text_content:
yield self._format_chunk({"chunk_type": "content_start", "data_type": "text"})
Copy link
Member

Choose a reason for hiding this comment

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

Does the reasoning API support reasoning content? Which would mean we'd need something like _stream_switch_content in

chunks, data_type = self._stream_switch_content("reasoning_content", data_type)

)()

yield self._format_chunk({"chunk_type": "content_start", "data_type": "tool", "data": mock_tool_call})
yield self._format_chunk({"chunk_type": "content_delta", "data_type": "tool", "data": mock_tool_call})
Copy link
Member

Choose a reason for hiding this comment

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

Here we are only ever emitting one delta per tool? Is this correct? Wouldn't we expect multiple?

yield self._format_chunk({"chunk_type": "content_delta", "data_type": "tool", "data": mock_tool_call})
yield self._format_chunk({"chunk_type": "content_stop", "data_type": "tool"})

finish_reason = "tool_calls" if tool_calls else "stop"
Copy link
Member

Choose a reason for hiding this comment

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

Based on your logic above finish_reason = "tool_calls" if tool_calls else "stop" we are never going t hit the length case in

           case "message_stop":
                match event["data"]:
                    case "tool_calls":
                        return {"messageStop": {"stopReason": "tool_use"}}
                    case "length":
                        return {"messageStop": {"stopReason": "max_tokens"}}
                    case _:
                        return {"messageStop": {"stopReason": "end_turn"}}

(),
{
"prompt_tokens": getattr(final_usage, "input_tokens", 0),
"completion_tokens": getattr(final_usage, "output_tokens", 0),
Copy link
Member

Choose a reason for hiding this comment

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

  1. What is the reason behind manipulating these names input_tokens->prompt_tokens
  2. Do we need to add something like what we do in openai v1 https://github.com/strands-agents/sdk-python/blob/main/src/strands/models/openai.py#L430C33-L430C46

...


class OpenAIResponsesModel(Model):
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to use system tools with the current implementation?


for message in messages:
role = message["role"]
if role == "system":
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this? this is format request and Messages should never be able to contain a Role of type system?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants