diff --git a/config/config.example.json b/config/config.example.json
index ff2969dcb..814c82503 100644
--- a/config/config.example.json
+++ b/config/config.example.json
@@ -48,6 +48,10 @@
"model": "deepseek/deepseek-chat",
"api_key": "sk-your-deepseek-key"
},
+ {
+ "model_name": "lmstudio-local",
+ "model": "lmstudio/openai/gpt-oss-20b"
+ },
{
"model_name": "longcat",
"model": "longcat/LongCat-Flash-Thinking",
diff --git a/docs/configuration.md b/docs/configuration.md
index 61eb24b14..58930cbfa 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -563,6 +563,7 @@ For complete documentation, see [`security_configuration.md`](security_configura
| **通义千问 (Qwen)** | `qwen/` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | [Get Key](https://dashscope.console.aliyun.com) |
| **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [Get Key](https://build.nvidia.com) |
| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | Local (no key needed) |
+| **LM Studio** | `lmstudio/` | `http://localhost:1234/v1` | OpenAI | Optional (local default: no key) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Get Key](https://openrouter.ai/keys) |
| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | Your LiteLLM proxy key |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
@@ -710,6 +711,21 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
+
+LM Studio (local)
+
+```json
+{
+ "model_name": "lmstudio-local",
+ "model": "lmstudio/openai/gpt-oss-20b"
+}
+```
+
+`api_base` defaults to `http://localhost:1234/v1`. API key is optional unless your LM Studio server enables authentication.
+PicoClaw sends OpenAI-compatible requests to LM Studio, and strips the `lmstudio/` prefix before sending requests, so `lmstudio/openai/gpt-oss-20b` sends `openai/gpt-oss-20b` to the LM Studio server.
+
+
+
Custom Proxy / LiteLLM
diff --git a/docs/providers.md b/docs/providers.md
index 43cba55b5..f45aa5f3b 100644
--- a/docs/providers.md
+++ b/docs/providers.md
@@ -56,6 +56,7 @@ This design also enables **multi-agent support** with flexible provider selectio
| **通义千问 (Qwen)** | `qwen/` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | [Get Key](https://dashscope.console.aliyun.com) |
| **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [Get Key](https://build.nvidia.com) |
| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | Local (no key needed) |
+| **LM Studio** | `lmstudio/` | `http://localhost:1234/v1` | OpenAI | Optional (local default: no key) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Get Key](https://openrouter.ai/keys) |
| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | Your LiteLLM proxy key |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
@@ -226,6 +227,18 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
}
```
+**LM Studio (local)**
+
+```json
+{
+ "model_name": "lmstudio-local",
+ "model": "lmstudio/openai/gpt-oss-20b"
+}
+```
+
+`api_base` defaults to `http://localhost:1234/v1`. API key is optional unless your LM Studio server enables authentication.
+PicoClaw sends OpenAI-compatible requests to LM Studio, and strips the `lmstudio/` prefix before sending requests, so `lmstudio/openai/gpt-oss-20b` sends `openai/gpt-oss-20b` to the LM Studio server.
+
**Custom Proxy/API**
```json
diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md
index d4366b6a8..a405df09c 100644
--- a/docs/zh/configuration.md
+++ b/docs/zh/configuration.md
@@ -365,6 +365,7 @@ Agent 读取 HEARTBEAT.md
| **通义千问 (Qwen)** | `qwen/` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | [获取](https://dashscope.console.aliyun.com) |
| **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [获取](https://build.nvidia.com) |
| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | 本地(无需 Key) |
+| **LM Studio** | `lmstudio/` | `http://localhost:1234/v1` | OpenAI | 可选(本地默认无需密钥) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取](https://openrouter.ai/keys) |
| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | 你的 LiteLLM 代理 Key |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 |
@@ -506,6 +507,21 @@ Agent 读取 HEARTBEAT.md
+
+LM Studio(本地)
+
+```json
+{
+ "model_name": "lmstudio-local",
+ "model": "lmstudio/openai/gpt-oss-20b"
+}
+```
+
+`api_base` 默认是 `http://localhost:1234/v1`。除非你在 LM Studio 侧启用了认证,否则不需要配置 API Key。
+PicoClaw 向 LM Studio 的 OpenAI 兼容终结点发送请求,且将移除首个 `lmstudio/` 前缀,因此 `lmstudio/openai/gpt-oss-20b` 会发送 `openai/gpt-oss-20b`。
+
+
+
自定义代理 / LiteLLM
diff --git a/docs/zh/providers.md b/docs/zh/providers.md
index d19dcdd89..04b2f7a88 100644
--- a/docs/zh/providers.md
+++ b/docs/zh/providers.md
@@ -53,6 +53,7 @@
| **通义千问 (Qwen)** | `qwen/` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | [获取密钥](https://dashscope.console.aliyun.com) |
| **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [获取密钥](https://build.nvidia.com) |
| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | 本地(无需密钥) |
+| **LM Studio** | `lmstudio/` | `http://localhost:1234/v1` | OpenAI | 可选(本地默认无需密钥) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) |
| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | 你的 LiteLLM 代理密钥 |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 |
@@ -211,6 +212,18 @@
}
```
+**LM Studio(本地)**
+
+```json
+{
+ "model_name": "lmstudio-local",
+ "model": "lmstudio/openai/gpt-oss-20b"
+}
+```
+
+`api_base` 默认是 `http://localhost:1234/v1`。除非你在 LM Studio 侧启用了认证,否则不需要配置 API Key。
+PicoClaw 向 LM Studio 的 OpenAI 兼容终结点发送请求,且将移除首个 `lmstudio/` 前缀,因此 `lmstudio/openai/gpt-oss-20b` 会发送 `openai/gpt-oss-20b`。
+
**自定义代理/API**
```json
diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go
index 962e6ae19..fc4c59e08 100644
--- a/pkg/providers/factory_provider.go
+++ b/pkg/providers/factory_provider.go
@@ -154,13 +154,13 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
}
return provider, modelID, nil
- case "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
+ case "litellm", "lmstudio", "openrouter", "groq", "zhipu", "gemini", "nvidia",
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
"vivgrid", "volcengine", "vllm", "qwen", "qwen-intl", "qwen-international", "dashscope-intl",
"qwen-us", "dashscope-us", "mistral", "avian", "longcat", "modelscope", "novita",
"coding-plan", "alibaba-coding", "qwen-coding", "mimo":
// All other OpenAI-compatible HTTP providers
- if cfg.APIKey() == "" && cfg.APIBase == "" {
+ if cfg.APIKey() == "" && cfg.APIBase == "" && !isEmptyAPIKeyAllowed(protocol) {
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
}
apiBase := cfg.APIBase
@@ -294,6 +294,15 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
}
}
+func isEmptyAPIKeyAllowed(protocol string) bool {
+ switch protocol {
+ case "ollama", "vllm", "lmstudio":
+ return true
+ default:
+ return false
+ }
+}
+
// getDefaultAPIBase returns the default API base URL for a given protocol.
func getDefaultAPIBase(protocol string) string {
switch protocol {
@@ -303,6 +312,8 @@ func getDefaultAPIBase(protocol string) string {
return "https://openrouter.ai/api/v1"
case "litellm":
return "http://localhost:4000/v1"
+ case "lmstudio":
+ return "http://localhost:1234/v1"
case "novita":
return "https://api.novita.ai/openai"
case "groq":
diff --git a/pkg/providers/factory_provider_test.go b/pkg/providers/factory_provider_test.go
index f1fe02cc2..ebd887220 100644
--- a/pkg/providers/factory_provider_test.go
+++ b/pkg/providers/factory_provider_test.go
@@ -121,6 +121,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
{"vllm", "vllm"},
{"deepseek", "deepseek"},
{"ollama", "ollama"},
+ {"lmstudio", "lmstudio"},
{"longcat", "longcat"},
{"modelscope", "modelscope"},
{"mimo", "mimo"},
@@ -153,6 +154,12 @@ func TestGetDefaultAPIBase_LiteLLM(t *testing.T) {
}
}
+func TestGetDefaultAPIBase_LMStudio(t *testing.T) {
+ if got := getDefaultAPIBase("lmstudio"); got != "http://localhost:1234/v1" {
+ t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "lmstudio", got, "http://localhost:1234/v1")
+ }
+}
+
func TestCreateProviderFromConfig_LiteLLM(t *testing.T) {
cfg := &config.ModelConfig{
ModelName: "test-litellm",
@@ -173,6 +180,135 @@ func TestCreateProviderFromConfig_LiteLLM(t *testing.T) {
}
}
+func TestCreateProviderFromConfig_LMStudio_WithAPIKey(t *testing.T) {
+ cfg := &config.ModelConfig{
+ ModelName: "test-lmstudio",
+ Model: "lmstudio/openai/gpt-oss-20b",
+ }
+ cfg.SetAPIKey("test-key")
+
+ provider, modelID, err := CreateProviderFromConfig(cfg)
+ if err != nil {
+ t.Fatalf("CreateProviderFromConfig() error = %v", err)
+ }
+ if provider == nil {
+ t.Fatal("CreateProviderFromConfig() returned nil provider")
+ }
+ if modelID != "openai/gpt-oss-20b" {
+ t.Errorf("modelID = %q, want %q", modelID, "openai/gpt-oss-20b")
+ }
+ if _, ok := provider.(*HTTPProvider); !ok {
+ t.Fatalf("expected *HTTPProvider, got %T", provider)
+ }
+}
+
+func TestCreateProviderFromConfig_LMStudio_NoAPIKey(t *testing.T) {
+ cfg := &config.ModelConfig{
+ ModelName: "test-lmstudio",
+ Model: "lmstudio/openai/gpt-oss-20b",
+ }
+
+ provider, modelID, err := CreateProviderFromConfig(cfg)
+ if err != nil {
+ t.Fatalf("CreateProviderFromConfig() error = %v", err)
+ }
+ if provider == nil {
+ t.Fatal("CreateProviderFromConfig() returned nil provider")
+ }
+ if modelID != "openai/gpt-oss-20b" {
+ t.Errorf("modelID = %q, want %q", modelID, "openai/gpt-oss-20b")
+ }
+ if _, ok := provider.(*HTTPProvider); !ok {
+ t.Fatalf("expected *HTTPProvider, got %T", provider)
+ }
+}
+
+func TestCreateProviderFromConfig_Ollama_WithAPIKey(t *testing.T) {
+ cfg := &config.ModelConfig{
+ ModelName: "test-ollama",
+ Model: "ollama/llama3.1:8b",
+ }
+ cfg.SetAPIKey("test-key")
+
+ provider, modelID, err := CreateProviderFromConfig(cfg)
+ if err != nil {
+ t.Fatalf("CreateProviderFromConfig() error = %v", err)
+ }
+ if provider == nil {
+ t.Fatal("CreateProviderFromConfig() returned nil provider")
+ }
+ if modelID != "llama3.1:8b" {
+ t.Errorf("modelID = %q, want %q", modelID, "llama3.1:8b")
+ }
+ if _, ok := provider.(*HTTPProvider); !ok {
+ t.Fatalf("expected *HTTPProvider, got %T", provider)
+ }
+}
+
+func TestCreateProviderFromConfig_Ollama_NoAPIKey(t *testing.T) {
+ cfg := &config.ModelConfig{
+ ModelName: "test-ollama",
+ Model: "ollama/llama3.1:8b",
+ }
+
+ provider, modelID, err := CreateProviderFromConfig(cfg)
+ if err != nil {
+ t.Fatalf("CreateProviderFromConfig() error = %v", err)
+ }
+ if provider == nil {
+ t.Fatal("CreateProviderFromConfig() returned nil provider")
+ }
+ if modelID != "llama3.1:8b" {
+ t.Errorf("modelID = %q, want %q", modelID, "llama3.1:8b")
+ }
+ if _, ok := provider.(*HTTPProvider); !ok {
+ t.Fatalf("expected *HTTPProvider, got %T", provider)
+ }
+}
+
+func TestCreateProviderFromConfig_VLLM_WithAPIKey(t *testing.T) {
+ cfg := &config.ModelConfig{
+ ModelName: "test-vllm",
+ Model: "vllm/Qwen/Qwen3-8B",
+ }
+ cfg.SetAPIKey("test-key")
+
+ provider, modelID, err := CreateProviderFromConfig(cfg)
+ if err != nil {
+ t.Fatalf("CreateProviderFromConfig() error = %v", err)
+ }
+ if provider == nil {
+ t.Fatal("CreateProviderFromConfig() returned nil provider")
+ }
+ if modelID != "Qwen/Qwen3-8B" {
+ t.Errorf("modelID = %q, want %q", modelID, "Qwen/Qwen3-8B")
+ }
+ if _, ok := provider.(*HTTPProvider); !ok {
+ t.Fatalf("expected *HTTPProvider, got %T", provider)
+ }
+}
+
+func TestCreateProviderFromConfig_VLLM_NoAPIKey(t *testing.T) {
+ cfg := &config.ModelConfig{
+ ModelName: "test-vllm",
+ Model: "vllm/Qwen/Qwen3-8B",
+ }
+
+ provider, modelID, err := CreateProviderFromConfig(cfg)
+ if err != nil {
+ t.Fatalf("CreateProviderFromConfig() error = %v", err)
+ }
+ if provider == nil {
+ t.Fatal("CreateProviderFromConfig() returned nil provider")
+ }
+ if modelID != "Qwen/Qwen3-8B" {
+ t.Errorf("modelID = %q, want %q", modelID, "Qwen/Qwen3-8B")
+ }
+ if _, ok := provider.(*HTTPProvider); !ok {
+ t.Fatalf("expected *HTTPProvider, got %T", provider)
+ }
+}
+
func TestCreateProviderFromConfig_LongCat(t *testing.T) {
cfg := &config.ModelConfig{
ModelName: "test-longcat",