diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index ad8f245d..016bb778 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -10024,6 +10024,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T4049517041"] = "We tried to
-- The provider '{0}' does not know the selected model. Please select another model.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T411393889"] = "The provider '{0}' does not know the selected model. Please select another model."
+-- The text was longer than the selected model accepts, which is {0} tokens. Please select a model which takes longer texts, or reduce the chunk size of the data source.
+UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T479223640"] = "The text was longer than the selected model accepts, which is {0} tokens. Please select a model which takes longer texts, or reduce the chunk size of the data source."
+
-- The provider '{0}' reported an error: {1}
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T700894460"] = "The provider '{0}' reported an error: {1}"
diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs
index 58166675..2c15830c 100644
--- a/app/MindWork AI Studio/Provider/BaseProvider.cs
+++ b/app/MindWork AI Studio/Provider/BaseProvider.cs
@@ -6,6 +6,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using AIStudio.Chat;
+using AIStudio.Models;
using AIStudio.Models.Live;
using AIStudio.Provider.Anthropic;
using AIStudio.Provider.OpenAI;
@@ -247,13 +248,34 @@ public abstract class BaseProvider : IProvider, ISecretId
}
}
- protected virtual string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason) => failureReason switch
+ ///
+ /// Says what a failed request means for the user.
+ ///
+ ///
+ /// The window is only ever known where the caller knows which model the request was for, which
+ /// is why it is optional rather than a second required argument: most failures say nothing
+ /// about a length and need no number to explain themselves.
+ ///
+ /// Why the request failed.
+ /// What the model reads, where that is known.
+ /// The message to show, or an empty string when we have nothing to say.
+ protected virtual string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason, ContextWindow contextWindow = default) => 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."),
ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY => string.Format(TB("The API key for the provider '{0}' is missing or was rejected. Please check the key in the settings."), this.InstanceName),
ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => string.Format(TB("The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region."), this.InstanceName),
ProviderRequestFailureReason.PROVIDER_UNAVAILABLE => string.Format(TB("The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again."), this.InstanceName),
ProviderRequestFailureReason.MODEL_NOT_FOUND => string.Format(TB("The provider '{0}' does not know the selected model. Please select another model."), this.InstanceName),
+ //
+ // Naming the number is the whole point of knowing it: "too long" leaves the user guessing
+ // by how much, while the window turns the next step into arithmetic. Where nobody knows the
+ // window, no number is invented -- the sentence below says the same thing without one.
+ //
+ // Written out in full rather than shortened the way the chat shortens it. The sentence ends
+ // by asking the user to set a chunk size, and 32.77k is not a number anybody types into a
+ // field.
+ //
+ ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED when contextWindow.IsKnown => string.Format(TB("The text was longer than the selected model accepts, which is {0} tokens. Please select a model which takes longer texts, or reduce the chunk size of the data source."), contextWindow.DefaultTokens.ToString("N0", I18N.I.Culture)),
ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED => TB("The text was longer than the selected model accepts. Please select a model which takes longer texts, or reduce the chunk size of the data source."),
ProviderRequestFailureReason.TOOLS_NOT_SUPPORTED => string.Format(TB("The selected model is not able to use tools. Please select a model which can, or open the settings of the provider '{0}', show its expert settings, and switch the function calling capability off there."), this.InstanceName),
ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED => string.Format(TB("The provider '{0}' cannot create embeddings. Please select a provider which offers an embedding model."), this.InstanceName),
@@ -279,10 +301,18 @@ public abstract class BaseProvider : IProvider, ISecretId
/// Shared with the providers which talk to an embedding endpoint of their own: what the user
/// needs to know does not depend on which route the request took.
///
- protected ProviderRequestException CreateEmbeddingRequestException(HttpStatusCode statusCode, string reasonPhrase, string responseBody)
+ protected ProviderRequestException CreateEmbeddingRequestException(HttpStatusCode statusCode, string reasonPhrase, string responseBody, Model embeddingModel)
{
+ //
+ // What the rules know about this model, corrected by whatever this installation reported
+ // about it. That is the same walk a configured chat provider takes, minus the expert
+ // settings: an embedding provider has none, so there is nothing above the two to ask.
+ //
+ var stated = this.Provider.GetModelProfile(embeddingModel);
+ var contextWindow = ListedModels.Shared.Of(this.ConfiguredProviderId, embeddingModel.Id).ApplyTo(stated).Context;
+
var failureReason = this.ClassifyEmbeddingRequestFailure(statusCode, responseBody);
- var userMessage = this.GetProviderRequestFailureUserMessage(failureReason);
+ var userMessage = this.GetProviderRequestFailureUserMessage(failureReason, contextWindow);
// We know nothing about this failure, so we pass on what the provider said about it:
if (string.IsNullOrWhiteSpace(userMessage))
@@ -1551,7 +1581,7 @@ public abstract class BaseProvider : IProvider, ISecretId
// thousands being indexed in the background or the one thing the user just asked
// for, and only it can decide how often the user should hear about it.
//
- throw this.CreateEmbeddingRequestException(response.StatusCode, response.ReasonPhrase ?? string.Empty, responseBody);
+ throw this.CreateEmbeddingRequestException(response.StatusCode, response.ReasonPhrase ?? string.Empty, responseBody, embeddingModel);
}
var embeddingResponse = JsonSerializer.Deserialize(responseBody, JSON_SERIALIZER_OPTIONS);
diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs
index 17363e1d..af4486f3 100644
--- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs
+++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs
@@ -116,7 +116,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
if (!response.IsSuccessStatusCode)
{
LOGGER.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
- throw this.CreateEmbeddingRequestException(response.StatusCode, response.ReasonPhrase ?? string.Empty, responseBody);
+ throw this.CreateEmbeddingRequestException(response.StatusCode, response.ReasonPhrase ?? string.Empty, responseBody, embeddingModel);
}
var embeddingResponse = JsonSerializer.Deserialize(responseBody, JSON_SERIALIZER_OPTIONS);
diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs
index debd89e9..2a225ae8 100644
--- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs
+++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs
@@ -2,6 +2,7 @@
using System.Runtime.CompilerServices;
using AIStudio.Chat;
+using AIStudio.Models;
using AIStudio.Models.Live;
using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
@@ -128,10 +129,10 @@ public sealed class ProviderHuggingFace : BaseProvider
}
///
- protected override string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason)
+ protected override string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason, ContextWindow contextWindow = default)
{
if (failureReason is not ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER)
- return base.GetProviderRequestFailureUserMessage(failureReason);
+ return base.GetProviderRequestFailureUserMessage(failureReason, contextWindow);
//
// When Hugging Face chose the provider itself, naming it back to the user would help
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs
index faa40eb6..7de4e08f 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs
@@ -5,6 +5,7 @@ using System.Text;
using System.Text.Json;
using AIStudio.Chat;
+using AIStudio.Models;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
@@ -48,10 +49,10 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
return base.ClassifyProviderRequestFailure(errorCode, errorType, errorMessage, responseBody);
}
- protected override string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason) => failureReason switch
+ protected override string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason, ContextWindow contextWindow = default) => 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),
+ _ => base.GetProviderRequestFailureUserMessage(failureReason, contextWindow),
};
///
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/I18N.cs b/app/MindWork AI Studio/Tools/PluginSystem/I18N.cs
index 134c9587..cb113149 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/I18N.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/I18N.cs
@@ -1,17 +1,38 @@
+using System.Globalization;
+
namespace AIStudio.Tools.PluginSystem;
public class I18N : ILang
{
public static readonly I18N I = new();
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger();
-
+
private ILanguagePlugin? language;
-
+
private I18N()
{
}
- public static void Init(ILanguagePlugin language) => I.language = language;
+ ///
+ /// How the language in use writes its numbers, or the invariant culture while none is loaded.
+ ///
+ ///
+ /// A number standing inside a translated sentence has to be written the way that language
+ /// writes numbers. AI Studio's language is chosen in its own settings and never moves the
+ /// thread's culture along with it, so a number formatted from the thread comes out with English
+ /// separators inside a German sentence. It lives here because it is the same decision as the
+ /// texts: whoever picked the language picked how its numbers look.
+ ///
+ /// Components which already hold the active plugin may keep deriving it themselves. This is for
+ /// the code which has no plugin to ask -- a provider building an error message, say.
+ ///
+ public CultureInfo Culture { get; private set; } = CultureInfo.InvariantCulture;
+
+ public static void Init(ILanguagePlugin language)
+ {
+ I.language = language;
+ I.Culture = CommonTools.DeriveActiveCultureOrInvariant(language.IETFTag);
+ }
#region Implementation of ILang