mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 03:33:37 +00:00
Classify embedding failures at the provider level
This commit is contained in:
parent
9875fbc646
commit
4e67504bca
@ -215,7 +215,21 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
|
||||
return;
|
||||
|
||||
var embeddingProvider = provider.CreateProvider();
|
||||
var embeddings = await embeddingProvider.EmbedTextAsync(provider.Model, this.SettingsManager, CancellationToken.None, inputText);
|
||||
IReadOnlyList<IReadOnlyList<float>> embeddings;
|
||||
try
|
||||
{
|
||||
embeddings = await embeddingProvider.EmbedTextAsync(provider.Model, this.SettingsManager, CancellationToken.None, inputText);
|
||||
}
|
||||
catch (ProviderRequestException exception)
|
||||
{
|
||||
//
|
||||
// The provider named what went wrong and what to do about it. Showing that beats the
|
||||
// sentence below, which used to be the same one for a missing API key, an unreachable
|
||||
// provider and a provider which cannot embed anything at all:
|
||||
//
|
||||
await this.DialogService.ShowMessageBox(T("Embedding Result"), exception.UserMessage, T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (embeddings.Count == 0)
|
||||
{
|
||||
|
||||
@ -194,7 +194,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
|
||||
/// <inhertidoc />
|
||||
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||
throw this.CreateEmbeddingsNotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -238,9 +238,106 @@ 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."),
|
||||
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),
|
||||
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.EMBEDDINGS_NOT_SUPPORTED => string.Format(TB("The provider '{0}' cannot create embeddings. Please select a provider which offers an embedding model."), this.InstanceName),
|
||||
ProviderRequestFailureReason.INVALID_RESPONSE => string.Format(TB("The provider '{0}' sent an answer AI Studio was not able to read."), this.InstanceName),
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Builds the failure a provider reports when it offers no embeddings at all.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Such a provider used to answer with an empty list, which the caller was not able to tell
|
||||
/// apart from a provider which simply produced nothing this time. Saying it outright is what
|
||||
/// lets the user go and pick a provider which can do the job.
|
||||
/// </remarks>
|
||||
protected ProviderRequestException CreateEmbeddingsNotSupportedException() => new(ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED,
|
||||
this.GetProviderRequestFailureUserMessage(ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED));
|
||||
|
||||
/// <summary>
|
||||
/// Builds the failure of an embedding request the provider answered with an error.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
protected ProviderRequestException CreateEmbeddingRequestException(HttpStatusCode statusCode, string reasonPhrase, string responseBody)
|
||||
{
|
||||
var failureReason = this.ClassifyEmbeddingRequestFailure(statusCode, responseBody);
|
||||
var userMessage = this.GetProviderRequestFailureUserMessage(failureReason);
|
||||
|
||||
// We know nothing about this failure, so we pass on what the provider said about it:
|
||||
if (string.IsNullOrWhiteSpace(userMessage))
|
||||
{
|
||||
var providerMessage = ReadProviderErrorMessage(responseBody);
|
||||
userMessage = string.IsNullOrWhiteSpace(providerMessage)
|
||||
? string.Format(TB("The provider '{0}' rejected the embedding request with the status code {1}."), this.InstanceName, (int)statusCode)
|
||||
: string.Format(TB("The provider '{0}' reported an error: {1}"), this.InstanceName, providerMessage);
|
||||
}
|
||||
|
||||
return new(failureReason, userMessage, statusCode, reasonPhrase, responseBody);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the failure of an embedding request which did not get an answer at all.
|
||||
/// </summary>
|
||||
/// <param name="exception">What went wrong while the request was on its way.</param>
|
||||
/// <param name="isTimeout">Whether the provider took longer than we were willing to wait.</param>
|
||||
protected ProviderRequestException CreateEmbeddingRequestException(Exception exception, bool isTimeout)
|
||||
{
|
||||
if (isTimeout)
|
||||
return new(ProviderRequestFailureReason.PROVIDER_UNAVAILABLE, this.GetProviderRequestFailureUserMessage(ProviderRequestFailureReason.PROVIDER_UNAVAILABLE), responseBody: exception.Message);
|
||||
|
||||
return new(ProviderRequestFailureReason.UNKNOWN, string.Format(TB("The embedding request to the provider '{0}' failed: {1}"), this.InstanceName, exception.Message), responseBody: exception.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classifies why an embedding request failed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Kept apart from the chat classification on purpose. The chat path turns most failures into
|
||||
/// a message and carries on, so classifying more cases there would change what every user
|
||||
/// sees. The embedding path has no such fallback: it either produces vectors or it fails, and
|
||||
/// then the caller has to be able to say why.
|
||||
/// </remarks>
|
||||
private ProviderRequestFailureReason ClassifyEmbeddingRequestFailure(HttpStatusCode statusCode, string responseBody)
|
||||
{
|
||||
//
|
||||
// Whatever the shared classification recognizes wins: it knows what a provider says about
|
||||
// quota and rate limits, and several providers refine it for their own error format.
|
||||
//
|
||||
var sharedFailureReason = this.ClassifyProviderRequestFailure(statusCode, responseBody);
|
||||
if (sharedFailureReason is not ProviderRequestFailureReason.NONE)
|
||||
return sharedFailureReason;
|
||||
|
||||
return statusCode switch
|
||||
{
|
||||
HttpStatusCode.Unauthorized => ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY,
|
||||
HttpStatusCode.Forbidden => ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
HttpStatusCode.NotFound => ProviderRequestFailureReason.MODEL_NOT_FOUND,
|
||||
HttpStatusCode.RequestEntityTooLarge => ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED,
|
||||
HttpStatusCode.BadRequest when IsContextLengthFailure(responseBody) => ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED,
|
||||
HttpStatusCode.RequestTimeout or HttpStatusCode.InternalServerError or HttpStatusCode.BadGateway or HttpStatusCode.ServiceUnavailable or HttpStatusCode.GatewayTimeout => ProviderRequestFailureReason.PROVIDER_UNAVAILABLE,
|
||||
_ => ProviderRequestFailureReason.UNKNOWN,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recognizes the answer a provider gives when the text was longer than the model accepts.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There is no common error code for this. What the answers have in common is that they talk
|
||||
/// about the context and about tokens, which is the same hint the chat path goes by.
|
||||
/// </remarks>
|
||||
private static bool IsContextLengthFailure(string responseBody) =>
|
||||
responseBody.Contains("context", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
responseBody.Contains("token", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
protected virtual ProviderRequestFailureReason ClassifyProviderRequestFailure(HttpStatusCode statusCode, string responseBody)
|
||||
{
|
||||
if (statusCode is not HttpStatusCode.TooManyRequests)
|
||||
@ -401,7 +498,14 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
/// </remarks>
|
||||
/// <param name="responseBody">The body of the failed response.</param>
|
||||
/// <returns>The message, or an empty string when the body carries none.</returns>
|
||||
private static string ReadProviderErrorMessage(string responseBody)
|
||||
/// <summary>
|
||||
/// Reads what the provider itself said about a failure out of its error response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Available to the providers because some of them talk to an endpoint of their own rather
|
||||
/// than through the shared request methods, and their users deserve the same explanation.
|
||||
/// </remarks>
|
||||
protected static string ReadProviderErrorMessage(string responseBody)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(responseBody))
|
||||
return string.Empty;
|
||||
@ -1339,9 +1443,9 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
if(!requestedSecret.Success)
|
||||
{
|
||||
this.logger.LogError("No valid API key available for embedding request.");
|
||||
return [];
|
||||
throw new ProviderRequestException(ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY, this.GetProviderRequestFailureUserMessage(ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY));
|
||||
}
|
||||
|
||||
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION));
|
||||
break;
|
||||
}
|
||||
@ -1354,21 +1458,13 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
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);
|
||||
|
||||
// We know nothing about this failure, so we pass on what the provider said about it:
|
||||
if (string.IsNullOrWhiteSpace(userMessage))
|
||||
{
|
||||
var providerMessage = ReadProviderErrorMessage(responseBody);
|
||||
if (!string.IsNullOrWhiteSpace(providerMessage))
|
||||
userMessage = string.Format(TB("The provider '{0}' reported an error: {1}"), this.InstanceName, providerMessage);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(userMessage))
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, userMessage));
|
||||
|
||||
return [];
|
||||
//
|
||||
// Thrown instead of shown: the caller knows whether this is one file out of
|
||||
// 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);
|
||||
}
|
||||
|
||||
var embeddingResponse = JsonSerializer.Deserialize<EmbeddingResponse>(responseBody, JSON_SERIALIZER_OPTIONS);
|
||||
@ -1382,16 +1478,32 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
else
|
||||
{
|
||||
this.logger.LogError("Was not able to deserialize the embedding response.");
|
||||
return [];
|
||||
throw new ProviderRequestException(ProviderRequestFailureReason.INVALID_RESPONSE, this.GetProviderRequestFailureUserMessage(ProviderRequestFailureReason.INVALID_RESPONSE));
|
||||
}
|
||||
}
|
||||
catch (ProviderRequestException)
|
||||
{
|
||||
// Already classified and carrying its user message. Wrapping it again would only
|
||||
// replace what we know with the fact that something went wrong:
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
//
|
||||
// The caller stopped the work, e.g. because the user removed the data source while it
|
||||
// was being indexed. That is not a failure of the provider and must not be recorded
|
||||
// as one:
|
||||
//
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.IsTimeoutException(e, token))
|
||||
var isTimeout = this.IsTimeoutException(e, token);
|
||||
if (isTimeout)
|
||||
await this.SendTimeoutError("creating embeddings");
|
||||
|
||||
this.logger.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
|
||||
return [];
|
||||
throw this.CreateEmbeddingRequestException(e, isTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -69,7 +69,7 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne
|
||||
/// <inhertidoc />
|
||||
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||
throw this.CreateEmbeddingsNotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -71,7 +71,7 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri(
|
||||
/// <inhertidoc />
|
||||
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||
throw this.CreateEmbeddingsNotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -79,16 +79,16 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
|
||||
if (string.IsNullOrWhiteSpace(modelName))
|
||||
{
|
||||
LOGGER.LogError("No model name provided for embedding request.");
|
||||
return [];
|
||||
throw new ProviderRequestException(ProviderRequestFailureReason.MODEL_NOT_FOUND, this.GetProviderRequestFailureUserMessage(ProviderRequestFailureReason.MODEL_NOT_FOUND));
|
||||
}
|
||||
|
||||
if (modelName.StartsWith("models/", StringComparison.OrdinalIgnoreCase))
|
||||
modelName = modelName.Substring("models/".Length);
|
||||
modelName = modelName["models/".Length..];
|
||||
|
||||
if (!requestedSecret.Success)
|
||||
{
|
||||
LOGGER.LogError("No valid API key available for embedding request.");
|
||||
return [];
|
||||
throw new ProviderRequestException(ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY, this.GetProviderRequestFailureUserMessage(ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY));
|
||||
}
|
||||
|
||||
// Prepare the Google Gemini embedding request:
|
||||
@ -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);
|
||||
return [];
|
||||
throw this.CreateEmbeddingRequestException(response.StatusCode, response.ReasonPhrase ?? string.Empty, responseBody);
|
||||
}
|
||||
|
||||
var embeddingResponse = JsonSerializer.Deserialize<GoogleEmbeddingResponse>(responseBody, JSON_SERIALIZER_OPTIONS);
|
||||
@ -130,17 +130,33 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
|
||||
else
|
||||
{
|
||||
LOGGER.LogError("Was not able to deserialize the embedding response.");
|
||||
return [];
|
||||
throw new ProviderRequestException(ProviderRequestFailureReason.INVALID_RESPONSE, this.GetProviderRequestFailureUserMessage(ProviderRequestFailureReason.INVALID_RESPONSE));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
catch (ProviderRequestException)
|
||||
{
|
||||
// Already classified and carrying its user message. Wrapping it again would only
|
||||
// replace what we know with the fact that something went wrong:
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
//
|
||||
// The caller stopped the work, e.g. because the user removed the data source while it
|
||||
// was being indexed. That is not a failure of the provider and must not be recorded
|
||||
// as one:
|
||||
//
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.IsTimeoutException(e, token))
|
||||
var isTimeout = this.IsTimeoutException(e, token);
|
||||
if (isTimeout)
|
||||
await this.SendTimeoutError("creating embeddings");
|
||||
|
||||
LOGGER.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
|
||||
return [];
|
||||
throw this.CreateEmbeddingRequestException(e, isTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a
|
||||
/// <inhertidoc />
|
||||
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||
throw this.CreateEmbeddingsNotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -63,7 +63,7 @@ public sealed class ProviderHetzner() : BaseProvider(LLMProviders.HETZNER, new U
|
||||
/// <inheritdoc />
|
||||
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||
throw this.CreateEmbeddingsNotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -77,7 +77,7 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
|
||||
/// <inhertidoc />
|
||||
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||
throw this.CreateEmbeddingsNotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -14,4 +14,56 @@ public enum ProviderRequestFailureReason
|
||||
/// meant to answer for it does not offer it.
|
||||
/// </remarks>
|
||||
MODEL_NOT_SUPPORTED_BY_PROVIDER,
|
||||
|
||||
/// <summary>
|
||||
/// No usable API key was available, or the provider rejected the one we sent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Both cases lead to the same place for the user: the key stored for this provider is not
|
||||
/// one the provider works with, and the settings are where they fix it.
|
||||
/// </remarks>
|
||||
INVALID_OR_MISSING_API_KEY,
|
||||
|
||||
/// <summary>
|
||||
/// The key was accepted, but the account is not allowed to do what we asked for.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Typical causes are a key without the required scope, a model the account has no access
|
||||
/// to, and providers which refuse requests from the user's region.
|
||||
/// </remarks>
|
||||
AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
|
||||
/// <summary>
|
||||
/// The provider could not be reached, or said that it cannot serve requests right now.
|
||||
/// </summary>
|
||||
PROVIDER_UNAVAILABLE,
|
||||
|
||||
/// <summary>
|
||||
/// The provider does not know the requested model at all.
|
||||
/// </summary>
|
||||
MODEL_NOT_FOUND,
|
||||
|
||||
/// <summary>
|
||||
/// The text we sent was longer than the model accepts.
|
||||
/// </summary>
|
||||
CONTEXT_LENGTH_EXCEEDED,
|
||||
|
||||
/// <summary>
|
||||
/// The provider cannot create embeddings at all.
|
||||
/// </summary>
|
||||
EMBEDDINGS_NOT_SUPPORTED,
|
||||
|
||||
/// <summary>
|
||||
/// The provider answered successfully, but with something we were not able to read.
|
||||
/// </summary>
|
||||
INVALID_RESPONSE,
|
||||
|
||||
/// <summary>
|
||||
/// The request failed and we were not able to tell why.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deliberately without a user message of its own: what the provider itself said about the
|
||||
/// failure tells the user more than a sentence which says nothing.
|
||||
/// </remarks>
|
||||
UNKNOWN,
|
||||
}
|
||||
@ -70,7 +70,7 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://
|
||||
/// <inhertidoc />
|
||||
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||
throw this.CreateEmbeddingsNotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user