mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-02-05 15:49:07 +00:00
Allow the use of an API key for self-hosted ollama
instances (#156)
This commit is contained in:
parent
776fa8ac58
commit
37e113af0e
@ -13,6 +13,7 @@ public partial class Changelog
|
|||||||
|
|
||||||
public static readonly Log[] LOGS =
|
public static readonly Log[] LOGS =
|
||||||
[
|
[
|
||||||
|
new (188, "v0.9.13, build 188 (2024-10-07 11:18 UTC)", "v0.9.13.md"),
|
||||||
new (187, "v0.9.12, build 187 (2024-09-15 20:49 UTC)", "v0.9.12.md"),
|
new (187, "v0.9.12, build 187 (2024-09-15 20:49 UTC)", "v0.9.12.md"),
|
||||||
new (186, "v0.9.11, build 186 (2024-09-15 10:33 UTC)", "v0.9.11.md"),
|
new (186, "v0.9.11, build 186 (2024-09-15 10:33 UTC)", "v0.9.11.md"),
|
||||||
new (185, "v0.9.10, build 185 (2024-09-12 20:52 UTC)", "v0.9.10.md"),
|
new (185, "v0.9.10, build 185 (2024-09-12 20:52 UTC)", "v0.9.10.md"),
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<MudTextField
|
<MudTextField
|
||||||
T="string"
|
T="string"
|
||||||
@bind-Text="@this.dataAPIKey"
|
@bind-Text="@this.dataAPIKey"
|
||||||
Label="API Key"
|
Label="@this.APIKeyText"
|
||||||
Disabled="@(!this.NeedAPIKey)"
|
Disabled="@(!this.NeedAPIKey)"
|
||||||
Class="mb-3"
|
Class="mb-3"
|
||||||
Adornment="Adornment.Start"
|
Adornment="Adornment.Start"
|
||||||
|
@ -133,7 +133,7 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
//
|
//
|
||||||
// We cannot load the API key for self-hosted providers:
|
// We cannot load the API key for self-hosted providers:
|
||||||
//
|
//
|
||||||
if (this.DataLLMProvider is LLMProviders.SELF_HOSTED)
|
if (this.DataLLMProvider is LLMProviders.SELF_HOSTED && this.DataHost is not Host.OLLAMA)
|
||||||
{
|
{
|
||||||
await this.ReloadModels();
|
await this.ReloadModels();
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
@ -149,7 +149,7 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load the API key:
|
// Load the API key:
|
||||||
var requestedSecret = await this.RustService.GetAPIKey(provider);
|
var requestedSecret = await this.RustService.GetAPIKey(provider, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED);
|
||||||
if(requestedSecret.Success)
|
if(requestedSecret.Success)
|
||||||
{
|
{
|
||||||
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
|
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
|
||||||
@ -158,10 +158,17 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
await this.ReloadModels();
|
await this.ReloadModels();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
this.dataAPIKey = string.Empty;
|
||||||
|
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED)
|
||||||
{
|
{
|
||||||
this.dataAPIKeyStorageIssue = $"Failed to load the API key from the operating system. The message was: {requestedSecret.Issue}. You might ignore this message and provide the API key again.";
|
this.dataAPIKeyStorageIssue = $"Failed to load the API key from the operating system. The message was: {requestedSecret.Issue}. You might ignore this message and provide the API key again.";
|
||||||
await this.form.Validate();
|
await this.form.Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We still try to load the models. Some local hosts don't need an API key:
|
||||||
|
await this.ReloadModels();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
@ -192,7 +199,7 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
// Use the data model to store the provider.
|
// Use the data model to store the provider.
|
||||||
// We just return this data to the parent component:
|
// We just return this data to the parent component:
|
||||||
var addedProviderSettings = this.CreateProviderSettings();
|
var addedProviderSettings = this.CreateProviderSettings();
|
||||||
if (addedProviderSettings.UsedLLMProvider != LLMProviders.SELF_HOSTED)
|
if (!string.IsNullOrWhiteSpace(this.dataAPIKey))
|
||||||
{
|
{
|
||||||
// We need to instantiate the provider to store the API key:
|
// We need to instantiate the provider to store the API key:
|
||||||
var provider = addedProviderSettings.CreateProvider(this.Logger);
|
var provider = addedProviderSettings.CreateProvider(this.Logger);
|
||||||
@ -363,10 +370,17 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
LLMProviders.ANTHROPIC => true,
|
LLMProviders.ANTHROPIC => true,
|
||||||
|
|
||||||
LLMProviders.FIREWORKS => true,
|
LLMProviders.FIREWORKS => true,
|
||||||
|
LLMProviders.SELF_HOSTED => this.DataHost is Host.OLLAMA,
|
||||||
|
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private string APIKeyText => this.DataLLMProvider switch
|
||||||
|
{
|
||||||
|
LLMProviders.SELF_HOSTED => "(Optional) API Key",
|
||||||
|
_ => "API Key",
|
||||||
|
};
|
||||||
|
|
||||||
private bool NeedHostname => this.DataLLMProvider switch
|
private bool NeedHostname => this.DataLLMProvider switch
|
||||||
{
|
{
|
||||||
LLMProviders.SELF_HOSTED => true,
|
LLMProviders.SELF_HOSTED => true,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@ -23,6 +24,9 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
|
public async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
// Get the API key:
|
||||||
|
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, isTrying: true);
|
||||||
|
|
||||||
// Prepare the system prompt:
|
// Prepare the system prompt:
|
||||||
var systemPrompt = new Message
|
var systemPrompt = new Message
|
||||||
{
|
{
|
||||||
@ -62,9 +66,16 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
|
|||||||
MaxTokens = -1,
|
MaxTokens = -1,
|
||||||
}, JSON_SERIALIZER_OPTIONS);
|
}, JSON_SERIALIZER_OPTIONS);
|
||||||
|
|
||||||
|
StreamReader? streamReader = default;
|
||||||
|
try
|
||||||
|
{
|
||||||
// Build the HTTP post request:
|
// Build the HTTP post request:
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, provider.Host.ChatURL());
|
var request = new HttpRequestMessage(HttpMethod.Post, provider.Host.ChatURL());
|
||||||
|
|
||||||
|
// Set the authorization header:
|
||||||
|
if (requestedSecret.Success)
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||||
|
|
||||||
// Set the content:
|
// Set the content:
|
||||||
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
|
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
@ -77,8 +88,15 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
|
|||||||
var providerStream = await response.Content.ReadAsStreamAsync(token);
|
var providerStream = await response.Content.ReadAsStreamAsync(token);
|
||||||
|
|
||||||
// Add a stream reader to read the stream, line by line:
|
// Add a stream reader to read the stream, line by line:
|
||||||
var streamReader = new StreamReader(providerStream);
|
streamReader = new StreamReader(providerStream);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
this.logger.LogError($"Failed to stream chat completion from self-hosted provider '{this.InstanceName}': {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamReader is not null)
|
||||||
|
{
|
||||||
// Read the stream, line by line:
|
// Read the stream, line by line:
|
||||||
while (!streamReader.EndOfStream)
|
while (!streamReader.EndOfStream)
|
||||||
{
|
{
|
||||||
@ -126,6 +144,7 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
|
|||||||
yield return providerResponse.Choices[0].Delta.Content;
|
yield return providerResponse.Choices[0].Delta.Content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -149,7 +168,21 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
|
|||||||
|
|
||||||
case Host.LM_STUDIO:
|
case Host.LM_STUDIO:
|
||||||
case Host.OLLAMA:
|
case Host.OLLAMA:
|
||||||
|
|
||||||
|
var secretKey = apiKeyProvisional switch
|
||||||
|
{
|
||||||
|
not null => apiKeyProvisional,
|
||||||
|
_ => await RUST_SERVICE.GetAPIKey(this, isTrying: true) switch
|
||||||
|
{
|
||||||
|
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||||
|
_ => null,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
|
var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
|
if(secretKey is not null)
|
||||||
|
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKeyProvisional);
|
||||||
|
|
||||||
var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token);
|
var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token);
|
||||||
if(!lmStudioResponse.IsSuccessStatusCode)
|
if(!lmStudioResponse.IsSuccessStatusCode)
|
||||||
return [];
|
return [];
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
namespace AIStudio.Tools.Rust;
|
namespace AIStudio.Tools.Rust;
|
||||||
|
|
||||||
public readonly record struct SelectSecretRequest(string Destination, string UserName);
|
public readonly record struct SelectSecretRequest(string Destination, string UserName, bool IsTrying);
|
@ -258,19 +258,21 @@ public sealed class RustService : IDisposable
|
|||||||
/// Try to get the API key for the given provider.
|
/// Try to get the API key for the given provider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="provider">The provider to get the API key for.</param>
|
/// <param name="provider">The provider to get the API key for.</param>
|
||||||
|
/// <param name="isTrying">Indicates if we are trying to get the API key. In that case, we don't log errors.</param>
|
||||||
/// <returns>The requested secret.</returns>
|
/// <returns>The requested secret.</returns>
|
||||||
public async Task<RequestedSecret> GetAPIKey(IProvider provider)
|
public async Task<RequestedSecret> GetAPIKey(IProvider provider, bool isTrying = false)
|
||||||
{
|
{
|
||||||
var secretRequest = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
|
var secretRequest = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", 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)
|
||||||
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
|
||||||
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue.");
|
return new RequestedSecret(false, new EncryptedText(string.Empty), "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)
|
if (!secret.Success && !isTrying)
|
||||||
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}': '{secret.Issue}'");
|
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}': '{secret.Issue}'");
|
||||||
|
|
||||||
return secret;
|
return secret;
|
||||||
@ -307,7 +309,7 @@ public sealed class RustService : IDisposable
|
|||||||
/// <returns>The delete secret response.</returns>
|
/// <returns>The delete secret response.</returns>
|
||||||
public async Task<DeleteSecretResponse> DeleteAPIKey(IProvider provider)
|
public async Task<DeleteSecretResponse> DeleteAPIKey(IProvider provider)
|
||||||
{
|
{
|
||||||
var request = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
|
var request = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", 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)
|
||||||
{
|
{
|
||||||
|
2
app/MindWork AI Studio/wwwroot/changelog/v0.9.13.md
Normal file
2
app/MindWork AI Studio/wwwroot/changelog/v0.9.13.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# v0.9.13, build 188 (2024-10-07 11:18 UTC)
|
||||||
|
- Allow the use of an API key for self-hosted `ollama` instances. Useful when using `ollama` with, e.g., Open WebUI.
|
@ -1,9 +1,9 @@
|
|||||||
0.9.12
|
0.9.13
|
||||||
2024-09-15 20:49:12 UTC
|
2024-10-07 11:18:05 UTC
|
||||||
187
|
188
|
||||||
8.0.108 (commit 665a05cea7)
|
8.0.108 (commit 665a05cea7)
|
||||||
8.0.8 (commit 08338fcaa5)
|
8.0.8 (commit 08338fcaa5)
|
||||||
1.81.0 (commit eeb90cda1)
|
1.81.0 (commit eeb90cda1)
|
||||||
7.8.0
|
7.8.0
|
||||||
1.7.1
|
1.7.1
|
||||||
8715054dda6, release
|
580ca9850b1, release
|
||||||
|
2
runtime/Cargo.lock
generated
2
runtime/Cargo.lock
generated
@ -2130,7 +2130,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mindwork-ai-studio"
|
name = "mindwork-ai-studio"
|
||||||
version = "0.9.12"
|
version = "0.9.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"arboard",
|
"arboard",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mindwork-ai-studio"
|
name = "mindwork-ai-studio"
|
||||||
version = "0.9.12"
|
version = "0.9.13"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "MindWork AI Studio"
|
description = "MindWork AI Studio"
|
||||||
authors = ["Thorsten Sommer"]
|
authors = ["Thorsten Sommer"]
|
||||||
|
@ -966,7 +966,10 @@ fn get_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<RequestedS
|
|||||||
},
|
},
|
||||||
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
if !request.is_trying {
|
||||||
error!(Source = "Secret Store"; "Failed to retrieve secret for '{service}' and user '{user_name}': {e}.");
|
error!(Source = "Secret Store"; "Failed to retrieve secret for '{service}' and user '{user_name}': {e}.");
|
||||||
|
}
|
||||||
|
|
||||||
Json(RequestedSecret {
|
Json(RequestedSecret {
|
||||||
success: false,
|
success: false,
|
||||||
secret: EncryptedText::new(String::from("")),
|
secret: EncryptedText::new(String::from("")),
|
||||||
@ -980,6 +983,7 @@ fn get_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<RequestedS
|
|||||||
struct RequestSecret {
|
struct RequestSecret {
|
||||||
destination: String,
|
destination: String,
|
||||||
user_name: String,
|
user_name: String,
|
||||||
|
is_trying: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "MindWork AI Studio",
|
"productName": "MindWork AI Studio",
|
||||||
"version": "0.9.12"
|
"version": "0.9.13"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
Loading…
Reference in New Issue
Block a user