mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 14:13:48 +00:00
Improved the OpenAI provider (#548)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
This commit is contained in:
1 parent
4e167d58ea
commit
38ec098430
54 files changed
+1116
-220
No files matched your search
@@ -0,0 +1,7 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an unknown annotation type.
|
||||
/// </summary>
|
||||
/// <param name="Type">The type of the unknown annotation.</param>
|
||||
public sealed record AnnotatingUnknown(string Type) : Annotation(Type);
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for different types of annotations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We use this base class to represent various annotation types for all types of LLM providers.
|
||||
/// </remarks>
|
||||
/// <param name="Type">The type of the annotation.</param>
|
||||
public abstract record Annotation(string Type);
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Custom JSON converter for the annotation class to handle polymorphic deserialization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We use this converter for chat completion API and responses API annotation deserialization.
|
||||
/// </remarks>
|
||||
public sealed class AnnotationConverter : JsonConverter<Annotation>
|
||||
{
|
||||
public override Annotation? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("type", out var typeElement))
|
||||
return null;
|
||||
|
||||
var type = typeElement.GetString();
|
||||
var rawText = root.GetRawText();
|
||||
|
||||
Annotation? annotation;
|
||||
switch (type)
|
||||
{
|
||||
case "url_citation":
|
||||
|
||||
// Let's check the responses API data type first:
|
||||
var responsesAnnotation = JsonSerializer.Deserialize<ResponsesAnnotatingUrlCitationData>(rawText, options);
|
||||
|
||||
// If it fails, let's try the chat completion API data type:
|
||||
if(responsesAnnotation is null || string.IsNullOrWhiteSpace(responsesAnnotation.Title) || string.IsNullOrWhiteSpace(responsesAnnotation.URL))
|
||||
{
|
||||
// Try chat completion API data type:
|
||||
var chatCompletionAnnotation = JsonSerializer.Deserialize<ChatCompletionAnnotatingURL>(rawText, options);
|
||||
|
||||
// If both fail, we return the unknown type:
|
||||
if(chatCompletionAnnotation is null)
|
||||
annotation = new AnnotatingUnknown(type);
|
||||
else
|
||||
annotation = chatCompletionAnnotation;
|
||||
}
|
||||
else
|
||||
annotation = responsesAnnotation;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
annotation = new AnnotatingUnknown(type ?? "unknown");
|
||||
break;
|
||||
}
|
||||
|
||||
return annotation;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, Annotation value, JsonSerializerOptions options)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value, value.GetType(), options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// The OpenAI's legacy chat completion request model.
|
||||
/// </summary>
|
||||
/// <param name="Model">Which model to use for chat completion.</param>
|
||||
/// <param name="Messages">The chat messages.</param>
|
||||
/// <param name="Stream">Whether to stream the chat completion.</param>
|
||||
public record ChatCompletionAPIRequest(
|
||||
string Model,
|
||||
IList<Message> Messages,
|
||||
bool Stream
|
||||
)
|
||||
{
|
||||
public ChatCompletionAPIRequest() : this(string.Empty, [], true)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Data structure for URL annotation in chat completions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Although this class is not directly intended for the Responses API, it is
|
||||
/// used there as a fallback solution. One day, one of the open source LLM
|
||||
/// drivers may use this data structure for their responses API.
|
||||
/// </remarks>
|
||||
/// <param name="Type">The type of annotation, typically "url_citation".</param>
|
||||
/// <param name="UrlCitation">The URL citation details.</param>
|
||||
public sealed record ChatCompletionAnnotatingURL(
|
||||
string Type,
|
||||
ChatCompletionUrlCitationData UrlCitation
|
||||
) : Annotation(Type);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Data structure representing a choice in a chat completion annotation response.
|
||||
/// </summary>
|
||||
/// <param name="Index">The index of the choice.</param>
|
||||
/// <param name="Delta">The delta information for the choice.</param>
|
||||
public record ChatCompletionAnnotationChoice(int Index, ChatCompletionAnnotationDelta Delta);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Data structure representing annotation deltas in chat completions.
|
||||
/// </summary>
|
||||
/// <param name="Annotations">The list of annotations, which can be null.</param>
|
||||
public record ChatCompletionAnnotationDelta(IList<Annotation>? Annotations);
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a line of a chat completion annotation stream.
|
||||
/// </summary>
|
||||
/// <param name="Id">The unique identifier of the chat completion.</param>
|
||||
/// <param name="Object">The type of object returned, typically "chat.completion".</param>
|
||||
/// <param name="Created">The creation timestamp of the chat completion in Unix epoch format.</param>
|
||||
/// <param name="Model">The model used for the chat completion.</param>
|
||||
/// <param name="SystemFingerprint">The system fingerprint associated with the chat completion.</param>
|
||||
/// <param name="Choices">The list of choices returned in the chat completion.</param>
|
||||
public record ChatCompletionAnnotationStreamLine(string Id, string Object, uint Created, string Model, string SystemFingerprint, IList<ChatCompletionAnnotationChoice> Choices) : IAnnotationStreamLine
|
||||
{
|
||||
#region Implementation of IAnnotationStreamLine
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsSources() => this.Choices.Any(choice => choice.Delta.Annotations is not null && choice.Delta.Annotations.Any(annotation => annotation is not AnnotatingUnknown));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IList<ISource> GetSources()
|
||||
{
|
||||
var sources = new List<ISource>();
|
||||
foreach (var choice in this.Choices)
|
||||
{
|
||||
if (choice.Delta.Annotations is null)
|
||||
continue;
|
||||
|
||||
// Iterate through all annotations:
|
||||
foreach (var annotation in choice.Delta.Annotations)
|
||||
{
|
||||
// Check if the annotation is of the expected type and extract the source information:
|
||||
if (annotation is ChatCompletionAnnotatingURL urlAnnotation)
|
||||
sources.Add(new Source(urlAnnotation.UrlCitation.Title, urlAnnotation.UrlCitation.URL));
|
||||
|
||||
//
|
||||
// Check for the unexpected annotation type of the Responses API.
|
||||
//
|
||||
// This seems weird at first. But there are two possibilities why this could happen:
|
||||
// - Anyone of the open source providers such as ollama, LM Studio, etc. could
|
||||
// implement & use the Responses API data structures for annotations in their
|
||||
// chat completion endpoint.
|
||||
//
|
||||
// - Our custom JSON converter checks for the Responses API data type first. If it
|
||||
// fails, it checks for the chat completion API data type. So, when the Responses
|
||||
// API data type is valid, it will be deserialized into that type, even though
|
||||
// we are calling the chat completion endpoint.
|
||||
//
|
||||
if (annotation is ResponsesAnnotatingUrlCitationData citationData)
|
||||
sources.Add(new Source(citationData.Title, citationData.URL));
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Data model for a choice made by the AI.
|
||||
/// </summary>
|
||||
/// <param name="Index">The index of the choice.</param>
|
||||
/// <param name="Delta">The delta text of the choice.</param>
|
||||
public record ChatCompletionChoice(int Index, ChatCompletionDelta Delta)
|
||||
{
|
||||
public ChatCompletionChoice() : this(0, new (string.Empty))
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// The delta text of a choice.
|
||||
/// </summary>
|
||||
/// <param name="Content">The content of the delta text.</param>
|
||||
public record ChatCompletionDelta(string Content)
|
||||
{
|
||||
public ChatCompletionDelta() : this(string.Empty)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Data model for a delta line in the chat completion response stream.
|
||||
/// </summary>
|
||||
/// <param name="Id">The id of the response.</param>
|
||||
/// <param name="Object">The object describing the response.</param>
|
||||
/// <param name="Created">The timestamp of the response.</param>
|
||||
/// <param name="Model">The model used for the response.</param>
|
||||
/// <param name="SystemFingerprint">The system fingerprint; together with the seed, this allows you to reproduce the response.</param>
|
||||
/// <param name="Choices">The choices made by the AI.</param>
|
||||
public record ChatCompletionDeltaStreamLine(string Id, string Object, uint Created, string Model, string SystemFingerprint, IList<ChatCompletionChoice> Choices) : IResponseStreamLine
|
||||
{
|
||||
public ChatCompletionDeltaStreamLine() : this(string.Empty, string.Empty, 0, string.Empty, string.Empty, [])
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsContent() => this.Choices.Count > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []);
|
||||
|
||||
#region Implementation of IAnnotationStreamLine
|
||||
|
||||
//
|
||||
// Please note that there are multiple options where LLM providers might stream sources:
|
||||
//
|
||||
// - As part of the delta content while streaming. That would be part of this class.
|
||||
// - By using a dedicated stream event and data structure. That would be another class implementing IResponseStreamLine.
|
||||
//
|
||||
// Right now, OpenAI uses the latter approach, so we don't have any sources here. And
|
||||
// because no other provider does it yet, we don't have any implementation here either.
|
||||
//
|
||||
// One example where sources are part of the delta content is the Perplexity provider.
|
||||
//
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsSources() => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IList<ISource> GetSources() => [];
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents citation data for a URL in a chat completion response.
|
||||
/// </summary>
|
||||
/// <param name="EndIndex">The end index of the citation in the response text.</param>
|
||||
/// <param name="StartIndex">The start index of the citation in the response text.</param>
|
||||
/// <param name="Title">The title of the cited source.</param>
|
||||
/// <param name="URL">The URL of the cited source.</param>
|
||||
public sealed record ChatCompletionUrlCitationData(
|
||||
int EndIndex,
|
||||
int StartIndex,
|
||||
string Title,
|
||||
string URL);
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// The OpenAI chat request model.
|
||||
/// </summary>
|
||||
/// <param name="Model">Which model to use for chat completion.</param>
|
||||
/// <param name="Messages">The chat messages.</param>
|
||||
/// <param name="Stream">Whether to stream the chat completion.</param>
|
||||
/// <param name="Seed">The seed for the chat completion.</param>
|
||||
/// <param name="FrequencyPenalty">The frequency penalty for the chat completion.</param>
|
||||
public readonly record struct ChatRequest(
|
||||
string Model,
|
||||
IList<Message> Messages,
|
||||
bool Stream,
|
||||
int Seed,
|
||||
|
||||
[Range(-2.0f, 2.0f)]
|
||||
float FrequencyPenalty
|
||||
);
|
||||
@@ -30,16 +30,20 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
yield break;
|
||||
|
||||
// Unfortunately, OpenAI changed the name of the system prompt based on the model.
|
||||
// All models that start with "o" (the omni aka reasoning models) and all GPT4o 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.
|
||||
// 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.
|
||||
//
|
||||
// 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:
|
||||
var systemPromptRole = chatModel.Id.StartsWith('o') || chatModel.Id.Contains("4o") ? "developer" : "system";
|
||||
var systemPromptRole =
|
||||
chatModel.Id.StartsWith('o') ||
|
||||
chatModel.Id.StartsWith("gpt-5", StringComparison.Ordinal) ||
|
||||
chatModel.Id.Contains("4o") ? "developer" : "system";
|
||||
|
||||
// Check if the model is an early version of the reasoning models:
|
||||
systemPromptRole = chatModel.Id switch
|
||||
@@ -51,53 +55,113 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
|
||||
_ => systemPromptRole,
|
||||
};
|
||||
|
||||
this.logger.LogInformation($"Using the system prompt role '{systemPromptRole}' for model '{chatModel.Id}'.");
|
||||
|
||||
// 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);
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new Message
|
||||
{
|
||||
Role = systemPromptRole,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
};
|
||||
|
||||
// Prepare the OpenAI HTTP chat request:
|
||||
var openAIChatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
|
||||
//
|
||||
// Prepare the tools we want to use:
|
||||
//
|
||||
IList<Tool> tools = modelCapabilities.Contains(Capability.WEB_SEARCH) switch
|
||||
{
|
||||
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
|
||||
true => [ Tools.WEB_SEARCH ],
|
||||
_ => []
|
||||
};
|
||||
|
||||
//
|
||||
// Create the request: either for the Responses API or the Chat Completion API
|
||||
//
|
||||
var openAIChatRequest = usingResponsesAPI switch
|
||||
{
|
||||
// Chat Completion API request:
|
||||
false => JsonSerializer.Serialize(new ChatCompletionAPIRequest
|
||||
{
|
||||
Role = n.Role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => systemPromptRole,
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
Content = n.Content switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
_ => string.Empty,
|
||||
}
|
||||
}).ToList()],
|
||||
|
||||
Seed = chatThread.Seed,
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
// 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
|
||||
{
|
||||
Role = n.Role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => systemPromptRole,
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
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
|
||||
{
|
||||
Role = n.Role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => systemPromptRole,
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
Content = n.Content switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
_ => string.Empty,
|
||||
}
|
||||
}).ToList()],
|
||||
|
||||
// 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),
|
||||
};
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, requestPath);
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
@@ -106,29 +170,35 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
if (usingResponsesAPI)
|
||||
await foreach (var content in this.StreamResponsesInternal<ResponsesDeltaStreamLine, ResponsesAnnotationStreamLine>("OpenAI", RequestBuilder, token))
|
||||
yield return content;
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("OpenAI", RequestBuilder, token))
|
||||
yield return content;
|
||||
else
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("OpenAI", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(["gpt-", "o1-", "o3-", "o4-"], token, apiKeyProvisional);
|
||||
var models = await this.LoadModels(["chatgpt-", "gpt-", "o1-", "o3-", "o4-"], token, apiKeyProvisional);
|
||||
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) &&
|
||||
!model.Id.Contains("transcribe", StringComparison.OrdinalIgnoreCase) &&
|
||||
!model.Id.Contains("o1-pro", StringComparison.OrdinalIgnoreCase));
|
||||
!model.Id.Contains("transcribe", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -147,6 +217,26 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
{
|
||||
var modelName = model.Id.ToLowerInvariant().AsSpan();
|
||||
|
||||
if (modelName is "gpt-4o-search-preview")
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.WEB_SEARCH,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
|
||||
if (modelName is "gpt-4o-mini-search-preview")
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.WEB_SEARCH,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
|
||||
if (modelName.StartsWith("o1-mini"))
|
||||
return
|
||||
[
|
||||
@@ -154,32 +244,63 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.ALWAYS_REASONING,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
|
||||
if(modelName is "gpt-3.5-turbo")
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
Capability.RESPONSES_API,
|
||||
];
|
||||
|
||||
if(modelName.StartsWith("gpt-3.5"))
|
||||
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"))
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING
|
||||
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
|
||||
Capability.RESPONSES_API,
|
||||
];
|
||||
|
||||
if (modelName.StartsWith("o4-mini") || modelName.StartsWith("o1") || modelName.StartsWith("o3"))
|
||||
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.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
|
||||
Capability.WEB_SEARCH,
|
||||
Capability.RESPONSES_API,
|
||||
];
|
||||
|
||||
if(modelName.StartsWith("gpt-3.5"))
|
||||
if (modelName.StartsWith("o1"))
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
];
|
||||
[
|
||||
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
|
||||
Capability.RESPONSES_API,
|
||||
];
|
||||
|
||||
if(modelName.StartsWith("gpt-4-turbo"))
|
||||
return
|
||||
@@ -187,7 +308,8 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.FUNCTION_CALLING
|
||||
Capability.FUNCTION_CALLING,
|
||||
Capability.RESPONSES_API,
|
||||
];
|
||||
|
||||
if(modelName is "gpt-4" || modelName.StartsWith("gpt-4-"))
|
||||
@@ -195,14 +317,37 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
[
|
||||
Capability.TEXT_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
Capability.RESPONSES_API,
|
||||
];
|
||||
|
||||
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,
|
||||
];
|
||||
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.FUNCTION_CALLING,
|
||||
Capability.RESPONSES_API,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Data model for a line in the response stream, for streaming completions.
|
||||
/// </summary>
|
||||
/// <param name="Id">The id of the response.</param>
|
||||
/// <param name="Object">The object describing the response.</param>
|
||||
/// <param name="Created">The timestamp of the response.</param>
|
||||
/// <param name="Model">The model used for the response.</param>
|
||||
/// <param name="SystemFingerprint">The system fingerprint; together with the seed, this allows you to reproduce the response.</param>
|
||||
/// <param name="Choices">The choices made by the AI.</param>
|
||||
public readonly record struct ResponseStreamLine(string Id, string Object, uint Created, string Model, string SystemFingerprint, IList<Choice> Choices) : IResponseStreamLine
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool ContainsContent() => this != default && this.Choices.Count > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data model for a choice made by the AI.
|
||||
/// </summary>
|
||||
/// <param name="Index">The index of the choice.</param>
|
||||
/// <param name="Delta">The delta text of the choice.</param>
|
||||
public readonly record struct Choice(int Index, Delta Delta);
|
||||
|
||||
/// <summary>
|
||||
/// The delta text of a choice.
|
||||
/// </summary>
|
||||
/// <param name="Content">The content of the delta text.</param>
|
||||
public readonly record struct Delta(string Content);
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// The request body for the Responses API.
|
||||
/// </summary>
|
||||
/// <param name="Model">Which model to use.</param>
|
||||
/// <param name="Input">The chat messages.</param>
|
||||
/// <param name="Stream">Whether to stream the response.</param>
|
||||
/// <param name="Store">Whether to store the response on the server (usually OpenAI's infrastructure).</param>
|
||||
/// <param name="Tools">The tools to use for the request.</param>
|
||||
public record ResponsesAPIRequest(
|
||||
string Model,
|
||||
IList<Message> Input,
|
||||
bool Stream,
|
||||
bool Store,
|
||||
IList<Tool> Tools)
|
||||
{
|
||||
public ResponsesAPIRequest() : this(string.Empty, [], true, false, [])
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Data structure for URL citation annotations in the OpenAI Responses API.
|
||||
/// </summary>
|
||||
/// <param name="Type">The type of annotation, typically "url_citation".</param>
|
||||
/// <param name="EndIndex">The end index of the annotated text in the response.</param>
|
||||
/// <param name="StartIndex">The start index of the annotated text in the response.</param>
|
||||
/// <param name="Title">The title of the cited URL.</param>
|
||||
/// <param name="URL">The URL being cited.</param>
|
||||
public sealed record ResponsesAnnotatingUrlCitationData(
|
||||
string Type,
|
||||
int EndIndex,
|
||||
int StartIndex,
|
||||
string Title,
|
||||
string URL) : Annotation(Type);
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Data structure for a line in the response stream of the Responses API, containing an annotation.
|
||||
/// </summary>
|
||||
/// <param name="Type">The type of the annotation.</param>
|
||||
/// <param name="AnnotationIndex">The continuous index of the annotation in the response.</param>
|
||||
/// <param name="Annotation">The annotation details.</param>
|
||||
public sealed record ResponsesAnnotationStreamLine(string Type, int AnnotationIndex, Annotation Annotation) : IAnnotationStreamLine
|
||||
{
|
||||
#region Implementation of IAnnotationStreamLine
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsSources()
|
||||
{
|
||||
return this.Annotation is not AnnotatingUnknown;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IList<ISource> GetSources()
|
||||
{
|
||||
//
|
||||
// Check for the unexpected annotation type of the chat completion API.
|
||||
//
|
||||
// This seems weird at first. But there are two possibilities why this could happen:
|
||||
// - Anyone of the open source providers such as ollama, LM Studio, etc. could
|
||||
// implement and use the chat completion API data structures for annotations in their
|
||||
// Responses API endpoint.
|
||||
//
|
||||
// - Our custom JSON converter checks for all possible annotation data types. So,
|
||||
// when the streamed data is valid for any annotation type, it will be deserialized
|
||||
// into that type, even though we are calling the Responses API endpoint.
|
||||
//
|
||||
if (this.Annotation is ChatCompletionAnnotatingURL urlAnnotation)
|
||||
return [new Source(urlAnnotation.UrlCitation.Title, urlAnnotation.UrlCitation.URL)];
|
||||
|
||||
// Check for the expected annotation type of the Responses API:
|
||||
if (this.Annotation is ResponsesAnnotatingUrlCitationData urlCitationData)
|
||||
return [new Source(urlCitationData.Title, urlCitationData.URL)];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Data model for a delta line in the Response API chat completion stream.
|
||||
/// </summary>
|
||||
/// <param name="Type">The type of the response.</param>
|
||||
/// <param name="Delta">The delta content of the response.</param>
|
||||
public record ResponsesDeltaStreamLine(
|
||||
string Type,
|
||||
string Delta) : IResponseStreamLine
|
||||
{
|
||||
#region Implementation of IResponseStreamLine
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsContent() => !string.IsNullOrWhiteSpace(this.Delta);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ContentStreamChunk GetContent() => new(this.Delta, this.GetSources());
|
||||
|
||||
//
|
||||
// Please note that there are multiple options where LLM providers might stream sources:
|
||||
//
|
||||
// - As part of the delta content while streaming. That would be part of this class.
|
||||
// - By using a dedicated stream event and data structure. That would be another class implementing IResponseStreamLine.
|
||||
//
|
||||
// Right now, OpenAI uses the latter approach, so we don't have any sources here. And
|
||||
// because no other provider does it yet, we don't have any implementation here either.
|
||||
//
|
||||
// One example where sources are part of the delta content is the Perplexity provider.
|
||||
//
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsSources() => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IList<ISource> GetSources() => [];
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a tool used by the AI model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Right now, only our OpenAI provider is using tools. Thus, this class is located in the
|
||||
/// OpenAI namespace. In the future, when other providers also support tools, this class can
|
||||
/// be moved into the provider namespace.
|
||||
/// </remarks>
|
||||
/// <param name="Type">The type of the tool.</param>
|
||||
public record Tool(string Type);
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Known tools for LLM providers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Right now, only our OpenAI provider is using tools. Thus, this class is located in the
|
||||
/// OpenAI namespace. In the future, when other providers also support tools, this class can
|
||||
/// be moved into the provider namespace.
|
||||
/// </remarks>
|
||||
public static class Tools
|
||||
{
|
||||
public static readonly Tool WEB_SEARCH = new("web_search");
|
||||
}
|
||||
Reference in new issue
Block a user