Add OpenAI Responses API support v2 - #7008
Open
dimitarproynov wants to merge 1 commit into
Open
dimitarproynov wants to merge 1 commit into
dimitarproynov wants to merge 1 commit into
Conversation
Teach OpenAiChatModel to talk to /v1/responses as well as /v1/chat/completions, which is the only way to combine reasoning with tool calling on GPT-5.4 and later. One ChatModel, one OpenAiChatOptions and one auto-configuration serve both endpoints, so nothing above the model - ChatClient, advisors, chat memory, tool calling, observability - has to know which one a request went to. OpenAiChatModel keeps the options merging, the observations and the common builder, and forwards to a package-private OpenAiChatApi: OpenAiChatCompletionsApi, the existing mapping moved out of the model unchanged, and OpenAiResponsesApi. Both are wired up front over the same pair of OpenAI clients and each request picks one, so a ChatClient call that overrides the model or the endpoint at runtime is answered by the same bean. spring.ai.openai.chat.api selects the endpoint and defaults to auto: the Responses API from GPT-5.4 onwards, or when the request uses a setting only it provides, and Chat Completions otherwise. Anything that does not parse as a GPT version - a Microsoft Foundry deployment name, a third-party model - stays on Chat Completions, the endpoint every OpenAI-compatible provider implements, and GitHub Models is refused the Responses API at startup. OpenAiChatApiSelection holds that rule together with the warnings: an option the chosen endpoint cannot honour is logged once per JVM and dropped rather than rejected, because the same options bean is routinely used against both. The Responses side is unchanged in substance. A reply is an ordered list of typed items rather than one message, and a reasoning model returns its chain of thought as an opaque encrypted blob that must be handed back verbatim to keep its train of thought across a tool call. Both are carried by the MessagePart list on AssistantMessage, one part per output item and in order, so a reasoning item still precedes the function calls it produced. Only function_call items become ToolCalls, so ToolCallingAdvisor never looks for a local callback named web_search, and reasoning is replayed only when its payload came from this endpoint. The stateless mode is the only one implemented: every request sends the whole Prompt and store=false, which is also what makes the encrypted reasoning come back at all. Also add reasoningSummary, maxToolCalls, hostedTools, include and truncation to OpenAiChatOptions and OpenAiChatProperties, map safetyIdentifier on both endpoints - the property was documented but never sent - and fold the Responses material into the OpenAI chat reference documentation. Signed-off-by: Dimitar Proynov <dimitar.proynov@broadcom.com>
dimitarproynov
requested review from
ericbottard,
ilayaperumalg,
sdeleuze,
sobychacko and
tzolov
September 17, 2026 17:35
| * @author Dimitar Proynov | ||
| * @since 2.1.0 | ||
| */ | ||
| final class OpenAiChatApiSelection { |
Member
There was a problem hiding this comment.
We need to verify the Api selection for the OpenAI compatible providers as well. I haven't explored the selection logic yet; but want to call this out here.
Contributor
Author
There was a problem hiding this comment.
See method isResponsesModel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Teach OpenAiChatModel to talk to /v1/responses as well as /v1/chat/completions, which is the only way to combine reasoning with tool calling on GPT-5.4 and later. One ChatModel, one OpenAiChatOptions and one auto-configuration serve both endpoints, so nothing above the model - ChatClient, advisors, chat memory, tool calling, observability
OpenAiChatModel keeps the options merging, the observations and the common builder, and forwards to a package-private OpenAiChatApi: OpenAiChatCompletionsApi, the existing mapping moved out of the model unchanged, and OpenAiResponsesApi. Both are wired up front over the same pair of OpenAI clients and each request picks one, so a ChatClient call that overrides the model or the endpoint at runtime is answered by the same bean.
spring.ai.openai.chat.api selects the endpoint and defaults to auto: the Responses API from GPT-5.4 onwards, or when the request uses a setting only it provides, and Chat Completions otherwise. Anything that does not parse as a GPT version - a Microsoft Foundry deployment name, a third-party model - stays on Chat Completions, the endpoint every OpenAI-compatible provider implements, and GitHub Models is refused the Responses API at startup. OpenAiChatApiSelection holds that rule together with the warnings: an option the chosen endpoint cannot honour is logged once per JVM and dropped rather than rejected, because the same options bean is routinely used against both.
The Responses side is unchanged in substance. A reply is an ordered list of typed items rather than one message, and a reasoning model returns its chain of thought as an opaque encrypted blob that must be handed back verbatim to keep its train of thought across a tool call. Both are carried by the MessagePart list on AssistantMessage, one part per output item and in order, so a reasoning item still precedes the function calls it produced. Only function_call items become ToolCalls, so ToolCallingAdvisor never looks for a local callback named web_search, and reasoning is replayed only when its payload came from this endpoint. The stateless mode is the only one implemented: every request sends the whole Prompt and store=false, which is also what makes the encrypted reasoning come back at all.
Also add reasoningSummary, maxToolCalls, hostedTools, include and truncation to OpenAiChatOptions and OpenAiChatProperties, map safetyIdentifier on both endpoints - the property was documented but never sent - and fold the Responses material into the OpenAI chat reference documentation.
Full build passed as well as integration tests