From 9858881ce63a3eccdbbe49878dd4d2b88eaa2bc6 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Mon, 7 Sep 2026 04:41:13 +0100 Subject: [PATCH] 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. --- agent/app/provider/catalog.go | 10 ++++++++++ agent/app/provider/openclaw.go | 4 ++-- agent/app/provider/verify.go | 4 ++-- agent/app/service/agents_utils.go | 2 +- agent/utils/terminal/ai/client.go | 2 +- agent/utils/terminal/ai/config_runtime.go | 2 +- frontend/src/utils/agent.ts | 2 +- frontend/src/views/ai/agents/model/add/index.vue | 2 +- 8 files changed, 19 insertions(+), 9 deletions(-) diff --git a/agent/app/provider/catalog.go b/agent/app/provider/catalog.go index b5d00eed6..e993b5279 100644 --- a/agent/app/provider/catalog.go +++ b/agent/app/provider/catalog.go @@ -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"}, diff --git a/agent/app/provider/openclaw.go b/agent/app/provider/openclaw.go index 9f6861554..bea657fd0 100644 --- a/agent/app/provider/openclaw.go +++ b/agent/app/provider/openclaw.go @@ -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, "/") diff --git a/agent/app/provider/verify.go b/agent/app/provider/verify.go index 9114f9fbe..f7da24811 100644 --- a/agent/app/provider/verify.go +++ b/agent/app/provider/verify.go @@ -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{}{ diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go index 7153f5824..068db9bd2 100644 --- a/agent/app/service/agents_utils.go +++ b/agent/app/service/agents_utils.go @@ -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 diff --git a/agent/utils/terminal/ai/client.go b/agent/utils/terminal/ai/client.go index 9af2a0cb9..e5be67e17 100644 --- a/agent/utils/terminal/ai/client.go +++ b/agent/utils/terminal/ai/client.go @@ -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) diff --git a/agent/utils/terminal/ai/config_runtime.go b/agent/utils/terminal/ai/config_runtime.go index 427141431..155288c7e 100644 --- a/agent/utils/terminal/ai/config_runtime.go +++ b/agent/utils/terminal/ai/config_runtime.go @@ -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{ diff --git a/frontend/src/utils/agent.ts b/frontend/src/utils/agent.ts index 3dba06c47..9170949bb 100644 --- a/frontend/src/utils/agent.ts +++ b/frontend/src/utils/agent.ts @@ -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'; diff --git a/frontend/src/views/ai/agents/model/add/index.vue b/frontend/src/views/ai/agents/model/add/index.vue index f40d26158..f955e83da 100644 --- a/frontend/src/views/ai/agents/model/add/index.vue +++ b/frontend/src/views/ai/agents/model/add/index.vue @@ -271,7 +271,7 @@ const manualVerifyIndex = ref(-1); const editModels = ref([]); 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({