mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-15 12:33:48 +00:00
Add llmman as a self-hosted host for chat and embedding providers (#947)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
2bd3d41421
commit
88d1f4d96a
20 files changed
+270
-89
No files matched your search
@@ -66,6 +66,14 @@ public static class ModelKindCorpus
|
||||
new(SELF_HOSTED, "LaBSE", EMBEDDING),
|
||||
new(SELF_HOSTED, "instructor-xl", EMBEDDING),
|
||||
new(SELF_HOSTED, "gtr-t5-large", EMBEDDING),
|
||||
|
||||
//
|
||||
// llmman lists every model by the full reference it pulled it from: a registry, a namespace,
|
||||
// the repository, and a tag. Whatever is in front of the model has to come off before the
|
||||
// word which says what it is for can be found:
|
||||
//
|
||||
new(SELF_HOSTED, "docker.io/ai/embeddinggemma:latest", EMBEDDING),
|
||||
new(SELF_HOSTED, "docker.io/ai/nomic-embed-text-v1.5:latest", EMBEDDING),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
@@ -144,6 +152,9 @@ public static class ModelKindCorpus
|
||||
new(ALIBABA_CLOUD, "qwen3-asr-flash", TRANSCRIPTION),
|
||||
new(ALIBABA_CLOUD, "qwen3-asr-1.7b", TRANSCRIPTION),
|
||||
new(ALIBABA_CLOUD, "qwen-audio-3.0-asr-flash-streaming", TRANSCRIPTION),
|
||||
|
||||
// Voxtral as llmman lists it, from the GGUF build on Hugging Face which llama.cpp serves:
|
||||
new(SELF_HOSTED, "hf.co/ggml-org/Voxtral-Mini-3B-2507-GGUF:latest", TRANSCRIPTION),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
@@ -395,6 +406,14 @@ public static class ModelKindCorpus
|
||||
new(GROQ, "groq/compound", CHAT),
|
||||
new(GROQ, "groq/compound-mini", CHAT),
|
||||
new(GOOGLE, "gemini-robotics-er-1.5-preview", CHAT),
|
||||
|
||||
//
|
||||
// Chat models as llmman lists them, with the registry, the namespace, and the tag still in
|
||||
// front of and behind the model. None of these parts may turn one into something else:
|
||||
//
|
||||
new(SELF_HOSTED, "hf.co/unsloth/Qwen3.5-0.8B-GGUF:latest", CHAT),
|
||||
new(SELF_HOSTED, "docker.io/ai/qwen3.5:0.8b", CHAT),
|
||||
new(SELF_HOSTED, "hf.co/HuggingFaceTB/SmolLM2-135M-Instruct:latest", CHAT),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -172,6 +172,25 @@ public sealed class ModelHostTests
|
||||
Assert.That(unwrapped.Original, Is.EqualTo("qwen3.8:27b-mlx"));
|
||||
}
|
||||
|
||||
[TestCase("hf.co/HuggingFaceTB/SmolLM2-135M-Instruct:latest", "SmolLM2-135M-Instruct:latest", ModelVendor.HUGGING_FACE, TestName = "Behind the registry, the organization from the hub names the vendor")]
|
||||
[TestCase("hf.co/unsloth/Qwen3.5-0.8B-GGUF:latest", "Qwen3.5-0.8B-GGUF:latest", null, TestName = "An organization which only repackaged the weights names nobody")]
|
||||
[TestCase("docker.io/ai/qwen3.5:0.8b", "qwen3.5:0.8b", null, TestName = "The registry and its namespace come off and the variant stays")]
|
||||
public void AnLlmmanReferenceLosesItsRegistryAndKeepsItsTag(string modelId, string expectedModel, ModelVendor? expectedVendor)
|
||||
{
|
||||
//
|
||||
// llmman lists the reference it pulled a model from, with the registry in front and the tag
|
||||
// behind. The registry is a path segment like any organization and comes off the same way.
|
||||
// The tag is what Ollama writes after its colon, too, and stays for the same reason.
|
||||
//
|
||||
var unwrapped = Unwrap(LLMProviders.SELF_HOSTED, modelId, out var vendor);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(unwrapped.Original, Is.EqualTo(expectedModel));
|
||||
Assert.That(vendor, Is.EqualTo(expectedVendor));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AResellerLeavesTheNameAloneAndOnlyTakesTheApiAway()
|
||||
{
|
||||
|
||||
@@ -46,11 +46,29 @@ public sealed class ReasoningDispatcherTests
|
||||
[TestCase(Host.LLAMA_CPP, """ "reasoning_budget": 0 """, ReasoningConfigurationState.EXPLICITLY_DISABLED)]
|
||||
[TestCase(Host.VLLM, """ "thinking_token_budget": 2048 """, ReasoningConfigurationState.EXPLICITLY_ENABLED)]
|
||||
[TestCase(Host.VLLM, """ "chat_template_kwargs": { "thinking": true } """, ReasoningConfigurationState.EXPLICITLY_ENABLED)]
|
||||
[TestCase(Host.LLMMAN, """ "reasoning_budget": 0 """, ReasoningConfigurationState.EXPLICITLY_DISABLED, Description = "llmman serves GGUF models with llama.cpp and hands the budget to it.")]
|
||||
[TestCase(Host.LLMMAN, """ "thinking_token_budget": 2048 """, ReasoningConfigurationState.EXPLICITLY_ENABLED, Description = "llmman serves other models with vLLM and hands the budget to it.")]
|
||||
public void EachSelfHostedEngineIsReadInItsOwn(Host host, string parameters, ReasoningConfigurationState wanted)
|
||||
{
|
||||
Assert.That(ReasoningDispatcher.WhatTheParametersSay(LLMProviders.SELF_HOSTED, host, parameters), Is.EqualTo(wanted));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LlmmanIsReadInEverythingItsEnginesAreReadIn()
|
||||
{
|
||||
//
|
||||
// llmman decides by itself whether llama.cpp or vLLM serves a model. A dialect either of
|
||||
// them learns later has to reach llmman as well, or a switch set for llmman goes unseen:
|
||||
//
|
||||
var llmman = ReasoningDispatcher.DialectsOf(LLMProviders.SELF_HOSTED, Host.LLMMAN);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(llmman, Is.SupersetOf(ReasoningDispatcher.DialectsOf(LLMProviders.SELF_HOSTED, Host.LLAMA_CPP)));
|
||||
Assert.That(llmman, Is.SupersetOf(ReasoningDispatcher.DialectsOf(LLMProviders.SELF_HOSTED, Host.VLLM)));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AProviderIsNotReadInADialectItDoesNotSpeak()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Provider.SelfHosted;
|
||||
|
||||
namespace AIStudio.Tests.Provider.SelfHosted;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that what each self-hosted engine can do is stated once, and that the statements fit together.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Which engines can list their models used to be written down twice, once for the provider and once
|
||||
/// for the dialogs, and the loading of the chat, embedding, and transcription lists repeated the
|
||||
/// engines a third time. A new engine had to be added to each of these lists, and one it was
|
||||
/// forgotten in left a list empty or a reload button disabled without anything failing. Now the
|
||||
/// host extensions state it, and these cases check that every other answer follows from them.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class HostExtensionsTests
|
||||
{
|
||||
private static readonly Host[] ALL_HOSTS = Enum.GetValues<Host>();
|
||||
|
||||
[TestCaseSource(nameof(ALL_HOSTS))]
|
||||
public void TheDialogsOfferToLoadModelsExactlyWhereTheEngineCanListThem(Host host)
|
||||
{
|
||||
Assert.That(LLMProviders.SELF_HOSTED.CanLoadModels(host, null), Is.EqualTo(host.IsModelListSupported()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryEngineForChatsCanListItsModels()
|
||||
{
|
||||
//
|
||||
// There is no fixed chat model to offer instead, so such an engine would offer an empty list:
|
||||
//
|
||||
var unlisted = ALL_HOSTS.Where(host => host is not Host.NONE && host.IsChatSupported() && !host.IsModelListSupported());
|
||||
Assert.That(unlisted, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryEngineForEmbeddingsCanListItsModels()
|
||||
{
|
||||
//
|
||||
// There is no fixed embedding model to offer instead, so such an engine would offer an empty list:
|
||||
//
|
||||
var unlisted = ALL_HOSTS.Where(host => host.IsEmbeddingSupported() && !host.IsModelListSupported());
|
||||
Assert.That(unlisted, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OnlyWhisperCppTranscribesWithoutListingItsModels()
|
||||
{
|
||||
//
|
||||
// The provider offers the one model of whisper.cpp under a fixed name. Any other engine
|
||||
// which transcribes but cannot list its models would offer an empty list:
|
||||
//
|
||||
var unlisted = ALL_HOSTS.Where(host => host.IsTranscriptionSupported() && !host.IsModelListSupported());
|
||||
Assert.That(unlisted, Is.EqualTo(new[] { Host.WHISPER_CPP }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheHostNoneLoadsNoModels()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(Host.NONE.IsModelListSupported(), Is.False);
|
||||
Assert.That(Host.NONE.IsEmbeddingSupported(), Is.False);
|
||||
Assert.That(Host.NONE.IsTranscriptionSupported(), Is.False);
|
||||
Assert.That(LLMProviders.SELF_HOSTED.CanLoadModels(Host.NONE, "a key"), Is.False);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user