using System.Net;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Models;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.ToolCallingSystem;
using AIStudio.Tools.ToolCallingSystem.Harness;
namespace AIStudio.Provider.OpenAI;
///
/// The OpenAI provider.
///
public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Uri("https://api.openai.com/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger();
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderOpenAI).Namespace, nameof(ProviderOpenAI));
#region Implementation of IProvider
///
public override string Id => LLMProviders.OPEN_AI.ToSecretId();
///
public override string InstanceName { get; set; } = "OpenAI";
///
public override bool HasModelLoadingCapability => true;
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, ContextWindow contextWindow = default) => 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, contextWindow),
};
///
public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
if(!requestedSecret.Success)
yield break;
// Read the model capabilities. Through the settings provider, so that the user's expert
// capability overrides apply:
var providerSettings = this.CreateSettingsProvider(chatModel);
var modelProfile = providerSettings.GetModelProfile();
// OpenAI changed the role of the system prompt from one generation to the next, and the
// early reasoning models take no system prompt at all. Which role a model takes is stated
// by the OpenAI families in Models/OpenAI:
var systemPromptRole = modelProfile.SystemPromptRole.ToOpenAIRole();
// Check if we are using the Responses API or the Chat Completion API:
var usingResponsesAPI = modelProfile.Has(Capability.RESPONSES_API);
// Prepare the request path based on the API we are using:
var requestPath = usingResponsesAPI ? "responses" : "chat/completions";
LOGGER.LogInformation("Using the system prompt role '{SystemPromptRole}' and the '{RequestPath}' API for model '{ChatModelId}'.", systemPromptRole, requestPath, chatModel.Id);
//
// Prepare the tools we want to use:
//
var toolRegistry = Program.SERVICE_PROVIDER.GetService();
var providerConfidence = this.Provider.GetConfidence(settingsManager).Level;
// Parse the API parameters:
var additionalApiParameters = this.ParseAdditionalApiParameters("input", "store", "tools");
if (!usingResponsesAPI)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion(
"OpenAI",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters, tools) =>
{
var messages = await chatThread.Blocks.BuildMessagesAsync(
providerSettings,
role => role switch
{
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => systemPromptRole,
_ => "user",
},
text => new SubContentText
{
Text = text,
},
async attachment => new SubContentImageUrlNested
{
ImageUrl = new SubContentImageUrlData
{
Url = await attachment.TryAsBase64(token: token) is (true, var base64Content)
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
},
});
return new ChatCompletionAPIRequest
{
Model = chatModel.Id,
Messages = [systemPrompt, ..messages],
Stream = true,
Tools = tools,
AdditionalApiParameters = apiParameters,
};
},
systemPromptRole: systemPromptRole,
requestPath: "chat/completions",
//
// OpenAI binds a strict function's arguments to its schema -- read on
// 2026-09-24 at https://developers.openai.com/api/docs/guides/function-calling.
// Most other hosts of this API do not: Groq applies strict mode to
// response formats only (https://console.groq.com/docs/structured-outputs,
// same day) and merely validates a tool call afterward, so it rejects
// every call that leaves out an argument the strict schema requires:
//
enforcesStrictToolSchemas: true,
token: token))
yield return content;
yield break;
}
var toolExecutor = Program.SERVICE_PROVIDER.GetService();
var currentAssistantContent = chatThread.Blocks.LastOrDefault(x => x.Role is ChatRole.AI)?.Content as ContentText;
currentAssistantContent?.BeginToolRun();
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools = toolRegistry is null
? []
: await toolRegistry.GetRunnableToolsAsync(
new ToolResolutionContext
{
Provider = providerSettings,
Component = chatThread.RuntimeComponent,
ProviderConfidence = providerConfidence,
ChatThread = chatThread,
},
chatThread.RuntimeSelectedToolIds,
chatThread.MayRunTools(settingsManager),
token);
var toolAwareDefinitions = toolExecutor is null
? Enumerable.Empty()
: runnableTools.Select(x => x.Definition);
var systemPrompt = new TextMessage
{
Role = systemPromptRole,
Content = chatThread.PrepareSystemPrompt(settingsManager, toolAwareDefinitions),
};
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesAsync(
providerSettings,
role => role switch
{
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => systemPromptRole,
_ => "user",
},
text => new SubContentInputText
{
Text = text,
},
async attachment => new SubContentInputImage
{
ImageUrl = await attachment.TryAsBase64(token: token) is (true, var base64Content)
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
});
var baseInput = new List