mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-14 10:13: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
@@ -557,21 +557,7 @@ public static class LLMProvidersExtensions
|
||||
public static bool CanLoadModels(this LLMProviders provider, Host host, string? apiKey)
|
||||
{
|
||||
if (provider is LLMProviders.SELF_HOSTED)
|
||||
{
|
||||
switch (host)
|
||||
{
|
||||
case Host.NONE:
|
||||
case Host.WHISPER_CPP:
|
||||
default:
|
||||
return false;
|
||||
|
||||
case Host.LLAMA_CPP:
|
||||
case Host.OLLAMA:
|
||||
case Host.LM_STUDIO:
|
||||
case Host.VLLM:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return host.IsModelListSupported();
|
||||
|
||||
if(provider is LLMProviders.NONE)
|
||||
return false;
|
||||
|
||||
@@ -125,6 +125,13 @@ public static class ReasoningDispatcher
|
||||
|
||||
Host.VLLM => [ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT, ReasoningDialect.QWEN_THINKING, ReasoningDialect.GOOGLE_THINKING, ReasoningDialect.VLLM],
|
||||
|
||||
//
|
||||
// llmman starts llama.cpp for GGUF models and vLLM, SGLang, or MLX for the others, and
|
||||
// hands their fields through unchanged. Which engine serves a model is up to llmman, so
|
||||
// it is read in everything llama.cpp and vLLM are read in:
|
||||
//
|
||||
Host.LLMMAN => [ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT, ReasoningDialect.QWEN_THINKING, ReasoningDialect.GOOGLE_THINKING, ReasoningDialect.LLAMA_CPP, ReasoningDialect.VLLM],
|
||||
|
||||
_ => EVERYTHING_A_GATEWAY_MIGHT_SERVE,
|
||||
},
|
||||
|
||||
|
||||
@@ -9,4 +9,5 @@ public enum Host
|
||||
WHISPER_CPP,
|
||||
OLLAMA,
|
||||
VLLM,
|
||||
LLMMAN,
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public static class HostExtensions
|
||||
Host.WHISPER_CPP => "whisper.cpp",
|
||||
Host.OLLAMA => "ollama",
|
||||
Host.VLLM => "vLLM",
|
||||
Host.LLMMAN => "llmman",
|
||||
|
||||
_ => "Unknown",
|
||||
};
|
||||
@@ -47,6 +48,7 @@ public static class HostExtensions
|
||||
case Host.VLLM:
|
||||
case Host.LM_STUDIO:
|
||||
case Host.LLAMA_CPP:
|
||||
case Host.LLMMAN:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -58,6 +60,7 @@ public static class HostExtensions
|
||||
case Host.LM_STUDIO:
|
||||
case Host.OLLAMA:
|
||||
case Host.VLLM:
|
||||
case Host.LLMMAN:
|
||||
return true;
|
||||
|
||||
default:
|
||||
@@ -73,12 +76,44 @@ public static class HostExtensions
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ namespace AIStudio.Provider.SelfHosted;
|
||||
/// <remarks>
|
||||
/// The context window is vLLM's addition to that route: it reports the window the operator started
|
||||
/// the engine with, which is the one number no rule about the weights could ever know. Ollama,
|
||||
/// LM Studio, and llama.cpp answer the same route without it, so it stays unknown there instead of
|
||||
/// being guessed.
|
||||
/// LM Studio, llama.cpp, and llmman answer the same route without it, so it stays unknown there
|
||||
/// instead of being guessed.
|
||||
///
|
||||
/// vLLM calls that field max_model_len, which reads like a limit on the model rather than on a
|
||||
/// conversation. The wire keeps their spelling, and this record says what the number means, so that
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
public override string InstanceName { get; set; } = "Self-hosted";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => host is Host.OLLAMA or Host.LM_STUDIO or Host.VLLM or Host.LLAMA_CPP;
|
||||
public override bool HasModelLoadingCapability => host.IsModelListSupported();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
@@ -40,7 +40,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
{
|
||||
// Build the list of messages. The image format depends on the host:
|
||||
// - Ollama uses the direct image URL format: { "type": "image_url", "image_url": "data:..." }
|
||||
// - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } }
|
||||
// - LM Studio, vLLM, llama.cpp, and llmman use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } }
|
||||
var messages = host switch
|
||||
{
|
||||
Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.CreateSettingsProvider(effectiveChatModel)),
|
||||
@@ -94,22 +94,17 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (host)
|
||||
if (host is Host.LLAMA_CPP)
|
||||
return await this.LoadLlamaCppTextModels(apiKeyProvisional, token);
|
||||
|
||||
if (!host.IsChatSupported() || !host.IsModelListSupported())
|
||||
return ModelLoadResult.FromModels([]);
|
||||
|
||||
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token);
|
||||
return result with
|
||||
{
|
||||
case Host.LLAMA_CPP:
|
||||
return await this.LoadLlamaCppTextModels(apiKeyProvisional, token);
|
||||
|
||||
case Host.LM_STUDIO:
|
||||
case Host.OLLAMA:
|
||||
case Host.VLLM:
|
||||
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(model => model.IsChatModel(this.Provider))]
|
||||
};
|
||||
}
|
||||
|
||||
return ModelLoadResult.FromModels([]);
|
||||
Models = [..result.Models.Where(model => model.IsChatModel(this.Provider))]
|
||||
};
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -128,19 +123,14 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (host)
|
||||
{
|
||||
case Host.LM_STUDIO:
|
||||
case Host.OLLAMA:
|
||||
case Host.VLLM:
|
||||
var result = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, apiKeyProvisional, token);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(model => model.IsEmbeddingModel(this.Provider))]
|
||||
};
|
||||
}
|
||||
if (!host.IsEmbeddingSupported() || !host.IsModelListSupported())
|
||||
return ModelLoadResult.FromModels([]);
|
||||
|
||||
return ModelLoadResult.FromModels([]);
|
||||
var result = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, apiKeyProvisional, token);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(model => model.IsEmbeddingModel(this.Provider))]
|
||||
};
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -154,33 +144,28 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (host)
|
||||
{
|
||||
case Host.WHISPER_CPP:
|
||||
return ModelLoadResult.FromModels(
|
||||
[
|
||||
new Provider.Model("loaded-model", TB("Model as configured by whisper.cpp")),
|
||||
]);
|
||||
|
||||
//
|
||||
// These two answer the models endpoint with everything they serve, and nothing in
|
||||
// that answer says which of them listens. Asking what each model is made for is the
|
||||
// only thing standing between this list and every chat and embedding model of the
|
||||
// installation, which is what it used to hold. An engine running no speech model at
|
||||
// all therefore offers nothing here, and says so, rather than offering models which
|
||||
// would fail the moment audio reaches them.
|
||||
//
|
||||
case Host.OLLAMA:
|
||||
case Host.VLLM:
|
||||
var result = await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, apiKeyProvisional, token);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(model => model.IsTranscriptionModel(this.Provider))]
|
||||
};
|
||||
if (host is Host.WHISPER_CPP)
|
||||
return ModelLoadResult.FromModels(
|
||||
[
|
||||
new Provider.Model("loaded-model", TB("Model as configured by whisper.cpp")),
|
||||
]);
|
||||
|
||||
default:
|
||||
return ModelLoadResult.FromModels([]);
|
||||
}
|
||||
if (!host.IsTranscriptionSupported() || !host.IsModelListSupported())
|
||||
return ModelLoadResult.FromModels([]);
|
||||
|
||||
//
|
||||
// The engines which get here answer the models endpoint with everything they serve, and
|
||||
// nothing in that answer says which of them listens. Asking what each model is made for
|
||||
// is the only thing standing between this list and every chat and embedding model of
|
||||
// the installation, which is what it used to hold. An engine running no speech model at
|
||||
// all therefore offers nothing here, and says so, rather than offering models which
|
||||
// would fail the moment audio reaches them.
|
||||
//
|
||||
var result = await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, apiKeyProvisional, token);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(model => model.IsTranscriptionModel(this.Provider))]
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user