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 (); // TODO cover with unit tests
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,225 @@ 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 = FunctionCallOutputExtractor .getOutputAsString (output );
128+ LLMObs .ToolResult toolResult =
129+ LLMObs .ToolResult .from ("" , "function_call_output" , callId , result );
130+ List <LLMObs .ToolResult > toolResults = Collections .singletonList (toolResult );
131+ return LLMObs .LLMMessage .fromToolResults ("user" , toolResults );
132+ }
133+ return null ;
134+ }
135+
136+ private LLMObs .LLMMessage extractMessageFromRawJson (com .openai .core .JsonValue jsonValue ) {
137+ Optional <Map <String , com .openai .core .JsonValue >> objOpt = jsonValue .asObject ();
138+ if (!objOpt .isPresent ()) {
139+ return null ;
140+ }
141+
142+ Map <String , com .openai .core .JsonValue > obj = objOpt .get ();
143+ com .openai .core .JsonValue typeValue = obj .get ("type" );
144+
145+ // Check if it's a function_call
146+ if (typeValue != null ) {
147+ Optional <String > typeStr = typeValue .asString ();
148+ if (typeStr .isPresent ()) {
149+ String type = typeStr .get ();
150+
151+ if ("function_call" .equals (type )) {
152+ // Extract function call details
153+ com .openai .core .JsonValue callIdValue = obj .get ("call_id" );
154+ com .openai .core .JsonValue nameValue = obj .get ("name" );
155+ com .openai .core .JsonValue argumentsValue = obj .get ("arguments" );
156+
157+ String callId = null ;
158+ String name = null ;
159+ String argumentsStr = null ;
160+
161+ if (callIdValue != null ) {
162+ Optional <String > opt = callIdValue .asString ();
163+ if (opt .isPresent ()) {
164+ callId = opt .get ();
165+ }
166+ }
167+ if (nameValue != null ) {
168+ Optional <String > opt = nameValue .asString ();
169+ if (opt .isPresent ()) {
170+ name = opt .get ();
171+ }
172+ }
173+ if (argumentsValue != null ) {
174+ Optional <String > opt = argumentsValue .asString ();
175+ if (opt .isPresent ()) {
176+ argumentsStr = opt .get ();
177+ }
178+ }
179+
180+ if (callId != null && name != null && argumentsStr != null ) {
181+ Map <String , Object > arguments = parseJsonString (argumentsStr );
182+ LLMObs .ToolCall toolCall =
183+ LLMObs .ToolCall .from (name , "function_call" , callId , arguments );
184+ return LLMObs .LLMMessage .from ("assistant" , null , Collections .singletonList (toolCall ));
185+ }
186+ } else if ("function_call_output" .equals (type )) {
187+ // Extract function call output
188+ com .openai .core .JsonValue callIdValue = obj .get ("call_id" );
189+ com .openai .core .JsonValue outputValue = obj .get ("output" );
190+
191+ String callId = null ;
192+ String output = null ;
193+
194+ if (callIdValue != null ) {
195+ Optional <String > opt = callIdValue .asString ();
196+ if (opt .isPresent ()) {
197+ callId = opt .get ();
198+ }
199+ }
200+ if (outputValue != null ) {
201+ Optional <String > opt = outputValue .asString ();
202+ if (opt .isPresent ()) {
203+ output = opt .get ();
204+ }
205+ }
206+
207+ if (callId != null && output != null ) {
208+ LLMObs .ToolResult toolResult =
209+ LLMObs .ToolResult .from ("" , "function_call_output" , callId , output );
210+ return LLMObs .LLMMessage .fromToolResults ("user" , Collections .singletonList (toolResult ));
211+ }
212+ }
213+ }
214+ }
215+
216+ // Otherwise, it's a regular message with role and content
217+ com .openai .core .JsonValue roleValue = obj .get ("role" );
218+ com .openai .core .JsonValue contentValue = obj .get ("content" );
219+
220+ String role = null ;
221+ String content = null ;
222+
223+ if (roleValue != null ) {
224+ Optional <String > opt = roleValue .asString ();
225+ if (opt .isPresent ()) {
226+ role = opt .get ();
227+ }
228+ }
229+ if (contentValue != null ) {
230+ Optional <String > opt = contentValue .asString ();
231+ if (opt .isPresent ()) {
232+ content = opt .get ();
233+ }
234+ }
235+
236+ if (role != null ) {
237+ return LLMObs .LLMMessage .from (role , content );
238+ }
239+
240+ return null ;
241+ }
242+
243+ private Map <String , Object > parseJsonString (String jsonStr ) {
244+ if (jsonStr == null || jsonStr .isEmpty ()) {
245+ return Collections .emptyMap ();
246+ }
247+ try {
248+ jsonStr = jsonStr .trim ();
249+ if (!jsonStr .startsWith ("{" ) || !jsonStr .endsWith ("}" )) {
250+ return Collections .emptyMap ();
251+ }
252+
253+ Map <String , Object > result = new HashMap <>();
254+ String content = jsonStr .substring (1 , jsonStr .length () - 1 ).trim ();
255+
256+ if (content .isEmpty ()) {
257+ return result ;
258+ }
259+
260+ // Parse JSON manually, respecting quoted strings
261+ List <String > pairs = splitByCommaRespectingQuotes (content );
262+
263+ for (String pair : pairs ) {
264+ int colonIdx = pair .indexOf (':' );
265+ if (colonIdx > 0 ) {
266+ String key = pair .substring (0 , colonIdx ).trim ();
267+ String value = pair .substring (colonIdx + 1 ).trim ();
268+
269+ // Remove quotes from key
270+ key = removeQuotes (key );
271+ // Remove quotes from value
272+ value = removeQuotes (value );
273+
274+ result .put (key , value );
275+ }
276+ }
277+
278+ return result ;
279+ } catch (Exception e ) {
280+ return Collections .emptyMap ();
281+ }
282+ }
283+
284+ private List <String > splitByCommaRespectingQuotes (String str ) {
285+ List <String > result = new ArrayList <>();
286+ StringBuilder current = new StringBuilder ();
287+ boolean inQuotes = false ;
288+
289+ for (int i = 0 ; i < str .length (); i ++) {
290+ char c = str .charAt (i );
291+
292+ if (c == '"' ) {
293+ inQuotes = !inQuotes ;
294+ current .append (c );
295+ } else if (c == ',' && !inQuotes ) {
296+ result .add (current .toString ());
297+ current = new StringBuilder ();
298+ } else {
299+ current .append (c );
300+ }
301+ }
302+
303+ if (current .length () > 0 ) {
304+ result .add (current .toString ());
305+ }
306+
307+ return result ;
308+ }
309+
310+ private String removeQuotes (String str ) {
311+ str = str .trim ();
312+ if (str .startsWith ("\" " ) && str .endsWith ("\" " ) && str .length () >= 2 ) {
313+ return str .substring (1 , str .length () - 1 );
314+ }
315+ return str ;
316+ }
317+
318+ private String extractInputMessageContent (ResponseInputItem .Message message ) {
319+ StringBuilder contentBuilder = new StringBuilder ();
320+ for (ResponseInputContent content : message .content ()) {
321+ if (content .isInputText ()) {
322+ contentBuilder .append (content .asInputText ().text ());
323+ }
324+ }
325+ String result = contentBuilder .toString ();
326+ return result .isEmpty () ? null : result ;
327+ }
328+
70329 private Optional <Map <String , String >> extractReasoningFromParams (ResponseCreateParams params ) {
71330 com .openai .core .JsonField <Reasoning > reasoningField = params ._reasoning ();
72331 if (reasoningField .isMissing ()) {
0 commit comments