Fixed error messages for provider requests (#778)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
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 / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2026-05-25 17:32:54 +02:00 committed by GitHub
parent 8417fa3984
commit 3e6e3bdcbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 541 additions and 77 deletions

View File

@ -328,22 +328,40 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
this.isProcessing = true; this.isProcessing = true;
this.StateHasChanged(); this.StateHasChanged();
// Use the selected provider to get the AI response. try
// By awaiting this line, we wait for the entire
// content to be streamed.
this.ChatThread = await aiText.CreateFromProviderAsync(this.ProviderSettings.CreateProvider(), this.ProviderSettings.Model, this.LastUserPrompt, this.ChatThread, this.CancellationTokenSource!.Token);
this.isProcessing = false;
this.StateHasChanged();
if(manageCancellationLocally)
{ {
this.CancellationTokenSource.Dispose(); // Use the selected provider to get the AI response.
this.CancellationTokenSource = null; // By awaiting this line, we wait for the entire
} // content to be streamed.
this.ChatThread = await aiText.CreateFromProviderAsync(this.ProviderSettings.CreateProvider(), this.ProviderSettings.Model, this.LastUserPrompt, this.ChatThread, this.CancellationTokenSource!.Token);
// Return the AI response: // Return the AI response:
return aiText.Text; return aiText.Text;
}
catch (ProviderRequestException e)
{
this.Logger.LogError(e, "The provider request failed for assistant '{AssistantTitle}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", this.Title, e.StatusCode, e.ReasonPhrase, e.ResponseBody);
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
if (this.resultingContentBlock is not null && string.IsNullOrWhiteSpace(aiText.Text))
{
this.ChatThread?.Blocks.Remove(this.resultingContentBlock);
this.resultingContentBlock = null;
}
return string.Empty;
}
finally
{
this.isProcessing = false;
this.StateHasChanged();
if(manageCancellationLocally)
{
this.CancellationTokenSource?.Dispose();
this.CancellationTokenSource = null;
}
}
} }
private async Task CancelStreaming() private async Task CancelStreaming()

View File

@ -6469,6 +6469,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Your stage directions"
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}' -- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'" UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"
-- The provider '{0}' reported an error while streaming the response.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1008706234"] = "The provider '{0}' reported an error while streaming the response."
-- The provider rejected the request because too many requests were sent. Please wait a moment and try again.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1028424693"] = "The provider rejected the request because too many requests were sent. Please wait a moment and try again."
-- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding. -- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding."
@ -6502,6 +6508,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3759732886"] = "We tried to
-- We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}' -- We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T4049517041"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'" UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T4049517041"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'"
-- The provider '{0}' reported an error: {1}
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T700894460"] = "The provider '{0}' reported an error: {1}"
-- The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe. -- The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe."
@ -6562,6 +6571,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected"
-- We could not load models from '{0}'. The account or API key does not have the required permissions. -- We could not load models from '{0}'. The account or API key does not have the required permissions.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T1143085203"] = "We could not load models from '{0}'. The account or API key does not have the required permissions." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T1143085203"] = "We could not load models from '{0}'. The account or API key does not have the required permissions."
-- We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T155481725"] = "We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again."
-- We could not load models from '{0}'. The API key is probably missing, invalid, or expired. -- We could not load models from '{0}'. The API key is probably missing, invalid, or expired.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2041046579"] = "We could not load models from '{0}'. The API key is probably missing, invalid, or expired." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2041046579"] = "We could not load models from '{0}'. The API key is probably missing, invalid, or expired."
@ -6571,9 +6583,15 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T21156887
-- We could not load models from '{0}' because the provider returned an unexpected response. -- We could not load models from '{0}' because the provider returned an unexpected response.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2186844789"] = "We could not load models from '{0}' because the provider returned an unexpected response." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2186844789"] = "We could not load models from '{0}' because the provider returned an unexpected response."
-- We could not load models from '{0}' because the account appears to have no API credits left.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T373339048"] = "We could not load models from '{0}' because the account appears to have no API credits left."
-- We could not load models from '{0}' due to an unknown error. -- We could not load models from '{0}' due to an unknown error.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "We could not load models from '{0}' due to an unknown error." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "We could not load models from '{0}' due to an unknown error."
-- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again."
-- Model as configured by whisper.cpp -- Model as configured by whisper.cpp
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp" UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp"

View File

