Added expert mode to providers (#562)
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 / 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 / Publish release (push) Blocked by required conditions

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Peer SchüttandThorsten Sommer authored and GitHub committed 2025-11-13 18:13:16 +01:00
1 parent d6f5dc143d
commit 9ef1a047f0
44 files changed
+860 -531

No files matched your search

@@ -36,6 +36,9 @@ public sealed class ProviderAlibabaCloud() : BaseProvider("https://dashscope-int
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the AlibabaCloud HTTP chat request:
var alibabaCloudChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{
@@ -63,6 +66,7 @@ public sealed class ProviderAlibabaCloud() : BaseProvider("https://dashscope-int
}
}).ToList()],
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -141,84 +145,6 @@ public sealed class ProviderAlibabaCloud() : BaseProvider("https://dashscope-int
return this.LoadModels(["text-embedding-"], token, apiKeyProvisional).ContinueWith(t => t.Result.Concat(additionalModels).OrderBy(x => x.Id).AsEnumerable(), token);
}
/// <inheritdoc />
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
// Qwen models:
if (modelName.StartsWith("qwen"))
{
// Check for omni models:
if (modelName.IndexOf("omni") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.AUDIO_INPUT, Capability.SPEECH_INPUT,
Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT, Capability.SPEECH_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
// Check for Qwen 3:
if(modelName.StartsWith("qwen3"))
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if(modelName.IndexOf("-vl-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
// QwQ models:
if (modelName.StartsWith("qwq"))
{
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
// QVQ models:
if (modelName.StartsWith("qvq"))
{
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
}
// Default to text input and output:
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
#endregion
@@ -1,3 +1,4 @@
using System.Text.Json.Serialization;
using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider.Anthropic;
@@ -16,4 +17,9 @@ public readonly record struct ChatRequest(
int MaxTokens,
bool Stream,
string System
);
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}
@@ -26,6 +26,9 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters("system");
// Prepare the Anthropic HTTP chat request:
var chatRequest = JsonSerializer.Serialize(new ChatRequest
@@ -52,10 +55,11 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
}).ToList()],
System = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
MaxTokens = 4_096,
MaxTokens = apiParameters.TryGetValue("max_tokens", out var value) && value is int intValue ? intValue : 4_096,
// Right now, we only support streaming completions:
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -113,49 +117,6 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
{
return Task.FromResult(Enumerable.Empty<Model>());
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
// Claude 4.x models:
if(modelName.StartsWith("claude-opus-4") || modelName.StartsWith("claude-sonnet-4"))
return [
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Claude 3.7 is able to do reasoning:
if(modelName.StartsWith("claude-3-7"))
return [
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// All other 3.x models are able to process text and images as input:
if(modelName.StartsWith("claude-3-"))
return [
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Any other model is able to process text only:
return [
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
#endregion
+76 -19
View File
@@ -40,7 +40,8 @@ public abstract class BaseProvider : IProvider, ISecretId
protected static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
Converters = { new AnnotationConverter() }
Converters = { new AnnotationConverter() },
AllowTrailingCommas = false
};
/// <summary>
@@ -63,7 +64,10 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <inheritdoc />
public abstract string InstanceName { get; set; }
/// <inheritdoc />
public string AdditionalJsonApiParameters { get; init; } = string.Empty;
/// <inheritdoc />
public abstract IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default);
@@ -78,9 +82,6 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <inheritdoc />
public abstract Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default);
/// <inheritdoc />
public abstract IReadOnlyCollection<Capability> GetModelCapabilities(Model model);
#endregion
@@ -129,8 +130,7 @@ public abstract class BaseProvider : IProvider, ISecretId
if (nextResponse.StatusCode is HttpStatusCode.Forbidden)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Block, string.Format(TB("Tried to communicate with the LLM provider '{0}'. You might not be able to use this provider from your location. The provider message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase)));
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
this.logger.LogDebug($"Error body: {errorBody}");
this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
errorMessage = nextResponse.ReasonPhrase;
break;
}
@@ -138,8 +138,7 @@ public abstract class BaseProvider : IProvider, ISecretId
if(nextResponse.StatusCode is HttpStatusCode.BadRequest)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. The required message format might be changed. The provider message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase)));
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
this.logger.LogDebug($"Error body: {errorBody}");
this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
errorMessage = nextResponse.ReasonPhrase;
break;
}
@@ -147,8 +146,7 @@ public abstract class BaseProvider : IProvider, ISecretId
if(nextResponse.StatusCode is HttpStatusCode.NotFound)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. Something was not found. The provider message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase)));
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
this.logger.LogDebug($"Error body: {errorBody}");
this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
errorMessage = nextResponse.ReasonPhrase;
break;
}
@@ -156,8 +154,7 @@ public abstract class BaseProvider : IProvider, ISecretId
if(nextResponse.StatusCode is HttpStatusCode.Unauthorized)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Key, string.Format(TB("Tried to communicate with the LLM provider '{0}'. The API key might be invalid. The provider message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase)));
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
this.logger.LogDebug($"Error body: {errorBody}");
this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
errorMessage = nextResponse.ReasonPhrase;
break;
}
@@ -165,8 +162,7 @@ public abstract class BaseProvider : IProvider, ISecretId
if(nextResponse.StatusCode is HttpStatusCode.InternalServerError)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. The server might be down or having issues. The provider message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase)));
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
this.logger.LogDebug($"Error body: {errorBody}");
this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
errorMessage = nextResponse.ReasonPhrase;
break;
}
@@ -174,8 +170,7 @@ public abstract class BaseProvider : IProvider, ISecretId
if(nextResponse.StatusCode is HttpStatusCode.ServiceUnavailable)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. The provider is overloaded. The message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase)));
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
this.logger.LogDebug($"Error body: {errorBody}");
this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
errorMessage = nextResponse.ReasonPhrase;
break;
}
@@ -185,13 +180,13 @@ public abstract class BaseProvider : IProvider, ISecretId
if(timeSeconds > 90)
timeSeconds = 90;
this.logger.LogDebug($"Failed request with status code {nextResponse.StatusCode} (message = '{errorMessage}'). Retrying in {timeSeconds:0.00} seconds.");
this.logger.LogDebug("Failed request with status code {ResponseStatusCode} (message = '{ErrorMessage}'). Retrying in {TimeSeconds:0.00} seconds.", nextResponse.StatusCode, errorMessage, timeSeconds);
await Task.Delay(TimeSpan.FromSeconds(timeSeconds), token);
}
if(retry >= MAX_RETRIES || !string.IsNullOrWhiteSpace(errorMessage))
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. Even after {1} retries, there were some problems with the request. The provider message is: '{2}'"), this.InstanceName, MAX_RETRIES, errorMessage)));
await MessageBus.INSTANCE.SendError(new DataErrorMessage(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. Even after {1} retries, there were some problems with the request. The provider message is: '{2}'."), this.InstanceName, MAX_RETRIES, errorMessage)));
return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response);
}
@@ -522,4 +517,66 @@ public abstract class BaseProvider : IProvider, ISecretId
streamReader.Dispose();
}
/// <summary>
/// Parse and convert API parameters from a provided JSON string into a dictionary,
/// optionally merging additional parameters and removing specific keys.
/// </summary>
/// <param name="keysToRemove">Optional list of keys to remove from the final dictionary
/// (case-insensitive). The parameters stream, model, and messages are removed by default.</param>
protected IDictionary<string, object> ParseAdditionalApiParameters(
params List<string> keysToRemove)
{
if(string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters))
return new Dictionary<string, object>();
try
{
// Wrap the user-provided parameters in curly brackets to form a valid JSON object:
var json = $"{{{this.AdditionalJsonApiParameters}}}";
var jsonDoc = JsonSerializer.Deserialize<JsonElement>(json, JSON_SERIALIZER_OPTIONS);
var dict = ConvertToDictionary(jsonDoc);
// Some keys are always removed because we set them:
keysToRemove.Add("stream");
keysToRemove.Add("model");
keysToRemove.Add("messages");
// Remove the specified keys (case-insensitive):
var removeSet = new HashSet<string>(keysToRemove, StringComparer.OrdinalIgnoreCase);
foreach (var key in removeSet)
dict.Remove(key);
return dict;
}
catch (JsonException ex)
{
this.logger.LogError("Failed to parse additional API parameters: {ExceptionMessage}", ex.Message);
return new Dictionary<string, object>();
}
}
private static IDictionary<string, object> ConvertToDictionary(JsonElement element)
{
return element.EnumerateObject()
.ToDictionary<JsonProperty, string, object>(
p => p.Name,
p => ConvertJsonValue(p.Value) ?? string.Empty
);
}
private static object? ConvertJsonValue(JsonElement element) => element.ValueKind switch
{
JsonValueKind.String => element.GetString(),
JsonValueKind.Number => element.TryGetInt32(out var i) ? i :
element.TryGetInt64(out var l) ? l :
element.TryGetDouble(out var d) ? d :
element.GetDecimal(),
JsonValueKind.True or JsonValueKind.False => element.GetBoolean(),
JsonValueKind.Null => string.Empty,
JsonValueKind.Object => ConvertToDictionary(element),
JsonValueKind.Array => element.EnumerateArray().Select(ConvertJsonValue).ToList(),
_ => string.Empty,
};
}
@@ -1,210 +0,0 @@
namespace AIStudio.Provider;
public static class CapabilitiesOpenSource
{
public static IReadOnlyCollection<Capability> GetCapabilities(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
//
// Checking for names in the case of open source models is a hard task.
// Let's assume we want to check for the llama 3.1 405b model.
//
// Here is a not complete list of how providers name this model:
// - Fireworks: accounts/fireworks/models/llama-v3p1-405b-instruct
// - Hugging Face -> Nebius AI Studio: meta-llama/Meta-Llama-3.1-405B-Instruct
// - Groq: llama-3.1-405b-instruct
// - LM Studio: llama-3.1-405b-instruct
// - Helmholtz Blablador: 1 - Llama3 405 the best general model
// - GWDG: Llama 3.1 405B Instruct
//
//
// Meta llama models:
//
if (modelName.IndexOf("llama") is not -1)
{
if (modelName.IndexOf("llama4") is not -1 ||
modelName.IndexOf("llama 4") is not -1 ||
modelName.IndexOf("llama-4") is not -1 ||
modelName.IndexOf("llama-v4") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// The old vision models cannot do function calling:
if (modelName.IndexOf("vision") is not -1)
return [
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
//
// All models >= 3.1 are able to do function calling:
//
if (modelName.IndexOf("llama3.") is not -1 ||
modelName.IndexOf("llama 3.") is not -1 ||
modelName.IndexOf("llama-3.") is not -1 ||
modelName.IndexOf("llama-v3p") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// All other llama models can only do text input and output:
return [
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
//
// DeepSeek models:
//
if (modelName.IndexOf("deepseek") is not -1)
{
if(modelName.IndexOf("deepseek-r1") is not -1 ||
modelName.IndexOf("deepseek r1") is not -1)
return [
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
return [
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
//
// Qwen models:
//
if (modelName.IndexOf("qwen") is not -1 || modelName.IndexOf("qwq") is not -1)
{
if (modelName.IndexOf("qwq") is not -1)
return [
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
return [
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
//
// Mistral models:
//
if (modelName.IndexOf("mistral") is not -1 ||
modelName.IndexOf("pixtral") is not -1)
{
if(modelName.IndexOf("pixtral") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if (modelName.IndexOf("3.1") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Default:
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
//
// Grok models:
//
if (modelName.IndexOf("grok") is not -1)
{
if(modelName.IndexOf("-vision-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
if(modelName.StartsWith("grok-3-mini"))
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if(modelName.StartsWith("grok-3"))
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
//
// OpenAI models:
//
if (modelName.IndexOf("gpt-oss") is not -1 ||
modelName.IndexOf("gpt-3.5") is not -1)
{
if(modelName.IndexOf("gpt-oss") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.WEB_SEARCH,
Capability.CHAT_COMPLETION_API,
];
if(modelName.IndexOf("gpt-3.5") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
// Default:
return [
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
}
@@ -36,6 +36,9 @@ public sealed class ProviderDeepSeek() : BaseProvider("https://api.deepseek.com/
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the DeepSeek HTTP chat request:
var deepSeekChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{
@@ -63,6 +66,7 @@ public sealed class ProviderDeepSeek() : BaseProvider("https://api.deepseek.com/
}
}).ToList()],
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -108,28 +112,6 @@ public sealed class ProviderDeepSeek() : BaseProvider("https://api.deepseek.com/
return Task.FromResult(Enumerable.Empty<Model>());
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
if(modelName.IndexOf("reasoner") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
#endregion
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.Fireworks;
/// <summary>
@@ -10,4 +12,9 @@ public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
bool Stream
);
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}
@@ -36,6 +36,9 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the Fireworks HTTP chat request:
var fireworksChatRequest = JsonSerializer.Serialize(new ChatRequest
{
@@ -65,6 +68,7 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere
// Right now, we only support streaming completions:
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -110,7 +114,5 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere
return Task.FromResult(Enumerable.Empty<Model>());
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => CapabilitiesOpenSource.GetCapabilities(model);
#endregion
}
@@ -36,6 +36,9 @@ public sealed class ProviderGWDG() : BaseProvider("https://chat-ai.academiccloud
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the GWDG HTTP chat request:
var gwdgChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{
@@ -63,6 +66,7 @@ public sealed class ProviderGWDG() : BaseProvider("https://chat-ai.academiccloud
}
}).ToList()],
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -110,8 +114,6 @@ public sealed class ProviderGWDG() : BaseProvider("https://chat-ai.academiccloud
return models.Where(model => model.Id.StartsWith("e5-", StringComparison.InvariantCultureIgnoreCase));
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => CapabilitiesOpenSource.GetCapabilities(model);
#endregion
private async Task<IEnumerable<Model>> LoadModels(CancellationToken token, string? apiKeyProvisional = null)
@@ -1,3 +1,4 @@
using System.Text.Json.Serialization;
using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider.Google;
@@ -12,4 +13,9 @@ public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
bool Stream
);
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}
@@ -36,6 +36,9 @@ public class ProviderGoogle() : BaseProvider("https://generativelanguage.googlea
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the Google HTTP chat request:
var geminiChatRequest = JsonSerializer.Serialize(new ChatRequest
{
@@ -65,6 +68,7 @@ public class ProviderGoogle() : BaseProvider("https://generativelanguage.googlea
// Right now, we only support streaming completions:
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -122,94 +126,6 @@ public class ProviderGoogle() : BaseProvider("https://generativelanguage.googlea
.Select(n => new Provider.Model(n.Name.Replace("models/", string.Empty), n.DisplayName));
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Provider.Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
if (modelName.IndexOf("gemini-") is not -1)
{
// Reasoning models:
if (modelName.IndexOf("gemini-2.5") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.AUDIO_INPUT,
Capability.SPEECH_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Image generation:
if(modelName.IndexOf("-2.0-flash-preview-image-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.AUDIO_INPUT,
Capability.SPEECH_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT, Capability.IMAGE_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
// Realtime model:
if(modelName.IndexOf("-2.0-flash-live-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.AUDIO_INPUT, Capability.SPEECH_INPUT,
Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT, Capability.SPEECH_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// The 2.0 flash models cannot call functions:
if(modelName.IndexOf("-2.0-flash-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.AUDIO_INPUT,
Capability.SPEECH_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
// The old 1.0 pro vision model:
if(modelName.IndexOf("pro-vision") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
// Default to all other Gemini models:
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.AUDIO_INPUT,
Capability.SPEECH_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
// Default for all other models:
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
#endregion
@@ -1,3 +1,4 @@
using System.Text.Json.Serialization;
using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider.Groq;
@@ -14,4 +15,9 @@ public readonly record struct ChatRequest(
IList<Message> Messages,
bool Stream,
int Seed
);
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}
@@ -36,6 +36,9 @@ public class ProviderGroq() : BaseProvider("https://api.groq.com/openai/v1/", LO
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the OpenAI HTTP chat request:
var groqChatRequest = JsonSerializer.Serialize(new ChatRequest
{
@@ -65,6 +68,7 @@ public class ProviderGroq() : BaseProvider("https://api.groq.com/openai/v1/", LO
// Right now, we only support streaming completions:
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -110,8 +114,6 @@ public class ProviderGroq() : BaseProvider("https://api.groq.com/openai/v1/", LO
return Task.FromResult(Enumerable.Empty<Model>());
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => CapabilitiesOpenSource.GetCapabilities(model);
#endregion
private async Task<IEnumerable<Model>> LoadModels(CancellationToken token, string? apiKeyProvisional = null)
@@ -36,6 +36,9 @@ public sealed class ProviderHelmholtz() : BaseProvider("https://api.helmholtz-bl
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the Helmholtz HTTP chat request:
var helmholtzChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{
@@ -63,6 +66,7 @@ public sealed class ProviderHelmholtz() : BaseProvider("https://api.helmholtz-bl
}
}).ToList()],
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -114,8 +118,6 @@ public sealed class ProviderHelmholtz() : BaseProvider("https://api.helmholtz-bl
model.Id.Contains("gritlm", StringComparison.InvariantCultureIgnoreCase));
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => CapabilitiesOpenSource.GetCapabilities(model);
#endregion
private async Task<IEnumerable<Model>> LoadModels(CancellationToken token, string? apiKeyProvisional = null)
@@ -41,6 +41,9 @@ public sealed class ProviderHuggingFace : BaseProvider
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the HuggingFace HTTP chat request:
var huggingfaceChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{
@@ -68,6 +71,7 @@ public sealed class ProviderHuggingFace : BaseProvider
}
}).ToList()],
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -113,7 +117,5 @@ public sealed class ProviderHuggingFace : BaseProvider
return Task.FromResult(Enumerable.Empty<Model>());
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => CapabilitiesOpenSource.GetCapabilities(model);
#endregion
}
+5 -6
View File
@@ -19,6 +19,11 @@ public interface IProvider
/// </summary>
public string InstanceName { get; }
/// <summary>
/// The additional API parameters.
/// </summary>
public string AdditionalJsonApiParameters { get; }
/// <summary>
/// Starts a chat completion stream.
/// </summary>
@@ -64,10 +69,4 @@ public interface IProvider
/// <returns>The list of embedding models.</returns>
public Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default);
/// <summary>
/// Get the capabilities of a model.
/// </summary>
/// <param name="model">The model to get the capabilities for.</param>
/// <returns>The capabilities of the model.</returns>
public IReadOnlyCollection<Capability> GetModelCapabilities(Model model);
}
@@ -144,7 +144,7 @@ public static class LLMProvidersExtensions
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings)
{
return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider);
return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.AdditionalJsonApiParameters);
}
/// <summary>
@@ -157,29 +157,29 @@ public static class LLMProvidersExtensions
return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE);
}
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider)
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider, string expertProviderApiParameter = "")
{
try
{
return provider switch
{
LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName },
LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName },
LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName },
LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName },
LLMProviders.X => new ProviderX { InstanceName = instanceName },
LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName },
LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName },
LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName },
LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.X => new ProviderX { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName },
LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName },
LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName },
LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName },
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName },
LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName },
LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter },
_ => new NoProvider(),
};
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.Mistral;
/// <summary>
@@ -14,4 +16,9 @@ public readonly record struct ChatRequest(
bool Stream,
int RandomSeed,
bool SafePrompt = false
);
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}
@@ -34,6 +34,9 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the Mistral HTTP chat request:
var mistralChatRequest = JsonSerializer.Serialize(new ChatRequest
{
@@ -63,7 +66,8 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
// Right now, we only support streaming completions:
Stream = true,
SafePrompt = false,
SafePrompt = apiParameters.TryGetValue("safe_prompt", out var value) && value is true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -122,56 +126,6 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
return Task.FromResult(Enumerable.Empty<Provider.Model>());
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Provider.Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
// Pixtral models are able to do process images:
if (modelName.IndexOf("pixtral") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Mistral medium:
if (modelName.IndexOf("mistral-medium-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Mistral small:
if (modelName.IndexOf("mistral-small-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Mistral saba:
if (modelName.IndexOf("mistral-saba-") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
// Default:
return CapabilitiesOpenSource.GetCapabilities(model);
}
#endregion
private async Task<ModelsResponse> LoadModelList(string? apiKeyProvisional, CancellationToken token)
@@ -13,6 +13,9 @@ public class NoProvider : IProvider
public string InstanceName { get; set; } = "None";
/// <inheritdoc />
public string AdditionalJsonApiParameters { get; init; } = string.Empty;
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.OpenAI;
/// <summary>
@@ -15,4 +17,8 @@ public record ChatCompletionAPIRequest(
public ChatCompletionAPIRequest() : this(string.Empty, [], true)
{
}
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}
@@ -59,7 +59,7 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
};
// Read the model capabilities:
var modelCapabilities = this.GetModelCapabilities(chatModel);
var modelCapabilities = ProviderExtensions.GetModelCapabilitiesOpenAI(chatModel);
// Check if we are using the Responses API or the Chat Completion API:
var usingResponsesAPI = modelCapabilities.Contains(Capability.RESPONSES_API);
@@ -85,6 +85,10 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
_ => []
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters("input", "store", "tools");
//
// Create the request: either for the Responses API or the Chat Completion API
//
@@ -119,6 +123,7 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
// Right now, we only support streaming completions:
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS),
// Responses API request:
@@ -157,6 +162,9 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
// Tools we want to use:
Tools = tools,
// Additional API parameters:
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS),
};
@@ -215,144 +223,6 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"
return this.LoadModels(["text-embedding-"], token, apiKeyProvisional);
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model)
{
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
[
Capability.TEXT_INPUT,
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.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,
];
if (modelName.StartsWith("o1"))
return
[
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
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.RESPONSES_API,
];
if(modelName is "gpt-4" || modelName.StartsWith("gpt-4-"))
return
[
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,
];
}
#endregion
private async Task<IEnumerable<Model>> LoadModels(string[] prefixes, CancellationToken token, string? apiKeyProvisional = null)
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.OpenAI;
/// <summary>
@@ -18,4 +20,8 @@ public record ResponsesAPIRequest(
public ResponsesAPIRequest() : this(string.Empty, [], true, false, [])
{
}
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}
@@ -45,6 +45,9 @@ public sealed class ProviderPerplexity() : BaseProvider("https://api.perplexity.
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the Perplexity HTTP chat request:
var perplexityChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{
@@ -72,6 +75,7 @@ public sealed class ProviderPerplexity() : BaseProvider("https://api.perplexity.
}
}).ToList()],
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -117,38 +121,6 @@ public sealed class ProviderPerplexity() : BaseProvider("https://api.perplexity.
return Task.FromResult(Enumerable.Empty<Model>());
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
if(modelName.IndexOf("reasoning") is not -1 ||
modelName.IndexOf("deep-research") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.IMAGE_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.WEB_SEARCH,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.IMAGE_OUTPUT,
Capability.WEB_SEARCH,
Capability.CHAT_COMPLETION_API,
];
}
#endregion
private Task<IEnumerable<Model>> LoadModels() => Task.FromResult<IEnumerable<Model>>(KNOWN_MODELS);
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.SelfHosted;
/// <summary>
@@ -10,4 +12,9 @@ public readonly record struct ChatRequest(
string Model,
IList<Message> Messages,
bool Stream
);
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}
@@ -32,6 +32,9 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the OpenAI HTTP chat request:
var providerChatRequest = JsonSerializer.Serialize(new ChatRequest
{
@@ -60,7 +63,8 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
}).ToList()],
// Right now, we only support streaming completions:
Stream = true
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -142,8 +146,6 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
}
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Provider.Model model) => CapabilitiesOpenSource.GetCapabilities(model);
#endregion
private async Task<IEnumerable<Provider.Model>> LoadModels(string[] ignorePhrases, string[] filterPhrases, CancellationToken token, string? apiKeyProvisional = null)
@@ -36,6 +36,9 @@ public sealed class ProviderX() : BaseProvider("https://api.x.ai/v1/", LOGGER)
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the xAI HTTP chat request:
var xChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{
@@ -65,6 +68,7 @@ public sealed class ProviderX() : BaseProvider("https://api.x.ai/v1/", LOGGER)
// Right now, we only support streaming completions:
Stream = true,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
@@ -111,8 +115,6 @@ public sealed class ProviderX() : BaseProvider("https://api.x.ai/v1/", LOGGER)
return Task.FromResult<IEnumerable<Model>>([]);
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => CapabilitiesOpenSource.GetCapabilities(model);
#endregion
private async Task<IEnumerable<Model>> LoadModels(string[] prefixes, CancellationToken token, string? apiKeyProvisional = null)