Name the model's window when a text was too long

This commit is contained in:
Thorsten Sommer 2026-09-13 13:55:32 +02:00
parent 9c3fcbf815
commit 142f4807bf
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
6 changed files with 68 additions and 12 deletions

View File

@ -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. -- 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." 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} -- The provider '{0}' reported an error: {1}
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T700894460"] = "The provider '{0}' reported an error: {1}" UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T700894460"] = "The provider '{0}' reported an error: {1}"

View File

@ -6,6 +6,7 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Models;
using AIStudio.Models.Live; using AIStudio.Models.Live;
using AIStudio.Provider.Anthropic; using AIStudio.Provider.Anthropic;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -247,13 +248,34 @@ public abstract class BaseProvider : IProvider, ISecretId
} }
} }
protected virtual string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason) => failureReason switch /// <summary>
/// Says what a failed request means for the user.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="failureReason">Why the request failed.</param>
/// <param name="contextWindow">What the model reads, where that is known.</param>
/// <returns>The message to show, or an empty string when we have nothing to say.</returns>
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.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.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.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.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), 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.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.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), 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 /// 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. /// needs to know does not depend on which route the request took.
/// </remarks> /// </remarks>
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 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: // We know nothing about this failure, so we pass on what the provider said about it:
if (string.IsNullOrWhiteSpace(userMessage)) 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 // 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. // 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<EmbeddingResponse>(responseBody, JSON_SERIALIZER_OPTIONS); var embeddingResponse = JsonSerializer.Deserialize<EmbeddingResponse>(responseBody, JSON_SERIALIZER_OPTIONS);

View File

@ -116,7 +116,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
LOGGER.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody); 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<GoogleEmbeddingResponse>(responseBody, JSON_SERIALIZER_OPTIONS); var embeddingResponse = JsonSerializer.Deserialize<GoogleEmbeddingResponse>(responseBody, JSON_SERIALIZER_OPTIONS);

View File

@ -2,6 +2,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Models;
using AIStudio.Models.Live; using AIStudio.Models.Live;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
using AIStudio.Settings; using AIStudio.Settings;
@ -128,10 +129,10 @@ public sealed class ProviderHuggingFace : BaseProvider
} }
/// <inheritdoc /> /// <inheritdoc />
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) 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 // When Hugging Face chose the provider itself, naming it back to the user would help

View File

@ -5,6 +5,7 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Models;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust; 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); 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."), 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),
}; };
/// <inheritdoc /> /// <inheritdoc />

View File

@ -1,3 +1,5 @@
using System.Globalization;
namespace AIStudio.Tools.PluginSystem; namespace AIStudio.Tools.PluginSystem;
public class I18N : ILang public class I18N : ILang
@ -11,7 +13,26 @@ public class I18N : ILang
{ {
} }
public static void Init(ILanguagePlugin language) => I.language = language; /// <summary>
/// How the language in use writes its numbers, or the invariant culture while none is loaded.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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 #region Implementation of ILang