@@ -60,6 +60,15 @@ public static bool TryGetOpenAIRequest(string content, ILogger logger, out OpenA
6060 return true ;
6161 }
6262
63+ // Responses API request - has "input" array with objects containing role/content
64+ // Must be checked before embedding request because both have "input"
65+ if ( IsResponsesApiRequest ( rawRequest ) )
66+ {
67+ logger . LogDebug ( "Request is a Responses API request" ) ;
68+ request = JsonSerializer . Deserialize < OpenAIResponsesRequest > ( content , ProxyUtils . JsonSerializerOptions ) ;
69+ return true ;
70+ }
71+
6372 // Embedding request
6473 if ( rawRequest . TryGetProperty ( "input" , out _ ) &&
6574 rawRequest . TryGetProperty ( "model" , out _ ) &&
@@ -112,6 +121,73 @@ public static bool TryGetOpenAIRequest(string content, ILogger logger, out OpenA
112121 return false ;
113122 }
114123 }
124+
125+ /// <summary>
126+ /// Tries to parse text generation OpenAI requests (completion, chat completion, and responses API).
127+ /// Used by plugins that only need to handle text-based generation requests, as opposed to
128+ /// embeddings, audio, images, or fine-tuning requests.
129+ /// </summary>
130+ public static bool TryGetCompletionLikeRequest ( string content , ILogger logger , out OpenAIRequest ? request )
131+ {
132+ logger . LogTrace ( "{Method} called" , nameof ( TryGetCompletionLikeRequest ) ) ;
133+
134+ request = null ;
135+
136+ if ( string . IsNullOrEmpty ( content ) )
137+ {
138+ logger . LogDebug ( "Request content is empty or null" ) ;
139+ return false ;
140+ }
141+
142+ try
143+ {
144+ logger . LogDebug ( "Checking if the request is a completion-like OpenAI request..." ) ;
145+
146+ var rawRequest = JsonSerializer . Deserialize < JsonElement > ( content , ProxyUtils . JsonSerializerOptions ) ;
147+
148+ // Completion request
149+ if ( rawRequest . TryGetProperty ( "prompt" , out _ ) )
150+ {
151+ logger . LogDebug ( "Request is a completion request" ) ;
152+ request = JsonSerializer . Deserialize < OpenAICompletionRequest > ( content , ProxyUtils . JsonSerializerOptions ) ;
153+ return true ;
154+ }
155+
156+ // Chat completion request
157+ if ( rawRequest . TryGetProperty ( "messages" , out _ ) )
158+ {
159+ logger . LogDebug ( "Request is a chat completion request" ) ;
160+ request = JsonSerializer . Deserialize < OpenAIChatCompletionRequest > ( content , ProxyUtils . JsonSerializerOptions ) ;
161+ return true ;
162+ }
163+
164+ // Responses API request - has "input" array with objects containing role/content
165+ if ( IsResponsesApiRequest ( rawRequest ) )
166+ {
167+ logger . LogDebug ( "Request is a Responses API request" ) ;
168+ request = JsonSerializer . Deserialize < OpenAIResponsesRequest > ( content , ProxyUtils . JsonSerializerOptions ) ;
169+ return true ;
170+ }
171+
172+ logger . LogDebug ( "Request is not a completion-like OpenAI request." ) ;
173+ return false ;
174+ }
175+ catch ( JsonException ex )
176+ {
177+ logger . LogDebug ( ex , "Failed to deserialize OpenAI request." ) ;
178+ return false ;
179+ }
180+ }
181+
182+ private static bool IsResponsesApiRequest ( JsonElement rawRequest )
183+ {
184+ return rawRequest . TryGetProperty ( "input" , out var inputProperty ) &&
185+ inputProperty . ValueKind == JsonValueKind . Array &&
186+ inputProperty . GetArrayLength ( ) > 0 &&
187+ inputProperty . EnumerateArray ( ) . First ( ) . ValueKind == JsonValueKind . Object &&
188+ ( inputProperty . EnumerateArray ( ) . First ( ) . TryGetProperty ( "role" , out _ ) ||
189+ inputProperty . EnumerateArray ( ) . First ( ) . TryGetProperty ( "type" , out _ ) ) ;
190+ }
115191}
116192
117193public class OpenAIResponse : ILanguageModelCompletionResponse
@@ -178,6 +254,23 @@ public class OpenAIResponseUsage
178254 public PromptTokenDetails ? PromptTokensDetails { get ; set ; }
179255 [ JsonPropertyName ( "total_tokens" ) ]
180256 public long TotalTokens { get ; set ; }
257+
258+ // Responses API uses different property names (input_tokens, output_tokens)
259+ // These property aliases allow the same class to deserialize both formats.
260+ // When JSON contains "input_tokens", it maps to PromptTokens.
261+ // When JSON contains "output_tokens", it maps to CompletionTokens.
262+ [ JsonPropertyName ( "input_tokens" ) ]
263+ public long InputTokens
264+ {
265+ get => PromptTokens ;
266+ set => PromptTokens = value ;
267+ }
268+ [ JsonPropertyName ( "output_tokens" ) ]
269+ public long OutputTokens
270+ {
271+ get => CompletionTokens ;
272+ set => CompletionTokens = value ;
273+ }
181274}
182275
183276public class PromptTokenDetails
@@ -409,3 +502,71 @@ public class OpenAIImageData
409502 [ JsonPropertyName ( "revised_prompt" ) ]
410503 public string ? RevisedPrompt { get ; set ; }
411504}
505+
506+ #region Responses API
507+
508+ public class OpenAIResponsesRequest : OpenAIRequest
509+ {
510+ public IEnumerable < OpenAIResponsesInputItem > ? Input { get ; set ; }
511+ public string ? Instructions { get ; set ; }
512+ [ JsonPropertyName ( "previous_response_id" ) ]
513+ public string ? PreviousResponseId { get ; set ; }
514+ [ JsonPropertyName ( "max_output_tokens" ) ]
515+ public long ? MaxOutputTokens { get ; set ; }
516+ }
517+
518+ public class OpenAIResponsesInputItem
519+ {
520+ public string Role { get ; set ; } = string . Empty ;
521+ [ JsonConverter ( typeof ( OpenAIContentPartJsonConverter ) ) ]
522+ public object Content { get ; set ; } = string . Empty ;
523+ public string ? Type { get ; set ; }
524+ }
525+
526+ public class OpenAIResponsesResponse : OpenAIResponse
527+ {
528+ [ JsonPropertyName ( "created_at" ) ]
529+ public long CreatedAt { get ; set ; }
530+ public string Status { get ; set ; } = string . Empty ;
531+ public IEnumerable < OpenAIResponsesOutputItem > ? Output { get ; set ; }
532+ [ JsonPropertyName ( "previous_response_id" ) ]
533+ public string ? PreviousResponseId { get ; set ; }
534+
535+ public override string ? Response => GetTextFromOutput ( ) ;
536+
537+ private string ? GetTextFromOutput ( )
538+ {
539+ if ( Output is null )
540+ {
541+ return null ;
542+ }
543+
544+ var messageOutput = Output . FirstOrDefault ( o =>
545+ string . Equals ( o . Type , "message" , StringComparison . OrdinalIgnoreCase ) ) ;
546+ if ( messageOutput ? . Content is null )
547+ {
548+ return null ;
549+ }
550+
551+ var textContent = messageOutput . Content . FirstOrDefault ( c =>
552+ string . Equals ( c . Type , "output_text" , StringComparison . OrdinalIgnoreCase ) ) ;
553+ return textContent ? . Text ;
554+ }
555+ }
556+
557+ public class OpenAIResponsesOutputItem
558+ {
559+ public string ? Type { get ; set ; }
560+ public string ? Id { get ; set ; }
561+ public string ? Role { get ; set ; }
562+ public IEnumerable < OpenAIResponsesOutputContent > ? Content { get ; set ; }
563+ public string ? Status { get ; set ; }
564+ }
565+
566+ public class OpenAIResponsesOutputContent
567+ {
568+ public string ? Type { get ; set ; }
569+ public string ? Text { get ; set ; }
570+ }
571+
572+ #endregion
0 commit comments