mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-07 19:29:40 +00:00
Rebuilt how AI Studio knows what a model can do (#960)
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
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
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
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
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
This commit is contained in:
1 parent
d21e09dd1e
commit
d85b4e71b6
287 files changed
+18341
-3677
No files matched your search
@@ -1,85 +1,78 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Provider.HuggingFace;
|
||||
using AIStudio.Models;
|
||||
using AIStudio.Models.Live;
|
||||
using AIStudio.Models.Registry;
|
||||
using AIStudio.Provider;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
public static partial class ProviderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The longest model ID we normalize without going to the heap.
|
||||
/// </summary>
|
||||
private const int MAX_STACK_ALLOCATED_MODEL_ID_LENGTH = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Brings a model ID into the form the capability rules are written in.
|
||||
/// Everything the app knows about the model this provider instance is configured with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every provider names the same model differently, and the difference is rarely in the words:
|
||||
/// it is in what sits between them. Ollama separates the variant with a colon
|
||||
/// ("qwen3.8:27b-mlx"), Blablador answers with a whole sentence ("10 - Muse Glimmer 30b - the
|
||||
/// newest META model"), Fireworks puts a path in front
|
||||
/// ("accounts/fireworks/models/llama-v3p1-405b-instruct"), and the hubs use hyphens. Without
|
||||
/// this, every rule would have to spell out each of those writings, which is what the Llama
|
||||
/// block used to do with four variants of one check.
|
||||
///
|
||||
/// The dots stay. They carry the version boundary: llama3 and llama3.1 are different models,
|
||||
/// and only the latter calls functions. Dropping them would merge the two.
|
||||
///
|
||||
/// The patterns in the rules are written in this normalized form already, which is why they
|
||||
/// use lowercase and hyphens throughout.
|
||||
/// The one door to that question. Behind it stand the links of the chain, in the order they
|
||||
/// win: what the person said about their own installation, then what the installation itself
|
||||
/// reported, then what the rules worked out from the name, and last what the app assumes when
|
||||
/// nothing else said anything.
|
||||
/// </remarks>
|
||||
/// <param name="modelId">The model ID as the provider reports it.</param>
|
||||
/// <returns>The model ID in lowercase, with every separator written as a single hyphen.</returns>
|
||||
private static string NormalizeModelId(string modelId)
|
||||
/// <param name="provider">The configured provider.</param>
|
||||
/// <returns>The profile of the configured model.</returns>
|
||||
public static ModelProfile GetModelProfile(this Provider provider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelId))
|
||||
return string.Empty;
|
||||
|
||||
//
|
||||
// Normalizing never makes a name longer, so the original length is always enough room.
|
||||
// Model IDs are short, which is why the buffer lives on the stack: the longest ones we
|
||||
// know of are the descriptive names Blablador answers with, at around 75 characters. A
|
||||
// provider reporting something longer still gets a correct answer, just from the heap.
|
||||
//
|
||||
Span<char> normalized = modelId.Length <= MAX_STACK_ALLOCATED_MODEL_ID_LENGTH
|
||||
? stackalloc char[modelId.Length]
|
||||
: new char[modelId.Length];
|
||||
|
||||
var length = 0;
|
||||
foreach (var character in modelId)
|
||||
{
|
||||
if (char.IsAsciiLetterOrDigit(character) || character is '.')
|
||||
{
|
||||
normalized[length++] = char.ToLowerInvariant(character);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Anything else separates two parts of the name. A leading separator, and a repeated
|
||||
// one, say nothing and would only get in the way of the patterns:
|
||||
if (length is 0 || normalized[length - 1] is '-')
|
||||
continue;
|
||||
|
||||
normalized[length++] = '-';
|
||||
}
|
||||
|
||||
// A trailing separator carries no meaning either:
|
||||
if (length > 0 && normalized[length - 1] is '-')
|
||||
length--;
|
||||
|
||||
return new string(normalized[..length]);
|
||||
var automatic = provider.GetAutomaticModelProfile();
|
||||
return provider.CapabilityOverrides?.ApplyTo(automatic) ?? automatic;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the capabilities of the model used by the configured provider.
|
||||
/// Everything known about the configured model except what the person themselves switched.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is what happens when somebody fills in nothing, which is why the expert dialog shows it
|
||||
/// as the automatic answer. It has to include what the provider reported: a person who leaves
|
||||
/// the window empty gets the number their own engine stated, and a placeholder showing them a
|
||||
/// different one would be a promise the app does not keep.
|
||||
/// </remarks>
|
||||
/// <param name="provider">The configured provider.</param>
|
||||
/// <returns>The capabilities of the configured model.</returns>
|
||||
public static List<Capability> GetModelCapabilities(this Provider provider)
|
||||
/// <returns>The profile of the configured model, without that provider's overrides.</returns>
|
||||
public static ModelProfile GetAutomaticModelProfile(this Provider provider)
|
||||
{
|
||||
var automaticCapabilities = provider.UsedLLMProvider.GetModelCapabilities(provider.Model);
|
||||
return provider.CapabilityOverrides?.ApplyTo(automaticCapabilities) ?? automaticCapabilities;
|
||||
var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model);
|
||||
return ListedModels.Shared.Of(provider.Id, provider.Model.Id).ApplyTo(stated);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Everything the rules know about a model at a provider, without anybody's own installation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The answer to the model as such, which is the same for everybody who uses that name at that
|
||||
/// provider -- and therefore the answer the registry caches. What one particular installation
|
||||
/// says about it is asked one link further up, where the instance is known.
|
||||
///
|
||||
/// The assumed profile fills in where no rule stated a single capability. It fills in the
|
||||
/// capabilities only: a modifier may well have said what the model is made for without any rule
|
||||
/// saying what it can do, and an embedding model nobody wrote a rule for stays an embedding
|
||||
/// model rather than turning into a chat model with an assumption attached.
|
||||
/// </remarks>
|
||||
/// <param name="provider">The LLM provider the model is reached through.</param>
|
||||
/// <param name="model">The model, named the way that provider names it.</param>
|
||||
/// <returns>The profile, which knows nothing when there is nothing to reach.</returns>
|
||||
public static ModelProfile GetModelProfile(this LLMProviders provider, Model model)
|
||||
{
|
||||
//
|
||||
// Without a provider there is nothing to reach the model through, and an empty name is what
|
||||
// a provider reports before anybody picked one. Neither is a model we could assume anything
|
||||
// about, so neither gets the assumption.
|
||||
//
|
||||
if (provider is LLMProviders.NONE || string.IsNullOrWhiteSpace(model.Id))
|
||||
return ModelProfile.UNKNOWN;
|
||||
|
||||
var stated = ModelRegistry.Shared.Profile(provider, model.Id);
|
||||
return stated.Capabilities is Capability.NONE
|
||||
? stated with { Capabilities = ModelProfile.ASSUMED.Capabilities }
|
||||
: stated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get whether the model used by the configured provider accepts images as input.
|
||||
/// </summary>
|
||||
@@ -91,61 +84,48 @@ public static partial class ProviderExtensions
|
||||
/// </remarks>
|
||||
/// <param name="provider">The configured provider.</param>
|
||||
/// <returns><c>true</c> when the model accepts image input.</returns>
|
||||
public static bool SupportsImageInput(this Provider provider)
|
||||
{
|
||||
var capabilities = provider.GetModelCapabilities();
|
||||
return capabilities.Contains(Capability.SINGLE_IMAGE_INPUT) || capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT);
|
||||
}
|
||||
public static bool SupportsImageInput(this Provider provider) => provider.GetModelProfile().HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT);
|
||||
|
||||
/// <summary>
|
||||
/// Get the capabilities of a model for a specific provider.
|
||||
/// Checks whether this model can be used for chatting.
|
||||
/// </summary>
|
||||
/// <param name="provider">The LLM provider.</param>
|
||||
/// <param name="model">The model to get the capabilities for.</param>
|
||||
/// <returns>>The capabilities of the model.</returns>
|
||||
public static List<Capability> GetModelCapabilities(this LLMProviders provider, Model model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.Id))
|
||||
return [];
|
||||
/// <remarks>
|
||||
/// What a model can do and what it is made for used to be two questions answered by two pieces
|
||||
/// of code, each walking the same name with rules of its own. They disagreed: a model like
|
||||
/// nomic-embed-text was an embedding model at one provider and a chat model at the next. Both
|
||||
/// come out of the same rules now, which is why this takes the provider -- the same name means
|
||||
/// different things depending on who serves it, and only the provider knows how to unwrap it.
|
||||
///
|
||||
/// The direction of the answer is deliberate. Everything not recognized as something else is a
|
||||
/// chat model, so a provider adding a family we have never seen keeps it visible to the person
|
||||
/// paying for it. Getting it wrong the other way would hide a model.
|
||||
/// </remarks>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <param name="provider">The provider serving it.</param>
|
||||
/// <returns>True, when the model is a chat model or when we recognize no other kind.</returns>
|
||||
public static bool IsChatModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT;
|
||||
|
||||
return provider switch
|
||||
{
|
||||
LLMProviders.OPEN_AI => GetModelCapabilitiesOpenAI(model),
|
||||
LLMProviders.MISTRAL => GetModelCapabilitiesMistral(model),
|
||||
LLMProviders.ANTHROPIC => GetModelCapabilitiesAnthropic(model),
|
||||
LLMProviders.GOOGLE => GetModelCapabilitiesGoogle(model),
|
||||
LLMProviders.X => GetModelCapabilitiesOpenSource(model),
|
||||
LLMProviders.DEEP_SEEK => GetModelCapabilitiesDeepSeek(model),
|
||||
LLMProviders.ALIBABA_CLOUD => GetModelCapabilitiesAlibaba(model),
|
||||
LLMProviders.PERPLEXITY => GetModelCapabilitiesPerplexity(model),
|
||||
LLMProviders.OPEN_ROUTER => GetModelCapabilitiesGateway(model),
|
||||
LLMProviders.HETZNER or LLMProviders.IONOS => GetModelCapabilitiesOpenSource(model),
|
||||
|
||||
//
|
||||
// LiteLLM is a gateway just like OpenRouter, and it names its models the same way:
|
||||
// "vendor/model", e.g. "anthropic/claude-opus-5" or "azure/gpt-5.6". So we let the
|
||||
// gateway detection handle it, which resolves the vendor prefix and asks the
|
||||
// provider who really knows the model. Everything it cannot place is treated as
|
||||
// an open source model, which is the right fallback for a freely named alias:
|
||||
//
|
||||
LLMProviders.LITE_LLM => GetModelCapabilitiesGateway(model),
|
||||
/// <summary>
|
||||
/// Checks whether this model creates embeddings.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <param name="provider">The provider serving it.</param>
|
||||
/// <returns>True, when the model is an embedding model.</returns>
|
||||
public static bool IsEmbeddingModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING;
|
||||
|
||||
LLMProviders.GROQ or LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model),
|
||||
/// <summary>
|
||||
/// Checks whether this model transcribes audio.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <param name="provider">The provider serving it.</param>
|
||||
/// <returns>True, when the model is a transcription model.</returns>
|
||||
public static bool IsTranscriptionModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION;
|
||||
|
||||
//
|
||||
// Hugging Face names its models the way the hub does, "org/model", which is the same
|
||||
// shape the other gateways use. So we let the gateway detection resolve the organization
|
||||
// and ask the provider implementation which really knows the model. The routing suffix
|
||||
// has to go first: it says which inference provider answers, not what the model is.
|
||||
//
|
||||
LLMProviders.HUGGINGFACE => GetModelCapabilitiesGateway(model.WithoutRoutingSuffix()),
|
||||
|
||||
LLMProviders.HELMHOLTZ => GetModelCapabilitiesOpenSource(model),
|
||||
LLMProviders.GWDG => GetModelCapabilitiesOpenSource(model),
|
||||
|
||||
LLMProviders.SELF_HOSTED => GetModelCapabilitiesOpenSource(model),
|
||||
|
||||
_ => []
|
||||
};
|
||||
}
|
||||
/// <summary>
|
||||
/// Checks whether this model generates images.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <param name="provider">The provider serving it.</param>
|
||||
/// <returns>True, when the model is an image generation model.</returns>
|
||||
public static bool IsImageModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION;
|
||||
}
|
||||
Reference in new issue
Block a user