@ -93,59 +93,70 @@ public sealed class ContentText : IContent
// Start another thread by using a task to uncouple // Start another thread by using a task to uncouple
// the UI thread from the AI processing: // the UI thread from the AI processing:
await Task.Run(async () => try
{ {
// We show the waiting animation until we get the first response: await Task.Run(async () =>
this.InitialRemoteWait = true;
// Iterate over the responses from the AI:
await foreach (var contentStreamChunk in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
{ {
// When the user cancels the request, we stop the loop: try
if (token.IsCancellationRequested)
break;
// Stop the waiting animation:
this.InitialRemoteWait = false;
this.IsStreaming = true;
// Add the response to the text:
this.Text += contentStreamChunk;
// Merge the sources:
this.Sources.MergeSources(contentStreamChunk.Sources);
// Notify the UI that the content has changed,
// depending on the energy saving mode:
var now = DateTimeOffset.Now;
switch (settings.ConfigurationData.App.IsSavingEnergy)
{ {
// Energy saving mode is off. We notify the UI // We show the waiting animation until we get the first response:
// as fast as possible -- no matter the odds: this.InitialRemoteWait = true;
case false:
await this.StreamingEvent();
break;
// Energy saving mode is on. We notify the UI // Iterate over the responses from the AI:
// only when the time between two events is await foreach (var contentStreamChunk in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
// greater than the minimum time: {
case true when now - last > MIN_TIME: // When the user cancels the request, we stop the loop:
last = now; if (token.IsCancellationRequested)
await this.StreamingEvent(); break;
break;
// Stop the waiting animation:
this.InitialRemoteWait = false;
this.IsStreaming = true;
// Add the response to the text:
this.Text += contentStreamChunk;
// Merge the sources:
this.Sources.MergeSources(contentStreamChunk.Sources);
// Notify the UI that the content has changed,
// depending on the energy saving mode:
var now = DateTimeOffset.Now;
switch (settings.ConfigurationData.App.IsSavingEnergy)
{
// Energy saving mode is off. We notify the UI
// as fast as possible -- no matter the odds:
case false:
await this.StreamingEvent();
break;
// Energy saving mode is on. We notify the UI
// only when the time between two events is
// greater than the minimum time:
case true when now - last > MIN_TIME:
last = now;
await this.StreamingEvent();
break;
}
}
} }
} finally
{
// Stop the waiting animation (in case the loop
// was stopped, or no content was received):
this.InitialRemoteWait = false;
this.IsStreaming = false;
}
}, token);
}
finally
{
this.Text = this.Text.RemoveThinkTags().Trim();
// Stop the waiting animation (in case the loop // Inform the UI that the streaming is done:
// was stopped, or no content was received): await this.StreamingDone();
this.InitialRemoteWait = false; }
this.IsStreaming = false;
}, token);
this.Text = this.Text.RemoveThinkTags().Trim();
// Inform the UI that the streaming is done:
await this.StreamingDone();
return chatThread; return chatThread;
} }

View File

@ -366,7 +366,10 @@ public partial class VoiceRecorder : MSGComponentBase
if (!transcriptionResult.Success) if (!transcriptionResult.Success)
{ {
this.Logger.LogWarning("The transcription request failed."); this.Logger.LogWarning("The transcription request failed.");
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, this.T("Unfortunately, there was an error communicating with the AI system."))); var userMessage = string.IsNullOrWhiteSpace(transcriptionResult.ErrorMessage)
? this.T("Unfortunately, there was an error communicating with the AI system.")
: transcriptionResult.ErrorMessage;
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, userMessage));
return; return;
} }

View File

@ -10,6 +10,7 @@ namespace AIStudio.Pages;
public partial class Writer : MSGComponentBase public partial class Writer : MSGComponentBase
{ {
private static readonly ILogger<Writer> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Writer>();
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500)); private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500));
@ -106,22 +107,38 @@ public partial class Writer : MSGComponentBase
InitialRemoteWait = true, InitialRemoteWait = true,
}; };
this.chatThread?.Blocks.Add(new ContentBlock var aiBlock = new ContentBlock
{ {
Time = time, Time = time,
ContentType = ContentType.TEXT, ContentType = ContentType.TEXT,
Role = ChatRole.AI, Role = ChatRole.AI,
Content = aiText, Content = aiText,
}); };
this.chatThread?.Blocks.Add(aiBlock);
this.isStreaming = true; this.isStreaming = true;
this.StateHasChanged(); this.StateHasChanged();
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.providerSettings.Model, lastUserPrompt, this.chatThread); try
this.suggestion = aiText.Text; {
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.providerSettings.Model, lastUserPrompt, this.chatThread);
this.suggestion = aiText.Text;
}
catch (ProviderRequestException e)
{
LOGGER.LogError(e, "The provider request failed for writer suggestions. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", e.StatusCode, e.ReasonPhrase, e.ResponseBody);
await this.MessageBus.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
this.suggestion = string.Empty;
this.isStreaming = false; if (string.IsNullOrWhiteSpace(aiText.Text))
this.StateHasChanged(); this.chatThread?.Blocks.Remove(aiBlock);
}
finally
{
this.isStreaming = false;
this.StateHasChanged();
}
} }
private void AcceptEntireSuggestion() private void AcceptEntireSuggestion()

View File

