2026-05-25 17:32:54 +02:00
using System.Net ;
2024-05-04 11:11:23 +02:00
using System.Net.Http.Headers ;
using System.Runtime.CompilerServices ;
using System.Text ;
using System.Text.Json ;
using AIStudio.Chat ;
2025-01-02 14:50:54 +01:00
using AIStudio.Settings ;
2026-05-25 17:32:54 +02:00
using AIStudio.Tools.PluginSystem ;
2024-05-04 11:11:23 +02:00
namespace AIStudio.Provider.OpenAI ;
/// <summary>
/// The OpenAI provider.
/// </summary>
2026-05-31 18:46:54 +02:00
public sealed class ProviderOpenAI () : BaseProvider ( LLMProviders . OPEN_AI , new Uri ( "https://api.openai.com/v1/" ), ExternalHttpTrustPolicy . SYSTEM_TRUST_ONLY , LOGGER )
2024-05-04 11:11:23 +02:00
{
2025-09-03 21:25:17 +02:00
private static readonly ILogger < ProviderOpenAI > LOGGER = Program . LOGGER_FACTORY . CreateLogger < ProviderOpenAI >();
2026-05-25 17:32:54 +02:00
private static string TB ( string fallbackEN ) => I18N . I . T ( fallbackEN , typeof ( ProviderOpenAI ). Namespace , nameof ( ProviderOpenAI ));
2024-05-04 11:11:23 +02:00
#region Implementation of IProvider
/// <inheritdoc />
2026-06-20 15:55:09 +02:00
public override string Id => LLMProviders . OPEN_AI . ToSecretId ();
2024-05-04 11:11:23 +02:00
/// <inheritdoc />
2024-12-03 15:24:40 +01:00
public override string InstanceName { get ; set ; } = "OpenAI" ;
2024-05-04 11:11:23 +02:00
2026-04-16 11:24:22 +02:00
/// <inheritdoc />
public override bool HasModelLoadingCapability => true ;
2026-05-25 17:32:54 +02:00
protected override ProviderRequestFailureReason ClassifyProviderRequestFailure ( HttpStatusCode statusCode , string responseBody )
{
if ( statusCode is HttpStatusCode . TooManyRequests && HasInsufficientQuotaError ( responseBody ))
return ProviderRequestFailureReason . INSUFFICIENT_QUOTA ;
return base . ClassifyProviderRequestFailure ( statusCode , responseBody );
}
protected override ProviderRequestFailureReason ClassifyProviderRequestFailure ( string? errorCode , string? errorType , string? errorMessage , string responseBody )
{
if ( IsInsufficientQuota ( errorCode ) || IsInsufficientQuota ( errorType ) || HasInsufficientQuotaError ( responseBody ))
return ProviderRequestFailureReason . INSUFFICIENT_QUOTA ;
return base . ClassifyProviderRequestFailure ( errorCode , errorType , errorMessage , responseBody );
}
protected override string GetProviderRequestFailureUserMessage ( ProviderRequestFailureReason failureReason ) => failureReason switch
{
ProviderRequestFailureReason . INSUFFICIENT_QUOTA => TB ( "It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again." ),
_ => base . GetProviderRequestFailureUserMessage ( failureReason ),
};
2024-05-04 11:11:23 +02:00
/// <inheritdoc />
2025-08-31 14:27:35 +02:00
public override async IAsyncEnumerable < ContentStreamChunk > StreamChatCompletion ( Model chatModel , ChatThread chatThread , SettingsManager settingsManager , [ EnumeratorCancellation ] CancellationToken token = default )
2024-05-04 11:11:23 +02:00
{
// Get the API key:
2026-06-10 21:01:27 +02:00
var requestedSecret = await Program . RUST_SERVICE . GetAPIKey ( this , SecretStoreType . LLM_PROVIDER );
2024-05-04 11:11:23 +02:00
if (! requestedSecret . Success )
yield break ;
2025-01-01 20:11:42 +01:00
// Unfortunately, OpenAI changed the name of the system prompt based on the model.
2025-09-03 10:08:04 +02: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 20:11:42 +01: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 10:08:04 +02:00
var systemPromptRole =
chatModel . Id . StartsWith ( 'o' ) ||
chatModel . Id . StartsWith ( "gpt-5" , StringComparison . Ordinal ) ||
chatModel . Id . Contains ( "4o" ) ? "developer" : "system" ;
2025-01-01 20:11:42 +01: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 11:11:23 +02:00
2025-09-03 10:08:04 +02:00
// Read the model capabilities:
2025-12-30 18:30:32 +01:00
var modelCapabilities = this . Provider . GetModelCapabilities ( chatModel );
2025-09-03 10:08:04 +02:00
// 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" ;
2025-09-03 21:25:17 +02:00
LOGGER . LogInformation ( "Using the system prompt role '{SystemPromptRole}' and the '{RequestPath}' API for model '{ChatModelId}'." , systemPromptRole , requestPath , chatModel . Id );
2025-09-03 10:08:04 +02:00
2024-05-04 11:11:23 +02:00
// Prepare the system prompt:
2025-12-28 14:10:20 +01:00
var systemPrompt = new TextMessage
2024-05-04 11:11:23 +02:00
{
2025-01-01 20:11:42 +01:00
Role = systemPromptRole ,
2026-01-18 20:36:04 +01:00
Content = chatThread . PrepareSystemPrompt ( settingsManager ),
2024-05-04 11:11:23 +02:00
};
2025-09-03 10:08:04 +02:00
//
// Prepare the tools we want to use:
//
2026-04-13 13:33:17 +02:00
IList < ProviderTool > providerTools = modelCapabilities . Contains ( Capability . WEB_SEARCH ) switch
2025-09-03 10:08:04 +02:00
{
2026-04-13 13:33:17 +02:00
true => [ ProviderTools . WEB_SEARCH ],
2025-09-03 10:08:04 +02:00
_ => []
};
2024-05-04 11:11:23 +02:00
2025-11-13 18:13:16 +01:00
// Parse the API parameters:
var apiParameters = this . ParseAdditionalApiParameters ( "input" , "store" , "tools" );
2025-12-10 13:48:13 +01:00
// Build the list of messages:
2025-12-30 18:30:32 +01:00
var messages = await chatThread . Blocks . BuildMessagesAsync (
this . Provider , chatModel ,
// OpenAI-specific role mapping:
role => role switch
2025-12-10 13:48:13 +01:00
{
ChatRole . USER => "user" ,
ChatRole . AI => "assistant" ,
ChatRole . AGENT => "assistant" ,
ChatRole . SYSTEM => systemPromptRole ,
_ => "user" ,
},
2025-12-30 18:30:32 +01:00
// OpenAI's text sub-content depends on the model, whether we are using
// the Responses API or the Chat Completion API:
text => usingResponsesAPI switch
2025-12-10 13:48:13 +01:00
{
2025-12-30 18:30:32 +01:00
// Responses API uses INPUT_TEXT:
true => new SubContentInputText
{
Text = text ,
},
// Chat Completion API uses TEXT:
false => new SubContentText
{
Text = text ,
},
},
// OpenAI's image sub-content depends on the model as well,
// whether we are using the Responses API or the Chat Completion API:
async attachment => usingResponsesAPI switch
{
// Responses API uses INPUT_IMAGE:
true => new SubContentInputImage
{
ImageUrl = await attachment . TryAsBase64 ( token : token ) is ( true , var base64Content )
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string . Empty ,
},
// Chat Completion API uses IMAGE_URL:
false => new SubContentImageUrlNested
{
ImageUrl = new SubContentImageUrlData
{
Url = await attachment . TryAsBase64 ( token : token ) is ( true , var base64Content )
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string . Empty ,
},
}
});
2025-11-13 18:13:16 +01:00
2025-09-03 10:08:04 +02:00
//
// Create the request: either for the Responses API or the Chat Completion API
//
var openAIChatRequest = usingResponsesAPI switch
2024-05-04 11:11:23 +02:00
{
2025-09-03 10:08:04 +02:00
// Chat Completion API request:
false => JsonSerializer . Serialize ( new ChatCompletionAPIRequest
2024-05-04 11:11:23 +02:00
{
2025-09-03 10:08:04 +02:00
Model = chatModel . Id ,
2025-12-28 14:10:20 +01:00
// All messages go into the messages field:
2025-12-10 13:48:13 +01:00
Messages = [ systemPrompt , .. messages ],
2025-09-03 10:08:04 +02:00
// Right now, we only support streaming completions:
Stream = true ,
2025-11-13 18:13:16 +01:00
AdditionalApiParameters = apiParameters
2025-09-03 10:08:04 +02:00
}, JSON_SERIALIZER_OPTIONS ),
// Responses API request:
true => JsonSerializer . Serialize ( new ResponsesAPIRequest
{
Model = chatModel . Id ,
2025-12-28 14:10:20 +01:00
// All messages go into the input field:
Input = [ systemPrompt , .. messages ],
2024-05-04 11:11:23 +02:00
2025-09-03 10:08:04 +02: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:
2026-04-13 13:33:17 +02:00
ProviderTools = providerTools ,
2025-09-03 10:08:04 +02:00
2025-11-13 18:13:16 +01:00
// Additional API parameters:
AdditionalApiParameters = apiParameters
2025-09-03 10:08:04 +02:00
}, JSON_SERIALIZER_OPTIONS ),
};
2025-12-30 18:30:32 +01:00
2025-01-04 14:11:32 +01:00
async Task < HttpRequestMessage > RequestBuilder ()
2025-01-01 15:49:27 +01:00
{
2025-01-04 14:11:32 +01:00
// Build the HTTP post request:
2025-09-03 10:08:04 +02:00
var request = new HttpRequestMessage ( HttpMethod . Post , requestPath );
2025-01-01 15:49:27 +01:00
2025-01-04 14:11:32 +01:00
// Set the authorization header:
2026-06-10 21:01:27 +02:00
request . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , await requestedSecret . Secret . Decrypt ( Program . ENCRYPTION ));
2025-01-01 15:49:27 +01:00
2025-01-04 14:11:32 +01:00
// Set the content:
request . Content = new StringContent ( openAIChatRequest , Encoding . UTF8 , "application/json" );
return request ;
2025-01-04 12:37:49 +01:00
}
2025-09-03 10:08:04 +02:00
if ( usingResponsesAPI )
await foreach ( var content in this . StreamResponsesInternal < ResponsesDeltaStreamLine , ResponsesAnnotationStreamLine >( "OpenAI" , RequestBuilder , token ))
yield return content ;
2024-05-04 11:11:23 +02:00
2025-09-03 10:08:04 +02:00
else
await foreach ( var content in this . StreamChatCompletionInternal < ChatCompletionDeltaStreamLine , ChatCompletionAnnotationStreamLine >( "OpenAI" , RequestBuilder , token ))
yield return content ;
2024-05-04 11:11:23 +02:00
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
2025-09-03 10:08:04 +02:00
2024-05-04 11:11:23 +02:00
/// <inheritdoc />
2024-12-03 15:24:40 +01: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 11:11:23 +02:00
{
yield break ;
}
2025-09-03 10:08:04 +02:00
2024-05-04 11:11:23 +02:00
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
2026-01-11 16:02:28 +01:00
/// <inheritdoc />
2026-05-23 11:25:18 +02:00
public override async Task < TranscriptionResult > TranscribeAudioAsync ( Model transcriptionModel , string audioFilePath , SettingsManager settingsManager , CancellationToken token = default )
2026-01-11 16:02:28 +01:00
{
2026-06-10 21:01:27 +02:00
var requestedSecret = await Program . RUST_SERVICE . GetAPIKey ( this , SecretStoreType . TRANSCRIPTION_PROVIDER );
2026-01-11 16:02:28 +01:00
return await this . PerformStandardTranscriptionRequest ( requestedSecret , transcriptionModel , audioFilePath , token : token );
}
2026-02-20 15:32:54 +01:00
/// <inhertidoc />
public override async Task < IReadOnlyList < IReadOnlyList < float >>> EmbedTextAsync ( Model embeddingModel , SettingsManager settingsManager , CancellationToken token = default , params List < string > texts )
{
2026-06-10 21:01:27 +02:00
var requestedSecret = await Program . RUST_SERVICE . GetAPIKey ( this , SecretStoreType . EMBEDDING_PROVIDER );
2026-02-20 15:32:54 +01:00
return await this . PerformStandardTextEmbeddingRequest ( requestedSecret , embeddingModel , token : token , texts : texts );
}
2024-05-04 11:11:23 +02:00
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override async Task < ModelLoadResult > GetTextModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-05-04 11:11:23 +02:00
{
2026-04-14 13:39:11 +02:00
var result = await this . LoadModels ( SecretStoreType . LLM_PROVIDER , [ "chatgpt-" , "gpt-" , "o1-" , "o3-" , "o4-" ], token , apiKeyProvisional );
return result with
{
Models =
[
..result.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) &&
!model.Id.Contains("transcribe", StringComparison.OrdinalIgnoreCase))
]
};
2024-05-04 11:11:23 +02:00
}
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override Task < ModelLoadResult > GetImageModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-05-04 11:11:23 +02:00
{
2026-01-11 16:02:28 +01:00
return this . LoadModels ( SecretStoreType . IMAGE_PROVIDER , [ "dall-e-" , "gpt-image" ], token , apiKeyProvisional );
2024-05-04 11:11:23 +02:00
}
2024-12-03 15:24:40 +01:00
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override Task < ModelLoadResult > GetEmbeddingModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-12-03 15:24:40 +01:00
{
2026-01-11 16:02:28 +01:00
return this . LoadModels ( SecretStoreType . EMBEDDING_PROVIDER , [ "text-embedding-" ], token , apiKeyProvisional );
2024-12-03 15:24:40 +01:00
}
2025-05-11 12:51:35 +02:00
2026-01-09 12:45:21 +01:00
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override async Task < ModelLoadResult > GetTranscriptionModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2026-01-09 12:45:21 +01:00
{
2026-04-14 13:39:11 +02:00
var result = await this . LoadModels ( SecretStoreType . TRANSCRIPTION_PROVIDER , [ "whisper-" , "gpt-" ], token , apiKeyProvisional );
return result with
{
Models =
[
..result.Models.Where(model => model.Id.StartsWith("whisper-", StringComparison.InvariantCultureIgnoreCase) ||
model.Id.Contains("-transcribe", StringComparison.InvariantCultureIgnoreCase))
]
};
2026-01-09 12:45:21 +01:00
}
2024-05-04 11:11:23 +02:00
#endregion
2026-04-14 13:39:11 +02:00
private Task < ModelLoadResult > LoadModels ( SecretStoreType storeType , string [] prefixes , CancellationToken token , string? apiKeyProvisional = null )
2024-05-04 11:11:23 +02:00
{
2026-04-14 13:39:11 +02:00
return this . LoadModelsResponse < ModelsResponse >(
storeType ,
"models" ,
modelResponse => modelResponse . Data . Where ( model => prefixes . Any ( prefix => model . Id . StartsWith ( prefix , StringComparison . InvariantCulture ))),
token ,
apiKeyProvisional );
2024-05-04 11:11:23 +02:00
}
2026-05-25 17:32:54 +02:00
private static bool HasInsufficientQuotaError ( string responseBody )
{
if ( string . IsNullOrWhiteSpace ( responseBody ))
return false ;
try
{
using var document = JsonDocument . Parse ( responseBody );
return HasInsufficientQuotaError ( document . RootElement );
}
catch ( JsonException )
{
return false ;
}
}
private static bool HasInsufficientQuotaError ( JsonElement element )
{
switch ( element . ValueKind )
{
case JsonValueKind . Object :
if ( HasJsonStringValue ( element , "type" , "insufficient_quota" ) ||
HasJsonStringValue ( element , "code" , "insufficient_quota" ))
return true ;
foreach ( var property in element . EnumerateObject ())
if ( HasInsufficientQuotaError ( property . Value ))
return true ;
return false ;
case JsonValueKind . Array :
foreach ( var item in element . EnumerateArray ())
if ( HasInsufficientQuotaError ( item ))
return true ;
return false ;
default :
return false ;
}
}
private static bool IsInsufficientQuota ( string? value )
{
return value is not null && value . Equals ( "insufficient_quota" , StringComparison . OrdinalIgnoreCase );
}
private static bool HasJsonStringValue ( JsonElement element , string propertyName , string expectedValue )
{
return element . TryGetProperty ( propertyName , out var propertyElement ) &&
propertyElement . ValueKind is JsonValueKind . String &&
string . Equals ( propertyElement . GetString (), expectedValue , StringComparison . OrdinalIgnoreCase );
}
2026-04-14 13:39:11 +02:00
}