Add one model host per provider and the unwrapping walk

This commit is contained in:
Thorsten Sommer committed 2026-09-11 19:28:06 +02:00
1 parent d3b28134d9
commit 31b655cf3b
24 files changed
+1329

No files matched your search

@@ -0,0 +1,21 @@
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// Alibaba Cloud Model Studio.
/// </summary>
/// <remarks>
/// Worth knowing about this one: several names mean a different model here than they do anywhere
/// else. "qwq" is the commercial qwq-plus on Model Studio and the open weights everywhere else.
/// That is not settled here but in the rules, which can bind themselves to a provider -- this host
/// exists so that they have a provider to bind to.
/// </remarks>
public sealed class HostAlibabaCloud : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.ALIBABA_CLOUD;
/// <inheritdoc />
public override ModelSource Source => new("https://www.alibabacloud.com/help/en/model-studio/compatibility-of-openai-with-dashscope", new DateOnly(2026, 9, 11), "Models are named plainly, and the app reaches them through the OpenAI-compatible endpoint.");
}
@@ -0,0 +1,15 @@
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// Anthropic's own cloud.
/// </summary>
public sealed class HostAnthropic : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.ANTHROPIC;
/// <inheritdoc />
public override ModelSource Source => new("https://docs.anthropic.com/en/api/messages", new DateOnly(2026, 9, 11), "Models are named plainly, and there is one API to reach them through.");
}
@@ -0,0 +1,20 @@
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// DeepSeek's own platform.
/// </summary>
/// <remarks>
/// It names its models by what they are for rather than by which checkpoint answers: "deepseek-chat"
/// and "deepseek-reasoner" both point at whatever is current. Those are aliases, not wrappings, so
/// there is nothing to take off -- the rules answer for the alias itself.
/// </remarks>
public sealed class HostDeepSeek : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.DEEP_SEEK;
/// <inheritdoc />
public override ModelSource Source => new("https://api-docs.deepseek.com/", new DateOnly(2026, 9, 11), "Models are named plainly, and the app reaches them through the OpenAI-compatible endpoint.");
}
@@ -0,0 +1,25 @@
using AIStudio.Models.Matching;
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// Fireworks AI, which puts a whole account path in front of every model.
/// </summary>
/// <remarks>
/// "accounts/fireworks/models/llama-v3p1-405b-instruct" is three segments of path and then the
/// model. Nothing here counts them: the same taking-off-one-segment the gateways use is asked
/// again until there is no path left. None of the three segments names a vendor we know, so none
/// of them claims to.
/// </remarks>
public sealed class HostFireworks : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.FIREWORKS;
/// <inheritdoc />
public override ModelSource Source => new("https://fireworks.ai/models?show=Serverless", new DateOnly(2026, 9, 11), "Models are named \"accounts/<account>/models/<model>\", served through the OpenAI-compatible chat completion API.");
/// <inheritdoc />
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor) => HostNaming.TrySplitOrganization(id, out inner, out declaredVendor);
}
@@ -0,0 +1,26 @@
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// The GWDG's academic cloud, which resells commercial models next to the open weights it runs.
/// </summary>
/// <remarks>
/// This is the host the transport rule was written for. It offers Claude and GPT under the very
/// names their vendors use -- "claude-sonnet-5", "gpt-5.5" -- so the rules recognize them and
/// answer with everything those models can do at their vendor. Everything except the API: a request
/// goes to Göttingen, not to San Francisco, and the Responses API is not served there.
///
/// The old code arrived at the same answer by having the open weights rules notice a Claude name
/// and call the Anthropic rules, then correct the result. Here the recognizing and the correcting
/// are two different things in two different places, which is why neither has to know about the
/// other.
/// </remarks>
public sealed class HostGWDG : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.GWDG;
/// <inheritdoc />
public override ModelSource Source => new("https://docs.hpc.gwdg.de/services/saia/index.html", new DateOnly(2026, 9, 11), "Open weights and resold commercial models alike are named plainly, and all of them are served through the OpenAI-compatible chat completion API.");
}
@@ -0,0 +1,15 @@
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// Google's own cloud.
/// </summary>
public sealed class HostGoogle : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.GOOGLE;
/// <inheritdoc />
public override ModelSource Source => new("https://ai.google.dev/gemini-api/docs/openai", new DateOnly(2026, 9, 11), "Models are named plainly, and the app reaches them through the OpenAI-compatible endpoint.");
}
@@ -0,0 +1,24 @@
using AIStudio.Models.Matching;
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// Groq, which serves open weights and writes some of their names the way the hub does.
/// </summary>
/// <remarks>
/// Both spellings appear side by side in its catalog: "llama-3.3-70b-versatile" carries no
/// organization, "moonshotai/kimi-k2-instruct" and "openai/gpt-oss-120b" do. Taking one off when
/// there is one settles both without a rule per spelling.
/// </remarks>
public sealed class HostGroq : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.GROQ;
/// <inheritdoc />
public override ModelSource Source => new("https://console.groq.com/docs/api-reference", new DateOnly(2026, 9, 11), "Models are named either plainly or as the hub names them, and served through the OpenAI-compatible chat completion API.");
/// <inheritdoc />
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor) => HostNaming.TrySplitOrganization(id, out inner, out declaredVendor);
}
@@ -0,0 +1,29 @@
using AIStudio.Models.Matching;
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// Helmholtz Blablador, which answers with the line a person would read in a menu.
/// </summary>
/// <remarks>
/// "1 - Llama3 405 the best general model" is a whole sentence, and the number in front is where
/// the entry sits in the list -- it moves whenever the operator adds a model. Taking it off is the
/// one thing this host does; the prose after the model name stays because there is no telling
/// where the name ends and the recommendation begins.
/// </remarks>
public sealed class HostHelmholtz : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.HELMHOLTZ;
/// <inheritdoc />
public override ModelSource Source => new("https://sdlaml.pages.jsc.fz-juelich.de/ai/guides/blablador_api_access/", new DateOnly(2026, 9, 11), "Models are named as menu entries, \"<position> - <description>\", and served through the OpenAI-compatible chat completion API.");
/// <inheritdoc />
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor)
{
declaredVendor = null;
return HostNaming.TryStripMenuPosition(id, out inner);
}
}
@@ -0,0 +1,15 @@
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// Hetzner's inference offering, which serves open weights under their plain names.
/// </summary>
public sealed class HostHetzner : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.HETZNER;
/// <inheritdoc />
public override ModelSource Source => new("https://experiments.hetzner.com/docs/inference", new DateOnly(2026, 9, 11), "Open weights named plainly, served through the OpenAI-compatible chat completion API.");
}
@@ -0,0 +1,36 @@
using AIStudio.Models.Matching;
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// The Hugging Face router, whose names carry two wrappings rather than one.
/// </summary>
/// <remarks>
/// "google/gemma-4-31B-it:novita" says three things at once: who published the weights, which model
/// it is, and which inference provider should answer. The suffix goes first, because it is the
/// outermost and because it says nothing about the model -- a request routed to Novita and one
/// routed to Together AI reach the same weights.
///
/// This is the case the whole walk was written for. A host which took both off at once would work
/// here and nowhere else; taking one off at a time is what also covers the account path Fireworks
/// puts in front, without either host knowing about the other.
/// </remarks>
public sealed class HostHuggingFace : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.HUGGINGFACE;
/// <inheritdoc />
public override ModelSource Source => new("https://huggingface.co/docs/inference-providers/index", new DateOnly(2026, 9, 11), "Models are named as the hub names them, \"organization/model\", optionally followed by a colon and the inference provider to route to.");
/// <inheritdoc />
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor)
{
declaredVendor = null;
if (HostNaming.TryStripRoutingSuffix(id, out inner))
return true;
return HostNaming.TrySplitOrganization(id, out inner, out declaredVendor);
}
}
@@ -0,0 +1,24 @@
using AIStudio.Models.Matching;
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// The IONOS AI Model Hub, which keeps the hub spelling of the models it serves.
/// </summary>
/// <remarks>
/// Its catalog reads like the hub's: "meta-llama/Llama-3.3-70B-Instruct",
/// "mistralai/Mistral-Small-24B-Instruct". So the organization comes off, and with it comes the
/// vendor -- stated rather than guessed from the name.
/// </remarks>
public sealed class HostIONOS : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.IONOS;
/// <inheritdoc />
public override ModelSource Source => new("https://docs.ionos.com/cloud/ai/ai-model-hub", new DateOnly(2026, 9, 11), "Open weights named as the hub names them, served through the OpenAI-compatible chat completion API.");
/// <inheritdoc />
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor) => HostNaming.TrySplitOrganization(id, out inner, out declaredVendor);
}
@@ -0,0 +1,30 @@
using AIStudio.Models.Matching;
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// A LiteLLM proxy, which somebody operates themselves and names as they please.
/// </summary>
/// <remarks>
/// Aliases here are whatever the operator wrote in their configuration. Many of them keep the
/// "vendor/model" shape, some name the cloud instead of the vendor ("azure/gpt-5.6"), and some are
/// a word ("the-fast-one"). Taking off a prefix costs nothing in the last case and helps in the
/// first two, and a prefix nobody recognizes states no vendor -- so a name the operator invented
/// is left for the rules to make what they can of.
///
/// This is also the host where a person is most likely to correct us by hand, which is what the
/// expert settings are for: an alias only its operator can decipher is not something rules will
/// ever get right.
/// </remarks>
public sealed class HostLiteLLM : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.LITE_LLM;
/// <inheritdoc />
public override ModelSource Source => new("https://docs.litellm.ai/docs/proxy/user_keys", new DateOnly(2026, 9, 11), "Models are whatever the operator named them, served through the OpenAI-compatible chat completion API.");
/// <inheritdoc />
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor) => HostNaming.TrySplitOrganization(id, out inner, out declaredVendor);
}
@@ -0,0 +1,20 @@
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// Mistral's own platform, which by now also serves models Mistral did not build.
/// </summary>
/// <remarks>
/// It names those under their plain names rather than prefixing them, so there is nothing to
/// unwrap here. Which model it is remains a question for the rules; what this host settles is that
/// whatever answers, it answers through Mistral's own API.
/// </remarks>
public sealed class HostMistral : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.MISTRAL;
/// <inheritdoc />
public override ModelSource Source => new("https://docs.mistral.ai/api/", new DateOnly(2026, 9, 11), "Models are named plainly, its own and the open weights it hosts alike.");
}
@@ -0,0 +1,28 @@
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// OpenAI's own cloud, the one place where the Responses API is actually spoken.
/// </summary>
/// <remarks>
/// This is the single host that does not put its models on the ordinary chat completion API,
/// because it is the single place the app sends a Responses API request from. Everywhere else a
/// GPT model is reached -- a gateway, a reseller, somebody's own proxy -- it is reached through the
/// ordinary API, and the host there says so.
/// </remarks>
public sealed class HostOpenAI : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.OPEN_AI;
/// <inheritdoc />
public override ModelSource Source => new("https://platform.openai.com/docs/api-reference/responses", new DateOnly(2026, 9, 11), "Models are named plainly, and both the Responses API and the chat completion API are served here.");
/// <inheritdoc />
/// <remarks>
/// Nothing is taken away: whichever of the two APIs a model states, it can be reached through
/// it here.
/// </remarks>
public override ModelProfile ApplyTransport(in ModelProfile profile) => profile;
}
@@ -0,0 +1,25 @@
using AIStudio.Models.Matching;
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// OpenRouter, which serves other people's models and says whose they are.
/// </summary>
/// <remarks>
/// The vendor prefix is the reason the old rules delegated between vendors in circles: a name such
/// as "anthropic/claude-opus-5" had to be handed to whoever knew Claude, and the same for every
/// other vendor. Here the prefix is simply taken off, and the vendor stated, and one set of rules
/// answers the bare name -- no matter which provider it arrived from.
/// </remarks>
public sealed class HostOpenRouter : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.OPEN_ROUTER;
/// <inheritdoc />
public override ModelSource Source => new("https://openrouter.ai/docs/api-reference/overview", new DateOnly(2026, 9, 11), "Models are named \"vendor/model\", and every one of them is served through the OpenAI-compatible chat completion API.");
/// <inheritdoc />
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor) => HostNaming.TrySplitOrganization(id, out inner, out declaredVendor);
}
@@ -0,0 +1,15 @@
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// Perplexity's own API.
/// </summary>
public sealed class HostPerplexity : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.PERPLEXITY;
/// <inheritdoc />
public override ModelSource Source => new("https://docs.perplexity.ai/api-reference/chat-completions-post", new DateOnly(2026, 9, 11), "Models are named plainly, and there is one API to reach them through.");
}
@@ -0,0 +1,31 @@
using AIStudio.Models.Matching;
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// Somebody's own engine: Ollama, LM Studio, vLLM, llama.cpp, or a proxy in front of them.
/// </summary>
/// <remarks>
/// vLLM serves whatever it was pointed at, and what it was pointed at is usually a hub repository:
/// "meta-llama/Llama-3.3-70B-Instruct", "01-ai/yi-large". So the organization comes off here too.
///
/// The colon does not. Ollama writes the variant after it -- "qwen3.8:27b-mlx" -- and taking that
/// off would leave a name which no longer says which build of the model is running. Only the host
/// which actually has a router treats a colon as routing.
///
/// Whatever the engine can do beyond this, only the engine knows: how large a context window the
/// operator configured, how many images it accepts. Those come from the model list of the running
/// installation, not from a rule written here.
/// </remarks>
public sealed class HostSelfHosted : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.SELF_HOSTED;
/// <inheritdoc />
public override ModelSource Source => new("https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html", new DateOnly(2026, 9, 11), "Models are named as the operator loaded them, often as a hub repository, and served through the OpenAI-compatible chat completion API.");
/// <inheritdoc />
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor) => HostNaming.TrySplitOrganization(id, out inner, out declaredVendor);
}
@@ -0,0 +1,15 @@
using AIStudio.Provider;
namespace AIStudio.Models.Hosting.Hosts;
/// <summary>
/// xAI's own API, where Grok comes from.
/// </summary>
public sealed class HostX : ModelHost
{
/// <inheritdoc />
public override LLMProviders Provider => LLMProviders.X;
/// <inheritdoc />
public override ModelSource Source => new("https://docs.x.ai/docs/api-reference", new DateOnly(2026, 9, 11), "Models are named plainly, and the app reaches them through the OpenAI-compatible endpoint.");
}