@ -6471,6 +6471,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Ihre Regieanweisungen"
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}' -- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Der Server ist möglicherweise nicht erreichbar oder hat Probleme. Die Nachricht des Anbieters lautet: „{2}“" UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Der Server ist möglicherweise nicht erreichbar oder hat Probleme. Die Nachricht des Anbieters lautet: „{2}“"
-- The provider '{0}' reported an error while streaming the response.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1008706234"] = "Der Anbieter „{0}“ hat einen Fehler beim Streamen der Antwort gemeldet."
-- The provider rejected the request because too many requests were sent. Please wait a moment and try again.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1028424693"] = "Der Anbieter hat die Anfrage abgelehnt, weil zu viele Anfragen gesendet wurden. Bitte warten Sie einen Moment und versuchen Sie es erneut."
-- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding. -- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "Die Anfrage an den LLM-Anbieter „{0}“ (Typ={1}) hat nach {2} während „{3}“ das Zeitlimit überschritten. Bitte versuchen Sie es erneut oder prüfen Sie, ob der Anbieter noch antwortet." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "Die Anfrage an den LLM-Anbieter „{0}“ (Typ={1}) hat nach {2} während „{3}“ das Zeitlimit überschritten. Bitte versuchen Sie es erneut oder prüfen Sie, ob der Anbieter noch antwortet."
@ -6504,6 +6510,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3759732886"] = "Wir haben ve
-- We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}' -- We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T4049517041"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Die Daten des Chats, einschließlich aller Dateianhänge, sind vermutlich zu groß für das ausgewählte Modell und den Anbieter. Die Nachricht des Anbieters lautet: „{2}“" UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T4049517041"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Die Daten des Chats, einschließlich aller Dateianhänge, sind vermutlich zu groß für das ausgewählte Modell und den Anbieter. Die Nachricht des Anbieters lautet: „{2}“"
-- The provider '{0}' reported an error: {1}
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T700894460"] = "Der Anbieter „{0}“ hat einen Fehler gemeldet: {1}"
-- The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe. -- The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "Das Vertrauensniveau dieses Anbieters wurde **noch nicht** gründlich **untersucht und bewertet**. Wir wissen nicht, ob ihre Daten sicher sind." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "Das Vertrauensniveau dieses Anbieters wurde **noch nicht** gründlich **untersucht und bewertet**. Wir wissen nicht, ob ihre Daten sicher sind."
@ -6564,6 +6573,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "Kein Modell ausgew
-- We could not load models from '{0}'. The account or API key does not have the required permissions. -- We could not load models from '{0}'. The account or API key does not have the required permissions.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T1143085203"] = "Wir konnten keine Modelle von '{0}' laden. Das Konto oder der API-Schlüssel verfügt nicht über die erforderlichen Berechtigungen." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T1143085203"] = "Wir konnten keine Modelle von '{0}' laden. Das Konto oder der API-Schlüssel verfügt nicht über die erforderlichen Berechtigungen."
-- We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T155481725"] = "Wir konnten keine Modelle von „{0}“ laden, da zu viele Anfragen gesendet wurden. Bitte warten Sie einen Moment und versuchen Sie es erneut."
-- We could not load models from '{0}'. The API key is probably missing, invalid, or expired. -- We could not load models from '{0}'. The API key is probably missing, invalid, or expired.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2041046579"] = "Modelle aus '{0}' konnten nicht geladen werden. Wahrscheinlich fehlt der API-Schlüssel, ist ungültig oder abgelaufen." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2041046579"] = "Modelle aus '{0}' konnten nicht geladen werden. Wahrscheinlich fehlt der API-Schlüssel, ist ungültig oder abgelaufen."
@ -6573,9 +6585,15 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T21156887
-- We could not load models from '{0}' because the provider returned an unexpected response. -- We could not load models from '{0}' because the provider returned an unexpected response.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2186844789"] = "Wir konnten keine Modelle von '{0}' laden, da der Anbieter eine unerwartete Antwort zurückgegeben hat." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2186844789"] = "Wir konnten keine Modelle von '{0}' laden, da der Anbieter eine unerwartete Antwort zurückgegeben hat."
-- We could not load models from '{0}' because the account appears to have no API credits left.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T373339048"] = "Modelle konnten nicht von „{0}“ geladen werden, da das Konto offenbar keine API-Guthaben mehr hat."
-- We could not load models from '{0}' due to an unknown error. -- We could not load models from '{0}' due to an unknown error.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "Wir konnten die Modelle aus '{0}' aufgrund eines unbekannten Fehlers nicht laden." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "Wir konnten die Modelle aus '{0}' aufgrund eines unbekannten Fehlers nicht laden."
-- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "Anscheinend haben Sie bei OpenAI kein API-Guthaben mehr. Bitte fügen Sie Ihrem Konto Guthaben hinzu und versuchen Sie es erneut."
-- Model as configured by whisper.cpp -- Model as configured by whisper.cpp
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Modell wie in whisper.cpp konfiguriert" UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Modell wie in whisper.cpp konfiguriert"

View File

@ -6471,6 +6471,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Your stage directions"
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}' -- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'" UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"
-- The provider '{0}' reported an error while streaming the response.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1008706234"] = "The provider '{0}' reported an error while streaming the response."
-- The provider rejected the request because too many requests were sent. Please wait a moment and try again.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1028424693"] = "The provider rejected the request because too many requests were sent. Please wait a moment and try again."
-- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding. -- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding."
@ -6504,6 +6510,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3759732886"] = "We tried to
-- We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}' -- We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T4049517041"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'" UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T4049517041"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'"
-- The provider '{0}' reported an error: {1}
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T700894460"] = "The provider '{0}' reported an error: {1}"
-- The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe. -- The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe."
@ -6564,6 +6573,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected"
-- We could not load models from '{0}'. The account or API key does not have the required permissions. -- We could not load models from '{0}'. The account or API key does not have the required permissions.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T1143085203"] = "We could not load models from '{0}'. The account or API key does not have the required permissions." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T1143085203"] = "We could not load models from '{0}'. The account or API key does not have the required permissions."
-- We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T155481725"] = "We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again."
-- We could not load models from '{0}'. The API key is probably missing, invalid, or expired. -- We could not load models from '{0}'. The API key is probably missing, invalid, or expired.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2041046579"] = "We could not load models from '{0}'. The API key is probably missing, invalid, or expired." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2041046579"] = "We could not load models from '{0}'. The API key is probably missing, invalid, or expired."
@ -6573,9 +6585,15 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T21156887
-- We could not load models from '{0}' because the provider returned an unexpected response. -- We could not load models from '{0}' because the provider returned an unexpected response.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2186844789"] = "We could not load models from '{0}' because the provider returned an unexpected response." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2186844789"] = "We could not load models from '{0}' because the provider returned an unexpected response."
-- We could not load models from '{0}' because the account appears to have no API credits left.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T373339048"] = "We could not load models from '{0}' because the account appears to have no API credits left."
-- We could not load models from '{0}' due to an unknown error. -- We could not load models from '{0}' due to an unknown error.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "We could not load models from '{0}' due to an unknown error." UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "We could not load models from '{0}' due to an unknown error."
-- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again."
-- Model as configured by whisper.cpp -- Model as configured by whisper.cpp
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp" UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp"

