Model Client Selection
AgentModelClient does not decide "which provider you use", but rather:
In what protocol shape the
AgentPromptassembled by the Agent runtime is ultimately sent to the model.
The two core implementations today are:
ChatModelClientResponsesModelClient
They are not simply an "old protocol" and a "new protocol", nor two losslessly-equivalent shells. The real differences land on:
- how prompt fields map
- how tools are passed through
- which intermediate signals the stream can emit
- what the final
memoryItemslook like - which advanced fields are in fact never passed through on a given path
1. Start with 6 key design decisions
1.1 This is a protocol adaptation choice, not a runtime choice
BaseAgentRuntime depends only on:
AgentModelClient
It does not directly depend on:
IChatServiceIResponsesService
Therefore:
- ReAct / CodeAct / DeepResearch are runtime semantic choices
- Chat / Responses are model protocol choices
Keep these two layers distinct.
1.2 systemPrompt and instructions are not mapped equivalently on the two paths
This is arguably the single most important fact on this page.
Chat path
systemPrompt-> one system messageinstructions-> an additional system message appended
Responses path
systemPrompt->ResponseRequest.instructionsinstructions-> wrapped assystemMessage(...)and inserted at the very front of the input items
In other words:
- On the Chat path both land in the message sequence
- On the Responses path both land in two different protocol positions
1.3 The Responses path passes through more top-level fields
ResponsesModelClient.toResponseRequest(...) pushes these fields straight down:
toolstoolChoiceparallelToolCallstemperaturetopPmaxOutputTokensreasoningstoreuserextraBody
ChatModelClient.toChatCompletion(...), by contrast, only passes through a subset.
This means certain Agent-level configurations, on the Chat path, are effectively "written into the prompt, but never become protocol-level top-level parameters".
1.4 The Chat path has a narrower toolChoice constraint
ChatModelClient only sets builder.toolChoice(...) when:
prompt.getToolChoice() instanceof String
ResponsesModelClient, instead, passes toolChoice through verbatim.
If you rely on more complex tool-choice structures, the Responses path is the more expressive one.
1.5 The memoryItems shape differs between the two paths
ChatModelClient.toModelResult(...) constructs the memory items itself:
- plain text answer ->
assistantmessage - with tool calls ->
assistantToolCallsMessage(...)
ResponsesModelClient.toModelResult(...) takes:
response.getOutput()
and stuffs it into memoryItems as-is.
So for the same call:
- the Chat path yields a converted, unified message shape
- the Responses path preserves an item list much closer to the raw response output
1.6 What stream sees depends not on how strong the underlying protocol is, but on what the current adapter actually emits
Many people assume upfront that:
- the Responses protocol is more structured
- therefore the Agent-level stream must be richer
The current implementation is not like that.
You need to look at what the current client's createStream(...) actually calls back.
2. What ChatModelClient actually does
2.1 How a prompt is translated into a chat completion
The core mappings of toChatCompletion(...) are:
systemPromptbecomes the first system messageinstructionsbecomes the second system messageitemsare converted one by one intoChatMessagetoolsis converted intoList<Tool>- when tools are non-empty, set
passThroughToolCalls(true)
The top-level fields it actually pushes down include:
modelmessagesstreamstreamExecutiontemperaturetopPmaxCompletionTokensuserparallelToolCallstoolChoice(string only)toolsextraBody
2.2 Which Agent fields are not pushed down at the protocol level on the Chat path
The current ChatModelClient does not explicitly push down:
reasoningstore
So if you treat these two as strong protocol parameters, the Responses path matches your intuition better.
2.3 items is not simple string concatenation
ChatModelClient.convertToMessage(...) accepts richer input shapes than just "user text".
It handles:
- a plain
ChatMessage - a map with
type=message - a map with
type=function_call_output - multimodal
input_text - multimodal
input_image
This shows that the Chat path does not only consume plain text messages; internally it is already doing a layer of item -> chat message protocol normalization.
2.4 The Chat streaming path preserves reasoning delta
StreamingSseListener.send() distinguishes:
isReasoning() == true->onReasoningDelta(delta)- otherwise ->
onDeltaText(delta)
So within the current Agent adaptation layer, the Chat path can surface reasoning token-level increments earlier.
This is valuable for real-time debug panels and trace timelines.
3. What ResponsesModelClient actually does
3.1 How a prompt is translated into a response request
The mapping order in toResponseRequest(...) is:
modelinput(buildItems(prompt))toolstoolChoiceparallelToolCallstemperaturetopPmaxOutputTokensreasoningstoreuserstreamstreamExecutionsystemPrompt -> instructionsextraBody
This path preserves the AgentPrompt top-level fields noticeably more completely.
3.2 instructions is front-inserted as an input item, not as a top-level instructions
The logic in buildItems(prompt) is:
- first copy
prompt.getItems() - if there is an
instructions - at index
0, insert anAgentInputItem.systemMessage(...)
So on the Responses path:
systemPromptis a request-level instructioninstructionsis a leading system message inside the input items
This is fundamentally different from the Chat path's "two system messages in sequence".
3.3 The Responses streaming path currently looks more like "text increments + a single unified wrap-up at the end"
ResponsesModelClient.createStream(...) currently calls back only:
onDeltaText(...)onRetry(...)onError(...)onComplete(...)
It does not, unlike the Chat path, emit reasoning deltas separately.
More importantly, tool calls are not surfaced directly through the stream listener mid-stream; instead:
- wait for the stream to finish
ResponseSseListener.getResponse()- then uniformly run
toModelResult(response) - finally the runtime continues from
AgentModelResult.toolCalls
So in the current implementation, the Responses path leans more toward:
- watch text during the stream
- watch the full structure after the stream
4. How the two paths ultimately converge back on unified Agent semantics
Even though the protocols differ, the runtime ultimately only understands AgentModelResult.
Chat path
toModelResult(ChatCompletionResponse) extracts:
outputTextreasoningTexttoolCallsmemoryItemsrawResponse
Responses path
toModelResult(Response) extracts:
outputTexttoolCallsmemoryItemsrawResponse
The key unifying move here is:
- different provider responses
-> uniformly projected into an
AgentModelResult
What happens afterward in the runtime does not care whether it came from Chat or Responses.
5. Where stream behavior truly differs
5.1 Chat path
Currently surfaces earlier:
- reasoning delta
- text delta
- retry
Then finally yields a unified AgentModelResult in onComplete(...).
5.2 Responses path
Currently mainly surfaces:
- text delta
- retry
Structured results, tool calls, and the final output are returned all at once after the stream ends.
5.3 Neither path "executes tools directly mid-stream"
Although AgentModelStreamListener has:
onToolCall(AgentToolCall call)
neither of the two current clients actually invokes this callback during streaming to surface tools early.
Actual tool execution still depends on:
createStream(...)completing- generating the final
AgentModelResult BaseAgentRuntimetakingtoolCallsfrom the result- then entering the unified tool loop
So do not mistakenly assume "the Chat stream emits tokens on one side while the runtime is internally executing tools in parallel".
6. The semantics of stream cancellation and thread interruption
Both paths share a very similar design:
- maintain an
ACTIVE_STREAMS - keyed by the current
Thread - valued by the underlying SSE listener
and expose:
cancelActiveStream(Thread thread)
Inside createStream(...), if the thread is interrupted:
- it first cancels the stream
- then throws
InterruptedException
This tells you the current cancellation semantics are:
- thread-level cancellation
- not a separate request-id / run-id cancellation protocol
This works well inside a CLI / session runtime, but it also means you must understand that the cancellation boundary is "the thread", not "any arbitrary logical task".
7. When to prefer Chat
Typical scenarios better suited to Chat:
- you already have a mature chat-completions-compatible pipeline
- you lean more on a message-sequence mental model
- you want to see reasoning delta earlier in the Agent-level streaming callbacks
- your current focus is classic conversation and tool loops, rather than the full expression of response top-level fields
8. When to prefer Responses
Typical scenarios better suited to Responses:
- you want fuller pass-through of
reasoning,store,toolChoice - you prefer to preserve the structure of response output items
- your downstream handling leans more toward structured results than a pure message stream
- you accept "during the stream mainly watch text; structured information is wrapped up at the end"
9. The things most often stated wrongly
9.1 "Responses is always more advanced than Chat"
False.
They are two different protocol tracks, not a simple junior/senior relationship.
9.2 "Chat and Responses are equivalent in prompt mapping"
False.
Especially for:
systemPromptinstructionsreasoningstoretoolChoice
The actual protocol positions and fidelity of these fields all differ.
9.3 "Responses streaming can naturally feed every structured event to the Agent UI in real time"
False.
The Agent-level stream adaptation of the current ResponsesModelClient is still mainly text increments.
9.4 "The Chat path executes tools directly within the stream"
False.
Tool execution in both current clients is still driven forward in a unified way after the runtime receives the final AgentModelResult.
10. A practical selection table
| Concern | Better fit |
|---|---|
| More mature chat-compatible wiring | ChatModelClient |
| Fuller pass-through of response top-level fields | ResponsesModelClient |
| Streaming reasoning visibility | ChatModelClient |
| Closer to response output item structure | ResponsesModelClient |
Complex toolChoice / reasoning / store expression | ResponsesModelClient |
11. Examples
Chat path
Agent agent = Agents.react()
.modelClient(new ChatModelClient(chatService))
.model("your-chat-model")
.systemPrompt("You are a concise assistant.")
.options(AgentOptions.builder().maxSteps(1).build())
.build();
Responses path
Agent agent = Agents.react()
.modelClient(new ResponsesModelClient(responsesService))
.model("gpt-4.1")
.systemPrompt("You are a concise assistant.")
.options(AgentOptions.builder().maxSteps(1).build())
.build();
On the surface only one line differs, but the protocol semantics are not the same.
12. Recommended source-code reading order
ai4j-agent/src/main/java/io/github/lnyocly/ai4j/agent/model/AgentModelClient.javaai4j-agent/src/main/java/io/github/lnyocly/ai4j/agent/model/AgentPrompt.javaai4j-agent/src/main/java/io/github/lnyocly/ai4j/agent/model/ChatModelClient.javaai4j-agent/src/main/java/io/github/lnyocly/ai4j/agent/model/ResponsesModelClient.javaai4j-agent/src/main/java/io/github/lnyocly/ai4j/agent/runtime/BaseAgentRuntime.java