From e8de7e9b89a789e895be7bec092d42458075e477 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Fri, 4 Sep 2026 13:11:35 +0100 Subject: [PATCH] feat(ai): add llmman as a local model provider 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 b5d00eed69f9..e993b5279ffd 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 9f6861554bc4..bea657fd0475 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 9114f9fbe67e..f7da248111a9 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 7153f5824ace..068db9bd23b9 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 9af2a0cb9b42..e5be67e17926 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 427141431c43..155288c7eff8 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 3dba06c471de..9170949bb727 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 f40d26158571..f955e83da772 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({