View File

@ -179,6 +179,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
{ {
System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR, System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
System.Net.HttpStatusCode.TooManyRequests => ModelLoadFailureReason.TOO_MANY_REQUESTS,
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE, _ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
}, },
requestConfigurator: (request, secretKey) => requestConfigurator: (request, secretKey) =>

View File

@ -167,10 +167,18 @@ public abstract class BaseProvider : IProvider, ISecretId
{ {
HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR, HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
HttpStatusCode.TooManyRequests => ModelLoadFailureReason.TOO_MANY_REQUESTS,
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE, _ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
}; };
protected ModelLoadFailureReason GetModelLoadFailureReason(HttpResponseMessage response, string responseBody) => this.ClassifyProviderRequestFailure(response.StatusCode, responseBody) switch
{
ProviderRequestFailureReason.INSUFFICIENT_QUOTA => ModelLoadFailureReason.INSUFFICIENT_QUOTA,
ProviderRequestFailureReason.TOO_MANY_REQUESTS => ModelLoadFailureReason.TOO_MANY_REQUESTS,
_ => GetDefaultModelLoadFailureReason(response),
};
protected async Task<ModelLoadResult> LoadModelsResponse<TResponse>( protected async Task<ModelLoadResult> LoadModelsResponse<TResponse>(
SecretStoreType storeType, SecretStoreType storeType,
string requestPath, string requestPath,
@ -198,7 +206,8 @@ public abstract class BaseProvider : IProvider, ISecretId
var responseBody = await response.Content.ReadAsStringAsync(token); var responseBody = await response.Content.ReadAsStringAsync(token);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
var failureReason = failureReasonSelector?.Invoke(response, responseBody) ?? GetDefaultModelLoadFailureReason(response); var failureReason = failureReasonSelector?.Invoke(response, responseBody) ?? this.GetModelLoadFailureReason(response, responseBody);
this.logger.LogError("Model loading request failed with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", response.StatusCode, response.ReasonPhrase, responseBody);
return FailedModelLoadResult(failureReason, $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{responseBody}'"); return FailedModelLoadResult(failureReason, $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{responseBody}'");
} }
@ -223,6 +232,168 @@ public abstract class BaseProvider : IProvider, ISecretId
} }
} }
protected virtual string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason) => failureReason switch
{
ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("The provider rejected the request because too many requests were sent. Please wait a moment and try again."),
_ => string.Empty,
};
protected virtual ProviderRequestFailureReason ClassifyProviderRequestFailure(HttpStatusCode statusCode, string responseBody)
{
if (statusCode is not HttpStatusCode.TooManyRequests)
return ProviderRequestFailureReason.NONE;
return ProviderRequestFailureReason.TOO_MANY_REQUESTS;
}
protected virtual ProviderRequestFailureReason ClassifyProviderRequestFailure(string? errorCode, string? errorType, string? errorMessage, string responseBody)
{
if (IsTooManyRequestsError(errorCode) || IsTooManyRequestsError(errorType) || IsTooManyRequestsError(errorMessage))
return ProviderRequestFailureReason.TOO_MANY_REQUESTS;
return ProviderRequestFailureReason.NONE;
}
private static bool IsTooManyRequestsError(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return false;
return value.Equals("rate_limit_exceeded", StringComparison.OrdinalIgnoreCase) ||
value.Equals("too_many_requests", StringComparison.OrdinalIgnoreCase) ||
value.Equals("too_many_request", StringComparison.OrdinalIgnoreCase) ||
value.Contains("too many requests", StringComparison.OrdinalIgnoreCase) ||
value.Contains("rate limit", StringComparison.OrdinalIgnoreCase) ||
value.Contains("rate_limit", StringComparison.OrdinalIgnoreCase) ||
value.Contains("throttl", StringComparison.OrdinalIgnoreCase);
}
private bool TryCreateProviderRequestExceptionFromStreamLine(string providerName, string line, out ProviderRequestException exception)
{
exception = new();
if (!line.StartsWith("data: ", StringComparison.InvariantCulture))
return false;
var jsonData = line[6..].Trim();
if (string.IsNullOrWhiteSpace(jsonData) || jsonData is "[DONE]")
return false;
try
{
using var document = JsonDocument.Parse(jsonData);
var root = document.RootElement;
if (!IsProviderStreamFailure(root))
return false;
var eventType = TryGetString(root, "type");
TryGetProviderStreamError(root, out var errorCode, out var errorType, out var errorMessage);
var failureReason = this.ClassifyProviderRequestFailure(errorCode, errorType, errorMessage, jsonData);
var userMessage = this.GetProviderRequestFailureUserMessage(failureReason);
if (string.IsNullOrWhiteSpace(userMessage))
{
userMessage = string.IsNullOrWhiteSpace(errorMessage)
? string.Format(TB("The provider '{0}' reported an error while streaming the response."), this.InstanceName)
: string.Format(TB("The provider '{0}' reported an error: {1}"), this.InstanceName, errorMessage);
}
this.logger.LogError("The {ProviderName} stream returned an error for provider '{ProviderInstanceName}' (provider={ProviderType}). EventType={StreamEventType}, ErrorCode={ErrorCode}, ErrorType={ErrorType}, ErrorMessage='{ErrorMessage}', Body='{ErrorBody}'", providerName, this.InstanceName, this.Provider, eventType, errorCode, errorType, errorMessage, jsonData);
exception = new ProviderRequestException(failureReason, userMessage, responseBody: jsonData);
return true;
}
catch (JsonException)
{
return false;
}
}
private static bool IsProviderStreamFailure(JsonElement root)
{
var eventType = TryGetString(root, "type");
if (eventType is not null && (
eventType.Equals("error", StringComparison.OrdinalIgnoreCase) ||
eventType.Equals("response.error", StringComparison.OrdinalIgnoreCase) ||
eventType.Equals("response.failed", StringComparison.OrdinalIgnoreCase)))
return true;
if (HasObjectProperty(root, "error"))
return true;
if (IsTooManyRequestsError(TryGetString(root, "code")) ||
IsTooManyRequestsError(TryGetString(root, "type")) ||
IsTooManyRequestsError(TryGetString(root, "message")))
return true;
if (TryGetString(root, "message") is not null &&
(TryGetString(root, "code") is not null || TryGetString(root, "type") is not null) &&
!root.TryGetProperty("choices", out _) &&
!root.TryGetProperty("delta", out _))
return true;
if (!root.TryGetProperty("response", out var responseElement) || responseElement.ValueKind is not JsonValueKind.Object)
return false;
if (HasObjectProperty(responseElement, "error"))
return true;
var responseStatus = TryGetString(responseElement, "status");
return responseStatus is not null && responseStatus.Equals("failed", StringComparison.OrdinalIgnoreCase);
}
private static bool HasObjectProperty(JsonElement element, string propertyName)
{
return element.ValueKind is JsonValueKind.Object &&
element.TryGetProperty(propertyName, out var propertyElement) &&
propertyElement.ValueKind is JsonValueKind.Object;
}
private static void TryGetProviderStreamError(JsonElement root, out string? errorCode, out string? errorType, out string? errorMessage)
{
errorCode = null;
errorType = null;
errorMessage = null;
if (TryGetErrorElement(root, out var errorElement))
{
errorCode = TryGetString(errorElement, "code");
errorType = TryGetString(errorElement, "type");
errorMessage = TryGetString(errorElement, "message");
return;
}
errorCode = TryGetString(root, "code");
errorType = TryGetString(root, "type");
errorMessage = TryGetString(root, "message");
}
private static bool TryGetErrorElement(JsonElement root, out JsonElement errorElement)
{
if (root.ValueKind is JsonValueKind.Object &&
root.TryGetProperty("error", out errorElement) &&
errorElement.ValueKind is JsonValueKind.Object)
return true;
if (root.ValueKind is JsonValueKind.Object &&
root.TryGetProperty("response", out var responseElement) &&
responseElement.ValueKind is JsonValueKind.Object &&
responseElement.TryGetProperty("error", out errorElement) &&
errorElement.ValueKind is JsonValueKind.Object)
return true;
errorElement = default;
return false;
}
private static string? TryGetString(JsonElement element, string propertyName)
{
if (element.ValueKind is not JsonValueKind.Object ||
!element.TryGetProperty(propertyName, out var propertyElement) ||
propertyElement.ValueKind is not JsonValueKind.String)
return null;
return propertyElement.GetString();
}
/// <summary> /// <summary>
/// Sends a request and handles rate limiting by exponential backoff. /// Sends a request and handles rate limiting by exponential backoff.
/// </summary> /// </summary>
@ -239,6 +410,10 @@ public abstract class BaseProvider : IProvider, ISecretId
var retry = 0; var retry = 0;
var response = default(HttpResponseMessage); var response = default(HttpResponseMessage);
var errorMessage = string.Empty; var errorMessage = string.Empty;
var lastProviderRequestFailure = ProviderRequestFailureReason.NONE;
HttpStatusCode? lastResponseStatusCode = null;
var lastResponseReasonPhrase = string.Empty;
var lastErrorBody = string.Empty;
while (retry++ < MAX_RETRIES) while (retry++ < MAX_RETRIES)
{ {
using var request = await requestBuilder(); using var request = await requestBuilder();
@ -266,10 +441,24 @@ public abstract class BaseProvider : IProvider, ISecretId
if (nextResponse.IsSuccessStatusCode) if (nextResponse.IsSuccessStatusCode)
{ {
response = nextResponse; response = nextResponse;
errorMessage = string.Empty;
lastProviderRequestFailure = ProviderRequestFailureReason.NONE;
break; break;
} }
var errorBody = await nextResponse.Content.ReadAsStringAsync(effectiveCancellationToken); var errorBody = await nextResponse.Content.ReadAsStringAsync(effectiveCancellationToken);
lastResponseStatusCode = nextResponse.StatusCode;
lastResponseReasonPhrase = nextResponse.ReasonPhrase ?? string.Empty;
lastErrorBody = errorBody;
var providerRequestFailure = this.ClassifyProviderRequestFailure(nextResponse.StatusCode, errorBody);
lastProviderRequestFailure = providerRequestFailure;
if (providerRequestFailure is ProviderRequestFailureReason.INSUFFICIENT_QUOTA)
{
var userMessage = this.GetProviderRequestFailureUserMessage(providerRequestFailure);
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
throw new ProviderRequestException(providerRequestFailure, userMessage, nextResponse.StatusCode, nextResponse.ReasonPhrase ?? string.Empty, errorBody);
}
if (nextResponse.StatusCode is HttpStatusCode.Forbidden) if (nextResponse.StatusCode is HttpStatusCode.Forbidden)
{ {
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Block, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). You might not be able to use this provider from your location. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase))); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Block, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). You might not be able to use this provider from your location. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
@ -340,6 +529,13 @@ public abstract class BaseProvider : IProvider, ISecretId
if(retry >= MAX_RETRIES || !string.IsNullOrWhiteSpace(errorMessage)) if(retry >= MAX_RETRIES || !string.IsNullOrWhiteSpace(errorMessage))
{ {
if (lastProviderRequestFailure is not ProviderRequestFailureReason.NONE)
{
var userMessage = this.GetProviderRequestFailureUserMessage(lastProviderRequestFailure);
this.logger.LogError("The request to provider '{ProviderInstanceName}' (provider={ProviderType}) failed after {MaxRetries} retries with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}'): {ErrorMessage}", this.InstanceName, this.Provider, MAX_RETRIES, lastResponseStatusCode, lastResponseReasonPhrase, lastErrorBody, userMessage);
throw new ProviderRequestException(lastProviderRequestFailure, userMessage, lastResponseStatusCode, lastResponseReasonPhrase, lastErrorBody);
}
await MessageBus.INSTANCE.SendError(new DataErrorMessage(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). Even after {2} retries, there were some problems with the request. The provider message is: '{3}'."), this.InstanceName, this.Provider, MAX_RETRIES, errorMessage))); await MessageBus.INSTANCE.SendError(new DataErrorMessage(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). Even after {2} retries, there were some problems with the request. The provider message is: '{3}'."), this.InstanceName, this.Provider, MAX_RETRIES, errorMessage)));
return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response); return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response);
} }
@ -380,6 +576,10 @@ public abstract class BaseProvider : IProvider, ISecretId
// Add a stream reader to read the stream, line by line: // Add a stream reader to read the stream, line by line:
streamReader = new StreamReader(providerStream); streamReader = new StreamReader(providerStream);
} }
catch(ProviderRequestException)
{
throw;
}
catch(Exception e) catch(Exception e)
{ {
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
@ -461,6 +661,9 @@ public abstract class BaseProvider : IProvider, ISecretId
if (string.IsNullOrWhiteSpace(line)) if (string.IsNullOrWhiteSpace(line))
continue; continue;
if (this.TryCreateProviderRequestExceptionFromStreamLine(providerName, line, out var providerRequestException))
throw providerRequestException;
// Skip lines that do not start with "data: ". Regard // Skip lines that do not start with "data: ". Regard
// to the specification, we only want to read the data lines: // to the specification, we only want to read the data lines:
if (!line.StartsWith("data: ", StringComparison.InvariantCulture)) if (!line.StartsWith("data: ", StringComparison.InvariantCulture))
@ -574,6 +777,10 @@ public abstract class BaseProvider : IProvider, ISecretId
// Add a stream reader to read the stream, line by line: // Add a stream reader to read the stream, line by line:
streamReader = new StreamReader(providerStream); streamReader = new StreamReader(providerStream);
} }
catch(ProviderRequestException)
{
throw;
}
catch(Exception e) catch(Exception e)
{ {
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
@ -655,6 +862,9 @@ public abstract class BaseProvider : IProvider, ISecretId
if (string.IsNullOrWhiteSpace(line)) if (string.IsNullOrWhiteSpace(line))
continue; continue;
if (this.TryCreateProviderRequestExceptionFromStreamLine(providerName, line, out var providerRequestException))
throw providerRequestException;
// Check if the line is the end of the stream: // Check if the line is the end of the stream:
if (line.StartsWith("event: response.completed", StringComparison.InvariantCulture)) if (line.StartsWith("event: response.completed", StringComparison.InvariantCulture))
yield break; yield break;
@ -869,7 +1079,8 @@ public abstract class BaseProvider : IProvider, ISecretId
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
this.logger.LogError("Transcription request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody); this.logger.LogError("Transcription request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
return TranscriptionResult.Failure(); var providerRequestFailure = this.ClassifyProviderRequestFailure(response.StatusCode, responseBody);
return TranscriptionResult.Failure(this.GetProviderRequestFailureUserMessage(providerRequestFailure));
} }
var transcriptionResponse = JsonSerializer.Deserialize<TranscriptionResponse>(responseBody, JSON_SERIALIZER_OPTIONS); var transcriptionResponse = JsonSerializer.Deserialize<TranscriptionResponse>(responseBody, JSON_SERIALIZER_OPTIONS);
@ -937,11 +1148,16 @@ public abstract class BaseProvider : IProvider, ISecretId
// Set the content: // Set the content:
request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json"); 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; var responseBody = await response.Content.ReadAsStringAsync(token);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
this.logger.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody); this.logger.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
var providerRequestFailure = this.ClassifyProviderRequestFailure(response.StatusCode, responseBody);
var userMessage = this.GetProviderRequestFailureUserMessage(providerRequestFailure);
if (!string.IsNullOrWhiteSpace(userMessage))
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, userMessage));
return []; return [];
} }

