feat(ai): add llmman as a local model provider (#13717)

llmman (https://github.com/llmmanorg/llmman) is a local model runner
serving Ollama- and OpenAI-compatible routes on 127.0.0.1:17434.
Register it in the agent provider catalog next to Ollama and extend
every Ollama special case (no API key, verification skipped, OpenClaw
placeholder key, manual initial model) to cover it as well.
This commit is contained in:
Eric Curtin
2026-09-07 04:41:13 +01:00
committed by GitHub
parent eb6a8c7646
commit 9858881ce6
8 changed files with 19 additions and 9 deletions

View File

@@ -46,6 +46,15 @@ var catalog = map[string]Meta{
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"),
},
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
"llmman": {
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
APIConfigs: []APIConfig{
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
{APIType: "openai-completions", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
{APIType: "openai-embeddings", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
},
},
"vllm": {
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
@@ -453,6 +462,7 @@ var legacyModelPrefixes = map[string][]string{
"custom": {"custom"},
"vllm": {"custom"},
"ollama": {"ollama"},
"llmman": {"llmman"},
"deepseek": {"deepseek"},
"bailian-coding-plan": {"bailian-coding-plan"},
"ark-coding-plan": {"ark-coding-plan"},

View File

@@ -43,8 +43,8 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
providerKey = "moonshot"
resolvedAPIType = "openai-completions"
usesBearer = false
case "ollama":
apiKey = "ollama"
case "ollama", "llmman":
apiKey = provider
usesBearer = false
case "openai", "openrouter", "anthropic":
preserveQualifiedModel = strings.Contains(modelName, "/")

View File

@@ -34,7 +34,7 @@ const (
func SkipVerification(provider string) bool {
switch provider {
case "vllm", "ollama", "kimi-coding":
case "vllm", "ollama", "llmman", "kimi-coding":
return true
default:
return false
@@ -128,7 +128,7 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": defaultVerifyMaxTokens, "stream": false})
default:
request.URL = baseURL + "/chat/completions"
if provider != "ollama" || strings.TrimSpace(apiKey) != "" {
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
headers["Authorization"] = "Bearer " + apiKey
}
request.Body = mustJSON(map[string]interface{}{

View File

@@ -1338,7 +1338,7 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
func requiresInitialAgentAccountModels(provider string) bool {
switch provider {
case "custom", "vllm", "ollama":
case "custom", "vllm", "ollama", "llmman":
return true
default:
return false

View File

@@ -92,7 +92,7 @@ func NewClient(cfg ClientConfig) (Client, error) {
if strings.TrimSpace(cfg.Model) == "" {
return nil, fmt.Errorf("model is required")
}
if strings.TrimSpace(cfg.APIKey) == "" && providerKey != "ollama" {
if strings.TrimSpace(cfg.APIKey) == "" && providerKey != "ollama" && providerKey != "llmman" {
return nil, fmt.Errorf("api key is required")
}
baseURL := normalizeBaseURL(providerKey, cfg.BaseURL)

View File

@@ -68,7 +68,7 @@ func ResolveGeneratorConfig(accountID uint) (GeneratorConfig, time.Duration, err
if apiKey == "" {
apiKey = lookupProviderAPIKey(provider)
}
if apiKey == "" && provider != "ollama" {
if apiKey == "" && provider != "ollama" && provider != "llmman" {
return GeneratorConfig{}, 0, fmt.Errorf("agent account api key is required")
}
return GeneratorConfig{

View File

@@ -16,7 +16,7 @@ export const getAgentProviderDisplayName = (provider: string, displayName?: stri
};
export const isAgentAccountVerificationSkipped = (provider?: string): boolean =>
['vllm', 'ollama', 'kimi-coding'].includes((provider || '').toLowerCase());
['vllm', 'ollama', 'llmman', 'kimi-coding'].includes((provider || '').toLowerCase());
export const supportsAgentModelConfig = (agentType: string): boolean => {
return agentType === 'openclaw' || agentType === 'hermes-agent';

View File

@@ -271,7 +271,7 @@ const manualVerifyIndex = ref(-1);
const editModels = ref<AI.AgentAccountModel[]>([]);
const modelConfigMode = ref<'discover' | 'manual'>('discover');
const modelDiscoveryFailed = ref(false);
const initialModelProviders = ['custom', 'vllm', 'ollama'];
const initialModelProviders = ['custom', 'vllm', 'ollama', 'llmman'];
const { isIntl } = useGlobalStore();
const form = reactive({