Moved all extension methods into extension blocks (#1155)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-04 10:46:18 +01:00
1 parent acc7d09fa2
commit c0a75f39ac
138 files changed
+5884 -4954

No files matched your search

@@ -7,50 +7,53 @@ public static class ConfidenceLevelExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfidenceLevelExtensions).Namespace, nameof(ConfidenceLevelExtensions));
public static string GetName(this ConfidenceLevel level) => level switch
extension(ConfidenceLevel level)
{
ConfidenceLevel.NONE => TB("No provider selected"),
public string GetName() => level switch
{
ConfidenceLevel.NONE => TB("No provider selected"),
ConfidenceLevel.UNTRUSTED => TB("Untrusted"),
ConfidenceLevel.UNKNOWN => TB("Unknown"),
ConfidenceLevel.VERY_LOW => TB("Very Low"),
ConfidenceLevel.LOW => TB("Low"),
ConfidenceLevel.MODERATE => TB("Moderate"),
ConfidenceLevel.MEDIUM => TB("Medium"),
ConfidenceLevel.HIGH => TB("High"),
ConfidenceLevel.UNTRUSTED => TB("Untrusted"),
ConfidenceLevel.UNKNOWN => TB("Unknown"),
ConfidenceLevel.VERY_LOW => TB("Very Low"),
ConfidenceLevel.LOW => TB("Low"),
ConfidenceLevel.MODERATE => TB("Moderate"),
ConfidenceLevel.MEDIUM => TB("Medium"),
ConfidenceLevel.HIGH => TB("High"),
_ => TB("Unknown confidence level"),
};
public static string GetColor(this ConfidenceLevel level, SettingsManager settingsManager) => (level, settingsManager.IsDarkMode) switch
{
(ConfidenceLevel.NONE, false) => "#cccccc",
(ConfidenceLevel.NONE, true) => "#666666",
_ => TB("Unknown confidence level"),
};
(ConfidenceLevel.UNKNOWN, false) => "#777777",
(ConfidenceLevel.UNKNOWN, true) => "#aaaaaa",
public string GetColor(SettingsManager settingsManager) => (level, settingsManager.IsDarkMode) switch
{
(ConfidenceLevel.NONE, false) => "#cccccc",
(ConfidenceLevel.NONE, true) => "#666666",
(ConfidenceLevel.UNKNOWN, false) => "#777777",
(ConfidenceLevel.UNKNOWN, true) => "#aaaaaa",
(ConfidenceLevel.UNTRUSTED, false) => "#ff0000",
(ConfidenceLevel.UNTRUSTED, true) => "#800000",
(ConfidenceLevel.UNTRUSTED, false) => "#ff0000",
(ConfidenceLevel.UNTRUSTED, true) => "#800000",
(ConfidenceLevel.VERY_LOW, false) => "#ff6600",
(ConfidenceLevel.VERY_LOW, true) => "#803300",
(ConfidenceLevel.VERY_LOW, false) => "#ff6600",
(ConfidenceLevel.VERY_LOW, true) => "#803300",
(ConfidenceLevel.LOW, false) => "#ffcc00",
(ConfidenceLevel.LOW, true) => "#806600",
(ConfidenceLevel.LOW, false) => "#ffcc00",
(ConfidenceLevel.LOW, true) => "#806600",
(ConfidenceLevel.MODERATE, false) => "#99cc00",
(ConfidenceLevel.MODERATE, true) => "#4d6600",
(ConfidenceLevel.MODERATE, false) => "#99cc00",
(ConfidenceLevel.MODERATE, true) => "#4d6600",
(ConfidenceLevel.MEDIUM, false) => "#86b300",
(ConfidenceLevel.MEDIUM, true) => "#394d00",
(ConfidenceLevel.MEDIUM, false) => "#86b300",
(ConfidenceLevel.MEDIUM, true) => "#394d00",
(ConfidenceLevel.HIGH, false) => "#009933",
(ConfidenceLevel.HIGH, true) => "#004d1a",
(ConfidenceLevel.HIGH, false) => "#009933",
(ConfidenceLevel.HIGH, true) => "#004d1a",
(_, false) => "#cc6600",
(_, true) => "#663300",
};
public static string SetColorStyle(this ConfidenceLevel level, SettingsManager settingsManager) => $"--confidence-color: {level.GetColor(settingsManager)};";
(_, false) => "#cc6600",
(_, true) => "#663300",
};
public string SetColorStyle(SettingsManager settingsManager) => $"--confidence-color: {level.GetColor(settingsManager)};";
}
}
@@ -6,153 +6,153 @@ public static class HFInferenceProviderExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(HFInferenceProviderExtensions).Namespace, nameof(HFInferenceProviderExtensions));
/// <summary>
/// The slug Hugging Face uses for this inference provider.
/// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>The slug, or an empty string for the routing strategies, which name no provider.</returns>
public static string EndpointsId(this HFInferenceProvider provider) => provider switch
extension(HFInferenceProvider provider)
{
HFInferenceProvider.BASETEN => "baseten",
HFInferenceProvider.CEREBRAS => "cerebras",
HFInferenceProvider.COHERE => "cohere",
HFInferenceProvider.DEEPINFRA => "deepinfra",
HFInferenceProvider.FEATHERLESS_AI => "featherless-ai",
HFInferenceProvider.FIREWORKS => "fireworks-ai",
HFInferenceProvider.GROQ => "groq",
HFInferenceProvider.NOVITA => "novita",
HFInferenceProvider.NSCALE => "nscale",
HFInferenceProvider.OVHCLOUD => "ovhcloud",
HFInferenceProvider.PUBLIC_AI => "publicai",
HFInferenceProvider.SCALEWAY => "scaleway",
HFInferenceProvider.TOGETHER_AI => "together",
HFInferenceProvider.ZAI => "zai-org",
/// <summary>
/// The slug Hugging Face uses for this inference provider.
/// </summary>
/// <returns>The slug, or an empty string for the routing strategies, which name no provider.</returns>
public string EndpointsId() => provider switch
{
HFInferenceProvider.BASETEN => "baseten",
HFInferenceProvider.CEREBRAS => "cerebras",
HFInferenceProvider.COHERE => "cohere",
HFInferenceProvider.DEEPINFRA => "deepinfra",
HFInferenceProvider.FEATHERLESS_AI => "featherless-ai",
HFInferenceProvider.FIREWORKS => "fireworks-ai",
HFInferenceProvider.GROQ => "groq",
HFInferenceProvider.NOVITA => "novita",
HFInferenceProvider.NSCALE => "nscale",
HFInferenceProvider.OVHCLOUD => "ovhcloud",
HFInferenceProvider.PUBLIC_AI => "publicai",
HFInferenceProvider.SCALEWAY => "scaleway",
HFInferenceProvider.TOGETHER_AI => "together",
HFInferenceProvider.ZAI => "zai-org",
_ => string.Empty,
};
_ => string.Empty,
};
/// <summary>
/// The suffix which tells the router where to send the request.
/// </summary>
/// <remarks>
/// The router serves every provider through one endpoint. Which provider answers is decided by
/// a suffix on the model name, e.g. "google/gemma-4-31B-it:novita". Without a suffix, the router
/// picks the fastest provider itself.
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>The suffix including its colon, or an empty string when the router should choose.</returns>
public static string ModelSuffix(this HFInferenceProvider provider) => provider switch
{
HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty,
/// <summary>
/// The suffix which tells the router where to send the request.
/// </summary>
/// <remarks>
/// The router serves every provider through one endpoint. Which provider answers is decided by
/// a suffix on the model name, e.g. "google/gemma-4-31B-it:novita". Without a suffix, the router
/// picks the fastest provider itself.
/// </remarks>
/// <returns>The suffix including its colon, or an empty string when the router should choose.</returns>
public string ModelSuffix() => provider switch
{
HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty,
HFInferenceProvider.CHEAPEST => ":cheapest",
HFInferenceProvider.PREFERRED => ":preferred",
HFInferenceProvider.CHEAPEST => ":cheapest",
HFInferenceProvider.PREFERRED => ":preferred",
_ => $":{provider.EndpointsId()}",
};
_ => $":{provider.EndpointsId()}",
};
/// <summary>
/// Whether this inference provider serves models to chat with.
/// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>True, when the provider serves chat models.</returns>
public static bool SupportsChat(this HFInferenceProvider provider) => provider is not HFInferenceProvider.NONE;
/// <summary>
/// Whether this inference provider serves models to chat with.
/// </summary>
/// <returns>True, when the provider serves chat models.</returns>
public bool SupportsChat() => provider is not HFInferenceProvider.NONE;
/// <summary>
/// Whether this inference provider creates embeddings for us.
/// </summary>
/// <remarks>
/// Embeddings are a much shorter story than chatting. The router serves them nowhere near its
/// own endpoint, only through the route of a provider, and only two of those answer the
/// OpenAI-compatible form we send. The routing strategies are out by their nature: without a
/// named provider there is no route to address.
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>True, when we can create embeddings through this provider.</returns>
public static bool SupportsEmbeddings(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
/// <summary>
/// Whether this inference provider creates embeddings for us.
/// </summary>
/// <remarks>
/// Embeddings are a much shorter story than chatting. The router serves them nowhere near its
/// own endpoint, only through the route of a provider, and only two of those answer the
/// OpenAI-compatible form we send. The routing strategies are out by their nature: without a
/// named provider there is no route to address.
/// </remarks>
/// <returns>True, when we can create embeddings through this provider.</returns>
public bool SupportsEmbeddings() => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
/// <summary>
/// Whether this inference provider transcribes audio for us.
/// </summary>
/// <remarks>
/// The same two providers as for embeddings, and for the same reason: transcription lives on a
/// provider's own route, and only these two answer the OpenAI-compatible form there. Others do
/// transcribe for Hugging Face, but not in a shape we could send an audio file to: fal-ai and
/// Replicate both turn the request down with "Model not supported by provider".
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>True, when we can transcribe audio through this provider.</returns>
public static bool SupportsTranscription(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
/// <summary>
/// Whether this inference provider transcribes audio for us.
/// </summary>
/// <remarks>
/// The same two providers as for embeddings, and for the same reason: transcription lives on a
/// provider's own route, and only these two answer the OpenAI-compatible form there. Others do
/// transcribe for Hugging Face, but not in a shape we could send an audio file to: fal-ai and
/// Replicate both turn the request down with "Model not supported by provider".
/// </remarks>
/// <returns>True, when we can transcribe audio through this provider.</returns>
public bool SupportsTranscription() => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
/// <summary>
/// The base URL of the provider's own OpenAI-compatible route.
/// </summary>
/// <remarks>
/// Only chatting goes through the router's own endpoint. Everything else has to address the
/// provider directly, and they do not agree on where their OpenAI-compatible API sits: DeepInfra
/// keeps it below an additional "openai" segment, and answers the path without it with
/// "Not allowed to POST /v1/embeddings for provider deepinfra".
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>The base URL, or an empty string when the provider has no such route.</returns>
public static string ProviderBaseURL(this HFInferenceProvider provider) => provider switch
{
HFInferenceProvider.TOGETHER_AI => "https://router.huggingface.co/together/v1/",
HFInferenceProvider.DEEPINFRA => "https://router.huggingface.co/deepinfra/v1/openai/",
/// <summary>
/// The base URL of the provider's own OpenAI-compatible route.
/// </summary>
/// <remarks>
/// Only chatting goes through the router's own endpoint. Everything else has to address the
/// provider directly, and they do not agree on where their OpenAI-compatible API sits: DeepInfra
/// keeps it below an additional "openai" segment, and answers the path without it with
/// "Not allowed to POST /v1/embeddings for provider deepinfra".
/// </remarks>
/// <returns>The base URL, or an empty string when the provider has no such route.</returns>
public string ProviderBaseURL() => provider switch
{
HFInferenceProvider.TOGETHER_AI => "https://router.huggingface.co/together/v1/",
HFInferenceProvider.DEEPINFRA => "https://router.huggingface.co/deepinfra/v1/openai/",
_ => string.Empty,
};
_ => string.Empty,
};
/// <summary>
/// The value to filter the Hugging Face model catalog by.
/// </summary>
/// <returns>The provider slug, or "all" when no particular provider was chosen.</returns>
public string CatalogFilter()
{
var slug = provider.EndpointsId();
return string.IsNullOrEmpty(slug) ? "all" : slug;
}
public string ToName() => provider switch
{
HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"),
HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"),
HFInferenceProvider.PREFERRED => TB("Automatic: your preferred order"),
HFInferenceProvider.BASETEN => "Baseten",
HFInferenceProvider.CEREBRAS => "Cerebras",
HFInferenceProvider.COHERE => "Cohere",
HFInferenceProvider.DEEPINFRA => "DeepInfra",
HFInferenceProvider.FEATHERLESS_AI => "Featherless AI",
HFInferenceProvider.FIREWORKS => "Fireworks AI",
HFInferenceProvider.GROQ => "Groq",
HFInferenceProvider.NOVITA => "Novita",
HFInferenceProvider.NSCALE => "Nscale",
HFInferenceProvider.OVHCLOUD => "OVHcloud",
HFInferenceProvider.PUBLIC_AI => "Public AI",
HFInferenceProvider.SCALEWAY => "Scaleway",
HFInferenceProvider.TOGETHER_AI => "Together AI",
HFInferenceProvider.ZAI => "Z.ai",
_ => string.Empty,
};
}
/// <summary>
/// Removes the routing suffix from a model, if it carries one.
/// </summary>
/// <remarks>
/// The suffix says where a request goes, not what the model is. Everything asking what a model
/// can do has to look at the bare name: "google/gemma-4-31B-it:novita" is the same model as
/// "google/gemma-4-31B-it", and a name detection which never heard of the suffix would miss it.
/// Model IDs on the hub are written as "org/model" and carry no colon of their own, so the last
/// colon always starts the suffix.
/// </remarks>
/// <param name="model">The model as it is configured.</param>
/// <returns>The model without its routing suffix.</returns>
public static Model WithoutRoutingSuffix(this Model model)
extension(Model model)
{
var separatorIndex = model.Id.LastIndexOf(':');
return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] };
/// <summary>
/// Removes the routing suffix from a model, if it carries one.
/// </summary>
/// <remarks>
/// The suffix says where a request goes, not what the model is. Everything asking what a model
/// can do has to look at the bare name: "google/gemma-4-31B-it:novita" is the same model as
/// "google/gemma-4-31B-it", and a name detection which never heard of the suffix would miss it.
/// Model IDs on the hub are written as "org/model" and carry no colon of their own, so the last
/// colon always starts the suffix.
/// </remarks>
/// <returns>The model without its routing suffix.</returns>
public Model WithoutRoutingSuffix()
{
var separatorIndex = model.Id.LastIndexOf(':');
return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] };
}
}
/// <summary>
/// The value to filter the Hugging Face model catalog by.
/// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>The provider slug, or "all" when no particular provider was chosen.</returns>
public static string CatalogFilter(this HFInferenceProvider provider)
{
var slug = provider.EndpointsId();
return string.IsNullOrEmpty(slug) ? "all" : slug;
}
public static string ToName(this HFInferenceProvider provider) => provider switch
{
HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"),
HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"),
HFInferenceProvider.PREFERRED => TB("Automatic: your preferred order"),
HFInferenceProvider.BASETEN => "Baseten",
HFInferenceProvider.CEREBRAS => "Cerebras",
HFInferenceProvider.COHERE => "Cohere",
HFInferenceProvider.DEEPINFRA => "DeepInfra",
HFInferenceProvider.FEATHERLESS_AI => "Featherless AI",
HFInferenceProvider.FIREWORKS => "Fireworks AI",
HFInferenceProvider.GROQ => "Groq",
HFInferenceProvider.NOVITA => "Novita",
HFInferenceProvider.NSCALE => "Nscale",
HFInferenceProvider.OVHCLOUD => "OVHcloud",
HFInferenceProvider.PUBLIC_AI => "Public AI",
HFInferenceProvider.SCALEWAY => "Scaleway",
HFInferenceProvider.TOGETHER_AI => "Together AI",
HFInferenceProvider.ZAI => "Z.ai",
_ => string.Empty,
};
}
File diff suppressed because it is too large. Load diff
@@ -5,41 +5,47 @@ public static class LLMProvidersIconExtensions
private const string ICON_ROOT = "/images/provider-icons";
private const string SVG_DATA_URL_PREFIX = "data:image/svg+xml;base64,";
public static string GetIconUrl(this AIStudio.Settings.Provider provider, bool isDarkMode)
=> provider.UsedLLMProvider.GetIconUrl(isDarkMode, provider.CustomIconDataUrl);
public static string GetIconUrl(this LLMProviders provider, bool isDarkMode, string? customIconDataUrl)
extension(AIStudio.Settings.Provider provider)
{
if (customIconDataUrl?.StartsWith(SVG_DATA_URL_PREFIX, StringComparison.Ordinal) == true)
return customIconDataUrl;
return provider.GetIconUrl(isDarkMode);
public string GetIconUrl(bool isDarkMode)
=> provider.UsedLLMProvider.GetIconUrl(isDarkMode, provider.CustomIconDataUrl);
}
public static string GetIconUrl(this LLMProviders provider, bool isDarkMode) => provider switch
extension(LLMProviders provider)
{
LLMProviders.NONE => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg",
LLMProviders.OPEN_AI => $"{ICON_ROOT}/openai{DarkVariant(isDarkMode)}.svg",
LLMProviders.ANTHROPIC => $"{ICON_ROOT}/anthropic{DarkVariant(isDarkMode)}.svg",
LLMProviders.MISTRAL => $"{ICON_ROOT}/mistral.svg",
LLMProviders.GOOGLE => $"{ICON_ROOT}/google.svg",
LLMProviders.X => $"{ICON_ROOT}/x{DarkVariant(isDarkMode)}.svg",
LLMProviders.DEEP_SEEK => $"{ICON_ROOT}/deepseek.svg",
LLMProviders.ALIBABA_CLOUD => $"{ICON_ROOT}/alibaba-cloud.svg",
LLMProviders.PERPLEXITY => $"{ICON_ROOT}/perplexity.svg",
LLMProviders.OPEN_ROUTER => $"{ICON_ROOT}/openrouter.svg",
LLMProviders.HETZNER => $"{ICON_ROOT}/hetzner.svg",
LLMProviders.IONOS => $"{ICON_ROOT}/ionos.svg",
LLMProviders.LITE_LLM => $"{ICON_ROOT}/litellm.svg",
LLMProviders.REQUESTY => $"{ICON_ROOT}/requesty.svg",
LLMProviders.GROQ => $"{ICON_ROOT}/groq.svg",
LLMProviders.FIREWORKS => $"{ICON_ROOT}/fireworks.svg",
LLMProviders.HUGGINGFACE => $"{ICON_ROOT}/hugging-face.svg",
LLMProviders.SELF_HOSTED => $"{ICON_ROOT}/self-hosted{DarkVariant(isDarkMode)}.svg",
LLMProviders.HELMHOLTZ => $"{ICON_ROOT}/helmholtz.svg",
LLMProviders.GWDG => $"{ICON_ROOT}/gwdg.svg",
_ => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg",
};
public string GetIconUrl(bool isDarkMode, string? customIconDataUrl)
{
if (customIconDataUrl?.StartsWith(SVG_DATA_URL_PREFIX, StringComparison.Ordinal) == true)
return customIconDataUrl;
return provider.GetIconUrl(isDarkMode);
}
public string GetIconUrl(bool isDarkMode) => provider switch
{
LLMProviders.NONE => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg",
LLMProviders.OPEN_AI => $"{ICON_ROOT}/openai{DarkVariant(isDarkMode)}.svg",
LLMProviders.ANTHROPIC => $"{ICON_ROOT}/anthropic{DarkVariant(isDarkMode)}.svg",
LLMProviders.MISTRAL => $"{ICON_ROOT}/mistral.svg",
LLMProviders.GOOGLE => $"{ICON_ROOT}/google.svg",
LLMProviders.X => $"{ICON_ROOT}/x{DarkVariant(isDarkMode)}.svg",
LLMProviders.DEEP_SEEK => $"{ICON_ROOT}/deepseek.svg",
LLMProviders.ALIBABA_CLOUD => $"{ICON_ROOT}/alibaba-cloud.svg",
LLMProviders.PERPLEXITY => $"{ICON_ROOT}/perplexity.svg",
LLMProviders.OPEN_ROUTER => $"{ICON_ROOT}/openrouter.svg",
LLMProviders.HETZNER => $"{ICON_ROOT}/hetzner.svg",
LLMProviders.IONOS => $"{ICON_ROOT}/ionos.svg",
LLMProviders.LITE_LLM => $"{ICON_ROOT}/litellm.svg",
LLMProviders.REQUESTY => $"{ICON_ROOT}/requesty.svg",
LLMProviders.GROQ => $"{ICON_ROOT}/groq.svg",
LLMProviders.FIREWORKS => $"{ICON_ROOT}/fireworks.svg",
LLMProviders.HUGGINGFACE => $"{ICON_ROOT}/hugging-face.svg",
LLMProviders.SELF_HOSTED => $"{ICON_ROOT}/self-hosted{DarkVariant(isDarkMode)}.svg",
LLMProviders.HELMHOLTZ => $"{ICON_ROOT}/helmholtz.svg",
LLMProviders.GWDG => $"{ICON_ROOT}/gwdg.svg",
_ => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg",
};
}
private static string DarkVariant(bool isDarkMode) => isDarkMode ? "-dark" : string.Empty;
}
@@ -6,16 +6,19 @@ public static class ModelLoadFailureReasonExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ModelLoadFailureReasonExtensions).Namespace, nameof(ModelLoadFailureReasonExtensions));
public static string ToUserMessage(this ModelLoadFailureReason failureReason, string providerName) => failureReason switch
extension(ModelLoadFailureReason failureReason)
{
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.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.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),
public string ToUserMessage(string providerName) => failureReason switch
{
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.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.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),
_ => string.Empty,
};
_ => string.Empty,
};
}
}
@@ -4,22 +4,25 @@ namespace AIStudio.Provider.OpenAI;
public static class SystemPromptRoleExtensions
{
/// <summary>
/// Spells a system prompt role the way OpenAI's APIs expect it.
/// </summary>
/// <remarks>
/// A role no rule states becomes "developer". OpenAI's current documentation names no other, a
/// model the rules do not know is almost always one released after they were written, and the
/// older generations which still want "system" say so in their families.
/// </remarks>
/// <param name="role">The role the model profile states.</param>
/// <returns>The role, as OpenAI names it.</returns>
public static string ToOpenAIRole(this SystemPromptRole role) => role switch
extension(SystemPromptRole role)
{
SystemPromptRole.SYSTEM => "system",
SystemPromptRole.DEVELOPER => "developer",
SystemPromptRole.USER => "user",
/// <summary>
/// Spells a system prompt role the way OpenAI's APIs expect it.
/// </summary>
/// <remarks>
/// A role no rule states becomes "developer". OpenAI's current documentation names no other, a
/// model the rules do not know is almost always one released after they were written, and the
/// older generations which still want "system" say so in their families.
/// </remarks>
/// <returns>The role, as OpenAI names it.</returns>
public string ToOpenAIRole() => role switch
{
SystemPromptRole.SYSTEM => "system",
SystemPromptRole.DEVELOPER => "developer",
SystemPromptRole.USER => "user",
_ => "developer",
};
_ => "developer",
};
}
}
@@ -6,42 +6,45 @@ public static class ProviderRequestFailureReasonExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderRequestFailureReasonExtensions).Namespace, nameof(ProviderRequestFailureReasonExtensions));
/// <summary>
/// Names the kind of failure in a few words.
/// </summary>
/// <remarks>
/// Meant as a label beside the full message, so a list of failures can be scanned instead of
/// read: twenty entries which all say API key are one problem, not twenty.
/// </remarks>
public static string GetName(this ProviderRequestFailureReason failureReason) => failureReason switch
extension(ProviderRequestFailureReason failureReason)
{
ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("No credits left"),
ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("Too many requests"),
ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER => TB("Model not offered"),
ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY => TB("API key problem"),
ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => TB("Not permitted"),
ProviderRequestFailureReason.PROVIDER_UNAVAILABLE => TB("Provider unreachable"),
ProviderRequestFailureReason.MODEL_NOT_FOUND => TB("Model unknown"),
ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED => TB("Text too long"),
ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED => TB("No embeddings"),
ProviderRequestFailureReason.INVALID_RESPONSE => TB("Unreadable answer"),
ProviderRequestFailureReason.UNKNOWN => TB("Unknown cause"),
/// <summary>
/// Names the kind of failure in a few words.
/// </summary>
/// <remarks>
/// Meant as a label beside the full message, so a list of failures can be scanned instead of
/// read: twenty entries which all say API key are one problem, not twenty.
/// </remarks>
public string GetName() => failureReason switch
{
ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("No credits left"),
ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("Too many requests"),
ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER => TB("Model not offered"),
ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY => TB("API key problem"),
ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => TB("Not permitted"),
ProviderRequestFailureReason.PROVIDER_UNAVAILABLE => TB("Provider unreachable"),
ProviderRequestFailureReason.MODEL_NOT_FOUND => TB("Model unknown"),
ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED => TB("Text too long"),
ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED => TB("No embeddings"),
ProviderRequestFailureReason.INVALID_RESPONSE => TB("Unreadable answer"),
ProviderRequestFailureReason.UNKNOWN => TB("Unknown cause"),
_ => string.Empty,
};
_ => string.Empty,
};
/// <summary>
/// Gets a value indicating whether the way out of this failure is in the provider settings.
/// </summary>
/// <remarks>
/// Only for the failures a setting actually fixes. Pointing at the settings for a provider
/// which is merely overloaded would send the user looking for a mistake they never made.
/// </remarks>
public static bool IsFixedInProviderSettings(this ProviderRequestFailureReason failureReason) => failureReason is
ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY or
ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR or
ProviderRequestFailureReason.MODEL_NOT_FOUND or
ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER or
ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED or
ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED;
/// <summary>
/// Gets a value indicating whether the way out of this failure is in the provider settings.
/// </summary>
/// <remarks>
/// Only for the failures a setting actually fixes. Pointing at the settings for a provider
/// which is merely overloaded would send the user looking for a mistake they never made.
/// </remarks>
public bool IsFixedInProviderSettings() => failureReason is
ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY or
ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR or
ProviderRequestFailureReason.MODEL_NOT_FOUND or
ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER or
ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED or
ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED;
}
}
@@ -2,141 +2,143 @@ namespace AIStudio.Provider.SelfHosted;
public static class HostExtensions
{
public static string Name(this Host host) => host switch
{
Host.NONE => "None",
Host.LM_STUDIO => "LM Studio",
Host.LLAMA_CPP => "llama.cpp",
Host.WHISPER_CPP => "whisper.cpp",
Host.OLLAMA => "ollama",
Host.VLLM => "vLLM",
Host.LLMMAN => "llmman",
_ => "Unknown",
};
public static string BaseURL(this Host host) => host switch
{
_ => "/v1/",
};
public static string ChatURL(this Host host) => host switch
{
_ => "chat/completions",
};
public static string TranscriptionURL(this Host host) => host switch
{
_ => "audio/transcriptions",
};
public static string EmbeddingURL(this Host host) => host switch
{
_ => "embeddings",
};
/// <summary>
/// The largest image file, in bytes, which the engine accepts in a chat request, where we know of one.
/// </summary>
/// <remarks>
/// Like the limit of a cloud provider, see LLMProvidersExtensions.GetMaxImageSizeBytes, this
/// belongs to the API of the engine, not to the model. What we actually send is asked through
/// IImageSourceExtensions.MaxImageSizeBytesFor.
/// </remarks>
/// <param name="host">The engine.</param>
/// <returns>The size limit of one image file, or null when the engine states none we know of.</returns>
public static long? GetMaxImageSizeBytes(this Host host) => host switch
extension(Host host)
{
//
// llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions
// keeps the default limit of its web framework, read on 2026-10-22 at
// https://github.com/llmmanorg/llmman/blob/main/src/cmd/serve.rs. An image of 1 MiB takes
// about 1.33 MiB as base64, which leaves room for the text of the conversation:
//
Host.LLMMAN => 1_048_576,
_ => null,
};
public static bool IsChatSupported(this Host host)
{
switch (host)
public string Name() => host switch
{
case Host.WHISPER_CPP:
return false;
Host.NONE => "None",
Host.LM_STUDIO => "LM Studio",
Host.LLAMA_CPP => "llama.cpp",
Host.WHISPER_CPP => "whisper.cpp",
Host.OLLAMA => "ollama",
Host.VLLM => "vLLM",
Host.LLMMAN => "llmman",
_ => "Unknown",
};
public string BaseURL() => host switch
{
_ => "/v1/",
};
public string ChatURL() => host switch
{
_ => "chat/completions",
};
public string TranscriptionURL() => host switch
{
_ => "audio/transcriptions",
};
public string EmbeddingURL() => host switch
{
_ => "embeddings",
};
/// <summary>
/// The largest image file, in bytes, which the engine accepts in a chat request, where we know of one.
/// </summary>
/// <remarks>
/// Like the limit of a cloud provider, see LLMProvidersExtensions.GetMaxImageSizeBytes, this
/// belongs to the API of the engine, not to the model. What we actually send is asked through
/// IImageSourceExtensions.MaxImageSizeBytesFor.
/// </remarks>
/// <returns>The size limit of one image file, or null when the engine states none we know of.</returns>
public long? GetMaxImageSizeBytes() => host switch
{
//
// llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions
// keeps the default limit of its web framework, read on 2026-10-22 at
// https://github.com/llmmanorg/llmman/blob/main/src/cmd/serve.rs. An image of 1 MiB takes
// about 1.33 MiB as base64, which leaves room for the text of the conversation:
//
Host.LLMMAN => 1_048_576,
_ => null,
};
public bool IsChatSupported()
{
switch (host)
{
case Host.WHISPER_CPP:
return false;
default:
case Host.OLLAMA:
case Host.VLLM:
case Host.LM_STUDIO:
case Host.LLAMA_CPP:
case Host.LLMMAN:
return true;
default:
case Host.OLLAMA:
case Host.VLLM:
case Host.LM_STUDIO:
case Host.LLAMA_CPP:
case Host.LLMMAN:
return true;
}
}
}
public static bool IsEmbeddingSupported(this Host host)
{
switch (host)
public bool IsEmbeddingSupported()
{
case Host.LM_STUDIO:
case Host.OLLAMA:
case Host.VLLM:
case Host.LLMMAN:
return true;
switch (host)
{
case Host.LM_STUDIO:
case Host.OLLAMA:
case Host.VLLM:
case Host.LLMMAN:
return true;
default:
case Host.LLAMA_CPP:
return false;
default:
case Host.LLAMA_CPP:
return false;
}
}
}
public static bool IsTranscriptionSupported(this Host host)
{
switch (host)
{
case Host.OLLAMA:
case Host.VLLM:
case Host.WHISPER_CPP:
case Host.LLMMAN:
return true;
default:
case Host.LM_STUDIO:
case Host.LLAMA_CPP:
return false;
public bool IsTranscriptionSupported()
{
switch (host)
{
case Host.OLLAMA:
case Host.VLLM:
case Host.WHISPER_CPP:
case Host.LLMMAN:
return true;
default:
case Host.LM_STUDIO:
case Host.LLAMA_CPP:
return false;
}
}
}
/// <summary>
/// Whether the engine answers the models route with the models it serves.
/// </summary>
/// <remarks>
/// This is the only place which says so. The provider asks it before it loads the models of a
/// chat, embedding, or transcription list, and the dialogs ask it before they offer to reload
/// them; a new engine therefore needs no more than its line here. whisper.cpp is not among
/// them: it serves the one model it was started with and has no route to list it, so the
/// provider offers that model under a fixed name instead. llama.cpp is, even though builds
/// older than the route answer it with 404; the provider handles those builds itself.
/// </remarks>
/// <param name="host">The engine.</param>
/// <returns>True when the models can be loaded from the engine; otherwise, false.</returns>
public static bool IsModelListSupported(this Host host)
{
switch (host)
/// <summary>
/// Whether the engine answers the models route with the models it serves.
/// </summary>
/// <remarks>
/// This is the only place which says so. The provider asks it before it loads the models of a
/// chat, embedding, or transcription list, and the dialogs ask it before they offer to reload
/// them; a new engine therefore needs no more than its line here. whisper.cpp is not among
/// them: it serves the one model it was started with and has no route to list it, so the
/// provider offers that model under a fixed name instead. llama.cpp is, even though builds
/// older than the route answer it with 404; the provider handles those builds itself.
/// </remarks>
/// <returns>True when the models can be loaded from the engine; otherwise, false.</returns>
public bool IsModelListSupported()
{
case Host.LLAMA_CPP:
case Host.OLLAMA:
case Host.LM_STUDIO:
case Host.VLLM:
case Host.LLMMAN:
return true;
switch (host)
{
case Host.LLAMA_CPP:
case Host.OLLAMA:
case Host.LM_STUDIO:
case Host.VLLM:
case Host.LLMMAN:
return true;
default:
case Host.NONE:
case Host.WHISPER_CPP:
return false;
default:
case Host.NONE:
case Host.WHISPER_CPP:
return false;
}
}
}
}