mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-11 15:53:47 +00:00
Merge branch 'main' into custom-tokenizer
This commit is contained in:
commit
e3cb7e9734
165 files changed
+4739
-1738
No files matched your search
@@ -1,7 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -20,56 +17,37 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "AlibabaCloud";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the AlibabaCloud HTTP chat request:
|
||||
var alibabaCloudChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
|
||||
"AlibabaCloud",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(alibabaCloudChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("AlibabaCloud", RequestBuilder, token))
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -95,7 +73,7 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var additionalModels = new[]
|
||||
{
|
||||
@@ -124,17 +102,21 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
||||
new Model("qwen2.5-vl-3b-instruct", "Qwen2.5-VL 3b"),
|
||||
};
|
||||
|
||||
return this.LoadModels(["q"], SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional).ContinueWith(t => t.Result.Concat(additionalModels).OrderBy(x => x.Id).AsEnumerable(), token);
|
||||
var result = await this.LoadModels(["q"], SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Concat(additionalModels).OrderBy(x => x.Id)]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
|
||||
var additionalModels = new[]
|
||||
@@ -142,45 +124,33 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
||||
new Model("text-embedding-v3", "text-embedding-v3"),
|
||||
};
|
||||
|
||||
return this.LoadModels(["text-embedding-"], SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional).ContinueWith(t => t.Result.Concat(additionalModels).OrderBy(x => x.Id).AsEnumerable(), token);
|
||||
var result = await this.LoadModels(["text-embedding-"], SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Concat(additionalModels).OrderBy(x => x.Id)]
|
||||
};
|
||||
}
|
||||
|
||||
#region Overrides of BaseProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(string[] prefixes, SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private Task<ModelLoadResult> LoadModels(string[] prefixes, SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture)));
|
||||
return this.LoadModelsResponse<ModelsResponse>(
|
||||
storeType,
|
||||
"models",
|
||||
modelResponse => modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture))),
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -15,10 +14,15 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.ANTHROPIC.ToName();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "Anthropic";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
@@ -124,7 +128,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var additionalModels = new[]
|
||||
{
|
||||
@@ -136,59 +140,52 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
|
||||
new Model("claude-3-opus-latest", "Claude 3 Opus (Latest)"),
|
||||
};
|
||||
|
||||
return this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional).ContinueWith(t => t.Result.Concat(additionalModels).OrderBy(x => x.Id).AsEnumerable(), token);
|
||||
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Concat(additionalModels).OrderBy(x => x.Id)]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
return this.LoadModelsResponse<ModelsResponse>(
|
||||
storeType,
|
||||
"models?limit=100",
|
||||
modelResponse => modelResponse.Data,
|
||||
token,
|
||||
apiKeyProvisional,
|
||||
failureReasonSelector: (response, _) => response.StatusCode switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models?limit=100");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Add("x-api-key", secretKey);
|
||||
|
||||
// Set the Anthropic version:
|
||||
request.Headers.Add("anthropic-version", "2023-06-01");
|
||||
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(JSON_SERIALIZER_OPTIONS, token);
|
||||
return modelResponse.Data;
|
||||
System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
|
||||
System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
|
||||
},
|
||||
requestConfigurator: (request, secretKey) =>
|
||||
{
|
||||
request.Headers.Add("x-api-key", secretKey);
|
||||
request.Headers.Add("anthropic-version", "2023-06-01");
|
||||
},
|
||||
jsonSerializerOptions: JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
/// <summary>
|
||||
/// The HTTP client to use it for all requests.
|
||||
/// </summary>
|
||||
protected readonly HttpClient httpClient = new();
|
||||
protected readonly HttpClient HttpClient = new();
|
||||
|
||||
/// <summary>
|
||||
/// The logger to use.
|
||||
@@ -73,7 +73,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
this.Provider = provider;
|
||||
|
||||
// Set the base URL:
|
||||
this.httpClient.BaseAddress = new(url);
|
||||
this.HttpClient.BaseAddress = new(url);
|
||||
}
|
||||
|
||||
#region Handling of IProvider, which all providers must implement
|
||||
@@ -93,6 +93,9 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
/// <inheritdoc />
|
||||
public string TokenizerPath { get; init; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool HasModelLoadingCapability { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default);
|
||||
|
||||
@@ -106,16 +109,16 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
public abstract Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
public abstract Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
public abstract Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
public abstract Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
public abstract Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -131,6 +134,71 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
public string SecretName => this.InstanceName;
|
||||
|
||||
#endregion
|
||||
|
||||
protected static ModelLoadResult SuccessfulModelLoadResult(IEnumerable<Model> models) => ModelLoadResult.FromModels(models);
|
||||
|
||||
protected static ModelLoadResult FailedModelLoadResult(ModelLoadFailureReason failureReason, string? technicalDetails = null) => ModelLoadResult.Failure(failureReason, technicalDetails);
|
||||
|
||||
protected async Task<string?> GetModelLoadingSecretKey(SecretStoreType storeType, string? apiKeyProvisional = null, bool isTryingSecret = false) => apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType, isTrying: isTryingSecret) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
protected static ModelLoadFailureReason GetDefaultModelLoadFailureReason(HttpResponseMessage response) => response.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
|
||||
HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
|
||||
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
|
||||
};
|
||||
|
||||
protected async Task<ModelLoadResult> LoadModelsResponse<TResponse>(
|
||||
SecretStoreType storeType,
|
||||
string requestPath,
|
||||
Func<TResponse, IEnumerable<Model>> modelFactory,
|
||||
CancellationToken token,
|
||||
string? apiKeyProvisional = null,
|
||||
Func<HttpResponseMessage, string, ModelLoadFailureReason>? failureReasonSelector = null,
|
||||
Action<HttpRequestMessage, string>? requestConfigurator = null,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
bool isTryingSecret = false)
|
||||
{
|
||||
var secretKey = await this.GetModelLoadingSecretKey(storeType, apiKeyProvisional, isTryingSecret);
|
||||
if (string.IsNullOrWhiteSpace(secretKey) && !isTryingSecret)
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, "No API key available for model loading.");
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, requestPath);
|
||||
if (requestConfigurator is not null)
|
||||
requestConfigurator(request, secretKey ?? string.Empty);
|
||||
else if (!string.IsNullOrWhiteSpace(secretKey))
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var failureReason = failureReasonSelector?.Invoke(response, responseBody) ?? GetDefaultModelLoadFailureReason(response);
|
||||
return FailedModelLoadResult(failureReason, $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{responseBody}'");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parsedResponse = JsonSerializer.Deserialize<TResponse>(responseBody, jsonSerializerOptions ?? JSON_SERIALIZER_OPTIONS);
|
||||
if (parsedResponse is null)
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, "Model list response could not be deserialized.");
|
||||
|
||||
return SuccessfulModelLoadResult(modelFactory(parsedResponse));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a request and handles rate limiting by exponential backoff.
|
||||
@@ -158,7 +226,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
// Please notice: We do not dispose the response here. The caller is responsible
|
||||
// for disposing the response object. This is important because the response
|
||||
// object is used to read the stream.
|
||||
var nextResponse = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
var nextResponse = await this.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
if (nextResponse.IsSuccessStatusCode)
|
||||
{
|
||||
response = nextResponse;
|
||||
@@ -568,6 +636,78 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
streamReader.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams the chat completion from an OpenAI-compatible provider using the Chat Completion API.
|
||||
/// </summary>
|
||||
/// <param name="providerName">The provider name for logging and error reporting.</param>
|
||||
/// <param name="chatModel">The selected chat model.</param>
|
||||
/// <param name="chatThread">The current chat thread.</param>
|
||||
/// <param name="settingsManager">The settings manager.</param>
|
||||
/// <param name="requestFactory">Builds the provider-specific request body.</param>
|
||||
/// <param name="storeType">The secret store type.</param>
|
||||
/// <param name="isTryingSecret">Whether the API key is optional.</param>
|
||||
/// <param name="systemPromptRole">The system prompt role to use.</param>
|
||||
/// <param name="requestPath">The request path, relative to the provider base URL.</param>
|
||||
/// <param name="headersAction">Optional additional headers to add.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <typeparam name="TRequest">The request DTO type.</typeparam>
|
||||
/// <typeparam name="TDelta">The delta stream line type.</typeparam>
|
||||
/// <typeparam name="TAnnotation">The annotation stream line type.</typeparam>
|
||||
/// <returns>The streamed content chunks.</returns>
|
||||
protected async IAsyncEnumerable<ContentStreamChunk> StreamOpenAICompatibleChatCompletion<TRequest, TDelta, TAnnotation>(
|
||||
string providerName,
|
||||
Model chatModel,
|
||||
ChatThread chatThread,
|
||||
SettingsManager settingsManager,
|
||||
Func<TextMessage, IDictionary<string, object>, Task<TRequest>> requestFactory,
|
||||
SecretStoreType storeType = SecretStoreType.LLM_PROVIDER,
|
||||
bool isTryingSecret = false,
|
||||
string systemPromptRole = "system",
|
||||
string requestPath = "chat/completions",
|
||||
Action<HttpRequestHeaders>? headersAction = null,
|
||||
[EnumeratorCancellation] CancellationToken token = default)
|
||||
where TDelta : IResponseStreamLine
|
||||
where TAnnotation : IAnnotationStreamLine
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, storeType, isTrying: isTryingSecret);
|
||||
if(!requestedSecret.Success && !isTryingSecret)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = systemPromptRole,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Prepare the provider HTTP chat request:
|
||||
var providerChatRequest = JsonSerializer.Serialize(await requestFactory(systemPrompt, apiParameters), JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, requestPath);
|
||||
|
||||
// Set the authorization header:
|
||||
if (requestedSecret.Success)
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set provider-specific headers:
|
||||
headersAction?.Invoke(request.Headers);
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<TDelta, TAnnotation>(providerName, RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
protected async Task<string> PerformStandardTranscriptionRequest(RequestedSecret requestedSecret, Model transcriptionModel, string audioFilePath, Host host = Host.NONE, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
@@ -627,7 +767,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
break;
|
||||
}
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
var responseBody = response.Content.ReadAsStringAsync(token).Result;
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -697,7 +837,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json");
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
var responseBody = response.Content.ReadAsStringAsync(token).Result;
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -20,56 +17,37 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "h
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "DeepSeek";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the DeepSeek HTTP chat request:
|
||||
var deepSeekChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
|
||||
"DeepSeek",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(deepSeekChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("DeepSeek", RequestBuilder, token))
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -94,54 +72,38 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "h
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return modelResponse.Data;
|
||||
return this.LoadModelsResponse<ModelsResponse>(
|
||||
storeType,
|
||||
"models",
|
||||
modelResponse => modelResponse.Data,
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.Provider.Fireworks;
|
||||
|
||||
/// <summary>
|
||||
/// The Fireworks 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>
|
||||
public readonly record struct ChatRequest(
|
||||
string Model,
|
||||
IList<IMessageBase> Messages,
|
||||
bool Stream
|
||||
)
|
||||
{
|
||||
// Attention: The "required" modifier is not supported for [JsonExtensionData].
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -21,56 +18,37 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https:/
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "Fireworks.ai";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ResponseStreamLine, ChatCompletionAnnotationStreamLine>(
|
||||
"Fireworks",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the Fireworks HTTP chat request:
|
||||
var fireworksChatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine, ChatCompletionAnnotationStreamLine>("Fireworks", RequestBuilder, token))
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -96,34 +74,33 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https:/
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
// Source: https://docs.fireworks.ai/api-reference/audio-transcriptions#param-model
|
||||
return Task.FromResult<IEnumerable<Model>>(
|
||||
new List<Model>
|
||||
{
|
||||
new("whisper-v3", "Whisper v3"),
|
||||
// new("whisper-v3-turbo", "Whisper v3 Turbo"), // does not work
|
||||
});
|
||||
return Task.FromResult(ModelLoadResult.FromModels(
|
||||
[
|
||||
new Model("whisper-v3", "Whisper v3"),
|
||||
// new("whisper-v3-turbo", "Whisper v3 Turbo"), // does not work
|
||||
]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -20,56 +17,37 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://ch
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "GWDG SAIA";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the GWDG HTTP chat request:
|
||||
var gwdgChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
|
||||
"GWDG",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(gwdgChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("GWDG", RequestBuilder, token))
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -95,61 +73,55 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://ch
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
return models.Where(model => !model.Id.StartsWith("e5-mistral-7b-instruct", StringComparison.InvariantCultureIgnoreCase));
|
||||
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(model => !model.Id.StartsWith("e5-mistral-7b-instruct", StringComparison.InvariantCultureIgnoreCase))]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional);
|
||||
return models.Where(model => model.Id.StartsWith("e5-", StringComparison.InvariantCultureIgnoreCase));
|
||||
var result = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(model => model.Id.StartsWith("e5-", StringComparison.InvariantCultureIgnoreCase))]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
// Source: https://docs.hpc.gwdg.de/services/saia/index.html#voice-to-text
|
||||
return Task.FromResult<IEnumerable<Model>>(
|
||||
new List<Model>
|
||||
{
|
||||
new("whisper-large-v2", "Whisper v2 Large"),
|
||||
});
|
||||
return Task.FromResult(ModelLoadResult.FromModels(
|
||||
[
|
||||
new Model("whisper-large-v2", "Whisper v2 Large"),
|
||||
]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private async Task<ModelLoadResult> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
var result = await this.LoadModelsResponse<ModelsResponse>(
|
||||
storeType,
|
||||
"models",
|
||||
modelResponse => modelResponse.Data,
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
if (!result.Success)
|
||||
LOGGER.LogWarning("Failed to load models for provider {ProviderId}. FailureReason: {FailureReason}. TechnicalDetails: {TechnicalDetails}", this.Id, result.FailureReason, result.TechnicalDetails);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return modelResponse.Data;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.Provider.Google;
|
||||
|
||||
/// <summary>
|
||||
/// The Google 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>
|
||||
public readonly record struct ChatRequest(
|
||||
string Model,
|
||||
IList<IMessageBase> Messages,
|
||||
bool Stream
|
||||
)
|
||||
{
|
||||
// Attention: The "required" modifier is not supported for [JsonExtensionData].
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -21,56 +20,37 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "Google Gemini";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
|
||||
"Google",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the Google HTTP chat request:
|
||||
var geminiChatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(geminiChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("Google", RequestBuilder, token))
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -129,7 +109,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
||||
// Set the content:
|
||||
request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -161,80 +141,64 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
return models.Where(model =>
|
||||
model.Id.StartsWith("gemini-", StringComparison.OrdinalIgnoreCase) &&
|
||||
!this.IsEmbeddingModel(model.Id))
|
||||
.Select(this.WithDisplayNameFallback);
|
||||
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..result.Models.Where(model =>
|
||||
model.Id.StartsWith("gemini-", StringComparison.OrdinalIgnoreCase) &&
|
||||
!this.IsEmbeddingModel(model.Id))
|
||||
.Select(this.WithDisplayNameFallback)
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
public override async Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional);
|
||||
return models.Where(model => this.IsEmbeddingModel(model.Id))
|
||||
.Select(this.WithDisplayNameFallback);
|
||||
var result = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..result.Models.Where(model => this.IsEmbeddingModel(model.Id))
|
||||
.Select(this.WithDisplayNameFallback)
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IReadOnlyList<Model>> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(secretKey))
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
LOGGER.LogError("Failed to load models with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, await response.Content.ReadAsStringAsync(token));
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
if (modelResponse == default || modelResponse.Data.Count is 0)
|
||||
{
|
||||
LOGGER.LogError("Google model list response did not contain a valid data array.");
|
||||
return [];
|
||||
}
|
||||
|
||||
return modelResponse.Data
|
||||
return this.LoadModelsResponse<ModelsResponse>(
|
||||
storeType,
|
||||
"models",
|
||||
modelResponse => modelResponse.Data
|
||||
.Where(model => !string.IsNullOrWhiteSpace(model.Id))
|
||||
.Select(model => new Model(this.NormalizeModelId(model.Id), model.DisplayName))
|
||||
.ToArray();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.LogError("Failed to parse Google model list response: '{Message}'.", e.Message);
|
||||
return [];
|
||||
}
|
||||
.Select(model => new Model(this.NormalizeModelId(model.Id), model.DisplayName)),
|
||||
token,
|
||||
apiKeyProvisional,
|
||||
failureReasonSelector: (response, _) => response.StatusCode switch
|
||||
{
|
||||
System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
|
||||
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
|
||||
});
|
||||
}
|
||||
|
||||
private bool IsEmbeddingModel(string modelId)
|
||||
@@ -256,4 +220,4 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
||||
? modelId["models/".Length..]
|
||||
: modelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.Provider.Groq;
|
||||
|
||||
/// <summary>
|
||||
/// The Groq 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>
|
||||
public readonly record struct ChatRequest(
|
||||
string Model,
|
||||
IList<IMessageBase> 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>();
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -21,56 +18,40 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq.
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "Groq";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
|
||||
"Groq",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
if (TryPopIntParameter(apiParameters, "seed", out var parsedSeed))
|
||||
apiParameters["seed"] = parsedSeed;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the OpenAI HTTP chat request:
|
||||
var groqChatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(groqChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("Groq", RequestBuilder, token))
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -95,57 +76,41 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq.
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<Model>>([]);
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return modelResponse.Data.Where(n =>
|
||||
!n.Id.StartsWith("whisper-", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.StartsWith("distil-", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("-tts", StringComparison.OrdinalIgnoreCase));
|
||||
return this.LoadModelsResponse<ModelsResponse>(
|
||||
storeType,
|
||||
"models",
|
||||
modelResponse => modelResponse.Data.Where(n =>
|
||||
!n.Id.StartsWith("whisper-", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.StartsWith("distil-", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("-tts", StringComparison.OrdinalIgnoreCase)),
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
@@ -20,56 +19,37 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "Helmholtz Blablador";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the Helmholtz HTTP chat request:
|
||||
var helmholtzChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
|
||||
"Helmholtz",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(helmholtzChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("Helmholtz", RequestBuilder, token))
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -95,60 +75,81 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
return models.Where(model => !model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
!model.Id.StartsWith("alias-embedding", StringComparison.InvariantCultureIgnoreCase));
|
||||
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..result.Models.Where(model => !model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
!model.Id.Contains("-embedding", StringComparison.InvariantCultureIgnoreCase)
|
||||
)
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional);
|
||||
return models.Where(model =>
|
||||
model.Id.StartsWith("alias-embedding", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
model.Id.Contains("gritlm", StringComparison.InvariantCultureIgnoreCase));
|
||||
var result = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..result.Models.Where(model =>
|
||||
model.Id.Contains("-embedding", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
model.Id.Contains("gritlm", StringComparison.InvariantCultureIgnoreCase))
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private async Task<ModelLoadResult> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
var secretKey = await this.GetModelLoadingSecretKey(storeType, apiKeyProvisional);
|
||||
if (string.IsNullOrWhiteSpace(secretKey))
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, "No API key available for model loading.");
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
var body = await response.Content.ReadAsStringAsync(token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return FailedModelLoadResult(GetDefaultModelLoadFailureReason(response), $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{body}'");
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return modelResponse.Data;
|
||||
try
|
||||
{
|
||||
var modelResponse = JsonSerializer.Deserialize<ModelsResponse>(body, JSON_SERIALIZER_OPTIONS);
|
||||
return SuccessfulModelLoadResult(modelResponse.Data);
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
if (body.Contains("API key", StringComparison.InvariantCultureIgnoreCase))
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, body);
|
||||
|
||||
LOGGER.LogError(e, "Unexpected error while parsing models from Helmholtz API response. Status Code: {StatusCode}. Reason: {ReasonPhrase}. Response Body: '{ResponseBody}'", response.StatusCode, response.ReasonPhrase, body);
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, body);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.LogError(e, "Unexpected error while loading models from Helmholtz API. Status Code: {StatusCode}. Reason: {ReasonPhrase}", response.StatusCode, response.ReasonPhrase);
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.UNKNOWN, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -26,55 +23,36 @@ public sealed class ProviderHuggingFace : BaseProvider
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "HuggingFace";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
|
||||
"HuggingFace",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var message = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the HuggingFace HTTP chat request:
|
||||
var huggingfaceChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..message],
|
||||
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(huggingfaceChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("HuggingFace", RequestBuilder, token))
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -99,28 +77,28 @@ public sealed class ProviderHuggingFace : BaseProvider
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,11 @@ public interface IProvider
|
||||
/// </summary>
|
||||
public string TokenizerPath { get; }
|
||||
|
||||
/// Whether this provider instance can load available models from the backend/API.
|
||||
/// This capability may differ by provider type, host, or modality.
|
||||
/// </summary>
|
||||
public bool HasModelLoadingCapability { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a chat completion stream.
|
||||
/// </summary>
|
||||
@@ -81,7 +86,7 @@ public interface IProvider
|
||||
/// <param name="apiKeyProvisional">The provisional API key to use. Useful when the user is adding a new provider. When null, the stored API key is used.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The list of text models.</returns>
|
||||
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
public Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Load all possible image models that can be used with this provider.
|
||||
@@ -89,7 +94,7 @@ public interface IProvider
|
||||
/// <param name="apiKeyProvisional">The provisional API key to use. Useful when the user is adding a new provider. When null, the stored API key is used.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The list of image models.</returns>
|
||||
public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
public Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Load all possible embedding models that can be used with this provider.
|
||||
@@ -97,7 +102,7 @@ public interface IProvider
|
||||
/// <param name="apiKeyProvisional">The provisional API key to use. Useful when the user is adding a new provider. When null, the stored API key is used.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The list of embedding models.</returns>
|
||||
public Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
public Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Load all possible transcription models that can be used with this provider.
|
||||
@@ -105,5 +110,5 @@ public interface IProvider
|
||||
/// <param name="apiKeyProvisional">The provisional API key to use. Useful when the user is adding a new provider. When null, the stored API key is used.</param>
|
||||
/// <param name="token">>The cancellation token.</param>
|
||||
/// <returns>>The list of transcription models.</returns>
|
||||
public Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
public Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.Provider.Mistral;
|
||||
|
||||
/// <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="RandomSeed">The seed for the chat completion.</param>
|
||||
/// <param name="SafePrompt">Whether to inject a safety prompt before all conversations.</param>
|
||||
public readonly record struct ChatRequest(
|
||||
string Model,
|
||||
IList<IMessageBase> Messages,
|
||||
bool Stream,
|
||||
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
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>();
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -15,65 +12,49 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.MISTRAL.ToName();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "Mistral";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
|
||||
"Mistral",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
if (TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt))
|
||||
apiParameters["safe_prompt"] = parsedSafePrompt;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
var safePrompt = TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt) && parsedSafePrompt;
|
||||
var randomSeed = TryPopIntParameter(apiParameters, "random_seed", out var parsedRandomSeed) ? parsedRandomSeed : (int?)null;
|
||||
if (TryPopIntParameter(apiParameters, "random_seed", out var parsedRandomSeed))
|
||||
apiParameters["random_seed"] = parsedRandomSeed;
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the Mistral HTTP chat request:
|
||||
var mistralChatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
RandomSeed = randomSeed,
|
||||
SafePrompt = safePrompt,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("Mistral", RequestBuilder, token))
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -100,72 +81,62 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var modelResponse = await this.LoadModelList(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token);
|
||||
if(modelResponse == default)
|
||||
return [];
|
||||
if(!modelResponse.Success)
|
||||
return modelResponse;
|
||||
|
||||
return modelResponse.Data.Where(n =>
|
||||
!n.Id.StartsWith("code", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("embed", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("moderation", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(n => new Provider.Model(n.Id, null));
|
||||
return modelResponse with
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..modelResponse.Models.Where(n =>
|
||||
!n.Id.StartsWith("code", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("embed", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("moderation", StringComparison.OrdinalIgnoreCase))
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Provider.Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var modelResponse = await this.LoadModelList(SecretStoreType.EMBEDDING_PROVIDER, apiKeyProvisional, token);
|
||||
if(modelResponse == default)
|
||||
return [];
|
||||
if(!modelResponse.Success)
|
||||
return modelResponse;
|
||||
|
||||
return modelResponse.Data.Where(n => n.Id.Contains("embed", StringComparison.InvariantCulture))
|
||||
.Select(n => new Provider.Model(n.Id, null));
|
||||
return modelResponse with
|
||||
{
|
||||
Models = [..modelResponse.Models.Where(n => n.Id.Contains("embed", StringComparison.InvariantCulture))]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Provider.Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Provider.Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Provider.Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
// Source: https://docs.mistral.ai/capabilities/audio_transcription
|
||||
return Task.FromResult<IEnumerable<Provider.Model>>(
|
||||
new List<Provider.Model>
|
||||
{
|
||||
new("voxtral-mini-latest", "Voxtral Mini Latest"),
|
||||
});
|
||||
return Task.FromResult(ModelLoadResult.FromModels(
|
||||
[
|
||||
new Provider.Model("voxtral-mini-latest", "Voxtral Mini Latest"),
|
||||
]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<ModelsResponse> LoadModelList(SecretStoreType storeType, string? apiKeyProvisional, CancellationToken token)
|
||||
private Task<ModelLoadResult> LoadModelList(SecretStoreType storeType, string? apiKeyProvisional, CancellationToken token)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return default;
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return default;
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return modelResponse;
|
||||
return this.LoadModelsResponse<ModelsResponse>(
|
||||
storeType,
|
||||
"models",
|
||||
modelResponse => modelResponse.Data.Select(n => new Provider.Model(n.Id, null)),
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
public enum ModelLoadFailureReason
|
||||
{
|
||||
NONE,
|
||||
INVALID_OR_MISSING_API_KEY,
|
||||
AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
PROVIDER_UNAVAILABLE,
|
||||
INVALID_RESPONSE,
|
||||
UNKNOWN,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
public static class ModelLoadFailureReasonExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ModelLoadFailureReasonExtensions).Namespace, nameof(ModelLoadFailureReasonExtensions));
|
||||
|
||||
public static string ToUserMessage(this ModelLoadFailureReason failureReason, string providerName) => failureReason switch
|
||||
{
|
||||
ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY => string.Format(TB("We could not load models from '{0}'. The API key is probably missing, invalid, or expired."), providerName),
|
||||
ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => string.Format(TB("We could not load models from '{0}'. The account or API key does not have the required permissions."), providerName),
|
||||
ModelLoadFailureReason.PROVIDER_UNAVAILABLE => string.Format(TB("We could not load models from '{0}' because the provider is currently unavailable or could not be reached."), providerName),
|
||||
ModelLoadFailureReason.INVALID_RESPONSE => string.Format(TB("We could not load models from '{0}' because the provider returned an unexpected response."), providerName),
|
||||
ModelLoadFailureReason.UNKNOWN => string.Format(TB("We could not load models from '{0}' due to an unknown error."), providerName),
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
public sealed record ModelLoadResult(
|
||||
IReadOnlyList<Model> Models,
|
||||
ModelLoadFailureReason FailureReason = ModelLoadFailureReason.NONE,
|
||||
string? TechnicalDetails = null)
|
||||
{
|
||||
public bool Success => this.FailureReason is ModelLoadFailureReason.NONE;
|
||||
|
||||
public static ModelLoadResult FromModels(IEnumerable<Model> models)
|
||||
{
|
||||
return new([..models]);
|
||||
}
|
||||
|
||||
public static ModelLoadResult Failure(ModelLoadFailureReason failureReason, string? technicalDetails = null)
|
||||
{
|
||||
return new([], failureReason, technicalDetails);
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,15 @@ public class NoProvider : IProvider
|
||||
/// <inheritdoc />
|
||||
public string TokenizerPath { get; init; } = string.Empty;
|
||||
|
||||
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
|
||||
public bool HasModelLoadingCapability => false;
|
||||
|
||||
public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
|
||||
public Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
|
||||
public Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
|
||||
public Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
|
||||
public Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
|
||||
public Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
|
||||
public Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
|
||||
public async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatChatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
|
||||
@@ -23,6 +23,9 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "OpenAI";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
@@ -79,9 +82,9 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
|
||||
//
|
||||
// Prepare the tools we want to use:
|
||||
//
|
||||
IList<Tool> tools = modelCapabilities.Contains(Capability.WEB_SEARCH) switch
|
||||
IList<ProviderTool> providerTools = modelCapabilities.Contains(Capability.WEB_SEARCH) switch
|
||||
{
|
||||
true => [ Tools.WEB_SEARCH ],
|
||||
true => [ ProviderTools.WEB_SEARCH ],
|
||||
_ => []
|
||||
};
|
||||
|
||||
@@ -178,7 +181,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
|
||||
Store = false,
|
||||
|
||||
// Tools we want to use:
|
||||
Tools = tools,
|
||||
ProviderTools = providerTools,
|
||||
|
||||
// Additional API parameters:
|
||||
AdditionalApiParameters = apiParameters
|
||||
@@ -233,61 +236,57 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(SecretStoreType.LLM_PROVIDER, ["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));
|
||||
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, ["chatgpt-", "gpt-", "o1-", "o3-", "o4-"], token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..result.Models.Where(model => !model.Id.Contains("image", StringComparison.OrdinalIgnoreCase) &&
|
||||
!model.Id.Contains("realtime", StringComparison.OrdinalIgnoreCase) &&
|
||||
!model.Id.Contains("audio", StringComparison.OrdinalIgnoreCase) &&
|
||||
!model.Id.Contains("tts", StringComparison.OrdinalIgnoreCase) &&
|
||||
!model.Id.Contains("transcribe", StringComparison.OrdinalIgnoreCase))
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.IMAGE_PROVIDER, ["dall-e-", "gpt-image"], token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, ["text-embedding-"], token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, ["whisper-", "gpt-"], token, apiKeyProvisional);
|
||||
return models.Where(model => model.Id.StartsWith("whisper-", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
model.Id.Contains("-transcribe", StringComparison.InvariantCultureIgnoreCase));
|
||||
var result = await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, ["whisper-", "gpt-"], token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..result.Models.Where(model => model.Id.StartsWith("whisper-", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
model.Id.Contains("-transcribe", StringComparison.InvariantCultureIgnoreCase))
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(SecretStoreType storeType, string[] prefixes, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, string[] prefixes, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture)));
|
||||
return this.LoadModelsResponse<ModelsResponse>(
|
||||
storeType,
|
||||
"models",
|
||||
modelResponse => modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture))),
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a tool used by the AI model.
|
||||
/// Represents a tool executed on the provider side.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Right now, only our OpenAI provider is using tools. Thus, this class is located in the
|
||||
@@ -9,4 +9,4 @@ namespace AIStudio.Provider.OpenAI;
|
||||
/// be moved into the provider namespace.
|
||||
/// </remarks>
|
||||
/// <param name="Type">The type of the tool.</param>
|
||||
public record Tool(string Type);
|
||||
public record ProviderTool(string Type);
|
||||
+4
-4
@@ -1,14 +1,14 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Known tools for LLM providers.
|
||||
/// Known provider-side 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 class ProviderTools
|
||||
{
|
||||
public static readonly Tool WEB_SEARCH = new("web_search");
|
||||
}
|
||||
public static readonly ProviderTool WEB_SEARCH = new("web_search");
|
||||
}
|
||||
@@ -9,13 +9,13 @@ namespace AIStudio.Provider.OpenAI;
|
||||
/// <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>
|
||||
/// <param name="ProviderTools">The provider-side tools to use for the request.</param>
|
||||
public record ResponsesAPIRequest(
|
||||
string Model,
|
||||
IList<IMessageBase> Input,
|
||||
bool Stream,
|
||||
bool Store,
|
||||
IList<Tool> Tools)
|
||||
[property: JsonPropertyName("tools")] IList<ProviderTool> ProviderTools)
|
||||
{
|
||||
public ResponsesAPIRequest() : this(string.Empty, [], true, false, [])
|
||||
{
|
||||
@@ -24,4 +24,4 @@ public record ResponsesAPIRequest(
|
||||
// Attention: The "required" modifier is not supported for [JsonExtensionData].
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -24,60 +22,43 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "OpenRouter";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
|
||||
"OpenRouter",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Prepare the OpenRouter HTTP chat request:
|
||||
var openRouterChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set custom headers for project identification:
|
||||
request.Headers.Add("HTTP-Referer", PROJECT_WEBSITE);
|
||||
request.Headers.Add("X-Title", PROJECT_NAME);
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(openRouterChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("OpenRouter", RequestBuilder, token))
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
headersAction: headers =>
|
||||
{
|
||||
// Set custom headers for project identification:
|
||||
headers.Add("HTTP-Referer", PROJECT_WEBSITE);
|
||||
headers.Add("X-Title", PROJECT_NAME);
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -103,102 +84,70 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadEmbeddingModels(token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
return this.LoadModelsResponse<OpenRouterModelsResponse>(
|
||||
storeType,
|
||||
"models",
|
||||
modelResponse => modelResponse.Data
|
||||
.Where(n =>
|
||||
!n.Id.Contains("whisper", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("dall-e", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("tts", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("embedding", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("moderation", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("stable-diffusion", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("flux", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("midjourney", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(n => new Model(n.Id, n.Name)),
|
||||
token,
|
||||
apiKeyProvisional,
|
||||
requestConfigurator: (request, secretKey) =>
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
// Set custom headers for project identification:
|
||||
request.Headers.Add("HTTP-Referer", PROJECT_WEBSITE);
|
||||
request.Headers.Add("X-Title", PROJECT_NAME);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<OpenRouterModelsResponse>(token);
|
||||
|
||||
// Filter out non-text models (image, audio, embedding models) and convert to Model
|
||||
return modelResponse.Data
|
||||
.Where(n =>
|
||||
!n.Id.Contains("whisper", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("dall-e", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("tts", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("embedding", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("moderation", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("stable-diffusion", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("flux", StringComparison.OrdinalIgnoreCase) &&
|
||||
!n.Id.Contains("midjourney", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(n => new Model(n.Id, n.Name));
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
request.Headers.Add("HTTP-Referer", PROJECT_WEBSITE);
|
||||
request.Headers.Add("X-Title", PROJECT_NAME);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadEmbeddingModels(CancellationToken token, string? apiKeyProvisional = null)
|
||||
private Task<ModelLoadResult> LoadEmbeddingModels(CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER) switch
|
||||
return this.LoadModelsResponse<OpenRouterModelsResponse>(
|
||||
SecretStoreType.EMBEDDING_PROVIDER,
|
||||
"embeddings/models",
|
||||
modelResponse => modelResponse.Data.Select(n => new Model(n.Id, n.Name)),
|
||||
token,
|
||||
apiKeyProvisional,
|
||||
requestConfigurator: (request, secretKey) =>
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "embeddings/models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
// Set custom headers for project identification:
|
||||
request.Headers.Add("HTTP-Referer", PROJECT_WEBSITE);
|
||||
request.Headers.Add("X-Title", PROJECT_NAME);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<OpenRouterModelsResponse>(token);
|
||||
|
||||
// Convert all embedding models to Model
|
||||
return modelResponse.Data.Select(n => new Model(n.Id, n.Name));
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
request.Headers.Add("HTTP-Referer", PROJECT_WEBSITE);
|
||||
request.Headers.Add("X-Title", PROJECT_NAME);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -29,55 +26,36 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "Perplexity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the Perplexity HTTP chat request:
|
||||
var perplexityChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ResponseStreamLine, NoChatCompletionAnnotationStreamLine>(
|
||||
"Perplexity",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(perplexityChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine, NoChatCompletionAnnotationStreamLine>("Perplexity", RequestBuilder, token))
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -102,30 +80,30 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Task<IEnumerable<Model>> LoadModels() => Task.FromResult<IEnumerable<Model>>(KNOWN_MODELS);
|
||||
private Task<ModelLoadResult> LoadModels() => Task.FromResult(ModelLoadResult.FromModels(KNOWN_MODELS));
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.Provider.SelfHosted;
|
||||
|
||||
/// <summary>
|
||||
/// The 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>
|
||||
public readonly record struct ChatRequest(
|
||||
string Model,
|
||||
IList<IMessageBase> Messages,
|
||||
bool Stream
|
||||
)
|
||||
{
|
||||
// Attention: The "required" modifier is not supported for [JsonExtensionData].
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -18,65 +16,51 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.SELF_HOSTED.ToName();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "Self-hosted";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => host is Host.OLLAMA or Host.LM_STUDIO or Host.VLLM;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER, isTrying: true);
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
|
||||
"self-hosted provider",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages. The image format depends on the host:
|
||||
// - Ollama uses the direct image URL format: { "type": "image_url", "image_url": "data:..." }
|
||||
// - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } }
|
||||
var messages = host switch
|
||||
{
|
||||
Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel),
|
||||
_ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
|
||||
};
|
||||
|
||||
// Build the list of messages. The image format depends on the host:
|
||||
// - Ollama uses the direct image URL format: { "type": "image_url", "image_url": "data:..." }
|
||||
// - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } }
|
||||
var messages = host switch
|
||||
{
|
||||
Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel),
|
||||
_ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
|
||||
};
|
||||
|
||||
// Prepare the OpenAI HTTP chat request:
|
||||
var providerChatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, host.ChatURL());
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the authorization header:
|
||||
if (requestedSecret.Success)
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("self-hosted provider", RequestBuilder, token))
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
isTryingSecret: true,
|
||||
requestPath: host.ChatURL(),
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -102,7 +86,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, host, token: token, texts: texts);
|
||||
}
|
||||
|
||||
public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -111,7 +95,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
case Host.LLAMA_CPP:
|
||||
// Right now, llama.cpp only supports one model.
|
||||
// There is no API to list the model(s).
|
||||
return [ new Provider.Model("as configured by llama.cpp", null) ];
|
||||
return ModelLoadResult.FromModels([ new Provider.Model("as configured by llama.cpp", null) ]);
|
||||
|
||||
case Host.LM_STUDIO:
|
||||
case Host.OLLAMA:
|
||||
@@ -119,22 +103,22 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
return await this.LoadModels( SecretStoreType.LLM_PROVIDER, ["embed"], [], token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
return [];
|
||||
return ModelLoadResult.FromModels([]);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOGGER.LogError($"Failed to load text models from self-hosted provider: {e.Message}");
|
||||
return [];
|
||||
return ModelLoadResult.Failure(ModelLoadFailureReason.UNKNOWN, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Provider.Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Provider.Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
public override async Task<IEnumerable<Provider.Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -146,69 +130,61 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
return await this.LoadModels( SecretStoreType.EMBEDDING_PROVIDER, [], ["embed"], token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
return [];
|
||||
return ModelLoadResult.FromModels([]);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOGGER.LogError($"Failed to load text models from self-hosted provider: {e.Message}");
|
||||
return [];
|
||||
return ModelLoadResult.Failure(ModelLoadFailureReason.UNKNOWN, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Provider.Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (host)
|
||||
{
|
||||
case Host.WHISPER_CPP:
|
||||
return new List<Provider.Model>
|
||||
{
|
||||
new("loaded-model", TB("Model as configured by whisper.cpp")),
|
||||
};
|
||||
return ModelLoadResult.FromModels(
|
||||
[
|
||||
new Provider.Model("loaded-model", TB("Model as configured by whisper.cpp")),
|
||||
]);
|
||||
|
||||
case Host.OLLAMA:
|
||||
case Host.VLLM:
|
||||
return await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, [], [], token, apiKeyProvisional);
|
||||
|
||||
default:
|
||||
return [];
|
||||
return ModelLoadResult.FromModels([]);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.LogError($"Failed to load transcription models from self-hosted provider: {e.Message}");
|
||||
return [];
|
||||
return ModelLoadResult.Failure(ModelLoadFailureReason.UNKNOWN, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Provider.Model>> LoadModels(SecretStoreType storeType, string[] ignorePhrases, string[] filterPhrases, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private async Task<ModelLoadResult> LoadModels(SecretStoreType storeType, string[] ignorePhrases, string[] filterPhrases, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType, isTrying: true) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
var secretKey = await this.GetModelLoadingSecretKey(storeType, apiKeyProvisional, true);
|
||||
|
||||
using var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
if(secretKey is not null)
|
||||
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKeyProvisional);
|
||||
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token);
|
||||
using var lmStudioResponse = await this.HttpClient.SendAsync(lmStudioRequest, token);
|
||||
if(!lmStudioResponse.IsSuccessStatusCode)
|
||||
return [];
|
||||
return FailedModelLoadResult(GetDefaultModelLoadFailureReason(lmStudioResponse), $"Status={(int)lmStudioResponse.StatusCode} {lmStudioResponse.ReasonPhrase}");
|
||||
|
||||
var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return lmStudioModelResponse.Data.
|
||||
return SuccessfulModelLoadResult(lmStudioModelResponse.Data.
|
||||
Where(model => !ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCulture)) &&
|
||||
filterPhrases.All( filter => model.Id.Contains(filter, StringComparison.InvariantCulture)))
|
||||
.Select(n => new Provider.Model(n.Id, null));
|
||||
.Select(n => new Provider.Model(n.Id, null)));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
@@ -21,56 +18,37 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "xAI";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
// Prepare the xAI HTTP chat request:
|
||||
var xChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
|
||||
"xAI",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(xChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("xAI", RequestBuilder, token))
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
token: token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
@@ -95,67 +73,49 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(SecretStoreType.LLM_PROVIDER, ["grok-"], token, apiKeyProvisional);
|
||||
return models.Where(n => !n.Id.Contains("-image", StringComparison.OrdinalIgnoreCase));
|
||||
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, ["grok-"], token, apiKeyProvisional);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(n => !n.Id.Contains("-image", StringComparison.OrdinalIgnoreCase))]
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<Model>>([]);
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<Model>>([]);
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(SecretStoreType storeType, string[] prefixes, CancellationToken token, string? apiKeyProvisional = null)
|
||||
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, string[] prefixes, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this, storeType) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
|
||||
//
|
||||
// The API does not return the alias model names, so we have to add them manually:
|
||||
// Right now, the only alias to add is `grok-2-latest`.
|
||||
//
|
||||
return modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture)))
|
||||
.Concat([
|
||||
new Model
|
||||
{
|
||||
Id = "grok-2-latest",
|
||||
DisplayName = "Grok 2.0 (latest)",
|
||||
}
|
||||
]);
|
||||
return this.LoadModelsResponse<ModelsResponse>(
|
||||
storeType,
|
||||
"models",
|
||||
modelResponse => modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture)))
|
||||
.Concat([
|
||||
new Model
|
||||
{
|
||||
Id = "grok-2-latest",
|
||||
DisplayName = "Grok 2.0 (latest)",
|
||||
}
|
||||
]),
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user