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({