using AIStudio.Provider; using AIStudio.Provider.HuggingFace; namespace AIStudio.Settings; public static partial class ProviderExtensions { /// /// The longest model ID we normalize without going to the heap. /// private const int MAX_STACK_ALLOCATED_MODEL_ID_LENGTH = 256; /// /// Brings a model ID into the form the capability rules are written in. /// /// /// 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 model ID as the provider reports it. /// The model ID in lowercase, with every separator written as a single hyphen. private static string NormalizeModelId(string modelId) { 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 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]); } /// /// Get the capabilities of the model used by the configured provider. /// /// The configured provider. /// The capabilities of the configured model. public static List GetModelCapabilities(this Provider provider) { var automaticCapabilities = provider.UsedLLMProvider.GetModelCapabilities(provider.Model); return provider.CapabilityOverrides?.ApplyTo(automaticCapabilities) ?? automaticCapabilities; } /// /// Get whether the model used by the configured provider accepts images as input. /// /// /// Two capabilities express image input, one for a single image and one for several. Anything that /// wants to know whether an image may be sent has to accept both, which is why the question is asked /// here instead of at each call site: attaching a file and validating an already attached file must /// never disagree about it. /// /// The configured provider. /// true when the model accepts image input. public static bool SupportsImageInput(this Provider provider) { var capabilities = provider.GetModelCapabilities(); return capabilities.Contains(Capability.SINGLE_IMAGE_INPUT) || capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT); } /// /// Get the capabilities of a model for a specific provider. /// /// The LLM provider. /// The model to get the capabilities for. /// >The capabilities of the model. public static List GetModelCapabilities(this LLMProviders provider, Model model) { if (string.IsNullOrWhiteSpace(model.Id)) return []; 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), LLMProviders.GROQ or LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model), // // 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), _ => [] }; } }