View File

@ -200,6 +200,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
{ {
System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR, System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
System.Net.HttpStatusCode.TooManyRequests => ModelLoadFailureReason.TOO_MANY_REQUESTS,
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE, _ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
}); });
} }

View File

@ -5,6 +5,8 @@ public enum ModelLoadFailureReason
NONE, NONE,
INVALID_OR_MISSING_API_KEY, INVALID_OR_MISSING_API_KEY,
AUTHENTICATION_OR_PERMISSION_ERROR, AUTHENTICATION_OR_PERMISSION_ERROR,
INSUFFICIENT_QUOTA,
TOO_MANY_REQUESTS,
PROVIDER_UNAVAILABLE, PROVIDER_UNAVAILABLE,
INVALID_RESPONSE, INVALID_RESPONSE,
UNKNOWN, UNKNOWN,

View File

@ -10,6 +10,8 @@ public static class ModelLoadFailureReasonExtensions
{ {
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.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.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.INSUFFICIENT_QUOTA => string.Format(TB("We could not load models from '{0}' because the account appears to have no API credits left."), providerName),
ModelLoadFailureReason.TOO_MANY_REQUESTS => string.Format(TB("We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again."), 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.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.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), ModelLoadFailureReason.UNKNOWN => string.Format(TB("We could not load models from '{0}' due to an unknown error."), providerName),

View File

@ -1,3 +1,4 @@
using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
@ -5,6 +6,7 @@ using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Provider.OpenAI; namespace AIStudio.Provider.OpenAI;
@ -15,6 +17,8 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
{ {
private static readonly ILogger<ProviderOpenAI> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderOpenAI>(); private static readonly ILogger<ProviderOpenAI> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderOpenAI>();
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderOpenAI).Namespace, nameof(ProviderOpenAI));
#region Implementation of IProvider #region Implementation of IProvider
/// <inheritdoc /> /// <inheritdoc />
@ -26,6 +30,28 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
/// <inheritdoc /> /// <inheritdoc />
public override bool HasModelLoadingCapability => true; public override bool HasModelLoadingCapability => true;
protected override ProviderRequestFailureReason ClassifyProviderRequestFailure(HttpStatusCode statusCode, string responseBody)
{
if (statusCode is HttpStatusCode.TooManyRequests && HasInsufficientQuotaError(responseBody))
return ProviderRequestFailureReason.INSUFFICIENT_QUOTA;
return base.ClassifyProviderRequestFailure(statusCode, responseBody);
}
protected override ProviderRequestFailureReason ClassifyProviderRequestFailure(string? errorCode, string? errorType, string? errorMessage, string responseBody)
{
if (IsInsufficientQuota(errorCode) || IsInsufficientQuota(errorType) || HasInsufficientQuotaError(responseBody))
return ProviderRequestFailureReason.INSUFFICIENT_QUOTA;
return base.ClassifyProviderRequestFailure(errorCode, errorType, errorMessage, responseBody);
}
protected override string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason) => failureReason switch
{
ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again."),
_ => base.GetProviderRequestFailureUserMessage(failureReason),
};
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{ {
@ -289,4 +315,59 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
token, token,
apiKeyProvisional); apiKeyProvisional);
} }
private static bool HasInsufficientQuotaError(string responseBody)
{
if (string.IsNullOrWhiteSpace(responseBody))
return false;
try
{
using var document = JsonDocument.Parse(responseBody);
return HasInsufficientQuotaError(document.RootElement);
}
catch (JsonException)
{
return false;
}
}
private static bool HasInsufficientQuotaError(JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
if (HasJsonStringValue(element, "type", "insufficient_quota") ||
HasJsonStringValue(element, "code", "insufficient_quota"))
return true;
foreach (var property in element.EnumerateObject())
if (HasInsufficientQuotaError(property.Value))
return true;
return false;
case JsonValueKind.Array:
foreach (var item in element.EnumerateArray())
if (HasInsufficientQuotaError(item))
return true;
return false;
default:
return false;
}
}
private static bool IsInsufficientQuota(string? value)
{
return value is not null && value.Equals("insufficient_quota", StringComparison.OrdinalIgnoreCase);
}
private static bool HasJsonStringValue(JsonElement element, string propertyName, string expectedValue)
{
return element.TryGetProperty(propertyName, out var propertyElement) &&
propertyElement.ValueKind is JsonValueKind.String &&
string.Equals(propertyElement.GetString(), expectedValue, StringComparison.OrdinalIgnoreCase);
}
} }

