99import com .openai .models .responses .Response ;
1010import com .openai .models .responses .ResponseCreateParams ;
1111import com .openai .models .responses .ResponseFunctionToolCall ;
12+ import com .openai .models .responses .ResponseInputContent ;
13+ import com .openai .models .responses .ResponseInputItem ;
1214import com .openai .models .responses .ResponseOutputItem ;
1315import com .openai .models .responses .ResponseOutputMessage ;
1416import com .openai .models .responses .ResponseOutputText ;
@@ -54,11 +56,49 @@ public void withResponseCreateParams(AgentSpan span, ResponseCreateParams params
5456 inputMessages .add (LLMObs .LLMMessage .from ("system" , instructions ));
5557 });
5658
57- Optional <String > textOpt = params ._input ().asString ();
59+ Optional <String > textOpt = params ._input ().asString (); // TODO cover with unit tests
5860 if (textOpt .isPresent ()) {
5961 inputMessages .add (LLMObs .LLMMessage .from ("user" , textOpt .get ()));
6062 }
6163
64+ Optional <ResponseCreateParams .Input > inputOpt = params ._input ().asKnown ();
65+ if (inputOpt .isPresent ()) {
66+ ResponseCreateParams .Input input = inputOpt .get ();
67+ if (input .isText ()) {
68+ inputMessages .add (LLMObs .LLMMessage .from ("user" , input .asText ()));
69+ } else if (input .isResponse ()) {
70+ List <ResponseInputItem > inputItems = input .asResponse ();
71+ for (ResponseInputItem item : inputItems ) {
72+ LLMObs .LLMMessage message = extractInputItemMessage (item );
73+ if (message != null ) {
74+ inputMessages .add (message );
75+ }
76+ }
77+ }
78+ }
79+
80+ // Handle raw list input (when SDK can't parse into known types)
81+ // This path is tested by "create streaming response with raw json tool input test"
82+ if (inputMessages .isEmpty ()) {
83+ try {
84+ Optional <com .openai .core .JsonValue > rawValueOpt = params ._input ().asUnknown ();
85+ if (rawValueOpt .isPresent ()) {
86+ com .openai .core .JsonValue rawValue = rawValueOpt .get ();
87+ Optional <List <com .openai .core .JsonValue >> rawListOpt = rawValue .asArray ();
88+ if (rawListOpt .isPresent ()) {
89+ for (com .openai .core .JsonValue item : rawListOpt .get ()) {
90+ LLMObs .LLMMessage message = extractMessageFromRawJson (item );
91+ if (message != null ) {
92+ inputMessages .add (message );
93+ }
94+ }
95+ }
96+ }
97+ } catch (Exception e ) {
98+ // Ignore parsing errors for raw input
99+ }
100+ }
101+
62102 if (!inputMessages .isEmpty ()) {
63103 span .setTag ("_ml_obs_tag.input" , inputMessages );
64104 }
@@ -67,6 +107,228 @@ public void withResponseCreateParams(AgentSpan span, ResponseCreateParams params
67107 .ifPresent (reasoningMap -> span .setTag ("_ml_obs_request.reasoning" , reasoningMap ));
68108 }
69109
110+ private LLMObs .LLMMessage extractInputItemMessage (ResponseInputItem item ) {
111+ if (item .isMessage ()) {
112+ ResponseInputItem .Message message = item .asMessage ();
113+ String role = message .role ().asString ();
114+ String content = extractInputMessageContent (message );
115+ return LLMObs .LLMMessage .from (role , content );
116+ } else if (item .isFunctionCall ()) {
117+ // Function call is mapped to assistant message with tool_calls
118+ ResponseFunctionToolCall functionCall = item .asFunctionCall ();
119+ LLMObs .ToolCall toolCall = ToolCallExtractor .getToolCall (functionCall );
120+ if (toolCall != null ) {
121+ List <LLMObs .ToolCall > toolCalls = Collections .singletonList (toolCall );
122+ return LLMObs .LLMMessage .from ("assistant" , null , toolCalls );
123+ }
124+ } else if (item .isFunctionCallOutput ()) {
125+ ResponseInputItem .FunctionCallOutput output = item .asFunctionCallOutput ();
126+ String callId = output .callId ();
127+ String result =
128+ output
129+ .output (); // TODO ResponseInputItem.FunctionCallOutput.output changed from String to
130+ // Output in 4.0+
131+ LLMObs .ToolResult toolResult =
132+ LLMObs .ToolResult .from ("" , "function_call_output" , callId , result );
133+ List <LLMObs .ToolResult > toolResults = Collections .singletonList (toolResult );
134+ return LLMObs .LLMMessage .fromToolResults ("user" , toolResults );
135+ }
136+ return null ;
137+ }
138+
139+ private LLMObs .LLMMessage extractMessageFromRawJson (com .openai .core .JsonValue jsonValue ) {
140+ Optional <Map <String , com .openai .core .JsonValue >> objOpt = jsonValue .asObject ();
141+ if (!objOpt .isPresent ()) {
142+ return null ;
143+ }
144+
145+ Map <String , com .openai .core .JsonValue > obj = objOpt .get ();
146+ com .openai .core .JsonValue typeValue = obj .get ("type" );
147+
148+ // Check if it's a function_call
149+ if (typeValue != null ) {
150+ Optional <String > typeStr = typeValue .asString ();
151+ if (typeStr .isPresent ()) {
152+ String type = typeStr .get ();
153+
154+ if ("function_call" .equals (type )) {
155+ // Extract function call details
156+ com .openai .core .JsonValue callIdValue = obj .get ("call_id" );
157+ com .openai .core .JsonValue nameValue = obj .get ("name" );
158+ com .openai .core .JsonValue argumentsValue = obj .get ("arguments" );
159+
160+ String callId = null ;
161+ String name = null ;
162+ String argumentsStr = null ;
163+
164+ if (callIdValue != null ) {
165+ Optional <String > opt = callIdValue .asString ();
166+ if (opt .isPresent ()) {
167+ callId = opt .get ();
168+ }
169+ }
170+ if (nameValue != null ) {
171+ Optional <String > opt = nameValue .asString ();
172+ if (opt .isPresent ()) {
173+ name = opt .get ();
174+ }
175+ }
176+ if (argumentsValue != null ) {
177+ Optional <String > opt = argumentsValue .asString ();
178+ if (opt .isPresent ()) {
179+ argumentsStr = opt .get ();
180+ }
181+ }
182+
183+ if (callId != null && name != null && argumentsStr != null ) {
184+ Map <String , Object > arguments = parseJsonString (argumentsStr );
185+ LLMObs .ToolCall toolCall =
186+ LLMObs .ToolCall .from (name , "function_call" , callId , arguments );
187+ return LLMObs .LLMMessage .from ("assistant" , null , Collections .singletonList (toolCall ));
188+ }
189+ } else if ("function_call_output" .equals (type )) {
190+ // Extract function call output
191+ com .openai .core .JsonValue callIdValue = obj .get ("call_id" );
192+ com .openai .core .JsonValue outputValue = obj .get ("output" );
193+
194+ String callId = null ;
195+ String output = null ;
196+
197+ if (callIdValue != null ) {
198+ Optional <String > opt = callIdValue .asString ();
199+ if (opt .isPresent ()) {
200+ callId = opt .get ();
201+ }
202+ }
203+ if (outputValue != null ) {
204+ Optional <String > opt = outputValue .asString ();
205+ if (opt .isPresent ()) {
206+ output = opt .get ();
207+ }
208+ }
209+
210+ if (callId != null && output != null ) {
211+ LLMObs .ToolResult toolResult =
212+ LLMObs .ToolResult .from ("" , "function_call_output" , callId , output );
213+ return LLMObs .LLMMessage .fromToolResults ("user" , Collections .singletonList (toolResult ));
214+ }
215+ }
216+ }
217+ }
218+
219+ // Otherwise, it's a regular message with role and content
220+ com .openai .core .JsonValue roleValue = obj .get ("role" );
221+ com .openai .core .JsonValue contentValue = obj .get ("content" );
222+
223+ String role = null ;
224+ String content = null ;
225+
226+ if (roleValue != null ) {
227+ Optional <String > opt = roleValue .asString ();
228+ if (opt .isPresent ()) {
229+ role = opt .get ();
230+ }
231+ }
232+ if (contentValue != null ) {
233+ Optional <String > opt = contentValue .asString ();
234+ if (opt .isPresent ()) {
235+ content = opt .get ();
236+ }
237+ }
238+
239+ if (role != null ) {
240+ return LLMObs .LLMMessage .from (role , content );
241+ }
242+
243+ return null ;
244+ }
245+
246+ private Map <String , Object > parseJsonString (String jsonStr ) {
247+ if (jsonStr == null || jsonStr .isEmpty ()) {
248+ return Collections .emptyMap ();
249+ }
250+ try {
251+ jsonStr = jsonStr .trim ();
252+ if (!jsonStr .startsWith ("{" ) || !jsonStr .endsWith ("}" )) {
253+ return Collections .emptyMap ();
254+ }
255+
256+ Map <String , Object > result = new HashMap <>();
257+ String content = jsonStr .substring (1 , jsonStr .length () - 1 ).trim ();
258+
259+ if (content .isEmpty ()) {
260+ return result ;
261+ }
262+
263+ // Parse JSON manually, respecting quoted strings
264+ List <String > pairs = splitByCommaRespectingQuotes (content );
265+
266+ for (String pair : pairs ) {
267+ int colonIdx = pair .indexOf (':' );
268+ if (colonIdx > 0 ) {
269+ String key = pair .substring (0 , colonIdx ).trim ();
270+ String value = pair .substring (colonIdx + 1 ).trim ();
271+
272+ // Remove quotes from key
273+ key = removeQuotes (key );
274+ // Remove quotes from value
275+ value = removeQuotes (value );
276+
277+ result .put (key , value );
278+ }
279+ }
280+
281+ return result ;
282+ } catch (Exception e ) {
283+ return Collections .emptyMap ();
284+ }
285+ }
286+
287+ private List <String > splitByCommaRespectingQuotes (String str ) {
288+ List <String > result = new ArrayList <>();
289+ StringBuilder current = new StringBuilder ();
290+ boolean inQuotes = false ;
291+
292+ for (int i = 0 ; i < str .length (); i ++) {
293+ char c = str .charAt (i );
294+
295+ if (c == '"' ) {
296+ inQuotes = !inQuotes ;
297+ current .append (c );
298+ } else if (c == ',' && !inQuotes ) {
299+ result .add (current .toString ());
300+ current = new StringBuilder ();
301+ } else {
302+ current .append (c );
303+ }
304+ }
305+
306+ if (current .length () > 0 ) {
307+ result .add (current .toString ());
308+ }
309+
310+ return result ;
311+ }
312+
313+ private String removeQuotes (String str ) {
314+ str = str .trim ();
315+ if (str .startsWith ("\" " ) && str .endsWith ("\" " ) && str .length () >= 2 ) {
316+ return str .substring (1 , str .length () - 1 );
317+ }
318+ return str ;
319+ }
320+
321+ private String extractInputMessageContent (ResponseInputItem .Message message ) {
322+ StringBuilder contentBuilder = new StringBuilder ();
323+ for (ResponseInputContent content : message .content ()) {
324+ if (content .isInputText ()) {
325+ contentBuilder .append (content .asInputText ().text ());
326+ }
327+ }
328+ String result = contentBuilder .toString ();
329+ return result .isEmpty () ? null : result ;
330+ }
331+
70332 private Optional <Map <String , String >> extractReasoningFromParams (ResponseCreateParams params ) {
71333 com .openai .core .JsonField <Reasoning > reasoningField = params ._reasoning ();
72334 if (reasoningField .isMissing ()) {
0 commit comments