15
15
*/
16
16
package org .springframework .ai .openai ;
17
17
18
- import java .util .ArrayList ;
19
- import java .util .Base64 ;
20
- import java .util .HashSet ;
21
- import java .util .List ;
22
- import java .util .Map ;
23
- import java .util .Set ;
24
- import java .util .concurrent .ConcurrentHashMap ;
25
-
18
+ import io .micrometer .observation .ObservationRegistry ;
26
19
import org .slf4j .Logger ;
27
20
import org .slf4j .LoggerFactory ;
28
21
import org .springframework .ai .chat .messages .AssistantMessage ;
33
26
import org .springframework .ai .chat .metadata .ChatResponseMetadata ;
34
27
import org .springframework .ai .chat .metadata .EmptyUsage ;
35
28
import org .springframework .ai .chat .metadata .RateLimit ;
36
- import org .springframework .ai .chat .model .AbstractToolCallSupport ;
37
- import org .springframework .ai .chat .model .ChatModel ;
38
- import org .springframework .ai .chat .model .ChatResponse ;
39
- import org .springframework .ai .chat .model .Generation ;
40
- import org .springframework .ai .chat .model .StreamingChatModel ;
29
+ import org .springframework .ai .chat .model .*;
30
+ import org .springframework .ai .chat .observation .*;
41
31
import org .springframework .ai .chat .prompt .ChatOptions ;
42
32
import org .springframework .ai .chat .prompt .Prompt ;
43
33
import org .springframework .ai .model .ModelOptionsUtils ;
44
34
import org .springframework .ai .model .function .FunctionCallback ;
45
35
import org .springframework .ai .model .function .FunctionCallbackContext ;
36
+ import org .springframework .ai .observation .AiOperationMetadata ;
37
+ import org .springframework .ai .observation .conventions .AiOperationType ;
38
+ import org .springframework .ai .observation .conventions .AiProvider ;
46
39
import org .springframework .ai .openai .api .OpenAiApi ;
47
40
import org .springframework .ai .openai .api .OpenAiApi .ChatCompletion ;
48
41
import org .springframework .ai .openai .api .OpenAiApi .ChatCompletion .Choice ;
59
52
import org .springframework .util .Assert ;
60
53
import org .springframework .util .CollectionUtils ;
61
54
import org .springframework .util .MimeType ;
62
-
55
+ import org . springframework . util . StringUtils ;
63
56
import reactor .core .publisher .Flux ;
64
57
import reactor .core .publisher .Mono ;
65
58
59
+ import java .util .*;
60
+ import java .util .concurrent .ConcurrentHashMap ;
61
+
66
62
/**
67
63
* {@link ChatModel} and {@link StreamingChatModel} implementation for {@literal OpenAI}
68
64
* backed by {@link OpenAiApi}.
@@ -86,6 +82,8 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
86
82
87
83
private static final Logger logger = LoggerFactory .getLogger (OpenAiChatModel .class );
88
84
85
+ private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention ();
86
+
89
87
/**
90
88
* The default options used for the chat completion requests.
91
89
*/
@@ -101,6 +99,16 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
101
99
*/
102
100
private final OpenAiApi openAiApi ;
103
101
102
+ /**
103
+ * Observation registry used for instrumentation.
104
+ */
105
+ private final ObservationRegistry observationRegistry ;
106
+
107
+ /**
108
+ * Conventions to use for generating observations.
109
+ */
110
+ private ChatModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION ;
111
+
104
112
/**
105
113
* Creates an instance of the OpenAiChatModel.
106
114
* @param openAiApi The OpenAiApi instance to be used for interacting with the OpenAI
@@ -147,65 +155,101 @@ public OpenAiChatModel(OpenAiApi openAiApi, OpenAiChatOptions options,
147
155
public OpenAiChatModel (OpenAiApi openAiApi , OpenAiChatOptions options ,
148
156
FunctionCallbackContext functionCallbackContext , List <FunctionCallback > toolFunctionCallbacks ,
149
157
RetryTemplate retryTemplate ) {
158
+ this (openAiApi , options , functionCallbackContext , toolFunctionCallbacks , retryTemplate ,
159
+ ObservationRegistry .NOOP );
160
+ }
161
+
162
+ /**
163
+ * Initializes a new instance of the OpenAiChatModel.
164
+ * @param openAiApi The OpenAiApi instance to be used for interacting with the OpenAI
165
+ * Chat API.
166
+ * @param options The OpenAiChatOptions to configure the chat model.
167
+ * @param functionCallbackContext The function callback context.
168
+ * @param toolFunctionCallbacks The tool function callbacks.
169
+ * @param retryTemplate The retry template.
170
+ * @param observationRegistry The ObservationRegistry used for instrumentation.
171
+ */
172
+ public OpenAiChatModel (OpenAiApi openAiApi , OpenAiChatOptions options ,
173
+ FunctionCallbackContext functionCallbackContext , List <FunctionCallback > toolFunctionCallbacks ,
174
+ RetryTemplate retryTemplate , ObservationRegistry observationRegistry ) {
150
175
super (functionCallbackContext , options , toolFunctionCallbacks );
151
176
152
177
Assert .notNull (openAiApi , "OpenAiApi must not be null" );
153
178
Assert .notNull (options , "Options must not be null" );
154
179
Assert .notNull (retryTemplate , "RetryTemplate must not be null" );
155
180
Assert .isTrue (CollectionUtils .isEmpty (options .getFunctionCallbacks ()),
156
181
"The default function callbacks must be set via the toolFunctionCallbacks constructor parameter" );
182
+ Assert .notNull (observationRegistry , "ObservationRegistry must not be null" );
157
183
158
184
this .openAiApi = openAiApi ;
159
185
this .defaultOptions = options ;
160
186
this .retryTemplate = retryTemplate ;
187
+ this .observationRegistry = observationRegistry ;
161
188
}
162
189
163
190
@ Override
164
191
public ChatResponse call (Prompt prompt ) {
165
192
166
193
ChatCompletionRequest request = createRequest (prompt , false );
167
194
168
- ResponseEntity <ChatCompletion > completionEntity = this .retryTemplate
169
- .execute (ctx -> this .openAiApi .chatCompletionEntity (request ));
195
+ ChatModelObservationContext observationContext = ChatModelObservationContext .builder ()
196
+ .prompt (prompt )
197
+ .operationMetadata (buildOperationMetadata ())
198
+ .requestOptions (buildRequestOptions (request ))
199
+ .build ();
170
200
171
- var chatCompletion = completionEntity .getBody ();
201
+ ChatResponse response = ChatModelObservationDocumentation .CHAT_MODEL_OPERATION
202
+ .observation (this .observationConvention , DEFAULT_OBSERVATION_CONVENTION , () -> observationContext ,
203
+ this .observationRegistry )
204
+ .observe (() -> {
172
205
173
- if (chatCompletion == null ) {
174
- logger .warn ("No chat completion returned for prompt: {}" , prompt );
175
- return new ChatResponse (List .of ());
176
- }
206
+ ResponseEntity <ChatCompletion > completionEntity = this .retryTemplate
207
+ .execute (ctx -> this .openAiApi .chatCompletionEntity (request ));
177
208
178
- List <Choice > choices = chatCompletion .choices ();
179
- if (choices == null ) {
180
- logger .warn ("No choices returned for prompt: {}" , prompt );
181
- return new ChatResponse (List .of ());
182
- }
209
+ var chatCompletion = completionEntity .getBody ();
210
+
211
+ if (chatCompletion == null ) {
212
+ logger .warn ("No chat completion returned for prompt: {}" , prompt );
213
+ return new ChatResponse (List .of ());
214
+ }
183
215
184
- List <Generation > generations = choices .stream ().map (choice -> {
216
+ List <Choice > choices = chatCompletion .choices ();
217
+ if (choices == null ) {
218
+ logger .warn ("No choices returned for prompt: {}" , prompt );
219
+ return new ChatResponse (List .of ());
220
+ }
221
+
222
+ List <Generation > generations = choices .stream ().map (choice -> {
185
223
// @formatter:off
186
- Map <String , Object > metadata = Map .of (
187
- "id" , chatCompletion .id () != null ? chatCompletion .id () : "" ,
188
- "role" , choice .message ().role () != null ? choice .message ().role ().name () : "" ,
189
- "index" , choice .index (),
190
- "finishReason" , choice .finishReason () != null ? choice .finishReason ().name () : "" );
191
- // @formatter:on
192
- return buildGeneration (choice , metadata );
193
- }).toList ();
224
+ Map <String , Object > metadata = Map .of (
225
+ "id" , chatCompletion .id () != null ? chatCompletion .id () : "" ,
226
+ "role" , choice .message ().role () != null ? choice .message ().role ().name () : "" ,
227
+ "index" , choice .index (),
228
+ "finishReason" , choice .finishReason () != null ? choice .finishReason ().name () : "" );
229
+ // @formatter:on
230
+ return buildGeneration (choice , metadata );
231
+ }).toList ();
232
+
233
+ // Non function calling.
234
+ RateLimit rateLimit = OpenAiResponseHeaderExtractor .extractAiResponseHeaders (completionEntity );
235
+
236
+ ChatResponse chatResponse = new ChatResponse (generations , from (completionEntity .getBody (), rateLimit ));
194
237
195
- // Non function calling.
196
- RateLimit rateLimit = OpenAiResponseHeaderExtractor .extractAiResponseHeaders (completionEntity );
238
+ observationContext .setResponse (chatResponse );
197
239
198
- ChatResponse chatResponse = new ChatResponse ( generations , from ( completionEntity . getBody (), rateLimit )) ;
240
+ return chatResponse ;
199
241
200
- if (isToolCall (chatResponse , Set .of (OpenAiApi .ChatCompletionFinishReason .TOOL_CALLS .name (),
242
+ });
243
+
244
+ if (response != null && isToolCall (response , Set .of (OpenAiApi .ChatCompletionFinishReason .TOOL_CALLS .name (),
201
245
OpenAiApi .ChatCompletionFinishReason .STOP .name ()))) {
202
- var toolCallConversation = handleToolCalls (prompt , chatResponse );
246
+ var toolCallConversation = handleToolCalls (prompt , response );
203
247
// Recursively call the call method with the tool call message
204
248
// conversation that contains the call responses.
205
249
return this .call (new Prompt (toolCallConversation , prompt .getOptions ()));
206
250
}
207
251
208
- return chatResponse ;
252
+ return response ;
209
253
}
210
254
211
255
@ Override
@@ -434,6 +478,25 @@ private List<OpenAiApi.FunctionTool> getFunctionTools(Set<String> functionNames)
434
478
}).toList ();
435
479
}
436
480
481
+ private AiOperationMetadata buildOperationMetadata () {
482
+ return AiOperationMetadata .builder ()
483
+ .operationType (AiOperationType .CHAT .value ())
484
+ .provider (AiProvider .OPENAI .value ())
485
+ .build ();
486
+ }
487
+
488
+ private ChatModelRequestOptions buildRequestOptions (OpenAiApi .ChatCompletionRequest request ) {
489
+ return ChatModelRequestOptions .builder ()
490
+ .model (StringUtils .hasText (request .model ()) ? request .model () : "unknown" )
491
+ .frequencyPenalty (request .frequencyPenalty ())
492
+ .maxTokens (request .maxTokens ())
493
+ .presencePenalty (request .presencePenalty ())
494
+ .stopSequences (request .stop ())
495
+ .temperature (request .temperature ())
496
+ .topP (request .topP ())
497
+ .build ();
498
+ }
499
+
437
500
@ Override
438
501
public ChatOptions getDefaultOptions () {
439
502
return OpenAiChatOptions .fromOptions (this .defaultOptions );
@@ -444,4 +507,13 @@ public String toString() {
444
507
return "OpenAiChatModel [defaultOptions=" + defaultOptions + "]" ;
445
508
}
446
509
510
+ /**
511
+ * Use the provided convention for reporting observation data
512
+ * @param observationConvention The provided convention
513
+ */
514
+ public void setObservationConvention (ChatModelObservationConvention observationConvention ) {
515
+ Assert .notNull (observationConvention , "observationConvention cannot be null" );
516
+ this .observationConvention = observationConvention ;
517
+ }
518
+
447
519
}
0 commit comments