mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-28 23:23:36 +00:00
Resolved 29 conflicting files. The notable decisions: Confidence: main's tool-calling gate (RequiredProviderConfidence) and this branch's local-RAG gate (DataConfidenceLevel) turned out to be the same rule on the same axis, so they are now one field. Both tool results and data sources raise it through RequireProviderConfidence(). The gate checks the level strictly and no longer exempts providers trusted by configuration: TrustedProviderIds is documented as applying to data-source security checks only, and organizations set confidence through DataConfidence .CustomConfidenceScheme instead. The security axis (DataSecurity, ERI, IsTrustedForDataSourceSecurityChecks) is unchanged. Provider creation: main's CreateProvider signature won (hfEndpointKind, capabilityOverrides, no model parameter); tokenizerPath was added to it and is set for every provider, including the new Hetzner, IONOS and LiteLLM. Provider and EmbeddingProvider combine the record parameters, Lua parsing and Lua serialization of both sides. File types: main's hierarchy (ODT leaf, WORD parent, PowerPoint without the legacy .ppt, TABULAR instead of DELIMITED_TABLE) plus this branch's SPREADSHEET parent with ODS and the xlsm/xlsb/xla/xlam extensions, which the runtime already reads. Both sides had added a conflicting HTML filter; the reading family keeps the name, and the export path uses a narrow HTML_DOCUMENT, following the existing LATEX/TEX split. Runtime: main's file_data.rs is the base, including the prompt-injection sanitizer and the extraction routes. Token counting and chunk segmentation moved into take_released, so they act on the text the filter has released rather than on text it is still holding. A failed count is logged and left out instead of ending the extraction, because the app counts such a segment itself. Data sources: the participating-provider checks of this branch are kept, and main's GetAllowedDataSources overload now builds on them. DirectChatService resolves the launched chat's data source options before the check, so filter and chat see the same options. .NET and Rust both build clean; I18N regenerated to 4060 keys.
298 lines
14 KiB
C#
298 lines
14 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
using AIStudio.Provider;
|
|
using AIStudio.Provider.HuggingFace;
|
|
using AIStudio.Tools.PluginSystem;
|
|
|
|
using SharedTools;
|
|
|
|
using Host = AIStudio.Provider.SelfHosted.Host;
|
|
using LuaTable = Lua.LuaTable;
|
|
|
|
namespace AIStudio.Settings;
|
|
|
|
/// <summary>
|
|
/// Data model for configured providers.
|
|
/// </summary>
|
|
/// <param name="Num">The provider's number.</param>
|
|
/// <param name="Id">The provider's ID.</param>
|
|
/// <param name="InstanceName">The provider's instance name. Useful for multiple instances of the same provider, e.g., to distinguish between different OpenAI API keys.</param>
|
|
/// <param name="UsedLLMProvider">The provider used.</param>
|
|
/// <param name="IsSelfHosted">Whether the provider is self-hosted.</param>
|
|
/// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param>
|
|
/// <param name="Model">The LLM model to use for chat.</param>
|
|
/// <param name="AllowUserProvidedAPIKey">When set by a configuration plugin, the user may set their own API key for this otherwise locked, enterprise-managed provider.</param>
|
|
/// <param name="CustomIconDataUrl">The validated custom SVG icon supplied by a configuration plugin.</param>
|
|
public sealed record Provider(
|
|
uint Num,
|
|
string Id,
|
|
string InstanceName,
|
|
LLMProviders UsedLLMProvider,
|
|
Model Model,
|
|
bool IsSelfHosted = false,
|
|
bool IsEnterpriseConfiguration = false,
|
|
Guid EnterpriseConfigurationPluginId = default,
|
|
string Hostname = "http://localhost:1234",
|
|
Host Host = Host.NONE,
|
|
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE,
|
|
string AdditionalJsonApiParameters = "",
|
|
string TokenizerPath = "",
|
|
ProviderCapabilityOverrides? CapabilityOverrides = null,
|
|
bool AllowUserProvidedAPIKey = false,
|
|
string CustomIconDataUrl = "") : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
|
|
{
|
|
private static readonly ILogger<Provider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Provider>();
|
|
|
|
public static readonly Provider NONE = new();
|
|
|
|
public Provider() : this(0, Guid.Empty.ToString(), string.Empty, LLMProviders.NONE, default, false, false, Guid.Empty)
|
|
{
|
|
}
|
|
|
|
#region Overrides of ValueType
|
|
|
|
/// <summary>
|
|
/// Returns a string that represents the current provider in a human-readable format.
|
|
/// We use this to display the provider in the chat UI.
|
|
/// </summary>
|
|
/// <returns>A string that represents the current provider in a human-readable format.</returns>
|
|
public override string ToString()
|
|
{
|
|
if(this.IsSelfHosted)
|
|
return $"{this.InstanceName} ({this.UsedLLMProvider.ToName()}, {this.Host}, {this.Hostname}, {this.Model})";
|
|
|
|
return $"{this.InstanceName} ({this.UsedLLMProvider.ToName()}, {this.Model})";
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Implementation of ISecretId
|
|
|
|
/// <inheritdoc />
|
|
[JsonIgnore]
|
|
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToSecretId()}" : this.UsedLLMProvider.ToSecretId();
|
|
|
|
/// <inheritdoc />
|
|
[JsonIgnore]
|
|
public string SecretName => this.InstanceName;
|
|
|
|
#endregion
|
|
|
|
#region Implementation of IConfigurationObject
|
|
|
|
public override string Name
|
|
{
|
|
get => this.InstanceName;
|
|
init => this.InstanceName = value;
|
|
}
|
|
|
|
#endregion
|
|
|
|
public static bool TryParseProviderTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject provider)
|
|
{
|
|
provider = NONE;
|
|
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})");
|
|
return false;
|
|
}
|
|
|
|
if (!table.TryGetValue("InstanceName", out var instanceNameValue) || !instanceNameValue.TryRead<string>(out var instanceName))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid instance name. (Plugin ID: {configPluginId})");
|
|
return false;
|
|
}
|
|
|
|
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid LLM provider enum value. (Plugin ID: {configPluginId})");
|
|
return false;
|
|
}
|
|
|
|
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid host enum value. (Plugin ID: {configPluginId})");
|
|
return false;
|
|
}
|
|
|
|
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})");
|
|
return false;
|
|
}
|
|
|
|
var hfInferenceProvider = HFInferenceProvider.NONE;
|
|
if (table.TryGetValue("HFInferenceProvider", out var hfInferenceProviderValue) && hfInferenceProviderValue.TryRead<string>(out var hfInferenceProviderText))
|
|
{
|
|
if (!Enum.TryParse(hfInferenceProviderText, true, out hfInferenceProvider))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid Hugging Face inference provider enum value. (Plugin ID: {configPluginId})");
|
|
hfInferenceProvider = HFInferenceProvider.NONE;
|
|
}
|
|
}
|
|
|
|
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model table. (Plugin ID: {configPluginId})");
|
|
return false;
|
|
}
|
|
|
|
if (!TryReadModelTable(idx, modelTable, configPluginId, out var model))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model configuration. (Plugin ID: {configPluginId})");
|
|
return false;
|
|
}
|
|
|
|
if (!table.TryGetValue("AdditionalJsonApiParameters", out var additionalJsonApiParametersValue) || !additionalJsonApiParametersValue.TryRead<string>(out var additionalJsonApiParameters))
|
|
{
|
|
// In this case, no reason exists to reject this provider, though.
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain valid additional JSON API parameters. (Plugin ID: {configPluginId})");
|
|
additionalJsonApiParameters = string.Empty;
|
|
}
|
|
|
|
var tokenizerPath = string.Empty;
|
|
if (table.TryGetValue("TokenizerPath", out var tokenizerPathValue) && !tokenizerPathValue.TryRead<string>(out tokenizerPath))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid tokenizer path. (Plugin ID: {configPluginId})");
|
|
tokenizerPath = string.Empty;
|
|
}
|
|
var capabilityOverrides = ProviderCapabilityOverrides.TryParseFromLuaTable(idx, table, configPluginId, LOGGER);
|
|
|
|
var allowUserProvidedApiKey = false;
|
|
if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead<bool>(out var allowUserProvidedApiKeyBool))
|
|
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
|
|
|
|
var customIconDataUrl = string.Empty;
|
|
if (table.TryGetValue("IconPath", out var iconPathValue))
|
|
{
|
|
if (!iconPathValue.TryRead<string>(out var iconPath))
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid icon path. Falling back to the built-in provider icon. (Plugin ID: {configPluginId})");
|
|
else if (!PluginIconFile.TryLoadDataUrl(iconPath, pluginPath, out customIconDataUrl, out var iconIssue))
|
|
LOGGER.LogWarning($"The configured provider {idx} contains an invalid icon path. Falling back to the built-in provider icon. Issue: {iconIssue} (Plugin ID: {configPluginId})");
|
|
}
|
|
|
|
provider = new Provider
|
|
{
|
|
Num = 0, // will be set later by the PluginConfigurationObject
|
|
Id = id.ToString(),
|
|
InstanceName = instanceName,
|
|
UsedLLMProvider = usedLLMProvider,
|
|
Model = model,
|
|
IsSelfHosted = usedLLMProvider is LLMProviders.SELF_HOSTED,
|
|
IsEnterpriseConfiguration = true,
|
|
EnterpriseConfigurationPluginId = configPluginId,
|
|
Hostname = hostname,
|
|
Host = host,
|
|
HFInferenceProvider = hfInferenceProvider,
|
|
AdditionalJsonApiParameters = additionalJsonApiParameters,
|
|
TokenizerPath = tokenizerPath,
|
|
CapabilityOverrides = capabilityOverrides,
|
|
AllowUserProvidedAPIKey = allowUserProvidedApiKey,
|
|
CustomIconDataUrl = customIconDataUrl,
|
|
};
|
|
|
|
// Handle an encrypted API key if present. When the user manages their own key for this
|
|
// provider, we must never enqueue an embedded key: doing so would overwrite the user's
|
|
// key in the OS keyring on every configuration reload.
|
|
if (allowUserProvidedApiKey)
|
|
{
|
|
if (table.TryGetValue("APIKey", out var ignoredApiKeyValue) && ignoredApiKeyValue.TryRead<string>(out var ignoredApiKeyText) && !string.IsNullOrWhiteSpace(ignoredApiKeyText))
|
|
LOGGER.LogWarning($"The configured provider {idx} sets both AllowUserProvidedAPIKey and an embedded APIKey. Ignoring the embedded key: the user manages their own key for this provider. (Plugin ID: {configPluginId})");
|
|
}
|
|
else if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
|
|
{
|
|
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
|
|
LOGGER.LogWarning($"The configured provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
|
|
else
|
|
{
|
|
var encryption = PluginFactory.EnterpriseEncryption;
|
|
if (encryption?.IsAvailable == true)
|
|
{
|
|
if (encryption.TryDecrypt(apiKeyText, out var decryptedApiKey))
|
|
{
|
|
// Queue the API key for storage in the OS keyring:
|
|
PendingEnterpriseApiKeys.Add(new(
|
|
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToSecretId()}",
|
|
instanceName,
|
|
decryptedApiKey,
|
|
SecretStoreType.LLM_PROVIDER));
|
|
LOGGER.LogDebug($"Successfully decrypted API key for provider {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})");
|
|
}
|
|
else
|
|
LOGGER.LogWarning($"Failed to decrypt API key for provider {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})");
|
|
}
|
|
else
|
|
LOGGER.LogWarning($"The configured provider {idx} contains an encrypted API key, but no encryption secret is configured. (Plugin ID: {configPluginId})");
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryReadModelTable(int idx, LuaTable table, Guid configPluginId, out Model model)
|
|
{
|
|
model = default;
|
|
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model ID. (Plugin ID: {configPluginId})");
|
|
return false;
|
|
}
|
|
|
|
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
|
|
{
|
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model display name. (Plugin ID: {configPluginId})");
|
|
return false;
|
|
}
|
|
|
|
model = new(id, displayName);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exports the provider configuration as a Lua configuration section.
|
|
/// </summary>
|
|
/// <param name="encryptedApiKey">Optional encrypted API key to include in the export.</param>
|
|
/// <returns>A Lua configuration section string.</returns>
|
|
public string ExportAsConfigurationSection(string? encryptedApiKey = null)
|
|
{
|
|
var hfInferenceProviderLine = string.Empty;
|
|
if (this.HFInferenceProvider is not HFInferenceProvider.NONE)
|
|
{
|
|
hfInferenceProviderLine = $"""
|
|
["HFInferenceProvider"] = "{this.HFInferenceProvider}",
|
|
""";
|
|
}
|
|
|
|
var apiKeyLine = string.Empty;
|
|
if (!string.IsNullOrWhiteSpace(encryptedApiKey))
|
|
{
|
|
apiKeyLine = $"""
|
|
["APIKey"] = "{LuaTools.EscapeLuaString(encryptedApiKey)}",
|
|
""";
|
|
}
|
|
|
|
var capabilityOverridesLine = this.CapabilityOverrides?.ExportAsLuaTable(" ") ?? string.Empty;
|
|
|
|
return $$"""
|
|
CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = {
|
|
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
|
["InstanceName"] = "{{LuaTools.EscapeLuaString(this.InstanceName)}}",
|
|
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
|
|
|
|
["TokenizerPath"] = "{{this.TokenizerPath}}",
|
|
|
|
["Host"] = "{{this.Host}}",
|
|
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
|
|
{{hfInferenceProviderLine}}
|
|
{{apiKeyLine}}
|
|
["AdditionalJsonApiParameters"] = "{{LuaTools.EscapeLuaString(this.AdditionalJsonApiParameters)}}",
|
|
{{capabilityOverridesLine}}
|
|
["Model"] = {
|
|
["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}",
|
|
["DisplayName"] = "{{LuaTools.EscapeLuaString(this.Model.DisplayName ?? this.Model.Id)}}",
|
|
},
|
|
}
|
|
""";
|
|
}
|
|
}
|