Observability
AI Service Observability
AI Service observability is an experimental feature. Its API and behavior may change in future versions.
AI Service observability mechanisms allow users to track what is happening during an AiService invocation. A single invocation may involve multiple LLM invocations, any of which may succeed or fail. AI Service observability allows users to track the full sequence of invocations and their outcomes.
The AI Service observability capabilities are only available when using AI Services. They are a higher-level construct that can not be applied to a ChatModel or StreamingChatModel.
The implementation was originally implemented in the Quarkus LangChain4j extension and was backported here.
Types of events
Each type of event has a unique identifier, which can be used to correlate events across multiple invocations.
Each type of event includes information encapsulated inside an
InvocationContext.
The following types of events are currently available:
| Event Name | Description |
|---|---|
AiServiceStartedEvent | Invoked when an LLM invocation has started. |
AiServiceRequestIssuedEvent | Invoked just before a request to an LLM is sent. Contains the details of the request being made. It is important to note that this can be invoked multiple times during a single AiService invocation when tools exist or an output guardrail reprompts the LLM. Contains information such as the system message and the user message. |
AiServiceResponseReceivedEvent | Invoked when a response from an LLM is received. Contains the LLM response along with the corresponding request. It is important to note that this can be invoked multiple times during a single AiService invocation when tools exist or an output guardrail reprompts the LLM. Contains information such as the system message and the user message. Not every invocation will receive this event. If an invocation fails it will receive an AiServiceErrorEvent instead, and if it is cancelled, an AiServiceCancelledEvent. |
AiServiceErrorEvent | Fired when an invocation with an LLM fails. The failure could be because of network failure, AiService unavailable, input/output guardrails blocking the request, or many other reasons. Contains information about the failure that occurred. |
AiServiceCompletedEvent | Invoked when an LLM invocation has completed successfully. Not every invocation will receive this event. If an invocation fails it will receive an AiServiceErrorEvent instead, and if it is cancelled, an AiServiceCancelledEvent.Contains information about the result of the invocation. |
AiServiceCancelledEvent | Invoked when an invocation is cancelled before it completes: the returned CompletableFuture or Flow.Publisher subscription is cancelled, or StreamingHandle.cancel() is called from a TokenStream callback that receives a context, such as onPartialResponseWithContext (see Streaming Cancellation). It means that cancellation was requested: the model provider or a tool may still finish work that was already running. A cancelled invocation receives neither an AiServiceCompletedEvent nor an AiServiceErrorEvent. No token usage is reported for a request that was cancelled, although the provider may still bill for it. Experimental. |
AiServiceInvocationSummaryEvent | Invoked once per AiService invocation, after its AiServiceCompletedEvent, AiServiceErrorEvent or AiServiceCancelledEvent.Contains every event fired during that invocation, in the order they were fired, so the whole invocation can be inspected in one place. This includes the AiServiceInvocationSummaryEvents of AI Services called from that invocation (for example from a tool). See Listening for the whole invocation for details. |
ToolExecutedEvent | Invoked when a tool invocation has completed. It is important to note that this can be invoked multiple times within a single LLM invocation. Contains information about the tool request and result, and whether the tool execution failed ( isError(), which throws an UnsupportedOperationException if the integration that created the event does not report it; AI Services created with AiServices always report it, but code that reads events reflectively, such as a JSON serializer, fails on events from an integration that does not).Also fired, with isError() returning true, when a tool error fails the AI Service invocation (for example with ToolExecutionErrorHandler.failInvocation()). The result is then the error text, which is not sent to the LLM, and an AiServiceErrorEvent follows. |
ToolCompensatedEvent | Invoked when a tool that had already completed is compensated, because the interaction was later cancelled or failed. Experimental; emitted by the non-blocking AI Service modes. Contains the tool request, its result and the reason for compensation. |
RetrievalAugmentedEvent | Invoked once per invocation of an AI Service that uses RAG (configured with contentRetriever(...) or retrievalAugmentor(...)), after the contents to add to the user message were retrieved, also when nothing was retrieved. Experimental.Contains the user message that was used for the retrieval and the retrieved contents (the same contents that a method returning Result<...> gives as its sources). To observe each content retriever call with the query it received, use a ContentRetrieverListener. |
InputGuardrailExecutedEvent | Invoked when an input guardrail validation has been executed. One of these events will be fired for each invocation of a guardrail. Contains information about the input to an individual input guardrail, its output (i.e. was it successful or a failure?), and the execution duration. |
OutputGuardrailExecutedEvent | Invoked when an output guardrail validation has been executed. One of these events will be fired for each invocation of a guardrail. Contains information about the input to an individual output guardrail, its output (i.e. was it successful? failure? a retry? reprompt?), and the execution duration. |
Every AI Service invocation that runs ends with exactly one of AiServiceCompletedEvent, AiServiceErrorEvent or AiServiceCancelledEvent. Cancelling an invocation that has already completed fires no event. (A TokenStream invocation runs, and ends, only once start() is called.) Events of work that was already running when the invocation was cancelled (for example a ToolExecutedEvent) can still arrive after the AiServiceCancelledEvent.
Listening for an event
Each of the types of events has its own listener that can be implemented to receive the event. You can pick and choose which events you want to listen for.
To listen for an event, create your own class implementing the listener interface you'd like to listen to. These are the available listener interfaces:
Once you've defined your listener(s), register them when you create your AI Services. There are various registerListener method variants on the AiServices class.
For example, you could do the following to create and register a listener for an AiServiceCompletedEvent:
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import dev.langchain4j.observability.api.AiServiceListenerRegistrar;
import dev.langchain4j.observability.api.event.AiServiceCompletedEvent;
import dev.langchain4j.observability.api.listener.AiServiceCompletedListener;
import dev.langchain4j.invocation.InvocationContext;
public class MyAiServiceCompletedListener implements AiServiceCompletedListener {
@Override
public void onEvent(AiServiceCompletedEvent event) {
InvocationContext invocationContext = event.invocationContext();
Optional<Object> result = event.result();
// The invocationId will be the same for all events related to the same AI Service invocation
UUID invocationId = invocationContext.invocationId();
// If this AI Service was called from inside another AI Service invocation (for example from one of its tools or guardrails):
// the invocationId of that outer invocation; otherwise null (see "Linking nested invocations" below)
UUID parentInvocationId = invocationContext.parentInvocationId();
String aiServiceInterfaceName = invocationContext.interfaceName();
String aiServiceMethodName = invocationContext.methodName();
List<Object> aiServiceMethodArgs = invocationContext.methodArguments();
Object chatMemoryId = invocationContext.chatMemoryId();
Instant eventTimestamp = invocationContext.timestamp();
// Do something with the data
}
}
// When creating your AI Service
MyAiServiceCompletedListener myListener = new MyAiServiceCompletedListener();
var myService = AiServices.builder(MyAiService.class)
.chatModel(chatModel) // Could also be .streamingChatModel(...)
.registerListener(myListener)
.build();
Listening for the whole invocation
Rather than listening for each individual event, you can register an AiServiceInvocationSummaryListener to receive a single AiServiceInvocationSummaryEvent once an invocation is over. Its events() method returns every event fired for the invocation (i.e. every event with the same invocationId), in the order they were fired, starting with the AiServiceStartedEvent and ending with the AiServiceCompletedEvent, AiServiceErrorEvent or AiServiceCancelledEvent. It also contains the other events of the invocation, such as RetrievalAugmentedEvents and ToolExecutedEvents. If the invocation calls other AI Services (for example from a tool), it also contains their AiServiceInvocationSummaryEvents, which have their own invocationId (see below).
var myService = AiServices.builder(MyAiService.class)
.chatModel(chatModel)
.registerListener((AiServiceInvocationSummaryListener) summary -> {
UUID invocationId = summary.invocationContext().invocationId();
List<AiServiceEvent> events = summary.events();
// Do something with the data
})
.build();
A few things to keep in mind:
- Events are collected for an invocation if an
AiServiceInvocationSummaryListeneris registered when it starts, or if it is nested in an invocation whose events are collected (see below). Otherwise no events are kept, so there is no overhead if you don't use a summary listener. - If an asynchronous or streaming invocation is canceled, it ends with an
AiServiceCancelledEvent, and theAiServiceInvocationSummaryEventcontains the events fired up until the cancellation, ending with theAiServiceCancelledEvent. - Events fired for an invocation after it has ended (for example
ToolCompensatedEvents fired once a canceled invocation has been rolled back) are not included. - An AI Service invoked while another AI Service invocation is in progress (for example, from within a tool) is a separate invocation with its own
invocationId. When it is linked to the outer invocation byInvocationContext.parentInvocationId()(see Linking nested invocations), it is nested in the outer summary:- Its
AiServiceInvocationSummaryEventis added to the outer invocation'sevents()when the inner invocation ends. The events of the inner invocation are only in that nested summary, which can itself contain the summaries of deeper invocations. - It is collected even if only the outer AI Service has a summary listener. Its events are then kept in memory until the outer invocation ends.
- Its summary is still delivered to the inner AI Service's own listeners. So a listener registered on both AI Services receives the inner summary twice: on its own, and inside the outer summary. When you add up tokens, costs or tool calls, count each invocation only once, for example by skipping the
AiServiceInvocationSummaryEvents found inevents(). - A summary listener of the outer AI Service also receives the prompts, responses and tool results of the inner invocations. If you filter or redact what you log or export, do the same for the nested summaries.
- Inner invocations called from the chat model, the chat memory or its
ChatMemoryStoreare nested. Inner invocations that end before the outer one has fired itsAiServiceStartedEvent(those called from theChatMemoryProvider, the system or user message provider or the system message transformer), or after the outer one has ended, are linked but not nested.
- Its
- For an AI Service method returning a
TokenStream, the invocation starts when the method is called but only ends once the stream has been started withstart(). Events collected for aTokenStreamthat is never started are never released, so always start the streams you create. Ifstart()fails because of a configuration error (for example, a missing error handler), the invocation ends with anAiServiceErrorEvent; callingstart()again once the configuration is fixed starts a new invocation. - Each subscription to the reactive stream returned by an AI Service method, and each call of
TokenStream.start(), is a separate invocation with its owninvocationId, so it results in its ownAiServiceInvocationSummaryEvent, also when several subscriptions run at the same time. A secondstart()of the sameTokenStreamreuses the retrieved contents, the input guardrails and the messages prepared when the AI Service method was called, so its summary contains noRetrievalAugmentedEventand no input guardrail events: only the model call, the tools and the output guardrails run again. - An
AiServiceInvocationSummaryEventis fired by the registrar itself. Trying to fire one viaAiServiceListenerRegistrar.fireEventresults in anIllegalArgumentException.
Linking nested invocations
An AI Service is often called from inside another AI Service, for example from one of its tools.
Each of the two calls is a separate invocation, with its own invocationId and its own events.
To link them, the InvocationContext of the inner invocation has parentInvocationId() (experimental):
the invocationId of the outer invocation, or null if there is none.
interface Assistant {
String chat(String message);
}
interface Translator {
String translate(String text);
}
class TranslationTools {
private final Translator translator;
TranslationTools(Translator translator) {
this.translator = translator;
}
@Tool("Translates a text to English")
String translateToEnglish(String text) {
return translator.translate(text); // the inner invocation
}
}
// Collects the summaries of inner invocations, grouped by the invocation that called them
Map<UUID, List<AiServiceInvocationSummaryEvent>> innerByOuter = new ConcurrentHashMap<>();
AiServiceInvocationSummaryListener listener = summary -> {
UUID parentInvocationId = summary.invocationContext().parentInvocationId();
if (parentInvocationId != null) {
innerByOuter.computeIfAbsent(parentInvocationId, id -> new CopyOnWriteArrayList<>()).add(summary);
}
};
Translator translator = AiServices.builder(Translator.class)
.chatModel(chatModel)
.registerListener(listener)
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(new TranslationTools(translator))
.registerListener(listener)
.build();
The inner invocation is linked to the outer one when it is called from one of these parts of the outer AI Service:
- a tool (
tools,toolProvider), a tool error handler (toolExecutionErrorHandler,toolArgumentsErrorHandler) or the strategy for hallucinated tool names (hallucinatedToolNameStrategy) - an input or output guardrail (
inputGuardrails,outputGuardrails) - a component of the retrieval augmentor (
retrievalAugmentor,contentRetriever), for example a query transformer, a query router or a content retriever - the system or user message provider (
systemMessageProvider,userMessageProvider) or the system message transformer (systemMessageTransformer) - the chat memory (
chatMemory,chatMemoryProvider), including itsChatMemoryStore, for example a chat memory that summarizes old messages with an AI Service - the moderation model (
moderationModel) - the
chatRequestTransformer - the chat model or streaming chat model itself (
chatModel,streamingChatModel), for example your ownChatModelimplementation
parentInvocationId() is null:
- for an AI Service that you call from your own code, for example from inside a listener, a
TokenStreamhandler, the subscriber of a returned stream or a callback of a returnedCompletableFuture, even while the outer invocation is still running - when your own code in one of the parts above calls the inner AI Service on another thread, for example with
CompletableFuture.supplyAsync(..., executor). Work that LangChain4j itself moves to another thread (for example tools executed concurrently) stays linked. So treat the link as best effort.
If you only need the summaries, a summary listener on the outer AI Service is enough: the summaries of the inner invocations linked to it are nested in its events() (see Listening for the whole invocation).
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(new TranslationTools(translator))
.registerListener((AiServiceInvocationSummaryListener) summary -> {
List<AiServiceInvocationSummaryEvent> innerSummaries = summary.events().stream()
.filter(AiServiceInvocationSummaryEvent.class::isInstance)
.map(AiServiceInvocationSummaryEvent.class::cast)
.toList();
})
.build();
Limits:
- It is filled by the AI Service implementation that LangChain4j provides. Frameworks that replace that implementation (for example through
AiServicesFactory) returnnulluntil they add support for it, also for AI Services they create throughAiServices.builder(...). - Agents run by an agentic workflow (sequence, parallel, loop, conditional) or by a supervisor are not linked yet. An agent called from a tool of another agent is.
Grouping model calls by invocation
The listeners of models and of other components get the invocationId of the AI Service invocation that made the call,
so you can see which model calls belong to which invocation.
The contexts of these listeners have invocationId() (experimental):
ChatModelListener(ChatModelRequestContext,ChatModelResponseContext,ChatModelErrorContext)EmbeddingModelListener,ModerationModelListener,DecisionModelListenerandScoringModelListenerEmbeddingStoreListenerandContentRetrieverListener
invocationId() is the id of the AI Service invocation in which the call is made: for a chat model, the one passed with
ChatRequestOptions.invocationId(...) if any, otherwise the invocation that makes the call (the AI Service's own calls to the
chat model, including the calls after tool executions, and the calls made by its tools, guardrails, retrieval, chat memory,
moderation, ...), otherwise null, for example when you call the model directly from your own code or from a listener.
It is the invocationId of the AI Service invocation, not an id of the model call.
Together with parentInvocationId() (see above), you can build a tree of invocations with their model calls.
// Counts the chat model calls of each AI Service invocation
Map<UUID, AtomicInteger> modelCallsByInvocation = new ConcurrentHashMap<>();
ChatModelListener listener = new ChatModelListener() {
@Override
public void onRequest(ChatModelRequestContext requestContext) {
UUID invocationId = requestContext.invocationId();
if (invocationId != null) { // null when the model is called outside of an AI Service invocation
modelCallsByInvocation.computeIfAbsent(invocationId, id -> new AtomicInteger()).incrementAndGet();
}
}
};
ChatModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.listeners(List.of(listener))
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.build();
To link model calls to something that is known to the AI Service but not to the model, for example the memory id
of the conversation, combine a model listener with AI Service listeners: the InvocationContext of the AI Service events
has both the invocationId() and the chatMemoryId().
interface PersonalAssistant {
String chat(@MemoryId String userId, @UserMessage String message);
}
// Adds up the token usage of each conversation
Map<UUID, Object> memoryIdByInvocation = new ConcurrentHashMap<>();
Map<Object, Integer> totalTokensByMemoryId = new ConcurrentHashMap<>();
ChatModelListener tokenCounter = new ChatModelListener() {
@Override
public void onResponse(ChatModelResponseContext responseContext) {
Object memoryId = responseContext.invocationId() == null
? null
: memoryIdByInvocation.get(responseContext.invocationId());
TokenUsage tokenUsage = responseContext.chatResponse().tokenUsage();
if (memoryId != null && tokenUsage != null && tokenUsage.totalTokenCount() != null) {
totalTokensByMemoryId.merge(memoryId, tokenUsage.totalTokenCount(), Integer::sum);
}
}
};
ChatModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.listeners(List.of(tokenCounter))
.build();
PersonalAssistant assistant = AiServices.builder(PersonalAssistant.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.registerListeners(
(AiServiceStartedListener) event -> memoryIdByInvocation.put(
event.invocationContext().invocationId(), event.invocationContext().chatMemoryId()),
(AiServiceCompletedListener) event -> memoryIdByInvocation.remove(event.invocationContext().invocationId()),
(AiServiceErrorListener) event -> memoryIdByInvocation.remove(event.invocationContext().invocationId()),
(AiServiceCancelledListener) event -> memoryIdByInvocation.remove(event.invocationContext().invocationId()))
.build();
The invocation id covers the AI Service's own calls to the chat model, including the calls after tool executions,
and the calls made by the parts of the AI Service listed above (tools, guardrails, retrieval, chat memory, moderation, ...),
in the same way as parentInvocationId().
You only need to pass the id yourself when your code moves a chat model call to another thread
(for example with CompletableFuture.supplyAsync(..., executor) inside a tool), or when you want to group your own chat model calls
under an invocation. This is possible only for chat models, with ChatRequestOptions: for the other models, embedding stores and
content retrievers, a call that your code moves to another thread has no invocationId. A tool gets the InvocationContext of its invocation as a parameter, and a guardrail gets it from its request
(request.requestParams().invocationContext()):
class ResearchTools {
private final ChatModel chatModel;
private final ExecutorService executor;
ResearchTools(ChatModel chatModel, ExecutorService executor) {
this.chatModel = chatModel;
this.executor = executor;
}
@Tool("Summarizes a text")
String summarize(String text, InvocationContext invocationContext) {
ChatRequest chatRequest = ChatRequest.builder()
.messages(UserMessage.from("Summarize: " + text))
.build();
ChatRequestOptions options = ChatRequestOptions.builder()
.invocationId(invocationContext.invocationId())
.build();
return CompletableFuture.supplyAsync(() -> chatModel.chat(chatRequest, options), executor)
.join()
.aiMessage()
.text();
}
}
Frameworks that run AI Services with their own implementation (for example through AiServicesFactory) may not fill the id,
or fill it only for some of the calls, depending on how they call the models.
Creating your own events and listeners
The AI Service observability capabilities are designed to be extensible. If you'd like to create your own events, you can do so by implementing the AiServiceEvent interface to define your own event.
Then, create your own event listener by implementing the AiServiceListener interface.
Once you have your event and listener, you need to fire the event by obtaining/managing an instance of AiServiceListenerRegistrar and calling the fireEvent(event) method.
Once the event is getting fired, you can then create listeners and register your listeners just like you would with the built-in events.
Extension points
You can also create your own custom AiServiceListenerRegistrar by implementing the AiServiceListenerRegistrarFactory and registering it with the Java Service Provider Interface (Java SPI).
This could be useful if you want to manage the way you register/unregister your listeners and/or how you want to fire your events.
Chat Model Observability
Certain implementations of ChatModel and StreamingChatModel
(see "Observability" column) allow configuring ChatModelListener(s) to listen for events such as:
- Requests to the LLM
- Response from the LLM
- Errors
These events include various attributes, as described in the OpenTelemetry Generative AI Semantic Conventions, such as:
- Request:
- Messages
- Model
- Temperature
- Top P
- Max Tokens
- Tools
- Response Format
- etc
- Response:
- Assistant Message
- ID
- Model
- Token Usage
- Finish Reason
- etc
Here is an example of using ChatModelListener:
ChatModelListener listener = new ChatModelListener() {
@Override
public void onRequest(ChatModelRequestContext requestContext) {
ChatRequest chatRequest = requestContext.chatRequest();
List<ChatMessage> messages = chatRequest.messages();
System.out.println(messages);
ChatRequestParameters parameters = chatRequest.parameters();
System.out.println(parameters.modelName());
System.out.println(parameters.temperature());
System.out.println(parameters.topP());
System.out.println(parameters.topK());
System.out.println(parameters.frequencyPenalty());
System.out.println(parameters.presencePenalty());
System.out.println(parameters.maxOutputTokens());
System.out.println(parameters.stopSequences());
System.out.println(parameters.toolSpecifications());
System.out.println(parameters.toolChoice());
System.out.println(parameters.responseFormat());
if (parameters instanceof OpenAiChatRequestParameters openAiParameters) {
System.out.println(openAiParameters.maxCompletionTokens());
System.out.println(openAiParameters.logitBias());
System.out.println(openAiParameters.parallelToolCalls());
System.out.println(openAiParameters.seed());
System.out.println(openAiParameters.user());
System.out.println(openAiParameters.store());
System.out.println(openAiParameters.metadata());
System.out.println(openAiParameters.serviceTier());
System.out.println(openAiParameters.reasoningEffort());
}
System.out.println(requestContext.modelProvider());
Map<Object, Object> attributes = requestContext.attributes();
attributes.put("my-attribute", "my-value");
}
@Override
public void onResponse(ChatModelResponseContext responseContext) {
ChatResponse chatResponse = responseContext.chatResponse();
AiMessage aiMessage = chatResponse.aiMessage();
System.out.println(aiMessage);
ChatResponseMetadata metadata = chatResponse.metadata();
System.out.println(metadata.id());
System.out.println(metadata.modelName());
System.out.println(metadata.finishReason());
if (metadata instanceof OpenAiChatResponseMetadata openAiMetadata) {
System.out.println(openAiMetadata.created());
System.out.println(openAiMetadata.serviceTier());
System.out.println(openAiMetadata.systemFingerprint());
}
TokenUsage tokenUsage = metadata.tokenUsage();
System.out.println(tokenUsage.inputTokenCount());
System.out.println(tokenUsage.outputTokenCount());
System.out.println(tokenUsage.totalTokenCount());
if (tokenUsage instanceof OpenAiTokenUsage openAiTokenUsage) {
System.out.println(openAiTokenUsage.inputTokensDetails().cachedTokens());
System.out.println(openAiTokenUsage.outputTokensDetails().reasoningTokens());
}
ChatRequest chatRequest = responseContext.chatRequest();
System.out.println(chatRequest);
System.out.println(responseContext.modelProvider());
Map<Object, Object> attributes = responseContext.attributes();
System.out.println(attributes.get("my-attribute"));
}
@Override
public void onError(ChatModelErrorContext errorContext) {
Throwable error = errorContext.error();
error.printStackTrace();
ChatRequest chatRequest = errorContext.chatRequest();
System.out.println(chatRequest);
System.out.println(errorContext.modelProvider());
Map<Object, Object> attributes = errorContext.attributes();
System.out.println(attributes.get("my-attribute"));
}
};
ChatModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.listeners(List.of(listener))
.build();
model.chat("Tell me a joke about Java");
The attributes map allows passing information between the onRequest, onResponse, and onError methods of the same
ChatModelListener, as well as between multiple ChatModelListeners.
If you need to provide per-invocation metadata to listeners, use ChatRequestOptions.
For example, you can pass tenant or correlation identifiers to ChatModelListeners
through listenerAttributes. These options are used only within the LangChain4j invocation chain;
they are not sent to the LLM provider.
ChatRequest chatRequest = ChatRequest.builder()
.messages(UserMessage.from("Tell me a joke about Java"))
.build();
ChatRequestOptions options = ChatRequestOptions.builder()
.addListenerAttribute("tenantId", "tenant-123")
.addListenerAttribute("correlationId", "corr-456")
.build();
model.chat(chatRequest, options);
The same applies to StreamingChatModel.chat(chatRequest, options, handler).
How listeners work
- Listeners are specified as a
List<ChatModelListener>and are called in the order of iteration. - Listeners are called synchronously and in the same thread. See more details about the streaming case below. The second listener is not called until the first one returns.
- The
ChatModelListener.onRequest()method is called right before calling the LLM provider API. - The
ChatModelListener.onRequest()method is called only once per request. If an error occurs while calling the LLM provider API and a retry happens,ChatModelListener.onRequest()will not be called for every retry. - The
ChatModelListener.onResponse()method is called only once, immediately after receiving a successful response from the LLM provider. - The
ChatModelListener.onError()method is called only once. If an error occurs while calling the LLM provider API and a retry happens,ChatModelListener.onError()will not be called for every retry. - If an exception is thrown from one of the
ChatModelListenermethods, it will be logged at theWARNlevel. The execution of subsequent listeners will continue as usual. - The
ChatRequestprovided viaChatModelRequestContext,ChatModelResponseContext, andChatModelErrorContextis the final request, containing both the defaultChatRequestParametersconfigured on theChatModeland the request-specificChatRequestParametersmerged together. - For
StreamingChatModel, theChatModelListener.onResponse()andChatModelListener.onError()are called on a different thread than theChatModelListener.onRequest(). The thread context is currently not propagated automatically, so you might want to use theattributesmap to propagate any necessary data fromChatModelListener.onRequest()toChatModelListener.onResponse()orChatModelListener.onError(). - For
StreamingChatModel, theChatModelListener.onResponse()is called before theStreamingChatResponseHandler.onCompleteResponse()is called. TheChatModelListener.onError()is called before theStreamingChatResponseHandler.onError()is called.