Class MockStreamingChatModel
- All Implemented Interfaces:
StreamingChatModel
StreamingChatModel for tests: it needs no network, streams the answers the test tells it to, and records
the requests it receives.
Script the answers step by step, one step per request. For example, a model that first calls a tool and then streams text:
MockStreamingChatModel model = MockStreamingChatModel.thatCallsTool("cancelBooking", Map.of("bookingNumber", "MS-777"))
.thenResponds("Booking MS-777 is cancelled.");
To control exactly which tokens are streamed, use thatStreams(String...). To test a stream that
breaks, let a response fail after its tokens:
MockStreamingChatModel model = MockStreamingChatModel.thatStreams("Hel", "lo")
.andFails(new RuntimeException("connection reset"));
Every that... method creates a mock with its first step (one step per message for the message-list forms),
and every then... method adds a step. Each step answers exactly one request. A request after the last step
gets an IllegalStateException in onError that shows the request, so a test notices when the code
calls the model more often than the script expects, unless the code under test handles the error itself. To answer
every further request with the last step, end the script with thenRepeats(), for example
MockStreamingChatModel.thatResponds("Hello!").thenRepeats(). A step can compute its answer from the request
with thatResponds(Function).
An answer is streamed the way a provider streams it: first its thinking, then its text, then its tool calls, each
tool call as partial tool calls followed by the complete tool call. Text, thinking and tool call arguments are
streamed in word-like tokens, the way a tokenizer splits text before it encodes it: a word with the space before it,
up to three digits of a number, a run of punctuation, or whitespace. So "Hello, world!" is streamed as
"Hello", ",", " world" and "!", and "Order 12345" as "Order",
" ", "123" and "45". Chinese and Japanese text, written without spaces between words, is
streamed one character per token. The complete response reports what a provider reports: the model name of the
request, and the finish reason FinishReason.TOOL_EXECUTION for an answer with tool calls, otherwise
FinishReason.STOP. To report token usage in every response, use withTokenUsage(TokenUsage). To
report anything else, for example FinishReason.LENGTH or a different token usage per step, give the whole
response: thatResponds(ChatResponse).
By default, the mock reports no capabilities. To test code that checks the capabilities of a model, for example a
RoutingStreamingChatModel that sends a request with a JSON schema response format only to the models that
support it, use withSupportedCapabilities(Capability.RESPONSE_FORMAT_JSON_SCHEMA).
Like a provider, the mock produces its answer on its own thread. chat(request, handler) returns immediately
and calls the handler on another thread, also for a failure, so a test has to wait for onCompleteResponse or
onError, for example with a CompletableFuture. The Publisher returned by
chat(request) produces its events on a new thread for each subscription and delivers each one only after the
subscriber requested it, either on that thread or, for an event that was waiting, on the thread that requests it.
Each subscription to that Publisher is one request: the mock records the request and takes the next step when
the Publisher is subscribed to, not when chat(request) returns, so a retry that subscribes again gets
the next step.
A mock remembers which steps were taken and which requests it received, so create a new one for each test. A mock
shared between tests, for example as a bean, fails with a clear message as soon as its script runs out, unless the
script ends with thenRepeats(). The steps are taken in the order the requests arrive. A mock keeps every
request it receives, so it is not meant for load tests or other long runs.
-
Method Summary
Modifier and TypeMethodDescriptionandFails(RuntimeException exception) Makes the response of the last step fail after its thinking and text were streamed, the way a broken connection does: the handler gets the partial responses and then the error, but no tool calls and no complete response.doChat(ChatRequest chatRequest) EmitsPartialThinkingandPartialResponseevents, thenPartialToolCallandCompleteToolCallevents, then theCompleteResponse.voiddoChat(ChatRequest chatRequest, StreamingChatResponseHandler handler) The last request received so far.request()The only request received so far.requests()A copy of the requests received so far, in the order they arrived.static MockStreamingChatModelthatCallsTool(String toolName, String jsonArguments) A model that answers the first request with a call of the given tool.static MockStreamingChatModelthatCallsTool(String toolName, Map<String, ?> arguments) A model that answers the first request with a call of the given tool.static MockStreamingChatModelthatFails(RuntimeException exception) A model whose first request fails with the given exception, before anything is streamed: the handler gets it inonError.static MockStreamingChatModelthatResponds(AiMessage... aiMessages) A model that streams the given messages in order, one per request, in word-like tokens.static MockStreamingChatModelthatResponds(ChatResponse chatResponse) A model that answers the first request by streaming the message of the given response, in word-like tokens, and completing with exactly that response, including its metadata, for example its token usage, model name or finish reason.static MockStreamingChatModelthatResponds(String text) A model that answers the first request by streaming the given text, in word-like tokens.static MockStreamingChatModelthatResponds(Function<ChatRequest, AiMessage> answers) A model that computes the answer to the first request from the request and streams it, in word-like tokens.static MockStreamingChatModelthatResponds(List<AiMessage> aiMessages) A model that streams the given messages in order, one per request, in word-like tokens.static MockStreamingChatModelthatStreams(String... tokens) A model that answers the first request by streaming exactly the given tokens, which together make up one answer:thatStreams("Hel", "lo")streams"Hel"and then"lo", and completes with"Hello".static MockStreamingChatModelthatStreams(List<String> tokens) A model that answers the first request by streaming exactly the given tokens, seethatStreams(String...).thenCallsTool(String toolName, String jsonArguments) Adds a step: the next request is answered with a call of the given tool.thenCallsTool(String toolName, Map<String, ?> arguments) Adds a step: the next request is answered with a call of the given tool.thenFails(RuntimeException exception) Adds a step: the next request fails with the given exception before anything is streamed.Answers every further request with the last step of the script.thenResponds(AiMessage aiMessage) Adds a step: the next request is answered by streaming the given message, in word-like tokens.thenResponds(ChatResponse chatResponse) Adds a step: the next request is answered by streaming the message of the given response, in word-like tokens, and completing with exactly that response, including its metadata, for example its token usage.thenResponds(String text) Adds a step: the next request is answered by streaming the given text, in word-like tokens.thenStreams(String... tokens) Adds a step: the next request is answered by streaming exactly the given tokens, which together make up one answer.thenStreams(List<String> tokens) Adds a step: the next request is answered by streaming exactly the given tokens, seethenStreams(String...).withListeners(ChatModelListener... listeners) Notifies the given listeners of each call, like a real model does.withListeners(List<ChatModelListener> listeners) Notifies the given listeners of each call, like a real model does.withSupportedCapabilities(Capability... capabilities) Reports the given capabilities fromsupportedCapabilities(), like a model that supports them.withSupportedCapabilities(Set<Capability> capabilities) Reports the given capabilities fromsupportedCapabilities(), seewithSupportedCapabilities(Capability...).withTokenUsage(@Nullable TokenUsage tokenUsage) Reports the given token usage in the response to every request, except for a step that gives the whole response: that one reports exactly its own token usage.
-
Method Details
-
thenResponds
Adds a step: the next request is answered by streaming the given text, in word-like tokens.- Throws:
IllegalArgumentException- if the text is empty, seethatResponds(String)IllegalStateException- if the script already ends withthenRepeats()
-
thenStreams
Adds a step: the next request is answered by streaming exactly the given tokens, which together make up one answer. Each argument is one token; to split a text into word-like tokens, usethenResponds(String). Without tokens, nothing is streamed and the answer has no text.- Throws:
IllegalArgumentException- if a token is emptyIllegalStateException- if the script already ends withthenRepeats()
-
thenStreams
Adds a step: the next request is answered by streaming exactly the given tokens, seethenStreams(String...).- Throws:
IllegalArgumentException- if a token is emptyIllegalStateException- if the script already ends withthenRepeats()
-
thenResponds
Adds a step: the next request is answered by streaming the given message, in word-like tokens.- Throws:
IllegalArgumentException- if the text of the message is empty, seethatResponds(String)IllegalStateException- if the script already ends withthenRepeats()
-
thenResponds
Adds a step: the next request is answered by streaming the message of the given response, in word-like tokens, and completing with exactly that response, including its metadata, for example its token usage. Only tool calls without an id get the next id of the mock, seeMockChatModel.toolCall(String, Map).- Throws:
IllegalStateException- if the script already ends withthenRepeats()
-
thenCallsTool
Adds a step: the next request is answered with a call of the given tool.- Parameters:
toolName- the name of the toolarguments- the arguments of the tool, by name- Throws:
IllegalStateException- if the script already ends withthenRepeats()
-
thenCallsTool
Adds a step: the next request is answered with a call of the given tool. Use it for arguments that cannot be given as a map, for example invalid JSON.- Parameters:
toolName- the name of the tooljsonArguments- the arguments of the tool, as JSON- Throws:
IllegalStateException- if the script already ends withthenRepeats()
-
thenFails
Adds a step: the next request fails with the given exception before anything is streamed. To let a response fail after its tokens, useandFails(RuntimeException).- Throws:
IllegalStateException- if the script already ends withthenRepeats()
-
andFails
Makes the response of the last step fail after its thinking and text were streamed, the way a broken connection does: the handler gets the partial responses and then the error, but no tool calls and no complete response. It does not add a step: to let the next request fail before anything is streamed, usethenFails(RuntimeException).- Throws:
IllegalStateException- if the last step already fails, computes its answer or has no text or thinking to stream, if the mock already answered the last step, or if the script already ends withthenRepeats()
-
thenRepeats
Answers every further request with the last step of the script. Only the last step repeats; the script does not start over. A last step that computes its answer computes it again for each request. Without it, a request after the last step fails. It must be the last call of the script.A step that calls a tool cannot repeat. To test a model that keeps calling a tool, for example the limit set with
maxToolCallingRoundTrips, compute the answer:thatResponds(request -> AiMessage.from(MockChatModel.toolCall("getWeather", Map.of()))).thenRepeats().- Throws:
IllegalStateException- if the last step calls a tool, because an AI Service would then call the tool again and again, or if the script already ends withthenRepeats()
-
withTokenUsage
Reports the given token usage in the response to every request, except for a step that gives the whole response: that one reports exactly its own token usage. For different token usages per step, give the whole responses, seethenResponds(ChatResponse).- Parameters:
tokenUsage- the token usage, ornullfor no token usage, which is also the default
-
withListeners
Notifies the given listeners of each call, like a real model does. -
withListeners
Notifies the given listeners of each call, like a real model does. -
withSupportedCapabilities
Reports the given capabilities fromsupportedCapabilities(), like a model that supports them. For example, withCapability.RESPONSE_FORMAT_JSON_SCHEMA, aRoutingStreamingChatModelcan send a request with a JSON schema response format to this mock. The mock only reports the capabilities: it still answers with the steps of its script, and it does not check the requests or the answers against the capabilities. -
withSupportedCapabilities
Reports the given capabilities fromsupportedCapabilities(), seewithSupportedCapabilities(Capability...). -
listeners
- Specified by:
listenersin interfaceStreamingChatModel
-
supportedCapabilities
- Specified by:
supportedCapabilitiesin interfaceStreamingChatModel
-
doChat
EmitsPartialThinkingandPartialResponseevents, thenPartialToolCallandCompleteToolCallevents, then theCompleteResponse. They are produced on a new thread for each subscription, and each one is delivered only after the subscriber requested it, either on that thread or, for an event that was waiting, on the thread that requests it. A failure is reported withonError, never thrown.- Specified by:
doChatin interfaceStreamingChatModel
-
doChat
- Specified by:
doChatin interfaceStreamingChatModel
-
requests
A copy of the requests received so far, in the order they arrived. -
request
The only request received so far. To check it, for example:LangChain4jAssertions.assertThat(model.request()).hasLastUserMessageContaining("MS-777").- Throws:
IllegalStateException- if the mock received no request or several requests
-
lastRequest
The last request received so far. For example, the request that follows the tool results.- Throws:
IllegalStateException- if the mock received no request
-
thatResponds
A model that answers the first request by streaming the given text, in word-like tokens. Add the next steps withthen...methods, or answer every request with this text:thatResponds("Hello!").thenRepeats(). To control exactly which tokens are streamed, usethatStreams(String...).The text cannot be empty: a provider that streams no text completes with no text (
null), not with"". To stream an answer without text, usethatStreams(String...)without tokens. To complete with exactly""anyway, give the whole response:thatResponds(ChatResponse).- Throws:
IllegalArgumentException- if the text is empty
-
thatStreams
A model that answers the first request by streaming exactly the given tokens, which together make up one answer:thatStreams("Hel", "lo")streams"Hel"and then"lo", and completes with"Hello". Each argument is one token, sothatStreams("Hello, world!")streams one token; to split a text into word-like tokens, usethatResponds(String). Without tokens, nothing is streamed and the answer has no text. Add the next steps withthen...methods.- Throws:
IllegalArgumentException- if a token is empty
-
thatStreams
A model that answers the first request by streaming exactly the given tokens, seethatStreams(String...).- Throws:
IllegalArgumentException- if a token is empty
-
thatResponds
A model that streams the given messages in order, one per request, in word-like tokens. Add further steps withthen...methods. Use it for answers thatthatCallsTool(String, Map)cannot express, for example several tool calls in one answer, built withMockChatModel.toolCall(String, Map).- Throws:
IllegalArgumentException- if no message is given, or if the text of a message is empty, seethatResponds(String)
-
thatResponds
A model that streams the given messages in order, one per request, in word-like tokens. Add further steps withthen...methods.- Throws:
IllegalArgumentException- if the list is empty, or if the text of a message is empty, seethatResponds(String)
-
thatResponds
A model that answers the first request by streaming the message of the given response, in word-like tokens, and completing with exactly that response, including its metadata, for example its token usage, model name or finish reason. Only tool calls without an id get the next id of the mock, seeMockChatModel.toolCall(String, Map). Add the next steps withthen...methods. -
thatResponds
A model that computes the answer to the first request from the request and streams it, in word-like tokens. Add the next steps withthen...methods, or compute the answer to every request:thatResponds(request -> ...).thenRepeats(). If the function throws, the request fails with that exception, unchanged. An answer with the text""fails the request with anIllegalArgumentException, seethatResponds(String). The function runs on the thread that streams the answer. -
thatCallsTool
A model that answers the first request with a call of the given tool. Add the next steps withthen...methods. The call gets the next id of the mock, seeMockChatModel.toolCall(String, Map).- Parameters:
toolName- the name of the toolarguments- the arguments of the tool, by name
-
thatCallsTool
A model that answers the first request with a call of the given tool. Add the next steps withthen...methods. Use it for arguments that cannot be given as a map, for example invalid JSON.- Parameters:
toolName- the name of the tooljsonArguments- the arguments of the tool, as JSON
-
thatFails
A model whose first request fails with the given exception, before anything is streamed: the handler gets it inonError. To let every request fail, addthenRepeats(). To let later requests succeed, add steps withthen...methods.
-