View File

@ -0,0 +1,25 @@
using System.Net;
namespace AIStudio.Provider;
public sealed class ProviderRequestException(
ProviderRequestFailureReason failureReason,
string userMessage,
HttpStatusCode? statusCode = null,
string reasonPhrase = "",
string responseBody = "") : Exception(userMessage)
{
public ProviderRequestException() : this(ProviderRequestFailureReason.NONE, string.Empty)
{
}
public ProviderRequestFailureReason FailureReason { get; } = failureReason;
public string UserMessage { get; } = userMessage;
public HttpStatusCode? StatusCode { get; } = statusCode;
public string ReasonPhrase { get; } = reasonPhrase;
public string ResponseBody { get; } = responseBody;
}

View File

@ -0,0 +1,8 @@
namespace AIStudio.Provider;
public enum ProviderRequestFailureReason
{
NONE,
INSUFFICIENT_QUOTA,
TOO_MANY_REQUESTS,
}

View File

@ -181,7 +181,11 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
using var lmStudioResponse = await this.HttpClient.SendAsync(lmStudioRequest, token); using var lmStudioResponse = await this.HttpClient.SendAsync(lmStudioRequest, token);
if(!lmStudioResponse.IsSuccessStatusCode) if(!lmStudioResponse.IsSuccessStatusCode)
return FailedModelLoadResult(GetDefaultModelLoadFailureReason(lmStudioResponse), $"Status={(int)lmStudioResponse.StatusCode} {lmStudioResponse.ReasonPhrase}"); {
var responseBody = await lmStudioResponse.Content.ReadAsStringAsync(token);
LOGGER.LogError("Model loading request failed with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", lmStudioResponse.StatusCode, lmStudioResponse.ReasonPhrase, responseBody);
return FailedModelLoadResult(this.GetModelLoadFailureReason(lmStudioResponse, responseBody), $"Status={(int)lmStudioResponse.StatusCode} {lmStudioResponse.ReasonPhrase}; Body='{responseBody}'");
}
var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync<ModelsResponse>(token); var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync<ModelsResponse>(token);
return SuccessfulModelLoadResult(lmStudioModelResponse.Data. return SuccessfulModelLoadResult(lmStudioModelResponse.Data.

View File

@ -1,8 +1,8 @@
namespace AIStudio.Provider; namespace AIStudio.Provider;
public sealed record TranscriptionResult(bool Success, string Text) public sealed record TranscriptionResult(bool Success, string Text, string ErrorMessage = "")
{ {
public static TranscriptionResult FromText(string text) => new(true, text); public static TranscriptionResult FromText(string text) => new(true, text);
public static TranscriptionResult Failure() => new(false, string.Empty); public static TranscriptionResult Failure(string errorMessage = "") => new(false, string.Empty, errorMessage);
} }

View File

@ -238,6 +238,13 @@ public sealed class AIJobService(
{ {
await this.CompleteChatGenerationAsync(state, AIJobStatus.CANCELED); await this.CompleteChatGenerationAsync(state, AIJobStatus.CANCELED);
} }
catch (ProviderRequestException e)
{
logger.LogError(e, "The provider request failed for chat generation job '{JobId}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", state.Snapshot.JobId, e.StatusCode, e.ReasonPhrase, e.ResponseBody);
RemoveEmptyAIResponse(state);
await this.CompleteChatGenerationAsync(state, AIJobStatus.FAILED, e.UserMessage);
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
}
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, "The chat generation job '{JobId}' failed.", state.Snapshot.JobId); logger.LogError(e, "The chat generation job '{JobId}' failed.", state.Snapshot.JobId);
@ -270,6 +277,19 @@ public sealed class AIJobService(
state.CancellationTokenSource.Dispose(); state.CancellationTokenSource.Dispose();
} }
private static void RemoveEmptyAIResponse(AIJobState state)
{
var aiText = state.ChatGenerationRequest.AIText;
if (!string.IsNullOrWhiteSpace(aiText.Text))
return;
var aiBlock = state.ChatGenerationRequest.ChatThread.Blocks
.LastOrDefault(block => ReferenceEquals(block.Content, aiText));
if (aiBlock is not null)
state.ChatGenerationRequest.ChatThread.Blocks.Remove(aiBlock);
}
private static void UpdateStatus(AIJobState state, AIJobStatus status) private static void UpdateStatus(AIJobState state, AIJobStatus status)
{ {
lock (state.SyncRoot) lock (state.SyncRoot)

View File

@ -15,6 +15,7 @@
- Fixed an issue where attached documents were detached when editing a previous prompt. They now remain attached. - Fixed an issue where attached documents were detached when editing a previous prompt. They now remain attached.
- Fixed an issue where failed transcription requests could be shown as empty transcription results instead of a clear error message. - Fixed an issue where failed transcription requests could be shown as empty transcription results instead of a clear error message.
- Fixed an issue where an AI response in chat could be interrupted when you interacted with workspaces, such as opening, closing, or resizing the workspace panel. - Fixed an issue where an AI response in chat could be interrupted when you interacted with workspaces, such as opening, closing, or resizing the workspace panel.
- Fixed error messages for provider requests so missing OpenAI API credits and too many requests are shown clearly in chats, assistants, transcription, and model loading.
- Fixed missing translations for file type names in file selection dialogs. - Fixed missing translations for file type names in file selection dialogs.
- Upgraded the native secret storage integration to `keyring-core`, keeping API keys in the secure credential store provided by the operating system. - Upgraded the native secret storage integration to `keyring-core`, keeping API keys in the secure credential store provided by the operating system.
- Upgraded Rust to v1.95.0. - Upgraded Rust to v1.95.0.