2024-05-04 09:11:23 +00:00
using System.Net.Http.Headers ;
using System.Runtime.CompilerServices ;
using System.Text ;
using System.Text.Json ;
using AIStudio.Chat ;
2025-01-02 13:50:54 +00:00
using AIStudio.Settings ;
2024-05-04 09:11:23 +00:00
namespace AIStudio.Provider.OpenAI ;
/// <summary>
/// The OpenAI provider.
/// </summary>
2024-12-03 14:24:40 +00:00
public sealed class ProviderOpenAI ( ILogger logger ) : BaseProvider ( "https://api.openai.com/v1/" , logger )
2024-05-04 09:11:23 +00:00
{
#region Implementation of IProvider
/// <inheritdoc />
2024-12-03 14:24:40 +00:00
public override string Id = > LLMProviders . OPEN_AI . ToName ( ) ;
2024-05-04 09:11:23 +00:00
/// <inheritdoc />
2024-12-03 14:24:40 +00:00
public override string InstanceName { get ; set ; } = "OpenAI" ;
2024-05-04 09:11:23 +00:00
/// <inheritdoc />
2025-08-31 12:27:35 +00:00
public override async IAsyncEnumerable < ContentStreamChunk > StreamChatCompletion ( Model chatModel , ChatThread chatThread , SettingsManager settingsManager , [ EnumeratorCancellation ] CancellationToken token = default )
2024-05-04 09:11:23 +00:00
{
// Get the API key:
2024-09-01 18:10:03 +00:00
var requestedSecret = await RUST_SERVICE . GetAPIKey ( this ) ;
2024-05-04 09:11:23 +00:00
if ( ! requestedSecret . Success )
yield break ;
2025-01-01 19:11:42 +00:00
// Unfortunately, OpenAI changed the name of the system prompt based on the model.
2025-09-03 08:08:04 +00:00
// All models that start with "o" (the omni aka reasoning models), all GPT4o models,
// and all newer models have the system prompt named "developer". All other models
// have the system prompt named "system". We need to check this to get the correct
// system prompt.
2025-01-01 19:11:42 +00:00
//
// To complicate it even more: The early versions of reasoning models, which are released
// before the 17th of December 2024, have no system prompt at all. We need to check this
// as well.
// Apply the basic rule first:
2025-09-03 08:08:04 +00:00
var systemPromptRole =
chatModel . Id . StartsWith ( 'o' ) | |
chatModel . Id . StartsWith ( "gpt-5" , StringComparison . Ordinal ) | |
chatModel . Id . Contains ( "4o" ) ? "developer" : "system" ;
2025-01-01 19:11:42 +00:00
// Check if the model is an early version of the reasoning models:
systemPromptRole = chatModel . Id switch
{
"o1-mini" = > "user" ,
"o1-mini-2024-09-12" = > "user" ,
"o1-preview" = > "user" ,
"o1-preview-2024-09-12" = > "user" ,
_ = > systemPromptRole ,
} ;
2024-05-04 09:11:23 +00:00
2025-09-03 08:08:04 +00:00
// Read the model capabilities:
var modelCapabilities = this . GetModelCapabilities ( chatModel ) ;
// Check if we are using the Responses API or the Chat Completion API:
var usingResponsesAPI = modelCapabilities . Contains ( Capability . RESPONSES_API ) ;
// Prepare the request path based on the API we are using:
var requestPath = usingResponsesAPI ? "responses" : "chat/completions" ;
this . logger . LogInformation ( "Using the system prompt role '{SystemPromptRole}' and the '{RequestPath}' API for model '{ChatModelId}'." , systemPromptRole , requestPath , chatModel . Id ) ;
2024-05-04 09:11:23 +00:00
// Prepare the system prompt:
var systemPrompt = new Message
{
2025-01-01 19:11:42 +00:00
Role = systemPromptRole ,
2025-01-02 13:50:54 +00:00
Content = chatThread . PrepareSystemPrompt ( settingsManager , chatThread , this . logger ) ,
2024-05-04 09:11:23 +00:00
} ;
2025-09-03 08:08:04 +00:00
//
// Prepare the tools we want to use:
//
IList < Tool > tools = modelCapabilities . Contains ( Capability . WEB_SEARCH ) switch
{
true = > [ Tools . WEB_SEARCH ] ,
_ = > [ ]
} ;
2024-05-04 09:11:23 +00:00
2025-09-03 08:08:04 +00:00
//
// Create the request: either for the Responses API or the Chat Completion API
//
var openAIChatRequest = usingResponsesAPI switch
2024-05-04 09:11:23 +00:00
{
2025-09-03 08:08:04 +00:00
// Chat Completion API request:
false = > JsonSerializer . Serialize ( new ChatCompletionAPIRequest
2024-05-04 09:11:23 +00:00
{
2025-09-03 08:08:04 +00:00
Model = chatModel . Id ,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [ systemPrompt , . . chatThread . Blocks . Where ( n = > n . ContentType is ContentType . TEXT & & ! string . IsNullOrWhiteSpace ( ( n . Content as ContentText ) ? . Text ) ) . Select ( n = > new Message
2024-05-04 09:11:23 +00:00
{
2025-09-03 08:08:04 +00:00
Role = n . Role switch
{
ChatRole . USER = > "user" ,
ChatRole . AI = > "assistant" ,
ChatRole . AGENT = > "assistant" ,
ChatRole . SYSTEM = > systemPromptRole ,
2024-05-04 09:11:23 +00:00
2025-09-03 08:08:04 +00:00
_ = > "user" ,
} ,
2024-05-04 09:11:23 +00:00
2025-09-03 08:08:04 +00:00
Content = n . Content switch
{
ContentText text = > text . Text ,
_ = > string . Empty ,
}
} ) . ToList ( ) ] ,
// Right now, we only support streaming completions:
Stream = true ,
} , JSON_SERIALIZER_OPTIONS ) ,
// Responses API request:
true = > JsonSerializer . Serialize ( new ResponsesAPIRequest
{
Model = chatModel . Id ,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Input = [ systemPrompt , . . chatThread . Blocks . Where ( n = > n . ContentType is ContentType . TEXT & & ! string . IsNullOrWhiteSpace ( ( n . Content as ContentText ) ? . Text ) ) . Select ( n = > new Message
2024-05-04 09:11:23 +00:00
{
2025-09-03 08:08:04 +00:00
Role = n . Role switch
{
ChatRole . USER = > "user" ,
ChatRole . AI = > "assistant" ,
ChatRole . AGENT = > "assistant" ,
ChatRole . SYSTEM = > systemPromptRole ,
2024-05-04 09:11:23 +00:00
2025-09-03 08:08:04 +00:00
_ = > "user" ,
} ,
Content = n . Content switch
{
ContentText text = > text . Text ,
_ = > string . Empty ,
}
} ) . ToList ( ) ] ,
2024-05-04 09:11:23 +00:00
2025-09-03 08:08:04 +00:00
// Right now, we only support streaming completions:
Stream = true ,
// We do not want to store any data on OpenAI's servers:
Store = false ,
// Tools we want to use:
Tools = tools ,
} , JSON_SERIALIZER_OPTIONS ) ,
} ;
2024-05-04 09:11:23 +00:00
2025-01-04 13:11:32 +00:00
async Task < HttpRequestMessage > RequestBuilder ( )
2025-01-01 14:49:27 +00:00
{
2025-01-04 13:11:32 +00:00
// Build the HTTP post request:
2025-09-03 08:08:04 +00:00
var request = new HttpRequestMessage ( HttpMethod . Post , requestPath ) ;
2025-01-01 14:49:27 +00:00
2025-01-04 13:11:32 +00:00
// Set the authorization header:
request . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , await requestedSecret . Secret . Decrypt ( ENCRYPTION ) ) ;
2025-01-01 14:49:27 +00:00
2025-01-04 13:11:32 +00:00
// Set the content:
request . Content = new StringContent ( openAIChatRequest , Encoding . UTF8 , "application/json" ) ;
return request ;
2025-01-04 11:37:49 +00:00
}
2025-09-03 08:08:04 +00:00
if ( usingResponsesAPI )
await foreach ( var content in this . StreamResponsesInternal < ResponsesDeltaStreamLine , ResponsesAnnotationStreamLine > ( "OpenAI" , RequestBuilder , token ) )
yield return content ;
2024-05-04 09:11:23 +00:00
2025-09-03 08:08:04 +00:00
else
await foreach ( var content in this . StreamChatCompletionInternal < ChatCompletionDeltaStreamLine , ChatCompletionAnnotationStreamLine > ( "OpenAI" , RequestBuilder , token ) )
yield return content ;
2024-05-04 09:11:23 +00:00
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
2025-09-03 08:08:04 +00:00
2024-05-04 09:11:23 +00:00
/// <inheritdoc />
2024-12-03 14:24:40 +00:00
public override async IAsyncEnumerable < ImageURL > StreamImageCompletion ( Model imageModel , string promptPositive , string promptNegative = FilterOperator . String . Empty , ImageURL referenceImageURL = default , [ EnumeratorCancellation ] CancellationToken token = default )
2024-05-04 09:11:23 +00:00
{
yield break ;
}
2025-09-03 08:08:04 +00:00
2024-05-04 09:11:23 +00:00
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
2025-04-24 10:45:43 +00:00
public override async Task < IEnumerable < Model > > GetTextModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-05-04 09:11:23 +00:00
{
2025-09-03 08:08:04 +00:00
var models = await this . LoadModels ( [ "chatgpt-" , "gpt-" , "o1-" , "o3-" , "o4-" ] , token , apiKeyProvisional ) ;
2025-04-24 10:45:43 +00:00
return models . Where ( model = > ! model . Id . Contains ( "image" , StringComparison . OrdinalIgnoreCase ) & &
! model . Id . Contains ( "realtime" , StringComparison . OrdinalIgnoreCase ) & &
! model . Id . Contains ( "audio" , StringComparison . OrdinalIgnoreCase ) & &
! model . Id . Contains ( "tts" , StringComparison . OrdinalIgnoreCase ) & &
2025-09-03 08:08:04 +00:00
! model . Id . Contains ( "transcribe" , StringComparison . OrdinalIgnoreCase ) ) ;
2024-05-04 09:11:23 +00:00
}
/// <inheritdoc />
2024-12-03 14:24:40 +00:00
public override Task < IEnumerable < Model > > GetImageModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-05-04 09:11:23 +00:00
{
2025-04-24 10:45:43 +00:00
return this . LoadModels ( [ "dall-e-" , "gpt-image" ] , token , apiKeyProvisional ) ;
2024-05-04 09:11:23 +00:00
}
2024-12-03 14:24:40 +00:00
/// <inheritdoc />
public override Task < IEnumerable < Model > > GetEmbeddingModels ( string? apiKeyProvisional = null , CancellationToken token = default )
{
return this . LoadModels ( [ "text-embedding-" ] , token , apiKeyProvisional ) ;
}
2025-05-11 10:51:35 +00:00
public override IReadOnlyCollection < Capability > GetModelCapabilities ( Model model )
{
var modelName = model . Id . ToLowerInvariant ( ) . AsSpan ( ) ;
2025-09-03 08:08:04 +00:00
if ( modelName is "gpt-4o-search-preview" )
2025-05-11 10:51:35 +00:00
return
[
Capability . TEXT_INPUT ,
Capability . TEXT_OUTPUT ,
2025-09-03 08:08:04 +00:00
Capability . WEB_SEARCH ,
Capability . CHAT_COMPLETION_API ,
2025-05-11 10:51:35 +00:00
] ;
2025-09-03 08:08:04 +00:00
if ( modelName is "gpt-4o-mini-search-preview" )
2025-05-11 10:51:35 +00:00
return
[
Capability . TEXT_INPUT ,
Capability . TEXT_OUTPUT ,
2025-09-03 08:08:04 +00:00
Capability . WEB_SEARCH ,
Capability . CHAT_COMPLETION_API ,
2025-05-11 10:51:35 +00:00
] ;
2025-09-03 08:08:04 +00:00
if ( modelName . StartsWith ( "o1-mini" ) )
2025-05-11 10:51:35 +00:00
return
[
2025-09-03 08:08:04 +00:00
Capability . TEXT_INPUT ,
2025-05-11 10:51:35 +00:00
Capability . TEXT_OUTPUT ,
2025-09-03 08:08:04 +00:00
Capability . ALWAYS_REASONING ,
Capability . CHAT_COMPLETION_API ,
2025-05-11 10:51:35 +00:00
] ;
2025-09-03 08:08:04 +00:00
if ( modelName is "gpt-3.5-turbo" )
return
[
Capability . TEXT_INPUT ,
Capability . TEXT_OUTPUT ,
Capability . RESPONSES_API ,
] ;
2025-05-11 10:51:35 +00:00
if ( modelName . StartsWith ( "gpt-3.5" ) )
2025-09-03 08:08:04 +00:00
return
[
Capability . TEXT_INPUT ,
Capability . TEXT_OUTPUT ,
Capability . CHAT_COMPLETION_API ,
] ;
if ( modelName . StartsWith ( "chatgpt-4o-" ) )
return
[
Capability . TEXT_INPUT , Capability . MULTIPLE_IMAGE_INPUT ,
Capability . TEXT_OUTPUT ,
Capability . RESPONSES_API ,
] ;
if ( modelName . StartsWith ( "o3-mini" ) )
2025-05-11 10:51:35 +00:00
return
[
Capability . TEXT_INPUT ,
Capability . TEXT_OUTPUT ,
2025-09-03 08:08:04 +00:00
Capability . ALWAYS_REASONING , Capability . FUNCTION_CALLING ,
Capability . RESPONSES_API ,
] ;
if ( modelName . StartsWith ( "o4-mini" ) | | modelName . StartsWith ( "o3" ) )
return
[
Capability . TEXT_INPUT , Capability . MULTIPLE_IMAGE_INPUT ,
Capability . TEXT_OUTPUT ,
Capability . ALWAYS_REASONING , Capability . FUNCTION_CALLING ,
Capability . WEB_SEARCH ,
Capability . RESPONSES_API ,
2025-05-11 10:51:35 +00:00
] ;
2025-09-03 08:08:04 +00:00
if ( modelName . StartsWith ( "o1" ) )
return
[
Capability . TEXT_INPUT , Capability . MULTIPLE_IMAGE_INPUT ,
Capability . TEXT_OUTPUT ,
Capability . ALWAYS_REASONING , Capability . FUNCTION_CALLING ,
Capability . RESPONSES_API ,
] ;
2025-05-11 10:51:35 +00:00
if ( modelName . StartsWith ( "gpt-4-turbo" ) )
return
[
Capability . TEXT_INPUT , Capability . MULTIPLE_IMAGE_INPUT ,
Capability . TEXT_OUTPUT ,
2025-09-03 08:08:04 +00:00
Capability . FUNCTION_CALLING ,
Capability . RESPONSES_API ,
2025-05-11 10:51:35 +00:00
] ;
if ( modelName is "gpt-4" | | modelName . StartsWith ( "gpt-4-" ) )
return
[
Capability . TEXT_INPUT ,
Capability . TEXT_OUTPUT ,
2025-09-03 08:08:04 +00:00
Capability . RESPONSES_API ,
2025-05-11 10:51:35 +00:00
] ;
2025-09-03 08:08:04 +00:00
if ( modelName . StartsWith ( "gpt-5-nano" ) )
return
[
Capability . TEXT_INPUT , Capability . MULTIPLE_IMAGE_INPUT ,
Capability . TEXT_OUTPUT ,
Capability . FUNCTION_CALLING , Capability . ALWAYS_REASONING ,
Capability . RESPONSES_API ,
] ;
if ( modelName is "gpt-5" | | modelName . StartsWith ( "gpt-5-" ) )
return
[
Capability . TEXT_INPUT , Capability . MULTIPLE_IMAGE_INPUT ,
Capability . TEXT_OUTPUT ,
Capability . FUNCTION_CALLING , Capability . ALWAYS_REASONING ,
Capability . WEB_SEARCH ,
Capability . RESPONSES_API ,
] ;
2025-05-11 10:51:35 +00:00
return
[
Capability . TEXT_INPUT , Capability . MULTIPLE_IMAGE_INPUT ,
Capability . TEXT_OUTPUT ,
Capability . FUNCTION_CALLING ,
2025-09-03 08:08:04 +00:00
Capability . RESPONSES_API ,
2025-05-11 10:51:35 +00:00
] ;
}
2024-05-04 09:11:23 +00:00
#endregion
2024-09-12 20:58:32 +00:00
private async Task < IEnumerable < Model > > LoadModels ( string [ ] prefixes , CancellationToken token , string? apiKeyProvisional = null )
2024-05-04 09:11:23 +00:00
{
2024-06-03 17:42:53 +00:00
var secretKey = apiKeyProvisional switch
{
not null = > apiKeyProvisional ,
2024-09-01 18:10:03 +00:00
_ = > await RUST_SERVICE . GetAPIKey ( this ) switch
2024-06-03 17:42:53 +00:00
{
2024-09-01 18:10:03 +00:00
{ Success : true } result = > await result . Secret . Decrypt ( ENCRYPTION ) ,
2024-06-03 17:42:53 +00:00
_ = > null ,
}
} ;
if ( secretKey is null )
2024-05-19 14:14:49 +00:00
return [ ] ;
2024-05-04 09:11:23 +00:00
2025-02-09 11:36:37 +00:00
using var request = new HttpRequestMessage ( HttpMethod . Get , "models" ) ;
2024-06-03 17:42:53 +00:00
request . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , secretKey ) ;
2024-05-19 14:14:49 +00:00
2025-02-09 11:36:37 +00:00
using var response = await this . httpClient . SendAsync ( request , token ) ;
2024-05-04 09:11:23 +00:00
if ( ! response . IsSuccessStatusCode )
2024-05-19 14:14:49 +00:00
return [ ] ;
2024-05-04 09:11:23 +00:00
var modelResponse = await response . Content . ReadFromJsonAsync < ModelsResponse > ( token ) ;
2024-09-12 20:58:32 +00:00
return modelResponse . Data . Where ( model = > prefixes . Any ( prefix = > model . Id . StartsWith ( prefix , StringComparison . InvariantCulture ) ) ) ;
2024-05-04 09:11:23 +00:00
}
}