mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-06-20 21:16:27 +00:00
Fixed self-hosted provider API key handling (#811)
Some checks are pending
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Publish release (push) Blocked by required conditions
Some checks are pending
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Publish release (push) Blocked by required conditions
This commit is contained in:
parent
24952e796e
commit
c3bf2563cd
@ -196,6 +196,7 @@ Multi-level confidence scheme allows users to control which providers see which
|
|||||||
- **Encryption** - Initialized before Rust service is marked ready
|
- **Encryption** - Initialized before Rust service is marked ready
|
||||||
- **Message Bus** - Singleton event bus for cross-component communication inside the .NET app
|
- **Message Bus** - Singleton event bus for cross-component communication inside the .NET app
|
||||||
- **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`.
|
- **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`.
|
||||||
|
- **Compatibility shims** - Temporary fallback or read-repair code must be documented in `documentation/compatibility-shims/` with an introduced date, remove-after date, code references, and removal checklist. Add a short code comment near the shim that references the document and remove-after date. Check this folder before adding similar fallback logic, and do not extend expired shims without explicit maintainer direction. Do not use this process for permanent settings schema migrations; those belong in `app/MindWork AI Studio/Settings/SettingsMigrations.cs`.
|
||||||
- **Empty lines** - Avoid adding extra empty lines at the end of files.
|
- **Empty lines** - Avoid adding extra empty lines at the end of files.
|
||||||
|
|
||||||
## Changelogs
|
## Changelogs
|
||||||
|
|||||||
@ -203,7 +203,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
|||||||
|
|
||||||
#region Implementation of ISecretId
|
#region Implementation of ISecretId
|
||||||
|
|
||||||
public string SecretId => this.DataLLMProvider.ToName();
|
public string SecretId => this.DataLLMProvider.ToSecretId();
|
||||||
|
|
||||||
public string SecretName => this.DataName;
|
public string SecretName => this.DataName;
|
||||||
|
|
||||||
|
|||||||
@ -231,7 +231,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
|||||||
|
|
||||||
#region Implementation of ISecretId
|
#region Implementation of ISecretId
|
||||||
|
|
||||||
public string SecretId => this.DataLLMProvider.ToName();
|
public string SecretId => this.DataLLMProvider.ToSecretId();
|
||||||
|
|
||||||
public string SecretName => this.DataInstanceName;
|
public string SecretName => this.DataInstanceName;
|
||||||
|
|
||||||
|
|||||||
@ -218,7 +218,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
|||||||
|
|
||||||
#region Implementation of ISecretId
|
#region Implementation of ISecretId
|
||||||
|
|
||||||
public string SecretId => this.DataLLMProvider.ToName();
|
public string SecretId => this.DataLLMProvider.ToSecretId();
|
||||||
|
|
||||||
public string SecretName => this.DataName;
|
public string SecretName => this.DataName;
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.ALIBABA_CLOUD.ToName();
|
public override string Id => LLMProviders.ALIBABA_CLOUD.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "AlibabaCloud";
|
public override string InstanceName { get; set; } = "AlibabaCloud";
|
||||||
|
|||||||
@ -15,7 +15,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.ANTHROPIC.ToName();
|
public override string Id => LLMProviders.ANTHROPIC.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "Anthropic";
|
public override string InstanceName { get; set; } = "Anthropic";
|
||||||
|
|||||||
@ -13,7 +13,7 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.DEEP_SEEK.ToName();
|
public override string Id => LLMProviders.DEEP_SEEK.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "DeepSeek";
|
public override string InstanceName { get; set; } = "DeepSeek";
|
||||||
|
|||||||
@ -13,7 +13,7 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri(
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.FIREWORKS.ToName();
|
public override string Id => LLMProviders.FIREWORKS.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "Fireworks.ai";
|
public override string InstanceName { get; set; } = "Fireworks.ai";
|
||||||
|
|||||||
@ -13,7 +13,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.GWDG.ToName();
|
public override string Id => LLMProviders.GWDG.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "GWDG SAIA";
|
public override string InstanceName { get; set; } = "GWDG SAIA";
|
||||||
|
|||||||
@ -15,7 +15,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.GOOGLE.ToName();
|
public override string Id => LLMProviders.GOOGLE.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "Google Gemini";
|
public override string InstanceName { get; set; } = "Google Gemini";
|
||||||
|
|||||||
@ -13,7 +13,7 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.GROQ.ToName();
|
public override string Id => LLMProviders.GROQ.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "Groq";
|
public override string InstanceName { get; set; } = "Groq";
|
||||||
|
|||||||
@ -15,7 +15,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.HELMHOLTZ.ToName();
|
public override string Id => LLMProviders.HELMHOLTZ.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "Helmholtz Blablador";
|
public override string InstanceName { get; set; } = "Helmholtz Blablador";
|
||||||
|
|||||||
@ -18,7 +18,7 @@ public sealed class ProviderHuggingFace : BaseProvider
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.HUGGINGFACE.ToName();
|
public override string Id => LLMProviders.HUGGINGFACE.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "HuggingFace";
|
public override string InstanceName { get; set; } = "HuggingFace";
|
||||||
|
|||||||
@ -29,6 +29,10 @@ public static class LLMProvidersExtensions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the human-readable name of the provider.
|
/// Returns the human-readable name of the provider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This value is UI text and may be localized. Do not use it for persisted IDs, secret namespaces,
|
||||||
|
/// or other stable identifiers.
|
||||||
|
/// </remarks>
|
||||||
/// <param name="llmProvider">The provider.</param>
|
/// <param name="llmProvider">The provider.</param>
|
||||||
/// <returns>The human-readable name of the provider.</returns>
|
/// <returns>The human-readable name of the provider.</returns>
|
||||||
public static string ToName(this LLMProviders llmProvider) => llmProvider switch
|
public static string ToName(this LLMProviders llmProvider) => llmProvider switch
|
||||||
@ -57,6 +61,41 @@ public static class LLMProvidersExtensions
|
|||||||
_ => TB("Unknown"),
|
_ => TB("Unknown"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the stable secret namespace for the provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// These values are used for OS keyring namespaces. They must never be localized or changed without
|
||||||
|
/// an explicit migration for existing API keys.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="llmProvider">The provider.</param>
|
||||||
|
/// <returns>The stable secret namespace for the provider.</returns>
|
||||||
|
public static string ToSecretId(this LLMProviders llmProvider) => llmProvider switch
|
||||||
|
{
|
||||||
|
LLMProviders.NONE => "No provider selected",
|
||||||
|
|
||||||
|
LLMProviders.OPEN_AI => "OpenAI",
|
||||||
|
LLMProviders.ANTHROPIC => "Anthropic",
|
||||||
|
LLMProviders.MISTRAL => "Mistral",
|
||||||
|
LLMProviders.GOOGLE => "Google",
|
||||||
|
LLMProviders.X => "xAI",
|
||||||
|
LLMProviders.DEEP_SEEK => "DeepSeek",
|
||||||
|
LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud",
|
||||||
|
LLMProviders.PERPLEXITY => "Perplexity",
|
||||||
|
LLMProviders.OPEN_ROUTER => "OpenRouter",
|
||||||
|
|
||||||
|
LLMProviders.GROQ => "Groq",
|
||||||
|
LLMProviders.FIREWORKS => "Fireworks.ai",
|
||||||
|
LLMProviders.HUGGINGFACE => "Hugging Face",
|
||||||
|
|
||||||
|
LLMProviders.SELF_HOSTED => "Self-hosted",
|
||||||
|
|
||||||
|
LLMProviders.HELMHOLTZ => "Helmholtz Blablador",
|
||||||
|
LLMProviders.GWDG => "GWDG SAIA",
|
||||||
|
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a provider's confidence.
|
/// Get a provider's confidence.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -13,7 +13,7 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.MISTRAL.ToName();
|
public override string Id => LLMProviders.MISTRAL.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "Mistral";
|
public override string InstanceName { get; set; } = "Mistral";
|
||||||
|
|||||||
@ -22,7 +22,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.OPEN_AI.ToName();
|
public override string Id => LLMProviders.OPEN_AI.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "OpenAI";
|
public override string InstanceName { get; set; } = "OpenAI";
|
||||||
|
|||||||
@ -17,7 +17,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.OPEN_ROUTER.ToName();
|
public override string Id => LLMProviders.OPEN_ROUTER.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "OpenRouter";
|
public override string InstanceName { get; set; } = "OpenRouter";
|
||||||
|
|||||||
@ -22,7 +22,7 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.PERPLEXITY.ToName();
|
public override string Id => LLMProviders.PERPLEXITY.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "Perplexity";
|
public override string InstanceName { get; set; } = "Perplexity";
|
||||||
|
|||||||
@ -18,7 +18,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.SELF_HOSTED.ToName();
|
public override string Id => LLMProviders.SELF_HOSTED.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "Self-hosted";
|
public override string InstanceName { get; set; } = "Self-hosted";
|
||||||
|
|||||||
@ -13,7 +13,7 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://
|
|||||||
#region Implementation of IProvider
|
#region Implementation of IProvider
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Id => LLMProviders.X.ToName();
|
public override string Id => LLMProviders.X.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string InstanceName { get; set; } = "xAI";
|
public override string InstanceName { get; set; } = "xAI";
|
||||||
|
|||||||
@ -44,7 +44,7 @@ public sealed record EmbeddingProvider(
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToName()}" : this.UsedLLMProvider.ToName();
|
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToSecretId()}" : this.UsedLLMProvider.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
@ -125,7 +125,7 @@ public sealed record EmbeddingProvider(
|
|||||||
{
|
{
|
||||||
// Queue the API key for storage in the OS keyring:
|
// Queue the API key for storage in the OS keyring:
|
||||||
PendingEnterpriseApiKeys.Add(new(
|
PendingEnterpriseApiKeys.Add(new(
|
||||||
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToName()}",
|
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToSecretId()}",
|
||||||
name,
|
name,
|
||||||
decryptedApiKey,
|
decryptedApiKey,
|
||||||
SecretStoreType.EMBEDDING_PROVIDER));
|
SecretStoreType.EMBEDDING_PROVIDER));
|
||||||
|
|||||||
@ -72,7 +72,7 @@ public sealed record Provider(
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToName()}" : this.UsedLLMProvider.ToName();
|
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToSecretId()}" : this.UsedLLMProvider.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
@ -182,7 +182,7 @@ public sealed record Provider(
|
|||||||
{
|
{
|
||||||
// Queue the API key for storage in the OS keyring:
|
// Queue the API key for storage in the OS keyring:
|
||||||
PendingEnterpriseApiKeys.Add(new(
|
PendingEnterpriseApiKeys.Add(new(
|
||||||
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToName()}",
|
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToSecretId()}",
|
||||||
instanceName,
|
instanceName,
|
||||||
decryptedApiKey,
|
decryptedApiKey,
|
||||||
SecretStoreType.LLM_PROVIDER));
|
SecretStoreType.LLM_PROVIDER));
|
||||||
|
|||||||
@ -44,7 +44,7 @@ public sealed record TranscriptionProvider(
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToName()}" : this.UsedLLMProvider.ToName();
|
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToSecretId()}" : this.UsedLLMProvider.ToSecretId();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
@ -125,7 +125,7 @@ public sealed record TranscriptionProvider(
|
|||||||
{
|
{
|
||||||
// Queue the API key for storage in the OS keyring:
|
// Queue the API key for storage in the OS keyring:
|
||||||
PendingEnterpriseApiKeys.Add(new(
|
PendingEnterpriseApiKeys.Add(new(
|
||||||
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToName()}",
|
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToSecretId()}",
|
||||||
name,
|
name,
|
||||||
decryptedApiKey,
|
decryptedApiKey,
|
||||||
SecretStoreType.TRANSCRIPTION_PROVIDER));
|
SecretStoreType.TRANSCRIPTION_PROVIDER));
|
||||||
|
|||||||
@ -4,6 +4,23 @@ namespace AIStudio.Tools.Services;
|
|||||||
|
|
||||||
public sealed partial class RustService
|
public sealed partial class RustService
|
||||||
{
|
{
|
||||||
|
private const string SELF_HOSTED_SECRET_ID = "Self-hosted";
|
||||||
|
|
||||||
|
// Temporary compatibility shim until 2026-12-19:
|
||||||
|
// documentation/compatibility-shims/2026-06-self-hosted-secret-id.md
|
||||||
|
private const string LEGACY_SELF_HOSTED_SECRET_ID_DE = "Selbst gehostet";
|
||||||
|
|
||||||
|
private static string APIKey(SecretStoreType storeType, ISecretId secretId) => $"{storeType.Prefix()}::{secretId.SecretId}::{secretId.SecretName}::api_key";
|
||||||
|
|
||||||
|
private static IEnumerable<string> LegacySelfHostedAPIKeys(ISecretId secretId, SecretStoreType storeType)
|
||||||
|
{
|
||||||
|
if (secretId.SecretId == SELF_HOSTED_SECRET_ID)
|
||||||
|
yield return $"{storeType.Prefix()}::{LEGACY_SELF_HOSTED_SECRET_ID_DE}::{secretId.SecretName}::api_key";
|
||||||
|
|
||||||
|
if (secretId.SecretId == $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{SELF_HOSTED_SECRET_ID}")
|
||||||
|
yield return $"{storeType.Prefix()}::{ISecretId.ENTERPRISE_KEY_PREFIX}::{LEGACY_SELF_HOSTED_SECRET_ID_DE}::{secretId.SecretName}::api_key";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to get the API key for the given secret ID.
|
/// Try to get the API key for the given secret ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -13,24 +30,55 @@ public sealed partial class RustService
|
|||||||
/// <returns>The requested secret.</returns>
|
/// <returns>The requested secret.</returns>
|
||||||
public async Task<RequestedSecret> GetAPIKey(ISecretId secretId, SecretStoreType storeType, bool isTrying = false)
|
public async Task<RequestedSecret> GetAPIKey(ISecretId secretId, SecretStoreType storeType, bool isTrying = false)
|
||||||
{
|
{
|
||||||
var prefix = storeType.Prefix();
|
var secretKey = APIKey(storeType, secretId);
|
||||||
var secretRequest = new SelectSecretRequest($"{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, isTrying);
|
var legacySecretKeys = LegacySelfHostedAPIKeys(secretId, storeType).ToList();
|
||||||
|
var secret = await this.GetAPIKeyByKey(secretKey, isTrying || legacySecretKeys.Count > 0);
|
||||||
|
if (secret.Success)
|
||||||
|
{
|
||||||
|
foreach (var legacySecretKey in legacySecretKeys)
|
||||||
|
await this.DeleteAPIKeyByKey(legacySecretKey, isTrying: true);
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var legacySecretKey in legacySecretKeys)
|
||||||
|
{
|
||||||
|
var legacySecret = await this.GetAPIKeyByKey(legacySecretKey, isTrying: true);
|
||||||
|
if (!legacySecret.Success)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
this.logger!.LogInformation($"Migrating legacy self-hosted API key namespace '{legacySecretKey}' to '{secretKey}'.");
|
||||||
|
var migrationResult = await this.StoreEncryptedAPIKeyByKey(secretKey, legacySecret.Secret);
|
||||||
|
if (migrationResult.Success)
|
||||||
|
await this.DeleteAPIKeyByKey(legacySecretKey, isTrying: true);
|
||||||
|
else
|
||||||
|
this.logger!.LogWarning($"Failed to migrate legacy self-hosted API key namespace '{legacySecretKey}' to '{secretKey}': '{migrationResult.Issue}'");
|
||||||
|
|
||||||
|
return legacySecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTrying)
|
||||||
|
this.logger!.LogError($"Failed to get the API key for '{secretKey}': '{secret.Issue}'");
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<RequestedSecret> GetAPIKeyByKey(string secretKey, bool isTrying)
|
||||||
|
{
|
||||||
|
var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
if(!isTrying)
|
if(!isTrying)
|
||||||
this.logger!.LogError($"Failed to get the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key' due to an API issue: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to get the API key for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||||
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the API key due to an API issue."));
|
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the API key due to an API issue."));
|
||||||
}
|
}
|
||||||
|
|
||||||
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
||||||
if (!secret.Success && !isTrying)
|
|
||||||
this.logger!.LogError($"Failed to get the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key': '{secret.Issue}'");
|
|
||||||
|
|
||||||
if (secret.Success)
|
if (secret.Success)
|
||||||
this.logger!.LogDebug($"Successfully retrieved the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key'.");
|
this.logger!.LogDebug($"Successfully retrieved the API key for '{secretKey}'.");
|
||||||
else if (isTrying)
|
else if (isTrying)
|
||||||
this.logger!.LogDebug($"No API key configured for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key' (try mode): '{secret.Issue}'");
|
this.logger!.LogDebug($"No API key configured for '{secretKey}' (try mode): '{secret.Issue}'");
|
||||||
|
|
||||||
return secret;
|
return secret;
|
||||||
}
|
}
|
||||||
@ -44,21 +92,34 @@ public sealed partial class RustService
|
|||||||
/// <returns>The store secret response.</returns>
|
/// <returns>The store secret response.</returns>
|
||||||
public async Task<StoreSecretResponse> SetAPIKey(ISecretId secretId, string key, SecretStoreType storeType)
|
public async Task<StoreSecretResponse> SetAPIKey(ISecretId secretId, string key, SecretStoreType storeType)
|
||||||
{
|
{
|
||||||
var prefix = storeType.Prefix();
|
|
||||||
var encryptedKey = await this.encryptor!.Encrypt(key);
|
var encryptedKey = await this.encryptor!.Encrypt(key);
|
||||||
var request = new StoreSecretRequest($"{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, encryptedKey);
|
var secretKey = APIKey(storeType, secretId);
|
||||||
|
var state = await this.StoreEncryptedAPIKeyByKey(secretKey, encryptedKey);
|
||||||
|
if (state.Success)
|
||||||
|
{
|
||||||
|
foreach (var legacySecretKey in LegacySelfHostedAPIKeys(secretId, storeType))
|
||||||
|
await this.DeleteAPIKeyByKey(legacySecretKey, isTrying: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<StoreSecretResponse> StoreEncryptedAPIKeyByKey(string secretKey, EncryptedText encryptedKey)
|
||||||
|
{
|
||||||
|
var request = new StoreSecretRequest(secretKey, Environment.UserName, encryptedKey);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to store the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key' due to an API issue: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to store the API key for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||||
return new StoreSecretResponse(false, TB("Failed to get the API key due to an API issue."));
|
return new StoreSecretResponse(false, TB("Failed to store the API key due to an API issue."));
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
||||||
if (!state.Success)
|
if (!state.Success)
|
||||||
this.logger!.LogError($"Failed to store the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key': '{state.Issue}'");
|
this.logger!.LogError($"Failed to store the API key for '{secretKey}': '{state.Issue}'");
|
||||||
|
else
|
||||||
|
this.logger!.LogDebug($"Successfully stored the API key for '{secretKey}'.");
|
||||||
|
|
||||||
this.logger!.LogDebug($"Successfully stored the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key'.");
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,18 +131,35 @@ public sealed partial class RustService
|
|||||||
/// <returns>The delete secret response.</returns>
|
/// <returns>The delete secret response.</returns>
|
||||||
public async Task<DeleteSecretResponse> DeleteAPIKey(ISecretId secretId, SecretStoreType storeType)
|
public async Task<DeleteSecretResponse> DeleteAPIKey(ISecretId secretId, SecretStoreType storeType)
|
||||||
{
|
{
|
||||||
var prefix = storeType.Prefix();
|
var deleteResult = await this.DeleteAPIKeyByKey(APIKey(storeType, secretId));
|
||||||
var request = new SelectSecretRequest($"{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, false);
|
if (!deleteResult.Success)
|
||||||
|
return deleteResult;
|
||||||
|
|
||||||
|
foreach (var legacySecretKey in LegacySelfHostedAPIKeys(secretId, storeType))
|
||||||
|
{
|
||||||
|
var legacyDeleteResult = await this.DeleteAPIKeyByKey(legacySecretKey, isTrying: true);
|
||||||
|
if (!legacyDeleteResult.Success)
|
||||||
|
return legacyDeleteResult;
|
||||||
|
|
||||||
|
deleteResult = deleteResult with { WasEntryFound = deleteResult.WasEntryFound || legacyDeleteResult.WasEntryFound };
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleteResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DeleteSecretResponse> DeleteAPIKeyByKey(string secretKey, bool isTrying = false)
|
||||||
|
{
|
||||||
|
var request = new SelectSecretRequest(secretKey, Environment.UserName, false);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to delete the API key for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the API key due to an API issue.")};
|
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the API key due to an API issue.")};
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
||||||
if (!state.Success)
|
if (!state.Success && !isTrying)
|
||||||
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
this.logger!.LogError($"Failed to delete the API key for '{secretKey}': '{state.Issue}'");
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
# v26.6.2, build 242 (2026-06-xx xx:xx UTC)
|
# v26.6.2, build 242 (2026-06-xx xx:xx UTC)
|
||||||
|
- Fixed self-hosted provider API keys sometimes being stored under a localized name. AI Studio now uses a stable key name, keeps correct entries working, and automatically migrates known localized entries for LLM, transcription, and embedding providers. Organizations using configuration plugins do not need to change their plugins; affected users who still see an invalid API key warning should open the provider, transcription, or embedding settings and update the API key once.
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
# Self-Hosted Provider Secret ID
|
||||||
|
|
||||||
|
- Status: Active
|
||||||
|
- Introduced: 2026-06-19
|
||||||
|
- Remove after: 2026-12-19
|
||||||
|
- Code references:
|
||||||
|
- `app/MindWork AI Studio/Tools/Services/RustService.APIKeys.cs`
|
||||||
|
- `app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs`
|
||||||
|
|
||||||
|
## User Impact
|
||||||
|
|
||||||
|
Some self-hosted provider API keys were stored under a localized OS keyring namespace. In German installations this could produce entries using `Selbst gehostet`, while the fixed canonical namespace is `Self-hosted`.
|
||||||
|
|
||||||
|
Without this shim, affected users may see an invalid or missing API key warning until they manually enter the key again.
|
||||||
|
|
||||||
|
## Compatibility Behavior
|
||||||
|
|
||||||
|
AI Studio uses `Self-hosted` as the canonical secret namespace. For a limited time, API key reads, writes, and deletes also consider the known German legacy namespace `Selbst gehostet`.
|
||||||
|
|
||||||
|
When a legacy entry is found, AI Studio stores the same encrypted API key under the canonical namespace and deletes the legacy entry. If the canonical entry already exists, AI Studio also attempts to delete the known legacy alias.
|
||||||
|
|
||||||
|
This applies to LLM provider, embedding provider, and transcription provider API keys, including enterprise configuration plugin namespaces.
|
||||||
|
|
||||||
|
## Removal Checklist
|
||||||
|
|
||||||
|
- Remove `LEGACY_SELF_HOSTED_SECRET_ID_DE`.
|
||||||
|
- Remove `LegacySelfHostedAPIKeys`.
|
||||||
|
- Remove legacy lookup, migration, and cleanup calls from API key read, write, and delete paths.
|
||||||
|
- Keep `LLMProvidersExtensions.ToSecretId()` and the canonical `Self-hosted` namespace.
|
||||||
|
- Update this document's status to `Removed`.
|
||||||
|
- Add a changelog entry only if removal is user-visible.
|
||||||
44
documentation/compatibility-shims/README.md
Normal file
44
documentation/compatibility-shims/README.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Compatibility Shims
|
||||||
|
|
||||||
|
Compatibility shims are temporary fallback paths that keep older installations, settings, secrets, plugin data, or external integrations working while users move to a newer release.
|
||||||
|
|
||||||
|
Use this folder for short-lived compatibility code such as legacy aliases, read-repair logic, temporary import fallbacks, or cleanup paths. Do not use it for permanent settings schema migrations; those belong in `app/MindWork AI Studio/Settings/SettingsMigrations.cs`.
|
||||||
|
|
||||||
|
Every compatibility shim must have:
|
||||||
|
|
||||||
|
- A Markdown file in this folder.
|
||||||
|
- A clear status.
|
||||||
|
- An introduced date.
|
||||||
|
- A remove-after date.
|
||||||
|
- Code references.
|
||||||
|
- A short explanation of user impact.
|
||||||
|
- The compatibility behavior.
|
||||||
|
- A removal checklist.
|
||||||
|
- A short code comment near the shim that references the Markdown file and remove-after date.
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
```md
|
||||||
|
# Short Title
|
||||||
|
|
||||||
|
- Status: Active
|
||||||
|
- Introduced: YYYY-MM-DD
|
||||||
|
- Remove after: YYYY-MM-DD
|
||||||
|
- Code references:
|
||||||
|
- path/to/file.cs
|
||||||
|
|
||||||
|
## User Impact
|
||||||
|
|
||||||
|
Describe who needs this compatibility path and what breaks without it.
|
||||||
|
|
||||||
|
## Compatibility Behavior
|
||||||
|
|
||||||
|
Describe the temporary fallback, alias, read-repair, or cleanup behavior.
|
||||||
|
|
||||||
|
## Removal Checklist
|
||||||
|
|
||||||
|
- Remove the temporary constants, fallback branches, aliases, or cleanup paths.
|
||||||
|
- Remove or update tests and static checks that mention the shim.
|
||||||
|
- Update this document's status to `Removed`.
|
||||||
|
- Add a changelog entry if removing the shim is user-visible.
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue
Block a user