diff --git a/cmd/picoclaw/internal/auth/helpers.go b/cmd/picoclaw/internal/auth/helpers.go
index 523f6a16a..10bb3a11c 100644
--- a/cmd/picoclaw/internal/auth/helpers.go
+++ b/cmd/picoclaw/internal/auth/helpers.go
@@ -59,7 +59,7 @@ func authLoginOpenAI(useDeviceCode bool, noBrowser bool) error {
// Update or add openai in ModelList
foundOpenAI := false
for i := range appCfg.ModelList {
- if isOpenAIModel(appCfg.ModelList[i].Model) {
+ if isOpenAIModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "oauth"
foundOpenAI = true
break
@@ -130,7 +130,7 @@ func authLoginGoogleAntigravity(noBrowser bool) error {
// Update or add antigravity in ModelList
foundAntigravity := false
for i := range appCfg.ModelList {
- if isAntigravityModel(appCfg.ModelList[i].Model) {
+ if isAntigravityModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "oauth"
foundAntigravity = true
break
@@ -206,7 +206,7 @@ func authLoginAnthropicSetupToken() error {
if err == nil {
found := false
for i := range appCfg.ModelList {
- if isAnthropicModel(appCfg.ModelList[i].Model) {
+ if isAnthropicModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "oauth"
found = true
break
@@ -282,7 +282,7 @@ func authLoginPasteToken(provider string) error {
// Update ModelList
found := false
for i := range appCfg.ModelList {
- if isAnthropicModel(appCfg.ModelList[i].Model) {
+ if isAnthropicModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "token"
found = true
break
@@ -300,7 +300,7 @@ func authLoginPasteToken(provider string) error {
// Update ModelList
found := false
for i := range appCfg.ModelList {
- if isOpenAIModel(appCfg.ModelList[i].Model) {
+ if isOpenAIModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "token"
found = true
break
@@ -342,15 +342,15 @@ func authLogoutCmd(provider string) error {
for i := range appCfg.ModelList {
switch provider {
case "openai":
- if isOpenAIModel(appCfg.ModelList[i].Model) {
+ if isOpenAIModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = ""
}
case "anthropic":
- if isAnthropicModel(appCfg.ModelList[i].Model) {
+ if isAnthropicModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = ""
}
case "google-antigravity", "antigravity":
- if isAntigravityModel(appCfg.ModelList[i].Model) {
+ if isAntigravityModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = ""
}
}
@@ -484,22 +484,20 @@ func authModelsCmd() error {
return nil
}
-// isAntigravityModel checks if a model string belongs to antigravity provider
-func isAntigravityModel(model string) bool {
- return model == "antigravity" ||
- model == "google-antigravity" ||
- strings.HasPrefix(model, "antigravity/") ||
- strings.HasPrefix(model, "google-antigravity/")
+// isAntigravityModel checks if a model config belongs to an Antigravity provider.
+func isAntigravityModel(modelCfg *config.ModelConfig) bool {
+ protocol, _ := providers.ExtractProtocol(modelCfg)
+ return protocol == "antigravity" || protocol == "google-antigravity"
}
-// isOpenAIModel checks if a model string belongs to openai provider
-func isOpenAIModel(model string) bool {
- return model == "openai" ||
- strings.HasPrefix(model, "openai/")
+// isOpenAIModel checks if a model config belongs to the OpenAI provider.
+func isOpenAIModel(modelCfg *config.ModelConfig) bool {
+ protocol, _ := providers.ExtractProtocol(modelCfg)
+ return protocol == "openai"
}
-// isAnthropicModel checks if a model string belongs to anthropic provider
-func isAnthropicModel(model string) bool {
- return model == "anthropic" ||
- strings.HasPrefix(model, "anthropic/")
+// isAnthropicModel checks if a model config belongs to the Anthropic provider.
+func isAnthropicModel(modelCfg *config.ModelConfig) bool {
+ protocol, _ := providers.ExtractProtocol(modelCfg)
+ return protocol == "anthropic"
}
diff --git a/cmd/picoclaw/internal/status/helpers.go b/cmd/picoclaw/internal/status/helpers.go
index e8e4fee9a..f80b1f9c7 100644
--- a/cmd/picoclaw/internal/status/helpers.go
+++ b/cmd/picoclaw/internal/status/helpers.go
@@ -3,12 +3,12 @@ package status
import (
"fmt"
"os"
- "strings"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
"github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/config"
+ "github.com/sipeed/picoclaw/pkg/providers"
)
func statusCmd() {
@@ -44,12 +44,13 @@ func statusCmd() {
// not depend on a legacy cfg.Providers field (which may not exist under some
// build tags). We infer provider availability from model_list entries.
hasProtocolKey := func(protocol string) bool {
- prefix := protocol + "/"
+ want := providers.NormalizeProvider(protocol)
for _, m := range cfg.ModelList {
if m == nil {
continue
}
- if strings.HasPrefix(m.Model, prefix) && m.APIKey() != "" {
+ got, _ := providers.ExtractProtocol(m)
+ if got == want && m.APIKey() != "" {
return true
}
}
@@ -67,12 +68,13 @@ func statusCmd() {
return "", false
}
findProtocolBase := func(protocol string) (string, bool) {
- prefix := protocol + "/"
+ want := providers.NormalizeProvider(protocol)
for _, m := range cfg.ModelList {
if m == nil {
continue
}
- if strings.HasPrefix(m.Model, prefix) && m.APIBase != "" {
+ got, _ := providers.ExtractProtocol(m)
+ if got == want && m.APIBase != "" {
return m.APIBase, true
}
}
diff --git a/cmd/picoclaw/internal/status/helpers_test.go b/cmd/picoclaw/internal/status/helpers_test.go
new file mode 100644
index 000000000..f037b6bfa
--- /dev/null
+++ b/cmd/picoclaw/internal/status/helpers_test.go
@@ -0,0 +1,89 @@
+package status
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/sipeed/picoclaw/pkg/config"
+)
+
+func captureStdout(t *testing.T, fn func()) string {
+ t.Helper()
+
+ oldStdout := os.Stdout
+ r, w, err := os.Pipe()
+ if err != nil {
+ t.Fatalf("os.Pipe() error = %v", err)
+ }
+ os.Stdout = w
+
+ fn()
+
+ _ = w.Close()
+ os.Stdout = oldStdout
+ defer r.Close()
+
+ var buf bytes.Buffer
+ if _, err := io.Copy(&buf, r); err != nil {
+ t.Fatalf("io.Copy() error = %v", err)
+ }
+ return buf.String()
+}
+
+func TestStatusCmd_RecognizesProviderFieldWithoutModelPrefix(t *testing.T) {
+ tmpDir := t.TempDir()
+ configPath := filepath.Join(tmpDir, "config.json")
+ workspace := filepath.Join(tmpDir, "workspace")
+ if err := os.MkdirAll(workspace, 0o755); err != nil {
+ t.Fatalf("os.MkdirAll() error = %v", err)
+ }
+
+ t.Setenv(config.EnvConfig, configPath)
+ t.Setenv(config.EnvHome, tmpDir)
+
+ cfg := &config.Config{
+ Agents: config.AgentsConfig{
+ Defaults: config.AgentDefaults{
+ ModelName: "gpt-5.4",
+ Workspace: workspace,
+ Provider: "openai",
+ MaxTokens: 65536,
+ Temperature: nil,
+ },
+ },
+ ModelList: []*config.ModelConfig{
+ {
+ ModelName: "gpt-5.4",
+ Provider: "openai",
+ Model: "gpt-5.4",
+ APIBase: "https://api.openai.com/v1",
+ APIKeys: config.SimpleSecureStrings("test-key"),
+ Enabled: true,
+ },
+ {
+ ModelName: "qwen-plus",
+ Provider: "qwen",
+ Model: "qwen-plus",
+ APIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ APIKeys: config.SimpleSecureStrings("test-key"),
+ Enabled: true,
+ },
+ },
+ }
+ if err := config.SaveConfig(configPath, cfg); err != nil {
+ t.Fatalf("config.SaveConfig() error = %v", err)
+ }
+
+ output := captureStdout(t, statusCmd)
+
+ if !strings.Contains(output, "OpenAI API: \u2713") {
+ t.Fatalf("status output missing OpenAI provider: %s", output)
+ }
+ if !strings.Contains(output, "Qwen API: \u2713") {
+ t.Fatalf("status output missing Qwen provider: %s", output)
+ }
+}
diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md
index bb58d5081..fc7c45fc7 100644
--- a/docs/guides/configuration.md
+++ b/docs/guides/configuration.md
@@ -493,7 +493,7 @@ The subagent has access to tools (message, web_search, etc.) and can communicate
### Model Configuration (model_list)
-> **What's New?** PicoClaw now uses a **model-centric** configuration approach. Simply specify `vendor/model` format (e.g., `zhipu/glm-4.7`) to add new providers — **zero code changes required!**
+> **What's New?** PicoClaw now prefers explicit `provider` + native `model` configuration (for example `"provider": "zhipu", "model": "glm-4.7"`). The legacy single-field `provider/model` form remains supported for compatibility when `provider` is omitted.
This design also enables **multi-agent support** with flexible provider selection:
@@ -546,7 +546,8 @@ chmod 600 ~/.picoclaw/.security.yml
"model_list": [
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4"
+ "provider": "openai",
+ "model": "gpt-5.4"
// api_key loaded from .security.yml
}
],
@@ -570,31 +571,31 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
#### All Supported Vendors
-| Vendor | `model` Prefix | Default API Base | Protocol | API Key |
+| Vendor | `provider` Value | Default API Base | Protocol | API Key |
| ----------------------- | ----------------- | --------------------------------------------------- | --------- | ---------------------------------------------------------------- |
-| **OpenAI** | `openai/` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) |
-| **Anthropic** | `anthropic/` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) |
-| **智谱 AI (GLM)** | `zhipu/` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
-| **DeepSeek** | `deepseek/` | `https://api.deepseek.com/v1` | OpenAI | [Get Key](https://platform.deepseek.com) |
-| **Google Gemini** | `gemini/` | `https://generativelanguage.googleapis.com/v1beta` | OpenAI | [Get Key](https://aistudio.google.com/api-keys) |
-| **Groq** | `groq/` | `https://api.groq.com/openai/v1` | OpenAI | [Get Key](https://console.groq.com) |
-| **Moonshot** | `moonshot/` | `https://api.moonshot.cn/v1` | OpenAI | [Get Key](https://platform.moonshot.cn) |
-| **通义千问 (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 |
-| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) |
-| **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://www.volcengine.com/activity/codingplan?utm_campaign=PicoClaw&utm_content=PicoClaw&utm_medium=devrel&utm_source=OWO&utm_term=PicoClaw) |
-| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | — |
-| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Get Key](https://www.byteplus.com) |
-| **Vivgrid** | `vivgrid/` | `https://api.vivgrid.com/v1` | OpenAI | [Get Key](https://vivgrid.com) |
-| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Get Key](https://longcat.chat/platform) |
-| **ModelScope (魔搭)** | `modelscope/` | `https://api-inference.modelscope.cn/v1` | OpenAI | [Get Token](https://modelscope.cn/my/tokens) |
-| **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth only |
-| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | — |
+| **OpenAI** | `openai` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) |
+| **Anthropic** | `anthropic` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) |
+| **智谱 AI (GLM)** | `zhipu` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
+| **DeepSeek** | `deepseek` | `https://api.deepseek.com/v1` | OpenAI | [Get Key](https://platform.deepseek.com) |
+| **Google Gemini** | `gemini` | `https://generativelanguage.googleapis.com/v1beta` | OpenAI | [Get Key](https://aistudio.google.com/api-keys) |
+| **Groq** | `groq` | `https://api.groq.com/openai/v1` | OpenAI | [Get Key](https://console.groq.com) |
+| **Moonshot** | `moonshot` | `https://api.moonshot.cn/v1` | OpenAI | [Get Key](https://platform.moonshot.cn) |
+| **通义千问 (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 |
+| **Cerebras** | `cerebras` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) |
+| **VolcEngine (Doubao)** | `volcengine` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://www.volcengine.com/activity/codingplan?utm_campaign=PicoClaw&utm_content=PicoClaw&utm_medium=devrel&utm_source=OWO&utm_term=PicoClaw) |
+| **神算云** | `shengsuanyun` | `https://router.shengsuanyun.com/api/v1` | OpenAI | — |
+| **BytePlus** | `byteplus` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Get Key](https://www.byteplus.com) |
+| **Vivgrid** | `vivgrid` | `https://api.vivgrid.com/v1` | OpenAI | [Get Key](https://vivgrid.com) |
+| **LongCat** | `longcat` | `https://api.longcat.chat/openai` | OpenAI | [Get Key](https://longcat.chat/platform) |
+| **ModelScope (魔搭)** | `modelscope` | `https://api-inference.modelscope.cn/v1` | OpenAI | [Get Token](https://modelscope.cn/my/tokens) |
+| **Antigravity** | `antigravity` | Google Cloud | Custom | OAuth only |
+| **GitHub Copilot** | `github-copilot` | `localhost:4321` | gRPC | — |
#### Basic Configuration
@@ -603,22 +604,26 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
"model_list": [
{
"model_name": "ark-code-latest",
- "model": "volcengine/ark-code-latest",
+ "provider": "volcengine",
+ "model": "ark-code-latest",
"api_keys": ["sk-your-api-key"]
},
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-your-openai-key"]
},
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6",
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6",
"api_keys": ["sk-ant-your-key"]
},
{
"model_name": "glm-4.7",
- "model": "zhipu/glm-4.7",
+ "provider": "zhipu",
+ "model": "glm-4.7",
"api_keys": ["your-zhipu-key"]
}
],
@@ -634,6 +639,13 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
>
> **Note**: The `enabled` field can be set to `false` to disable a model entry without removing it. When omitted, it defaults to `true` during migration for models that have API keys.
+Resolution rules:
+
+- Prefer explicit `"provider": "openai", "model": "gpt-5.4"`.
+- If `provider` is set, PicoClaw sends `model` unchanged.
+- If `provider` is omitted, PicoClaw treats the first `/` segment in `model` as the provider and everything after that first `/` as the runtime model ID.
+- This means `"model": "openrouter/openai/gpt-5.4"` still works as a compatibility form and sends `openai/gpt-5.4` to OpenRouter.
+
#### Vendor-Specific Examples
> **Tip**: You can omit `api_key` fields and store them in `.security.yml` for better security. See [Security Configuration](#-security-configuration-recommended).
@@ -644,7 +656,8 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
```json
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4"
+ "provider": "openai",
+ "model": "gpt-5.4"
// api_key: set in .security.yml
}
```
@@ -657,7 +670,8 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
```json
{
"model_name": "ark-code-latest",
- "model": "volcengine/ark-code-latest"
+ "provider": "volcengine",
+ "model": "ark-code-latest"
// api_key: set in .security.yml
}
```
@@ -670,7 +684,8 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
```json
{
"model_name": "glm-4.7",
- "model": "zhipu/glm-4.7"
+ "provider": "zhipu",
+ "model": "glm-4.7"
// api_key: set in .security.yml
}
```
@@ -683,7 +698,8 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
```json
{
"model_name": "deepseek-chat",
- "model": "deepseek/deepseek-chat"
+ "provider": "deepseek",
+ "model": "deepseek-chat"
// api_key: set in .security.yml
}
```
@@ -696,7 +712,8 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
```json
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6"
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6"
// api_key: set in .security.yml
}
```
@@ -708,7 +725,8 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
```json
{
"model_name": "claude-opus-4-6",
- "model": "anthropic-messages/claude-opus-4-6",
+ "provider": "anthropic-messages",
+ "model": "claude-opus-4-6",
"api_keys": ["sk-ant-your-key"],
"api_base": "https://api.anthropic.com"
}
@@ -724,7 +742,8 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
```json
{
"model_name": "llama3",
- "model": "ollama/llama3"
+ "provider": "ollama",
+ "model": "llama3"
}
```
@@ -736,12 +755,13 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
```json
{
"model_name": "lmstudio-local",
- "model": "lmstudio/openai/gpt-oss-20b"
+ "provider": "lmstudio",
+ "model": "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.
+With explicit `provider`, PicoClaw sends `openai/gpt-oss-20b` unchanged to LM Studio. The legacy compatibility form `"model": "lmstudio/openai/gpt-oss-20b"` still resolves to the same upstream model ID when `provider` is omitted.
@@ -751,13 +771,14 @@ PicoClaw sends OpenAI-compatible requests to LM Studio, and strips the `lmstudio
```json
{
"model_name": "my-custom-model",
- "model": "openai/custom-model",
+ "provider": "openai",
+ "model": "custom-model",
"api_base": "https://my-proxy.com/v1"
// api_key: set in .security.yml
}
```
-PicoClaw strips only the outer `litellm/` prefix before sending the request, so `litellm/lite-gpt4` sends `lite-gpt4`, while `litellm/openai/gpt-4o` sends `openai/gpt-4o`.
+With explicit `provider`, PicoClaw sends `model` unchanged. That means `"provider": "litellm", "model": "lite-gpt4"` sends `lite-gpt4`, while `"provider": "litellm", "model": "openai/gpt-4o"` sends `openai/gpt-4o`. The legacy compatibility forms `litellm/lite-gpt4` and `litellm/openai/gpt-4o` still resolve the same way when `provider` is omitted.
@@ -782,7 +803,8 @@ model_list:
"model_list": [
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_base": "https://api.openai.com/v1"
// api_keys loaded from .security.yml
}
@@ -797,13 +819,15 @@ model_list:
"model_list": [
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_base": "https://api1.example.com/v1",
"api_keys": ["sk-key1"]
},
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_base": "https://api2.example.com/v1",
"api_keys": ["sk-key2"]
}
@@ -862,7 +886,7 @@ This keeps the runtime lightweight while making new OpenAI-compatible backends m
{
"agents": {
"defaults": {
- "model": "anthropic/claude-opus-4-5"
+ "model_name": "claude-opus-4-5"
}
},
"session": {
diff --git a/docs/guides/configuration.zh.md b/docs/guides/configuration.zh.md
index ecaef6eb7..c5956d6b8 100644
--- a/docs/guides/configuration.zh.md
+++ b/docs/guides/configuration.zh.md
@@ -424,7 +424,7 @@ Agent 读取 HEARTBEAT.md
### 模型配置 (model_list)
-> **新特性:** PicoClaw 现在采用**以模型为中心**的配置方式。只需指定 `vendor/model` 格式(例如 `zhipu/glm-4.7`)即可接入新提供商——**无需修改任何代码!**
+> **新特性:** PicoClaw 现在优先推荐显式 `provider` + 原生 `model` 的配置方式,例如 `"provider": "zhipu", "model": "glm-4.7"`。如果未设置 `provider`,旧的单字段 `provider/model` 写法仍然兼容。
这一设计同时支持**多 Agent**场景,灵活选择提供商:
@@ -435,31 +435,31 @@ Agent 读取 HEARTBEAT.md
#### 所有支持的厂商
-| 厂商 | `model` 前缀 | 默认 API Base | 协议 | API Key |
+| 厂商 | `provider` 值 | 默认 API Base | 协议 | API Key |
| ----------------------- | ----------------- | --------------------------------------------------- | --------- | ---------------------------------------------------------------- |
-| **OpenAI** | `openai/` | `https://api.openai.com/v1` | OpenAI | [获取](https://platform.openai.com) |
-| **Anthropic** | `anthropic/` | `https://api.anthropic.com/v1` | Anthropic | [获取](https://console.anthropic.com) |
-| **智谱 AI (GLM)** | `zhipu/` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [获取](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
-| **DeepSeek** | `deepseek/` | `https://api.deepseek.com/v1` | OpenAI | [获取](https://platform.deepseek.com) |
-| **Google Gemini** | `gemini/` | `https://generativelanguage.googleapis.com/v1beta` | OpenAI | [获取](https://aistudio.google.com/api-keys) |
-| **Groq** | `groq/` | `https://api.groq.com/openai/v1` | OpenAI | [获取](https://console.groq.com) |
-| **Moonshot** | `moonshot/` | `https://api.moonshot.cn/v1` | OpenAI | [获取](https://platform.moonshot.cn) |
-| **通义千问 (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 | 本地 |
-| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [获取](https://cerebras.ai) |
-| **火山引擎 (豆包)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [获取](https://www.volcengine.com/activity/codingplan?utm_campaign=PicoClaw&utm_content=PicoClaw&utm_medium=devrel&utm_source=OWO&utm_term=PicoClaw) |
-| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | — |
-| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [获取](https://www.byteplus.com) |
-| **Vivgrid** | `vivgrid/` | `https://api.vivgrid.com/v1` | OpenAI | [获取](https://vivgrid.com) |
-| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [获取](https://longcat.chat/platform) |
-| **ModelScope (魔搭)** | `modelscope/` | `https://api-inference.modelscope.cn/v1` | OpenAI | [获取](https://modelscope.cn/my/tokens) |
-| **Antigravity** | `antigravity/` | Google Cloud | Custom | 仅 OAuth |
-| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | — |
+| **OpenAI** | `openai` | `https://api.openai.com/v1` | OpenAI | [获取](https://platform.openai.com) |
+| **Anthropic** | `anthropic` | `https://api.anthropic.com/v1` | Anthropic | [获取](https://console.anthropic.com) |
+| **智谱 AI (GLM)** | `zhipu` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [获取](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
+| **DeepSeek** | `deepseek` | `https://api.deepseek.com/v1` | OpenAI | [获取](https://platform.deepseek.com) |
+| **Google Gemini** | `gemini` | `https://generativelanguage.googleapis.com/v1beta` | OpenAI | [获取](https://aistudio.google.com/api-keys) |
+| **Groq** | `groq` | `https://api.groq.com/openai/v1` | OpenAI | [获取](https://console.groq.com) |
+| **Moonshot** | `moonshot` | `https://api.moonshot.cn/v1` | OpenAI | [获取](https://platform.moonshot.cn) |
+| **通义千问 (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 | 本地 |
+| **Cerebras** | `cerebras` | `https://api.cerebras.ai/v1` | OpenAI | [获取](https://cerebras.ai) |
+| **火山引擎 (豆包)** | `volcengine` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [获取](https://www.volcengine.com/activity/codingplan?utm_campaign=PicoClaw&utm_content=PicoClaw&utm_medium=devrel&utm_source=OWO&utm_term=PicoClaw) |
+| **神算云** | `shengsuanyun` | `https://router.shengsuanyun.com/api/v1` | OpenAI | — |
+| **BytePlus** | `byteplus` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [获取](https://www.byteplus.com) |
+| **Vivgrid** | `vivgrid` | `https://api.vivgrid.com/v1` | OpenAI | [获取](https://vivgrid.com) |
+| **LongCat** | `longcat` | `https://api.longcat.chat/openai` | OpenAI | [获取](https://longcat.chat/platform) |
+| **ModelScope (魔搭)** | `modelscope` | `https://api-inference.modelscope.cn/v1` | OpenAI | [获取](https://modelscope.cn/my/tokens) |
+| **Antigravity** | `antigravity` | Google Cloud | Custom | 仅 OAuth |
+| **GitHub Copilot** | `github-copilot` | `localhost:4321` | gRPC | — |
#### 基础配置
@@ -468,22 +468,26 @@ Agent 读取 HEARTBEAT.md
"model_list": [
{
"model_name": "ark-code-latest",
- "model": "volcengine/ark-code-latest",
+ "provider": "volcengine",
+ "model": "ark-code-latest",
"api_keys": ["sk-your-api-key"]
},
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-your-openai-key"]
},
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6",
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6",
"api_keys": ["sk-ant-your-key"]
},
{
"model_name": "glm-4.7",
- "model": "zhipu/glm-4.7",
+ "provider": "zhipu",
+ "model": "glm-4.7",
"api_keys": ["your-zhipu-key"]
}
],
@@ -495,6 +499,13 @@ Agent 读取 HEARTBEAT.md
}
```
+解析规则:
+
+- 推荐显式写成 `"provider": "openai", "model": "gpt-5.4"`。
+- 如果设置了 `provider`,PicoClaw 会将 `model` 原样发送。
+- 如果未设置 `provider`,PicoClaw 会把 `model` 第一个 `/` 之前的字段当作 provider,并把第一个 `/` 之后的全部内容当作最终模型 ID。
+- 这意味着 `"model": "openrouter/openai/gpt-5.4"` 这样的兼容写法仍然可用,并会把 `openai/gpt-5.4` 发送给 OpenRouter。
+
#### 各厂商配置示例
@@ -503,7 +514,8 @@ Agent 读取 HEARTBEAT.md
```json
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-..."]
}
```
@@ -516,7 +528,8 @@ Agent 读取 HEARTBEAT.md
```json
{
"model_name": "ark-code-latest",
- "model": "volcengine/ark-code-latest",
+ "provider": "volcengine",
+ "model": "ark-code-latest",
"api_keys": ["sk-..."]
}
```
@@ -529,7 +542,8 @@ Agent 读取 HEARTBEAT.md
```json
{
"model_name": "glm-4.7",
- "model": "zhipu/glm-4.7",
+ "provider": "zhipu",
+ "model": "glm-4.7",
"api_keys": ["your-key"]
}
```
@@ -542,7 +556,8 @@ Agent 读取 HEARTBEAT.md
```json
{
"model_name": "deepseek-chat",
- "model": "deepseek/deepseek-chat",
+ "provider": "deepseek",
+ "model": "deepseek-chat",
"api_keys": ["sk-..."]
}
```
@@ -555,7 +570,8 @@ Agent 读取 HEARTBEAT.md
```json
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6",
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6",
"api_keys": ["sk-ant-your-key"]
}
```
@@ -567,7 +583,8 @@ Agent 读取 HEARTBEAT.md
```json
{
"model_name": "claude-opus-4-6",
- "model": "anthropic-messages/claude-opus-4-6",
+ "provider": "anthropic-messages",
+ "model": "claude-opus-4-6",
"api_keys": ["sk-ant-your-key"],
"api_base": "https://api.anthropic.com"
}
@@ -583,7 +600,8 @@ Agent 读取 HEARTBEAT.md
```json
{
"model_name": "llama3",
- "model": "ollama/llama3"
+ "provider": "ollama",
+ "model": "llama3"
}
```
@@ -595,12 +613,13 @@ Agent 读取 HEARTBEAT.md
```json
{
"model_name": "lmstudio-local",
- "model": "lmstudio/openai/gpt-oss-20b"
+ "provider": "lmstudio",
+ "model": "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`。
+显式设置 `provider` 后,PicoClaw 会把 `openai/gpt-oss-20b` 原样发送给 LM Studio。旧的兼容写法 `"model": "lmstudio/openai/gpt-oss-20b"` 在未设置 `provider` 时也会解析成相同的上游模型 ID。
@@ -610,13 +629,14 @@ PicoClaw 向 LM Studio 的 OpenAI 兼容终结点发送请求,且将移除首
```json
{
"model_name": "my-custom-model",
- "model": "openai/custom-model",
+ "provider": "openai",
+ "model": "custom-model",
"api_base": "https://my-proxy.com/v1",
"api_keys": ["sk-..."]
}
```
-PicoClaw 只剥离最外层的 `litellm/` 前缀再发送请求,因此 `litellm/lite-gpt4` 发送 `lite-gpt4`,而 `litellm/openai/gpt-4o` 发送 `openai/gpt-4o`。
+显式设置 `provider` 后,PicoClaw 会将 `model` 原样发送。因此 `"provider": "litellm", "model": "lite-gpt4"` 会发送 `lite-gpt4`,而 `"provider": "litellm", "model": "openai/gpt-4o"` 会发送 `openai/gpt-4o`。旧的兼容写法 `litellm/lite-gpt4` 和 `litellm/openai/gpt-4o` 在未设置 `provider` 时也会得到相同结果。
@@ -629,13 +649,15 @@ PicoClaw 只剥离最外层的 `litellm/` 前缀再发送请求,因此 `litell
"model_list": [
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_base": "https://api1.example.com/v1",
"api_keys": ["sk-key1"]
},
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_base": "https://api2.example.com/v1",
"api_keys": ["sk-key2"]
}
@@ -689,7 +711,7 @@ PicoClaw 按协议族路由提供商:
{
"agents": {
"defaults": {
- "model": "anthropic/claude-opus-4-5"
+ "model_name": "claude-opus-4-5"
}
},
"session": {
diff --git a/docs/guides/docker.md b/docs/guides/docker.md
index 3ccc7a2a7..97299bab0 100644
--- a/docs/guides/docker.md
+++ b/docs/guides/docker.md
@@ -94,19 +94,22 @@ picoclaw onboard
"model_list": [
{
"model_name": "ark-code-latest",
- "model": "volcengine/ark-code-latest",
+ "provider": "volcengine",
+ "model": "ark-code-latest",
"api_keys": ["sk-your-api-key"],
"api_base":"https://ark.cn-beijing.volces.com/api/coding/v3"
},
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["your-api-key"],
"request_timeout": 300
},
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6",
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6",
"api_keys": ["your-anthropic-key"]
}
],
diff --git a/docs/guides/docker.zh.md b/docs/guides/docker.zh.md
index 521747d16..f5d483af4 100644
--- a/docs/guides/docker.zh.md
+++ b/docs/guides/docker.zh.md
@@ -93,19 +93,22 @@ picoclaw onboard
"model_list": [
{
"model_name": "ark-code-latest",
- "model": "volcengine/ark-code-latest",
+ "provider": "volcengine",
+ "model": "ark-code-latest",
"api_keys": ["sk-your-api-key"],
"api_base":"https://ark.cn-beijing.volces.com/api/coding/v3"
},
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["your-api-key"],
"request_timeout": 300
},
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6",
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6",
"api_keys": ["your-anthropic-key"]
}
],
diff --git a/docs/guides/providers.md b/docs/guides/providers.md
index 41f3caae0..2d15114b7 100644
--- a/docs/guides/providers.md
+++ b/docs/guides/providers.md
@@ -33,7 +33,7 @@
### Model Configuration (model_list)
-> **What's New?** PicoClaw now uses a **model-centric** configuration approach. Simply specify `vendor/model` format (e.g., `zhipu/glm-4.7`) to add new providers—**zero code changes required!**
+> **What's New?** PicoClaw now prefers explicit `provider` + native `model` configuration (for example `"provider": "zhipu", "model": "glm-4.7"`). The legacy single-field `provider/model` form remains supported for compatibility when `provider` is omitted.
For agent dispatch and light-model routing examples, see the [Routing Guide](routing-guide.md).
@@ -46,35 +46,35 @@ This design also enables **multi-agent support** with flexible provider selectio
#### 📋 All Supported Vendors
-| Vendor | `model` Prefix | Default API Base | Protocol | API Key |
+| Vendor | `provider` Value | Default API Base | Protocol | API Key |
| ------------------- | ----------------- |-----------------------------------------------------| --------- | ---------------------------------------------------------------- |
-| **OpenAI** | `openai/` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) |
-| **Venice AI** | `venice/` | `https://api.venice.ai/api/v1` | OpenAI | [Get Key](https://venice.ai) |
-| **Anthropic** | `anthropic/` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) |
-| **智谱 AI (GLM)** | `zhipu/` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
-| **Z.AI Coding Plan** | `openai/` | `https://api.z.ai/api/coding/paas/v4` | OpenAI | [Get Key](https://z.ai/manage-apikey/apikey-list) |
-| **DeepSeek** | `deepseek/` | `https://api.deepseek.com/v1` | OpenAI | [Get Key](https://platform.deepseek.com) |
-| **Google Gemini** | `gemini/` | `https://generativelanguage.googleapis.com/v1beta` | OpenAI | [Get Key](https://aistudio.google.com/api-keys) |
-| **Groq** | `groq/` | `https://api.groq.com/openai/v1` | OpenAI | [Get Key](https://console.groq.com) |
-| **Moonshot** | `moonshot/` | `https://api.moonshot.cn/v1` | OpenAI | [Get Key](https://platform.moonshot.cn) |
-| **通义千问 (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 |
-| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) |
-| **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://www.volcengine.com/activity/codingplan?utm_campaign=PicoClaw&utm_content=PicoClaw&utm_medium=devrel&utm_source=OWO&utm_term=PicoClaw) |
-| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
-| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Get Key](https://www.byteplus.com) |
-| **Vivgrid** | `vivgrid/` | `https://api.vivgrid.com/v1` | OpenAI | [Get Key](https://vivgrid.com) |
-| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Get Key](https://longcat.chat/platform) |
-| **ModelScope (魔搭)**| `modelscope/` | `https://api-inference.modelscope.cn/v1` | OpenAI | [Get Token](https://modelscope.cn/my/tokens) |
-| **Xiaomi MiMo** | `mimo/` | `https://api.xiaomimimo.com/v1` | OpenAI | [Get Key](https://platform.xiaomimimo.com) |
-| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [Get Key](https://portal.azure.com) |
-| **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth only |
-| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - |
+| **OpenAI** | `openai` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) |
+| **Venice AI** | `venice` | `https://api.venice.ai/api/v1` | OpenAI | [Get Key](https://venice.ai) |
+| **Anthropic** | `anthropic` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) |
+| **智谱 AI (GLM)** | `zhipu` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
+| **Z.AI Coding Plan** | `openai` | `https://api.z.ai/api/coding/paas/v4` | OpenAI | [Get Key](https://z.ai/manage-apikey/apikey-list) |
+| **DeepSeek** | `deepseek` | `https://api.deepseek.com/v1` | OpenAI | [Get Key](https://platform.deepseek.com) |
+| **Google Gemini** | `gemini` | `https://generativelanguage.googleapis.com/v1beta` | OpenAI | [Get Key](https://aistudio.google.com/api-keys) |
+| **Groq** | `groq` | `https://api.groq.com/openai/v1` | OpenAI | [Get Key](https://console.groq.com) |
+| **Moonshot** | `moonshot` | `https://api.moonshot.cn/v1` | OpenAI | [Get Key](https://platform.moonshot.cn) |
+| **通义千问 (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 |
+| **Cerebras** | `cerebras` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) |
+| **VolcEngine (Doubao)** | `volcengine` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://www.volcengine.com/activity/codingplan?utm_campaign=PicoClaw&utm_content=PicoClaw&utm_medium=devrel&utm_source=OWO&utm_term=PicoClaw) |
+| **神算云** | `shengsuanyun` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
+| **BytePlus** | `byteplus` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Get Key](https://www.byteplus.com) |
+| **Vivgrid** | `vivgrid` | `https://api.vivgrid.com/v1` | OpenAI | [Get Key](https://vivgrid.com) |
+| **LongCat** | `longcat` | `https://api.longcat.chat/openai` | OpenAI | [Get Key](https://longcat.chat/platform) |
+| **ModelScope (魔搭)**| `modelscope` | `https://api-inference.modelscope.cn/v1` | OpenAI | [Get Token](https://modelscope.cn/my/tokens) |
+| **Xiaomi MiMo** | `mimo` | `https://api.xiaomimimo.com/v1` | OpenAI | [Get Key](https://platform.xiaomimimo.com) |
+| **Azure OpenAI** | `azure` | `https://{resource}.openai.azure.com` | Azure | [Get Key](https://portal.azure.com) |
+| **Antigravity** | `antigravity` | Google Cloud | Custom | OAuth only |
+| **GitHub Copilot** | `github-copilot` | `localhost:4321` | gRPC | - |
#### Basic Configuration
@@ -83,22 +83,26 @@ This design also enables **multi-agent support** with flexible provider selectio
"model_list": [
{
"model_name": "ark-code-latest",
- "model": "volcengine/ark-code-latest",
+ "provider": "volcengine",
+ "model": "ark-code-latest",
"api_keys": ["sk-your-api-key"]
},
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-your-openai-key"]
},
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6",
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6",
"api_keys": ["sk-ant-your-key"]
},
{
"model_name": "glm-4.7",
- "model": "zhipu/glm-4.7",
+ "provider": "zhipu",
+ "model": "glm-4.7",
"api_keys": ["your-zhipu-key"]
}
],
@@ -115,7 +119,8 @@ This design also enables **multi-agent support** with flexible provider selectio
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `model_name` | string | Yes | Unique name used to reference this model in agent config |
-| `model` | string | Yes | Vendor/model identifier (e.g., `openai/gpt-5.4`, `azure/gpt-5.4`, `anthropic/claude-sonnet-4.6`) |
+| `provider` | string | No | Preferred provider identifier. When present, PicoClaw sends `model` unchanged to that provider |
+| `model` | string | Yes | Native model ID when `provider` is set. If `provider` is omitted, the legacy `provider/model` form is still supported |
| `api_keys` | string[] | Yes* | API key(s) for authentication. Multiple keys enable per-request rotation. Not required for local providers (Ollama, LM Studio, VLLM) |
| `api_base` | string | No | Override the default API endpoint URL |
| `proxy` | string | No | HTTP proxy URL for this model entry |
@@ -129,6 +134,22 @@ This design also enables **multi-agent support** with flexible provider selectio
| `fallbacks` | string[] | No | Fallback model names for automatic failover |
| `enabled` | bool | No | Whether this model entry is active (default: `true`) |
+#### Provider / Model Resolution
+
+PicoClaw resolves `provider` and the runtime model ID using these rules:
+
+- If `provider` is set, `model` is used as-is.
+- If `provider` is omitted, PicoClaw treats the first `/` segment in `model` as the provider and everything after that first `/` as the runtime model ID.
+
+Examples:
+
+| Config | Resolved Provider | Model Sent Upstream |
+| --- | --- | --- |
+| `"provider": "openai", "model": "gpt-5.4"` | `openai` | `gpt-5.4` |
+| `"model": "openai/gpt-5.4"` | `openai` | `gpt-5.4` |
+| `"provider": "openrouter", "model": "openai/gpt-5.4"` | `openrouter` | `openai/gpt-5.4` |
+| `"model": "openrouter/openai/gpt-5.4"` | `openrouter` | `openai/gpt-5.4` |
+
#### Voice Transcription
You can configure a dedicated model for audio transcription with `voice.model_name`. This lets you reuse existing multimodal providers that support audio input instead of relying only on Groq.
@@ -140,7 +161,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
"model_list": [
{
"model_name": "voice-gemini",
- "model": "gemini/gemini-2.5-flash",
+ "provider": "gemini",
+ "model": "gemini-2.5-flash",
"api_keys": ["your-gemini-key"]
}
],
@@ -163,7 +185,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-..."]
}
```
@@ -173,7 +196,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json
{
"model_name": "ark-code-latest",
- "model": "volcengine/ark-code-latest",
+ "provider": "volcengine",
+ "model": "ark-code-latest",
"api_keys": ["sk-..."]
}
```
@@ -183,7 +207,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json
{
"model_name": "glm-4.7",
- "model": "zhipu/glm-4.7",
+ "provider": "zhipu",
+ "model": "glm-4.7",
"api_keys": ["your-key"]
}
```
@@ -193,7 +218,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json
{
"model_name": "glm-4.7",
- "model": "openai/glm-4.7",
+ "provider": "openai",
+ "model": "glm-4.7",
"api_keys": ["your-z.ai-key"],
"api_base": "https://api.z.ai/api/coding/paas/v4"
}
@@ -204,7 +230,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json
{
"model_name": "deepseek-chat",
- "model": "deepseek/deepseek-chat",
+ "provider": "deepseek",
+ "model": "deepseek-chat",
"api_keys": ["sk-..."]
}
```
@@ -214,7 +241,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6",
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6",
"api_keys": ["sk-ant-your-key"]
}
```
@@ -228,7 +256,8 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
```json
{
"model_name": "claude-opus-4-6",
- "model": "anthropic-messages/claude-opus-4-6",
+ "provider": "anthropic-messages",
+ "model": "claude-opus-4-6",
"api_keys": ["sk-ant-your-key"],
"api_base": "https://api.anthropic.com"
}
@@ -246,7 +275,8 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
```json
{
"model_name": "llama3",
- "model": "ollama/llama3"
+ "provider": "ollama",
+ "model": "llama3"
}
```
@@ -255,19 +285,21 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
```json
{
"model_name": "lmstudio-local",
- "model": "lmstudio/openai/gpt-oss-20b"
+ "provider": "lmstudio",
+ "model": "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.
+With explicit `provider`, PicoClaw sends `openai/gpt-oss-20b` unchanged to the LM Studio server. The legacy compatibility form `"model": "lmstudio/openai/gpt-oss-20b"` still resolves to the same upstream model ID when `provider` is omitted.
**Custom Proxy/API**
```json
{
"model_name": "my-custom-model",
- "model": "openai/custom-model",
+ "provider": "openai",
+ "model": "custom-model",
"api_base": "https://my-proxy.com/v1",
"api_keys": ["sk-..."],
"user_agent": "MyApp/1.0",
@@ -280,13 +312,14 @@ PicoClaw sends OpenAI-compatible requests to LM Studio, and strips the `lmstudio
```json
{
"model_name": "lite-gpt4",
- "model": "litellm/lite-gpt4",
+ "provider": "litellm",
+ "model": "lite-gpt4",
"api_base": "http://localhost:4000/v1",
"api_keys": ["sk-..."]
}
```
-PicoClaw strips only the outer `litellm/` prefix before sending the request, so proxy aliases like `litellm/lite-gpt4` send `lite-gpt4`, while `litellm/openai/gpt-4o` sends `openai/gpt-4o`.
+With explicit `provider`, PicoClaw sends `model` unchanged. That means `"provider": "litellm", "model": "lite-gpt4"` sends `lite-gpt4`, while `"provider": "litellm", "model": "openai/gpt-4o"` sends `openai/gpt-4o`. The legacy compatibility forms `litellm/lite-gpt4` and `litellm/openai/gpt-4o` still resolve the same way when `provider` is omitted.
**Z.AI Coding Plan**
@@ -295,7 +328,8 @@ If the standard Zhipu endpoint (`https://open.bigmodel.cn/api/paas/v4`) returns
```json
{
"model_name": "glm-4.7",
- "model": "openai/glm-4.7",
+ "provider": "openai",
+ "model": "glm-4.7",
"api_keys": ["your-zhipu-api-key"],
"api_base": "https://api.z.ai/api/coding/paas/v4"
}
@@ -312,13 +346,15 @@ Configure multiple endpoints for the same model name—PicoClaw will automatical
"model_list": [
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_base": "https://api1.example.com/v1",
"api_keys": ["sk-key1"]
},
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_base": "https://api2.example.com/v1",
"api_keys": ["sk-key2"]
}
@@ -337,18 +373,21 @@ It also applies cooldown tracking per candidate to avoid immediately retrying a
"model_list": [
{
"model_name": "qwen-main",
- "model": "openai/qwen3.5:cloud",
+ "provider": "openai",
+ "model": "qwen3.5:cloud",
"api_base": "https://api.example.com/v1",
"api_keys": ["sk-main"]
},
{
"model_name": "deepseek-backup",
- "model": "deepseek/deepseek-chat",
+ "provider": "deepseek",
+ "model": "deepseek-chat",
"api_keys": ["sk-backup-1"]
},
{
"model_name": "gemini-backup",
- "model": "gemini/gemini-2.5-flash",
+ "provider": "gemini",
+ "model": "gemini-2.5-flash",
"api_keys": ["sk-backup-2"]
}
],
@@ -396,7 +435,8 @@ The old `providers` configuration is **deprecated** and has been removed in V2.
"model_list": [
{
"model_name": "glm-4.7",
- "model": "zhipu/glm-4.7",
+ "provider": "zhipu",
+ "model": "glm-4.7",
"api_keys": ["your-key"]
}
],
@@ -464,7 +504,7 @@ picoclaw agent -m "Hello"
{
"agents": {
"defaults": {
- "model_name": "anthropic/claude-opus-4-5"
+ "model_name": "claude-opus-4-5"
}
},
"session": {
diff --git a/docs/guides/providers.zh.md b/docs/guides/providers.zh.md
index 1f1031043..421b23030 100644
--- a/docs/guides/providers.zh.md
+++ b/docs/guides/providers.zh.md
@@ -32,7 +32,7 @@
### 模型配置 (model_list)
-> **新功能!** PicoClaw 现在采用**以模型为中心**的配置方式。只需使用 `厂商/模型` 格式(如 `zhipu/glm-4.7`)即可添加新的 provider——**无需修改任何代码!**
+> **新功能!** PicoClaw 现在优先推荐显式 `provider` + 原生 `model` 的配置方式,例如 `"provider": "zhipu", "model": "glm-4.7"`。如果未设置 `provider`,旧的单字段 `provider/model` 写法仍然兼容。
如果你想看 agent 分发和轻量模型路由的完整示例,请看 [路由使用指南](routing-guide.zh.md)。
@@ -45,33 +45,33 @@
#### 📋 所有支持的厂商
-| 厂商 | `model` 前缀 | 默认 API Base | 协议 | 获取 API Key |
+| 厂商 | `provider` 值 | 默认 API Base | 协议 | 获取 API Key |
| ------------------- | ----------------- | --------------------------------------------------- | --------- | ----------------------------------------------------------------- |
-| **OpenAI** | `openai/` | `https://api.openai.com/v1` | OpenAI | [获取密钥](https://platform.openai.com) |
-| **Venice AI** | `venice/` | `https://api.venice.ai/api/v1` | OpenAI | [获取密钥](https://venice.ai) |
-| **Anthropic** | `anthropic/` | `https://api.anthropic.com/v1` | Anthropic | [获取密钥](https://console.anthropic.com) |
-| **智谱 AI (GLM)** | `zhipu/` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [获取密钥](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
-| **DeepSeek** | `deepseek/` | `https://api.deepseek.com/v1` | OpenAI | [获取密钥](https://platform.deepseek.com) |
-| **Google Gemini** | `gemini/` | `https://generativelanguage.googleapis.com/v1beta` | OpenAI | [获取密钥](https://aistudio.google.com/api-keys) |
-| **Groq** | `groq/` | `https://api.groq.com/openai/v1` | OpenAI | [获取密钥](https://console.groq.com) |
-| **Moonshot** | `moonshot/` | `https://api.moonshot.cn/v1` | OpenAI | [获取密钥](https://platform.moonshot.cn) |
-| **通义千问 (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 | 本地 |
-| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [获取密钥](https://cerebras.ai) |
-| **火山引擎(Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [获取密钥](https://www.volcengine.com/activity/codingplan?utm_campaign=PicoClaw&utm_content=PicoClaw&utm_medium=devrel&utm_source=OWO&utm_term=PicoClaw) |
-| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
-| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [获取密钥](https://www.byteplus.com) |
-| **Vivgrid** | `vivgrid/` | `https://api.vivgrid.com/v1` | OpenAI | [获取密钥](https://vivgrid.com) |
-| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [获取密钥](https://longcat.chat/platform) |
-| **ModelScope (魔搭)**| `modelscope/` | `https://api-inference.modelscope.cn/v1` | OpenAI | [获取 Token](https://modelscope.cn/my/tokens) |
-| **小米 MiMo** | `mimo/` | `https://api.xiaomimimo.com/v1` | OpenAI | [获取密钥](https://platform.xiaomimimo.com) |
-| **Antigravity** | `antigravity/` | Google Cloud | 自定义 | 仅 OAuth |
-| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - |
+| **OpenAI** | `openai` | `https://api.openai.com/v1` | OpenAI | [获取密钥](https://platform.openai.com) |
+| **Venice AI** | `venice` | `https://api.venice.ai/api/v1` | OpenAI | [获取密钥](https://venice.ai) |
+| **Anthropic** | `anthropic` | `https://api.anthropic.com/v1` | Anthropic | [获取密钥](https://console.anthropic.com) |
+| **智谱 AI (GLM)** | `zhipu` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [获取密钥](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
+| **DeepSeek** | `deepseek` | `https://api.deepseek.com/v1` | OpenAI | [获取密钥](https://platform.deepseek.com) |
+| **Google Gemini** | `gemini` | `https://generativelanguage.googleapis.com/v1beta` | OpenAI | [获取密钥](https://aistudio.google.com/api-keys) |
+| **Groq** | `groq` | `https://api.groq.com/openai/v1` | OpenAI | [获取密钥](https://console.groq.com) |
+| **Moonshot** | `moonshot` | `https://api.moonshot.cn/v1` | OpenAI | [获取密钥](https://platform.moonshot.cn) |
+| **通义千问 (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 | 本地 |
+| **Cerebras** | `cerebras` | `https://api.cerebras.ai/v1` | OpenAI | [获取密钥](https://cerebras.ai) |
+| **火山引擎(Doubao)** | `volcengine` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [获取密钥](https://www.volcengine.com/activity/codingplan?utm_campaign=PicoClaw&utm_content=PicoClaw&utm_medium=devrel&utm_source=OWO&utm_term=PicoClaw) |
+| **神算云** | `shengsuanyun` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
+| **BytePlus** | `byteplus` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [获取密钥](https://www.byteplus.com) |
+| **Vivgrid** | `vivgrid` | `https://api.vivgrid.com/v1` | OpenAI | [获取密钥](https://vivgrid.com) |
+| **LongCat** | `longcat` | `https://api.longcat.chat/openai` | OpenAI | [获取密钥](https://longcat.chat/platform) |
+| **ModelScope (魔搭)**| `modelscope` | `https://api-inference.modelscope.cn/v1` | OpenAI | [获取 Token](https://modelscope.cn/my/tokens) |
+| **小米 MiMo** | `mimo` | `https://api.xiaomimimo.com/v1` | OpenAI | [获取密钥](https://platform.xiaomimimo.com) |
+| **Antigravity** | `antigravity` | Google Cloud | 自定义 | 仅 OAuth |
+| **GitHub Copilot** | `github-copilot` | `localhost:4321` | gRPC | - |
#### 基础配置示例
@@ -80,22 +80,26 @@
"model_list": [
{
"model_name": "ark-code-latest",
- "model": "volcengine/ark-code-latest",
+ "provider": "volcengine",
+ "model": "ark-code-latest",
"api_keys": ["sk-your-api-key"]
},
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-your-openai-key"]
},
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6",
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6",
"api_keys": ["sk-ant-your-key"]
},
{
"model_name": "glm-4.7",
- "model": "zhipu/glm-4.7",
+ "provider": "zhipu",
+ "model": "glm-4.7",
"api_keys": ["your-zhipu-key"]
}
],
@@ -112,7 +116,8 @@
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `model_name` | string | 是 | 在 agent 配置中引用此模型的唯一名称 |
-| `model` | string | 是 | 厂商/模型标识符(如 `openai/gpt-5.4`、`azure/gpt-5.4`、`anthropic/claude-sonnet-4.6`) |
+| `provider` | string | 否 | 推荐的 provider 标识。设置后,PicoClaw 会将 `model` 原样发送给该 provider |
+| `model` | string | 是 | 当设置 `provider` 时,这里填写 provider 原生模型 ID。若未设置 `provider`,仍兼容旧的 `provider/model` 写法 |
| `api_keys` | string[] | 是* | 认证密钥。多个密钥可按请求轮换。本地 provider(Ollama、LM Studio、VLLM)不需要 |
| `api_base` | string | 否 | 覆盖默认的 API 端点 URL |
| `proxy` | string | 否 | 此模型条目的 HTTP 代理 URL |
@@ -126,6 +131,22 @@
| `fallbacks` | string[] | 否 | 自动故障转移的备用模型名称 |
| `enabled` | bool | 否 | 是否启用此模型条目(默认:`true`) |
+#### `provider` / `model` 解析规则
+
+PicoClaw 按下面的规则解析 `provider` 和最终发给上游的模型 ID:
+
+- 如果设置了 `provider`,则直接使用 `model`。
+- 如果未设置 `provider`,则把 `model` 中第一个 `/` 之前的字段当作 provider,第一个 `/` 之后的全部内容当作最终模型 ID。
+
+示例:
+
+| 配置 | 解析后的 Provider | 实际发送的模型 ID |
+| --- | --- | --- |
+| `"provider": "openai", "model": "gpt-5.4"` | `openai` | `gpt-5.4` |
+| `"model": "openai/gpt-5.4"` | `openai` | `gpt-5.4` |
+| `"provider": "openrouter", "model": "openai/gpt-5.4"` | `openrouter` | `openai/gpt-5.4` |
+| `"model": "openrouter/openai/gpt-5.4"` | `openrouter` | `openai/gpt-5.4` |
+
#### 语音转录
你可以通过 `voice.model_name` 为语音转录指定一个专用模型。这样可以直接复用已经配置好的、支持音频输入的多模态 provider,而不必只依赖 Groq。
@@ -137,7 +158,8 @@
"model_list": [
{
"model_name": "voice-gemini",
- "model": "gemini/gemini-2.5-flash",
+ "provider": "gemini",
+ "model": "gemini-2.5-flash",
"api_keys": ["your-gemini-key"]
}
],
@@ -160,7 +182,8 @@
```json
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-..."]
}
```
@@ -170,7 +193,8 @@
```json
{
"model_name": "ark-code-latest",
- "model": "volcengine/ark-code-latest",
+ "provider": "volcengine",
+ "model": "ark-code-latest",
"api_keys": ["sk-..."]
}
```
@@ -180,7 +204,8 @@
```json
{
"model_name": "glm-4.7",
- "model": "zhipu/glm-4.7",
+ "provider": "zhipu",
+ "model": "glm-4.7",
"api_keys": ["your-key"]
}
```
@@ -190,7 +215,8 @@
```json
{
"model_name": "deepseek-chat",
- "model": "deepseek/deepseek-chat",
+ "provider": "deepseek",
+ "model": "deepseek-chat",
"api_keys": ["sk-..."]
}
```
@@ -200,7 +226,8 @@
```json
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6",
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6",
"auth_method": "oauth"
}
```
@@ -214,7 +241,8 @@
```json
{
"model_name": "claude-opus-4-6",
- "model": "anthropic-messages/claude-opus-4-6",
+ "provider": "anthropic-messages",
+ "model": "claude-opus-4-6",
"api_keys": ["sk-ant-your-key"],
"api_base": "https://api.anthropic.com"
}
@@ -232,7 +260,8 @@
```json
{
"model_name": "llama3",
- "model": "ollama/llama3"
+ "provider": "ollama",
+ "model": "llama3"
}
```
@@ -241,19 +270,21 @@
```json
{
"model_name": "lmstudio-local",
- "model": "lmstudio/openai/gpt-oss-20b"
+ "provider": "lmstudio",
+ "model": "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`。
+显式设置 `provider` 后,PicoClaw 会把 `openai/gpt-oss-20b` 原样发送给 LM Studio。旧的兼容写法 `"model": "lmstudio/openai/gpt-oss-20b"` 在未设置 `provider` 时也会解析成相同的上游模型 ID。
**自定义代理/API**
```json
{
"model_name": "my-custom-model",
- "model": "openai/custom-model",
+ "provider": "openai",
+ "model": "custom-model",
"api_base": "https://my-proxy.com/v1",
"api_keys": ["sk-..."],
"user_agent": "MyApp/1.0",
@@ -266,13 +297,14 @@ PicoClaw 向 LM Studio 的 OpenAI 兼容终结点发送请求,且将移除首
```json
{
"model_name": "lite-gpt4",
- "model": "litellm/lite-gpt4",
+ "provider": "litellm",
+ "model": "lite-gpt4",
"api_base": "http://localhost:4000/v1",
"api_keys": ["sk-..."]
}
```
-PicoClaw 在发送请求前仅去除外层 `litellm/` 前缀,因此 `litellm/lite-gpt4` 会发送 `lite-gpt4`,而 `litellm/openai/gpt-4o` 会发送 `openai/gpt-4o`。
+显式设置 `provider` 后,PicoClaw 会将 `model` 原样发送。因此 `"provider": "litellm", "model": "lite-gpt4"` 会发送 `lite-gpt4`,而 `"provider": "litellm", "model": "openai/gpt-4o"` 会发送 `openai/gpt-4o`。旧的兼容写法 `litellm/lite-gpt4` 和 `litellm/openai/gpt-4o` 在未设置 `provider` 时也会得到相同结果。
#### 负载均衡
@@ -283,13 +315,15 @@ PicoClaw 在发送请求前仅去除外层 `litellm/` 前缀,因此 `litellm/l
"model_list": [
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_base": "https://api1.example.com/v1",
"api_keys": ["sk-key1"]
},
{
"model_name": "gpt-5.4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_base": "https://api2.example.com/v1",
"api_keys": ["sk-key2"]
}
@@ -308,18 +342,21 @@ PicoClaw 在发送请求前仅去除外层 `litellm/` 前缀,因此 `litellm/l
"model_list": [
{
"model_name": "qwen-main",
- "model": "openai/qwen3.5:cloud",
+ "provider": "openai",
+ "model": "qwen3.5:cloud",
"api_base": "https://api.example.com/v1",
"api_keys": ["sk-main"]
},
{
"model_name": "deepseek-backup",
- "model": "deepseek/deepseek-chat",
+ "provider": "deepseek",
+ "model": "deepseek-chat",
"api_keys": ["sk-backup-1"]
},
{
"model_name": "gemini-backup",
- "model": "gemini/gemini-2.5-flash",
+ "provider": "gemini",
+ "model": "gemini-2.5-flash",
"api_keys": ["sk-backup-2"]
}
],
@@ -367,7 +404,8 @@ PicoClaw 在发送请求前仅去除外层 `litellm/` 前缀,因此 `litellm/l
"model_list": [
{
"model_name": "glm-4.7",
- "model": "zhipu/glm-4.7",
+ "provider": "zhipu",
+ "model": "glm-4.7",
"api_keys": ["your-key"]
}
],
@@ -435,7 +473,7 @@ picoclaw agent -m "你好"
{
"agents": {
"defaults": {
- "model_name": "anthropic/claude-opus-4-5"
+ "model_name": "claude-opus-4-5"
}
},
"session": {
diff --git a/docs/guides/routing-guide.md b/docs/guides/routing-guide.md
index abeaf0285..a47984324 100644
--- a/docs/guides/routing-guide.md
+++ b/docs/guides/routing-guide.md
@@ -69,12 +69,14 @@ This guide explains how to configure both for real deployments.
"model_list": [
{
"model_name": "gpt-main",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-main"]
},
{
"model_name": "flash-light",
- "model": "gemini/gemini-2.0-flash-exp",
+ "provider": "gemini",
+ "model": "gemini-2.0-flash-exp",
"api_keys": ["sk-light"]
}
],
diff --git a/docs/guides/routing-guide.zh.md b/docs/guides/routing-guide.zh.md
index 58c9f14e2..713cbeb04 100644
--- a/docs/guides/routing-guide.zh.md
+++ b/docs/guides/routing-guide.zh.md
@@ -69,12 +69,14 @@ PicoClaw 里用户能直接感知到的“路由”主要有两部分:
"model_list": [
{
"model_name": "gpt-main",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-main"]
},
{
"model_name": "flash-light",
- "model": "gemini/gemini-2.0-flash-exp",
+ "provider": "gemini",
+ "model": "gemini-2.0-flash-exp",
"api_keys": ["sk-light"]
}
],
diff --git a/docs/migration/model-list-migration.md b/docs/migration/model-list-migration.md
index 15d531cf7..4fb37c580 100644
--- a/docs/migration/model-list-migration.md
+++ b/docs/migration/model-list-migration.md
@@ -8,7 +8,7 @@ The new `model_list` configuration offers several advantages:
- **Zero-code provider addition**: Add OpenAI-compatible providers with configuration only
- **Load balancing**: Configure multiple endpoints for the same model
-- **Protocol-based routing**: Use prefixes like `openai/`, `anthropic/`, etc.
+- **Explicit provider resolution**: Prefer `provider` + native `model`, with legacy `provider/model` compatibility when needed
- **Cleaner configuration**: Model-centric instead of vendor-centric
## Timeline
@@ -54,18 +54,21 @@ The new `model_list` configuration offers several advantages:
"model_list": [
{
"model_name": "gpt4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-your-openai-key"],
"api_base": "https://api.openai.com/v1"
},
{
"model_name": "claude-sonnet-4.6",
- "model": "anthropic/claude-sonnet-4.6",
+ "provider": "anthropic",
+ "model": "claude-sonnet-4.6",
"api_keys": ["sk-ant-your-key"]
},
{
"model_name": "deepseek",
- "model": "deepseek/deepseek-chat",
+ "provider": "deepseek",
+ "model": "deepseek-chat",
"api_keys": ["sk-your-deepseek-key"]
}
],
@@ -79,40 +82,46 @@ The new `model_list` configuration offers several advantages:
> **Note**: The `enabled` field can be omitted — during V1→V2 migration it is auto-inferred (models with API keys or the `local-model` name are enabled by default). For new configs, you can explicitly set `"enabled": false` to disable a model entry without removing it.
-## Protocol Prefixes
+## Provider / Model Resolution
-The `model` field uses a protocol prefix format: `[protocol/]model-identifier`
+Preferred format:
-| Prefix | Description | Example |
-|--------|-------------|---------|
-| `openai/` | OpenAI API (default) | `openai/gpt-5.4` |
-| `anthropic/` | Anthropic API | `anthropic/claude-opus-4` |
-| `antigravity/` | Google via Antigravity OAuth | `antigravity/gemini-2.0-flash` |
-| `gemini/` | Google Gemini API | `gemini/gemini-2.0-flash-exp` |
-| `claude-cli/` | Claude CLI (local) | `claude-cli/claude-sonnet-4.6` |
-| `codex-cli/` | Codex CLI (local) | `codex-cli/codex-4` |
-| `github-copilot/` | GitHub Copilot | `github-copilot/gpt-4o` |
-| `openrouter/` | OpenRouter | `openrouter/anthropic/claude-sonnet-4.6` |
-| `groq/` | Groq API | `groq/llama-3.1-70b` |
-| `deepseek/` | DeepSeek API | `deepseek/deepseek-chat` |
-| `cerebras/` | Cerebras API | `cerebras/llama-3.3-70b` |
-| `qwen/` | Alibaba Qwen | `qwen/qwen-max` |
-| `zhipu/` | Zhipu AI | `zhipu/glm-4` |
-| `nvidia/` | NVIDIA NIM | `nvidia/llama-3.1-nemotron-70b` |
-| `ollama/` | Ollama (local) | `ollama/llama3` |
-| `vllm/` | vLLM (local) | `vllm/my-model` |
-| `moonshot/` | Moonshot AI | `moonshot/moonshot-v1-8k` |
-| `shengsuanyun/` | ShengSuanYun | `shengsuanyun/deepseek-v3` |
-| `volcengine/` | Volcengine | `volcengine/doubao-pro-32k` |
+```json
+{
+ "provider": "openai",
+ "model": "gpt-5.4"
+}
+```
-**Note**: If no prefix is specified, `openai/` is used as the default.
+Legacy compatibility format:
+
+```json
+{
+ "model": "openai/gpt-5.4"
+}
+```
+
+Resolution rules:
+
+1. If `provider` is set, PicoClaw sends `model` unchanged.
+2. If `provider` is omitted, PicoClaw treats the first `/` segment in `model` as the provider and everything after that first `/` as the runtime model ID.
+
+Examples:
+
+| Config | Resolved Provider | Model Sent Upstream |
+|--------|-------------------|---------------------|
+| `"provider": "openai", "model": "gpt-5.4"` | `openai` | `gpt-5.4` |
+| `"model": "openai/gpt-5.4"` | `openai` | `gpt-5.4` |
+| `"provider": "openrouter", "model": "google/gemini-2.0-flash-exp:free"` | `openrouter` | `google/gemini-2.0-flash-exp:free` |
+| `"model": "openrouter/google/gemini-2.0-flash-exp:free"` | `openrouter` | `google/gemini-2.0-flash-exp:free` |
## ModelConfig Fields
| Field | Required | Description |
|-------|----------|-------------|
| `model_name` | Yes | User-facing alias for the model |
-| `model` | Yes | Protocol and model identifier (e.g., `openai/gpt-5.4`) |
+| `provider` | No | Preferred provider identifier. When set, `model` is sent unchanged |
+| `model` | Yes | Native model ID when `provider` is set, or legacy `provider/model` when `provider` is omitted |
| `api_base` | No | API endpoint URL |
| `api_keys` | No | API authentication keys (array; supports multiple keys for load balancing) |
| `enabled` | No | Whether this model entry is active. Defaults to `true` during migration for models with API keys or named `local-model`. Set to `false` to disable. |
@@ -136,7 +145,8 @@ There are two ways to configure load balancing:
"model_list": [
{
"model_name": "gpt4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-key1", "sk-key2", "sk-key3"],
"api_base": "https://api.openai.com/v1"
}
@@ -162,19 +172,22 @@ model_list:
"model_list": [
{
"model_name": "gpt4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-key1"],
"api_base": "https://api1.example.com/v1"
},
{
"model_name": "gpt4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-key2"],
"api_base": "https://api2.example.com/v1"
},
{
"model_name": "gpt4",
- "model": "openai/gpt-5.4",
+ "provider": "openai",
+ "model": "gpt-5.4",
"api_keys": ["sk-key3"],
"api_base": "https://api3.example.com/v1"
}
@@ -193,7 +206,8 @@ With `model_list`, adding a new provider requires zero code changes:
"model_list": [
{
"model_name": "my-custom-llm",
- "model": "openai/my-model-v1",
+ "provider": "openai",
+ "model": "my-model-v1",
"api_keys": ["your-api-key"],
"api_base": "https://api.your-provider.com/v1"
}
@@ -201,7 +215,7 @@ With `model_list`, adding a new provider requires zero code changes:
}
```
-Just specify `openai/` as the protocol (or omit it for the default), and provide your provider's API base URL.
+Just set `provider` to `openai` (or another supported provider), and provide your provider's API base URL.
## Backward Compatibility
@@ -216,7 +230,7 @@ During the migration period, your existing V0/V1 config will be auto-migrated to
- [ ] Identify all providers you're currently using
- [ ] Create `model_list` entries for each provider
-- [ ] Use appropriate protocol prefixes
+- [ ] Prefer explicit `provider` values and native model IDs
- [ ] Update `agents.defaults.model_name` to reference the new `model_name`
- [ ] Test that all models work correctly
- [ ] Remove or comment out the old `providers` section
@@ -234,10 +248,10 @@ model "xxx" not found in model_list or providers
### Unknown protocol error
```
-unknown protocol "xxx" in model "xxx/model-name"
+unknown provider "xxx" in model "xxx/model-name"
```
-**Solution**: Use a supported protocol prefix. See the [Protocol Prefixes](#protocol-prefixes) table above.
+**Solution**: Use a supported `provider` value, or use the legacy `provider/model` compatibility form correctly. See [Provider / Model Resolution](#provider--model-resolution).
### Missing API key error
diff --git a/docs/operations/troubleshooting.md b/docs/operations/troubleshooting.md
index 096beec78..16229f369 100644
--- a/docs/operations/troubleshooting.md
+++ b/docs/operations/troubleshooting.md
@@ -7,16 +7,22 @@
- `Error creating provider: model "openrouter/free" not found in model_list`
- OpenRouter returns 400: `"free is not a valid model ID"`
-**Cause:** The `model` field in your `model_list` entry is what gets sent to the API. For OpenRouter you must use the **full** model ID, not a shorthand.
+**Cause:** PicoClaw now resolves provider/model in two steps:
-- **Wrong:** `"model": "free"` → OpenRouter receives `free` and rejects it.
-- **Right:** `"model": "openrouter/free"` → OpenRouter receives `openrouter/free` (auto free-tier routing).
+- If `provider` is set, the `model` field is sent to that provider unchanged.
+- If `provider` is omitted, PicoClaw infers the provider from the first `/` segment and sends everything after that first `/` as the runtime model ID.
+
+For OpenRouter free-tier routing, the preferred config is explicit `provider`.
+
+- **Wrong:** `"model": "free"` → no OpenRouter provider is selected, so `free` is not a valid OpenRouter model route.
+- **Right:** `"provider": "openrouter", "model": "free"` → OpenRouter receives `free`.
+- **Also supported:** `"model": "openrouter/free"` → provider resolves to `openrouter`, runtime model ID resolves to `free`.
**Fix:** In `~/.picoclaw/config.json` (or your config path):
1. **agents.defaults.model_name** must match a `model_name` in `model_list` (e.g. `"openrouter-free"`).
-2. That entry’s **model** must be a valid OpenRouter model ID, for example:
- - `"openrouter/free"` – auto free-tier
+2. That entry should preferably set **provider** to `openrouter`, and **model** should be a valid OpenRouter model ID, for example:
+ - `"free"` – auto free-tier
- `"google/gemini-2.0-flash-exp:free"`
- `"meta-llama/llama-3.1-8b-instruct:free"`
@@ -32,8 +38,9 @@ Example snippet:
"model_list": [
{
"model_name": "openrouter-free",
- "model": "openrouter/free",
- "api_key": "sk-or-v1-YOUR_OPENROUTER_KEY",
+ "provider": "openrouter",
+ "model": "free",
+ "api_keys": ["sk-or-v1-YOUR_OPENROUTER_KEY"],
"api_base": "https://openrouter.ai/api/v1"
}
]
diff --git a/docs/operations/troubleshooting.zh.md b/docs/operations/troubleshooting.zh.md
index fd519a8b2..1569e3385 100644
--- a/docs/operations/troubleshooting.zh.md
+++ b/docs/operations/troubleshooting.zh.md
@@ -9,16 +9,22 @@
- `Error creating provider: model "openrouter/free" not found in model_list`
- OpenRouter 返回 400:`"free is not a valid model ID"`
-**原因:** `model_list` 条目中的 `model` 字段是发送给 API 的内容。对于 OpenRouter,你必须使用**完整的**模型 ID,而不是简写。
+**原因:** PicoClaw 现在按两步解析 provider 和 model:
-- **错误:** `"model": "free"` → OpenRouter 收到 `free` 并拒绝。
-- **正确:** `"model": "openrouter/free"` → OpenRouter 收到 `openrouter/free`(自动免费层路由)。
+- 如果设置了 `provider`,则会把 `model` 原样发送给该 provider。
+- 如果未设置 `provider`,则会把 `model` 第一个 `/` 之前的字段当作 provider,并把第一个 `/` 之后的全部内容当作最终发送的模型 ID。
+
+对于 OpenRouter 免费层路由,推荐显式设置 `provider`。
+
+- **错误:** `"model": "free"` → 不会选中 OpenRouter,`free` 也不是可直接路由的 OpenRouter 模型配置。
+- **正确:** `"provider": "openrouter", "model": "free"` → OpenRouter 收到 `free`。
+- **也兼容:** `"model": "openrouter/free"` → provider 解析为 `openrouter`,最终模型 ID 解析为 `free`。
**修复方法:** 在 `~/.picoclaw/config.json`(或你的配置路径)中:
1. **agents.defaults.model_name** 必须匹配 `model_list` 中的某个 `model_name`(例如 `"openrouter-free"`)。
-2. 该条目的 **model** 必须是有效的 OpenRouter 模型 ID,例如:
- - `"openrouter/free"` – 自动免费层
+2. 该条目推荐显式设置 **provider** 为 `openrouter`,并在 **model** 中填写有效的 OpenRouter 模型 ID,例如:
+ - `"free"` – 自动免费层
- `"google/gemini-2.0-flash-exp:free"`
- `"meta-llama/llama-3.1-8b-instruct:free"`
@@ -34,8 +40,9 @@
"model_list": [
{
"model_name": "openrouter-free",
- "model": "openrouter/free",
- "api_key": "sk-or-v1-YOUR_OPENROUTER_KEY",
+ "provider": "openrouter",
+ "model": "free",
+ "api_keys": ["sk-or-v1-YOUR_OPENROUTER_KEY"],
"api_base": "https://openrouter.ai/api/v1"
}
]
diff --git a/docs/reference/rate-limiting.md b/docs/reference/rate-limiting.md
index b54c757f8..d491c9c56 100644
--- a/docs/reference/rate-limiting.md
+++ b/docs/reference/rate-limiting.md
@@ -39,20 +39,23 @@ Set `rpm` on any model in `model_list`:
```yaml
model_list:
- model_name: gpt-4o-free
- model: openai/gpt-4o
+ provider: openai
+ model: gpt-4o
api_base: https://api.openai.com/v1
rpm: 3 # max 3 requests per minute
api_keys:
- sk-...
- model_name: claude-haiku
- model: anthropic/claude-haiku-4-5
+ provider: anthropic
+ model: claude-haiku-4-5
rpm: 60 # 60 rpm (Anthropic free tier)
api_keys:
- sk-ant-...
- model_name: local-llm
- model: openai/llama3
+ provider: ollama
+ model: llama3
api_base: http://localhost:11434/v1
# no rpm → unrestricted
```
@@ -68,7 +71,8 @@ When a model has fallbacks configured, each candidate is rate-limited **independ
```yaml
model_list:
- model_name: gpt4-with-fallback
- model: openai/gpt-4o
+ provider: openai
+ model: gpt-4o
rpm: 5
fallbacks:
- gpt-4o-mini # must also be in model_list; its own rpm applies
diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go
index 5cdac186c..61a292cb7 100644
--- a/pkg/agent/agent_test.go
+++ b/pkg/agent/agent_test.go
@@ -128,7 +128,7 @@ func useTestSideQuestionProvider(al *AgentLoop, provider providers.LLMProvider)
al.providerFactory = func(mc *config.ModelConfig) (providers.LLMProvider, string, error) {
model := provider.GetDefaultModel()
if mc != nil {
- if _, modelID := providers.ExtractProtocol(mc.Model); modelID != "" {
+ if _, modelID := providers.ExtractProtocol(mc); modelID != "" {
model = modelID
}
}
diff --git a/pkg/agent/hooks_test.go b/pkg/agent/hooks_test.go
index cd1586e75..f024cba04 100644
--- a/pkg/agent/hooks_test.go
+++ b/pkg/agent/hooks_test.go
@@ -881,7 +881,11 @@ func TestAgentLoop_HookRespond_InterruptSkipsRemaining(t *testing.T) {
resultCh <- result{resp: resp, err: err}
}()
- time.Sleep(50 * time.Millisecond)
+ select {
+ case <-tool1ExecCh:
+ case <-time.After(3 * time.Second):
+ t.Fatal("timeout waiting for tool execution to start")
+ }
if err := al.InterruptGraceful("stop now"); err != nil {
t.Fatalf("InterruptGraceful failed: %v", err)
diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go
index 5bcb83087..d0b25a0a8 100644
--- a/pkg/agent/instance.go
+++ b/pkg/agent/instance.go
@@ -270,8 +270,8 @@ func populateCandidateProvidersFromNames(
map[string]any{"name": name, "error": err.Error()})
continue
}
- protocol, modelID := providers.ExtractProtocol(strings.TrimSpace(mc.Model))
- key := providers.ModelKey(providers.NormalizeProvider(protocol), modelID)
+ protocol, modelID := providers.ExtractProtocol(mc)
+ key := providers.ModelKey(protocol, modelID)
if _, exists := out[key]; exists {
continue
}
diff --git a/pkg/agent/instance_test.go b/pkg/agent/instance_test.go
index 8c71296ed..790ab16ca 100644
--- a/pkg/agent/instance_test.go
+++ b/pkg/agent/instance_test.go
@@ -104,6 +104,7 @@ func TestNewAgentInstance_ResolveCandidatesFromModelListAlias(t *testing.T) {
name string
aliasName string
modelName string
+ provider string
apiBase string
wantProvider string
wantModel string
@@ -124,6 +125,15 @@ func TestNewAgentInstance_ResolveCandidatesFromModelListAlias(t *testing.T) {
wantProvider: "openai",
wantModel: "glm-5",
},
+ {
+ name: "explicit provider overrides model prefix",
+ aliasName: "nvidia-gpt",
+ modelName: "z-ai/glm-5.1",
+ provider: "nvidia",
+ apiBase: "https://integrate.api.nvidia.com/v1",
+ wantProvider: "nvidia",
+ wantModel: "z-ai/glm-5.1",
+ },
}
for _, tt := range tests {
@@ -145,6 +155,7 @@ func TestNewAgentInstance_ResolveCandidatesFromModelListAlias(t *testing.T) {
{
ModelName: tt.aliasName,
Model: tt.modelName,
+ Provider: tt.provider,
APIBase: tt.apiBase,
},
},
diff --git a/pkg/agent/model_resolution.go b/pkg/agent/model_resolution.go
index 7cbf3a8d6..1c23cee50 100644
--- a/pkg/agent/model_resolution.go
+++ b/pkg/agent/model_resolution.go
@@ -37,14 +37,14 @@ func candidateFromModelConfig(
return providers.FallbackCandidate{}, false
}
- ref := providers.ParseModelRef(ensureProtocolModel(mc.Model), defaultProvider)
- if ref == nil {
+ protocol, modelID := providers.ExtractProtocol(mc)
+ if strings.TrimSpace(modelID) == "" {
return providers.FallbackCandidate{}, false
}
return providers.FallbackCandidate{
- Provider: ref.Provider,
- Model: ref.Model,
+ Provider: protocol,
+ Model: modelID,
RPM: mc.RPM,
IdentityKey: modelConfigIdentityKey(mc),
}, true
@@ -72,7 +72,7 @@ func lookupModelConfigByRef(cfg *config.Config, raw string) *config.ModelConfig
if fullModel == raw {
return mc
}
- _, modelID := providers.ExtractProtocol(fullModel)
+ _, modelID := providers.ExtractProtocol(mc)
if modelID == raw {
return mc
}
diff --git a/pkg/audio/asr/asr.go b/pkg/audio/asr/asr.go
index d15dc3f09..1482f40bb 100644
--- a/pkg/audio/asr/asr.go
+++ b/pkg/audio/asr/asr.go
@@ -19,16 +19,16 @@ type TranscriptionResponse struct {
Duration float64 `json:"duration,omitempty"`
}
-func supportsAudioTranscription(model string) bool {
- protocol, _ := providers.ExtractProtocol(model)
+func supportsAudioTranscription(modelCfg *config.ModelConfig) bool {
+ protocol, _ := providers.ExtractProtocol(modelCfg)
switch protocol {
case "openai", "azure", "azure-openai",
"litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
- "vivgrid", "volcengine", "vllm", "qwen", "qwen-intl", "qwen-international", "dashscope-intl",
+ "vivgrid", "volcengine", "vllm", "qwen", "qwen-portal", "qwen-intl", "qwen-international", "dashscope-intl",
"qwen-us", "dashscope-us", "mistral", "avian", "minimax", "longcat", "modelscope", "novita",
- "coding-plan", "alibaba-coding", "qwen-coding":
+ "coding-plan", "alibaba-coding", "qwen-coding", "zai":
// These protocols all go through the OpenAI-compatible or Azure provider path in
// providers.CreateProviderFromConfig, so they are the only ones that can supply
// the audio media payload shape expected by NewAudioModelTranscriber.
@@ -41,15 +41,15 @@ func supportsAudioTranscription(model string) bool {
}
}
-func supportsWhisperTranscription(model string) bool {
- protocol, _ := providers.ExtractProtocol(model)
+func supportsWhisperTranscription(modelCfg *config.ModelConfig) bool {
+ protocol, _ := providers.ExtractProtocol(modelCfg)
switch protocol {
case "openai", "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
- "vivgrid", "volcengine", "vllm", "qwen", "qwen-intl", "qwen-international", "dashscope-intl",
+ "vivgrid", "volcengine", "vllm", "qwen", "qwen-portal", "qwen-intl", "qwen-international", "dashscope-intl",
"qwen-us", "dashscope-us", "mistral", "avian", "minimax", "longcat", "modelscope", "novita",
- "coding-plan", "alibaba-coding", "qwen-coding", "mimo":
+ "coding-plan", "alibaba-coding", "qwen-coding", "zai", "mimo":
return true
default:
return false
@@ -61,11 +61,11 @@ func whisperModelID(modelCfg *config.ModelConfig) string {
return ""
}
- if !supportsWhisperTranscription(modelCfg.Model) {
+ if !supportsWhisperTranscription(modelCfg) {
return ""
}
- _, modelID := providers.ExtractProtocol(strings.TrimSpace(modelCfg.Model))
+ _, modelID := providers.ExtractProtocol(modelCfg)
if strings.Contains(strings.ToLower(modelID), "whisper") {
return modelID
}
@@ -77,14 +77,14 @@ func transcriberFromModelConfig(modelCfg *config.ModelConfig) Transcriber {
return nil
}
- protocol, _ := providers.ExtractProtocol(modelCfg.Model)
+ protocol, _ := providers.ExtractProtocol(modelCfg)
if protocol == "elevenlabs" && modelCfg.APIKey() != "" {
return NewElevenLabsTranscriber(modelCfg.APIKey(), modelCfg.APIBase)
}
if modelID := whisperModelID(modelCfg); modelID != "" {
return NewWhisperTranscriber(modelCfg)
}
- if supportsAudioTranscription(modelCfg.Model) {
+ if supportsAudioTranscription(modelCfg) {
return NewAudioModelTranscriber(modelCfg)
}
return nil
@@ -95,7 +95,7 @@ func fallbackTranscriberFromModelConfig(modelCfg *config.ModelConfig) Transcribe
return nil
}
- protocol, _ := providers.ExtractProtocol(modelCfg.Model)
+ protocol, _ := providers.ExtractProtocol(modelCfg)
if protocol == "elevenlabs" && modelCfg.APIKey() != "" {
return NewElevenLabsTranscriber(modelCfg.APIKey(), modelCfg.APIBase)
}
diff --git a/pkg/audio/asr/whisper_transcriber.go b/pkg/audio/asr/whisper_transcriber.go
index 406710a8a..fc1101e1c 100644
--- a/pkg/audio/asr/whisper_transcriber.go
+++ b/pkg/audio/asr/whisper_transcriber.go
@@ -32,7 +32,7 @@ func NewWhisperTranscriber(modelCfg *config.ModelConfig) *WhisperTranscriber {
return nil
}
- protocol, modelID := providers.ExtractProtocol(modelCfg.Model)
+ protocol, modelID := providers.ExtractProtocol(modelCfg)
if modelID == "" {
modelID = strings.TrimSpace(modelCfg.Model)
}
diff --git a/pkg/audio/tts/tts.go b/pkg/audio/tts/tts.go
index 99a9ef203..7ae85c8da 100644
--- a/pkg/audio/tts/tts.go
+++ b/pkg/audio/tts/tts.go
@@ -24,7 +24,7 @@ func providerFromModelConfig(mc *config.ModelConfig) TTSProvider {
return nil
}
- protocol, modelID := providers.ExtractProtocol(mc.Model)
+ protocol, modelID := providers.ExtractProtocol(mc)
if modelID == "" {
modelID = strings.TrimSpace(mc.Model)
}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 5bc96fb12..a39cb55ae 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -523,15 +523,16 @@ type VoiceConfig struct {
// ModelConfig represents a model-centric provider configuration.
// It allows adding new providers (especially OpenAI-compatible ones) via configuration only.
-// The model field uses protocol prefix format: [protocol/]model-identifier
-// Supported protocols include openai, anthropic, antigravity, claude-cli,
+// The Model field may be either a plain model identifier or a provider-prefixed
+// identifier such as "openai/gpt-5.4" or "nvidia/z-ai/glm-5.1".
+// Supported providers include openai, anthropic, antigravity, claude-cli,
// codex-cli, github-copilot, and named OpenAI-compatible protocols such as
// groq, deepseek, modelscope, and novita.
-// Default protocol is "openai" if no prefix is specified.
type ModelConfig struct {
// Required fields
ModelName string `json:"model_name"` // User-facing alias for the model
- Model string `json:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4.6")
+ Provider string `json:"provider"` // Provider name for routing and selection. When empty, provider resolution infers it from Model.
+ Model string `json:"model"` // Model identifier, optionally provider-prefixed.
// HTTP-based providers
APIBase string `json:"api_base,omitempty"` // API endpoint URL
@@ -1411,6 +1412,7 @@ func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
// Create a copy for the additional key
additionalEntry := &ModelConfig{
ModelName: expandedName,
+ Provider: m.Provider,
Model: m.Model,
APIBase: m.APIBase,
APIKeys: SimpleSecureStrings(keys[i]),
@@ -1434,6 +1436,7 @@ func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
// Create the primary entry with first key and fallbacks
primaryEntry := &ModelConfig{
ModelName: originalName,
+ Provider: m.Provider,
Model: m.Model,
APIBase: m.APIBase,
Proxy: m.Proxy,
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index d9ca0cb9d..85a98402e 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -1933,7 +1933,7 @@ func TestDefaultConfig_MinimaxExtraBody(t *testing.T) {
var minimaxCfg *ModelConfig
for i := range cfg.ModelList {
- if cfg.ModelList[i].Model == "minimax/MiniMax-M2.5" {
+ if cfg.ModelList[i].Provider == "minimax" && cfg.ModelList[i].Model == "MiniMax-M2.5" {
minimaxCfg = cfg.ModelList[i]
break
}
diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go
index 3d12c6ba5..35ef7cdd8 100644
--- a/pkg/config/defaults.go
+++ b/pkg/config/defaults.go
@@ -61,129 +61,148 @@ func DefaultConfig() *Config {
// Zhipu AI (智谱) - https://open.bigmodel.cn/usercenter/apikeys
{
ModelName: "glm-4.7",
- Model: "zhipu/glm-4.7",
+ Provider: "zhipu",
+ Model: "glm-4.7",
APIBase: "https://open.bigmodel.cn/api/paas/v4",
},
// OpenAI - https://platform.openai.com/api-keys
{
ModelName: "gpt-5.4",
- Model: "openai/gpt-5.4",
+ Provider: "openai",
+ Model: "gpt-5.4",
APIBase: "https://api.openai.com/v1",
},
// Anthropic Claude - https://console.anthropic.com/settings/keys
{
ModelName: "claude-sonnet-4.6",
- Model: "anthropic/claude-sonnet-4.6",
+ Provider: "anthropic",
+ Model: "claude-sonnet-4.6",
APIBase: "https://api.anthropic.com/v1",
},
// DeepSeek - https://platform.deepseek.com/
{
ModelName: "deepseek-chat",
- Model: "deepseek/deepseek-chat",
+ Provider: "deepseek",
+ Model: "deepseek-chat",
APIBase: "https://api.deepseek.com/v1",
},
// Venice AI - https://venice.ai
{
ModelName: "venice-uncensored",
- Model: "venice/venice-uncensored",
+ Provider: "venice",
+ Model: "venice-uncensored",
APIBase: "https://api.venice.ai/api/v1",
},
// Google Gemini - https://ai.google.dev/
{
ModelName: "gemini-2.0-flash",
- Model: "gemini/gemini-2.0-flash-exp",
+ Provider: "gemini",
+ Model: "gemini-2.0-flash-exp",
APIBase: "https://generativelanguage.googleapis.com/v1beta",
},
// Qwen (通义千问) - https://dashscope.console.aliyun.com/apiKey
{
ModelName: "qwen-plus",
- Model: "qwen/qwen-plus",
+ Provider: "qwen",
+ Model: "qwen-plus",
APIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
// Moonshot (月之暗面) - https://platform.moonshot.cn/console/api-keys
{
ModelName: "moonshot-v1-8k",
- Model: "moonshot/moonshot-v1-8k",
+ Provider: "moonshot",
+ Model: "moonshot-v1-8k",
APIBase: "https://api.moonshot.cn/v1",
},
// Groq - https://console.groq.com/keys
{
ModelName: "llama-3.3-70b",
- Model: "groq/llama-3.3-70b-versatile",
+ Provider: "groq",
+ Model: "llama-3.3-70b-versatile",
APIBase: "https://api.groq.com/openai/v1",
},
// OpenRouter (100+ models) - https://openrouter.ai/keys
{
ModelName: "openrouter-auto",
- Model: "openrouter/auto",
+ Provider: "openrouter",
+ Model: "auto",
APIBase: "https://openrouter.ai/api/v1",
},
{
ModelName: "openrouter-gpt-5.4",
- Model: "openrouter/openai/gpt-5.4",
+ Provider: "openrouter",
+ Model: "openai/gpt-5.4",
APIBase: "https://openrouter.ai/api/v1",
},
// NVIDIA - https://build.nvidia.com/
{
ModelName: "nemotron-4-340b",
- Model: "nvidia/nemotron-4-340b-instruct",
+ Provider: "nvidia",
+ Model: "nemotron-4-340b-instruct",
APIBase: "https://integrate.api.nvidia.com/v1",
},
// Cerebras - https://inference.cerebras.ai/
{
ModelName: "cerebras-llama-3.3-70b",
- Model: "cerebras/llama-3.3-70b",
+ Provider: "cerebras",
+ Model: "llama-3.3-70b",
APIBase: "https://api.cerebras.ai/v1",
},
// Vivgrid - https://vivgrid.com
{
ModelName: "vivgrid-auto",
- Model: "vivgrid/auto",
+ Provider: "vivgrid",
+ Model: "auto",
APIBase: "https://api.vivgrid.com/v1",
},
// Volcengine (火山引擎) - https://console.volcengine.com/ark
{
ModelName: "ark-code-latest",
- Model: "volcengine/ark-code-latest",
+ Provider: "volcengine",
+ Model: "ark-code-latest",
APIBase: "https://ark.cn-beijing.volces.com/api/v3",
},
{
ModelName: "doubao-pro",
- Model: "volcengine/doubao-pro-32k",
+ Provider: "volcengine",
+ Model: "doubao-pro-32k",
APIBase: "https://ark.cn-beijing.volces.com/api/v3",
},
// ShengsuanYun (神算云)
{
ModelName: "deepseek-v3",
- Model: "shengsuanyun/deepseek-v3",
+ Provider: "shengsuanyun",
+ Model: "deepseek-v3",
APIBase: "https://api.shengsuanyun.com/v1",
},
// Antigravity (Google Cloud Code Assist) - OAuth only
{
ModelName: "gemini-flash",
- Model: "antigravity/gemini-3-flash",
+ Provider: "antigravity",
+ Model: "gemini-3-flash",
AuthMethod: "oauth",
},
// GitHub Copilot - https://github.com/settings/tokens
{
ModelName: "copilot-gpt-5.4",
- Model: "github-copilot/gpt-5.4",
+ Provider: "github-copilot",
+ Model: "gpt-5.4",
APIBase: "http://localhost:4321",
AuthMethod: "oauth",
},
@@ -191,33 +210,38 @@ func DefaultConfig() *Config {
// Ollama (local) - https://ollama.com
{
ModelName: "llama3",
- Model: "ollama/llama3",
+ Provider: "ollama",
+ Model: "llama3",
APIBase: "http://localhost:11434/v1",
},
// Mistral AI - https://console.mistral.ai/api-keys
{
ModelName: "mistral-small",
- Model: "mistral/mistral-small-latest",
+ Provider: "mistral",
+ Model: "mistral-small-latest",
APIBase: "https://api.mistral.ai/v1",
},
// Avian - https://avian.io
{
ModelName: "deepseek-v3.2",
- Model: "avian/deepseek/deepseek-v3.2",
+ Provider: "avian",
+ Model: "deepseek/deepseek-v3.2",
APIBase: "https://api.avian.io/v1",
},
{
ModelName: "kimi-k2.5",
- Model: "avian/moonshotai/kimi-k2.5",
+ Provider: "avian",
+ Model: "moonshotai/kimi-k2.5",
APIBase: "https://api.avian.io/v1",
},
// Minimax - https://api.minimaxi.com/
{
ModelName: "MiniMax-M2.5",
- Model: "minimax/MiniMax-M2.5",
+ Provider: "minimax",
+ Model: "MiniMax-M2.5",
APIBase: "https://api.minimaxi.com/v1",
ExtraBody: map[string]any{"reasoning_split": true},
},
@@ -225,28 +249,32 @@ func DefaultConfig() *Config {
// LongCat - https://longcat.chat/platform
{
ModelName: "LongCat-Flash-Thinking",
- Model: "longcat/LongCat-Flash-Thinking",
+ Provider: "longcat",
+ Model: "LongCat-Flash-Thinking",
APIBase: "https://api.longcat.chat/openai",
},
// ModelScope (魔搭社区) - https://modelscope.cn/my/tokens
{
ModelName: "modelscope-qwen",
- Model: "modelscope/Qwen/Qwen3-235B-A22B-Instruct-2507",
+ Provider: "modelscope",
+ Model: "Qwen/Qwen3-235B-A22B-Instruct-2507",
APIBase: "https://api-inference.modelscope.cn/v1",
},
// VLLM (local) - http://localhost:8000
{
ModelName: "local-model",
- Model: "vllm/custom-model",
+ Provider: "vllm",
+ Model: "custom-model",
APIBase: "http://localhost:8000/v1",
},
// LM Studio (local) - http://localhost:1234
{
ModelName: "lmstudio-local",
- Model: "lmstudio/openai/gpt-oss-20b",
+ Provider: "lmstudio",
+ Model: "openai/gpt-oss-20b",
APIBase: "http://localhost:1234/v1",
},
@@ -254,7 +282,8 @@ func DefaultConfig() *Config {
// model_name is a user-friendly alias; the model field's path after "azure/" is your deployment name
{
ModelName: "azure-gpt5",
- Model: "azure/my-gpt5-deployment",
+ Provider: "azure",
+ Model: "my-gpt5-deployment",
APIBase: "https://your-resource.openai.azure.com",
},
},
diff --git a/pkg/config/multikey_test.go b/pkg/config/multikey_test.go
index 947e942da..cb55db938 100644
--- a/pkg/config/multikey_test.go
+++ b/pkg/config/multikey_test.go
@@ -188,6 +188,7 @@ func TestExpandMultiKeyModels_Deduplication(t *testing.T) {
func TestExpandMultiKeyModels_PreservesOtherFields(t *testing.T) {
modelCfg := &ModelConfig{
ModelName: "gpt-4",
+ Provider: "openrouter",
Model: "openai/gpt-4o",
APIBase: "https://api.example.com",
Proxy: "http://proxy:8080",
@@ -206,6 +207,9 @@ func TestExpandMultiKeyModels_PreservesOtherFields(t *testing.T) {
if primary.APIBase != "https://api.example.com" {
t.Errorf("expected api_base preserved, got %q", primary.APIBase)
}
+ if primary.Provider != "openrouter" {
+ t.Errorf("expected provider preserved, got %q", primary.Provider)
+ }
if primary.Proxy != "http://proxy:8080" {
t.Errorf("expected proxy preserved, got %q", primary.Proxy)
}
@@ -224,6 +228,9 @@ func TestExpandMultiKeyModels_PreservesOtherFields(t *testing.T) {
// Check additional entry also preserves fields
additional := result[0]
+ if additional.Provider != "openrouter" {
+ t.Errorf("expected additional provider preserved, got %q", additional.Provider)
+ }
if additional.APIBase != "https://api.example.com" {
t.Errorf("expected additional api_base preserved, got %q", additional.APIBase)
}
diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go
index ab68b326a..2f956c7a0 100644
--- a/pkg/providers/factory_provider.go
+++ b/pkg/providers/factory_provider.go
@@ -41,6 +41,7 @@ var protocolMetaByName = map[string]protocolMeta{
"vivgrid": {defaultAPIBase: "https://api.vivgrid.com/v1"},
"volcengine": {defaultAPIBase: "https://ark.cn-beijing.volces.com/api/v3"},
"qwen": {defaultAPIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1"},
+ "qwen-portal": {defaultAPIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1"},
"qwen-intl": {defaultAPIBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"},
"qwen-international": {defaultAPIBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"},
"dashscope-intl": {defaultAPIBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"},
@@ -51,6 +52,7 @@ var protocolMetaByName = map[string]protocolMeta{
"qwen-coding": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/v1"},
"coding-plan-anthropic": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"},
"alibaba-coding-anthropic": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"},
+ "zai": {defaultAPIBase: "https://api.z.ai/api/coding/paas/v4"},
"vllm": {defaultAPIBase: "http://localhost:8000/v1", emptyAPIKeyAllowed: true},
"mistral": {defaultAPIBase: "https://api.mistral.ai/v1"},
"avian": {defaultAPIBase: "https://api.avian.io/v1"},
@@ -84,19 +86,42 @@ func createCodexAuthProvider() (LLMProvider, error) {
return NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil
}
-// ExtractProtocol extracts the protocol prefix and model identifier from a model string.
-// If no prefix is specified, it defaults to "openai".
+// ExtractProtocol extracts the effective protocol and model identifier from a
+// model configuration.
+//
+// The explicit Provider field takes precedence. When Provider is empty, the
+// protocol is inferred from Model. Plain model names default to "openai".
+// Provider-prefixed models strip the first slash-separated segment from the
+// returned model ID.
+//
+// The returned protocol is normalized to the provider's canonical spelling.
// Examples:
-// - "openai/gpt-4o" -> ("openai", "gpt-4o")
-// - "anthropic/claude-sonnet-4.6" -> ("anthropic", "claude-sonnet-4.6")
-// - "gpt-4o" -> ("openai", "gpt-4o") // default protocol
-func ExtractProtocol(model string) (protocol, modelID string) {
- model = strings.TrimSpace(model)
- protocol, modelID, found := strings.Cut(model, "/")
+// - Model "openai/gpt-4o" -> ("openai", "gpt-4o")
+// - Model "nvidia/z-ai/glm-5.1" -> ("nvidia", "z-ai/glm-5.1")
+// - Provider "nvidia", Model "z-ai/glm-5.1" -> ("nvidia", "z-ai/glm-5.1")
+// - Model "gpt-4o" -> ("openai", "gpt-4o")
+func ExtractProtocol(cfg *config.ModelConfig) (protocol, modelID string) {
+ if cfg == nil {
+ return "", ""
+ }
+
+ model := strings.TrimSpace(cfg.Model)
+ if provider := strings.TrimSpace(cfg.Provider); provider != "" {
+ return NormalizeProvider(provider), model
+ }
+ if model == "" {
+ return "", ""
+ }
+
+ protocol, rest, found := strings.Cut(model, "/")
if !found {
return "openai", model
}
- return protocol, modelID
+ protocol = strings.TrimSpace(protocol)
+ if protocol == "" {
+ return "", strings.TrimSpace(rest)
+ }
+ return NormalizeProvider(protocol), strings.TrimSpace(rest)
}
// ResolveAPIBase returns the configured API base, or the protocol default when
@@ -108,16 +133,16 @@ func ResolveAPIBase(cfg *config.ModelConfig) string {
if apiBase := strings.TrimSpace(cfg.APIBase); apiBase != "" {
return strings.TrimRight(apiBase, "/")
}
- protocol, _ := ExtractProtocol(cfg.Model)
+ protocol, _ := ExtractProtocol(cfg)
return strings.TrimRight(getDefaultAPIBase(protocol), "/")
}
// CreateProviderFromConfig creates a provider based on the ModelConfig.
-// It uses the protocol prefix in the Model field to determine which provider to create.
+// It uses ExtractProtocol to determine which provider to create.
// Supported protocol families include OpenAI-compatible prefixes (e.g., openai, openrouter, groq),
// Azure OpenAI, Amazon Bedrock, Anthropic (including messages), and various CLI/compatibility shims.
// See the switch on protocol in this function for the authoritative list.
-// Returns the provider, the model ID (without protocol prefix), and any error.
+// Returns the provider, the effective model ID from ExtractProtocol, and any error.
func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) {
if cfg == nil {
return nil, "", fmt.Errorf("config is nil")
@@ -127,7 +152,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
return nil, "", fmt.Errorf("model is required")
}
- protocol, modelID := ExtractProtocol(cfg.Model)
+ protocol, modelID := ExtractProtocol(cfg)
userAgent := cfg.UserAgent
if userAgent == "" {
@@ -220,9 +245,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
case "litellm", "lmstudio", "openrouter", "groq", "zhipu", "nvidia", "venice",
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
- "vivgrid", "volcengine", "vllm", "qwen", "qwen-intl", "qwen-international", "dashscope-intl",
+ "vivgrid", "volcengine", "vllm", "qwen", "qwen-portal", "qwen-intl", "qwen-international", "dashscope-intl",
"qwen-us", "dashscope-us", "mistral", "avian", "longcat", "modelscope", "novita",
- "coding-plan", "alibaba-coding", "qwen-coding", "mimo":
+ "coding-plan", "alibaba-coding", "qwen-coding", "zai", "mimo":
// All other OpenAI-compatible HTTP providers
if cfg.APIKey() == "" && cfg.APIBase == "" && !isEmptyAPIKeyAllowed(protocol) {
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
diff --git a/pkg/providers/factory_provider_test.go b/pkg/providers/factory_provider_test.go
index 20cdd8a30..62b838acb 100644
--- a/pkg/providers/factory_provider_test.go
+++ b/pkg/providers/factory_provider_test.go
@@ -19,68 +19,91 @@ import (
func TestExtractProtocol(t *testing.T) {
tests := []struct {
name string
- model string
+ config *config.ModelConfig
wantProtocol string
wantModelID string
}{
{
name: "openai with prefix",
- model: "openai/gpt-4o",
+ config: &config.ModelConfig{Model: "openai/gpt-4o"},
wantProtocol: "openai",
wantModelID: "gpt-4o",
},
{
name: "anthropic with prefix",
- model: "anthropic/claude-sonnet-4.6",
+ config: &config.ModelConfig{Model: "anthropic/claude-sonnet-4.6"},
wantProtocol: "anthropic",
wantModelID: "claude-sonnet-4.6",
},
{
name: "no prefix - defaults to openai",
- model: "gpt-4o",
+ config: &config.ModelConfig{Model: "gpt-4o"},
wantProtocol: "openai",
wantModelID: "gpt-4o",
},
{
name: "groq with prefix",
- model: "groq/llama-3.1-70b",
+ config: &config.ModelConfig{Model: "groq/llama-3.1-70b"},
wantProtocol: "groq",
wantModelID: "llama-3.1-70b",
},
{
name: "empty string",
- model: "",
- wantProtocol: "openai",
+ config: &config.ModelConfig{Model: ""},
+ wantProtocol: "",
wantModelID: "",
},
{
name: "with whitespace",
- model: " openai/gpt-4 ",
+ config: &config.ModelConfig{Model: " openai/gpt-4 "},
wantProtocol: "openai",
wantModelID: "gpt-4",
},
{
name: "multiple slashes",
- model: "nvidia/meta/llama-3.1-8b",
+ config: &config.ModelConfig{Model: "nvidia/meta/llama-3.1-8b"},
wantProtocol: "nvidia",
wantModelID: "meta/llama-3.1-8b",
},
+ {
+ name: "normalizes provider",
+ config: &config.ModelConfig{Model: "z.ai/glm-5.1"},
+ wantProtocol: "zai",
+ wantModelID: "glm-5.1",
+ },
{
name: "azure with prefix",
- model: "azure/my-gpt5-deployment",
+ config: &config.ModelConfig{Model: "azure/my-gpt5-deployment"},
wantProtocol: "azure",
wantModelID: "my-gpt5-deployment",
},
+ {
+ name: "explicit provider keeps model",
+ config: &config.ModelConfig{Provider: "nvidia", Model: "z-ai/glm-5.1"},
+ wantProtocol: "nvidia",
+ wantModelID: "z-ai/glm-5.1",
+ },
+ {
+ name: "empty provider segment",
+ config: &config.ModelConfig{Model: "/gpt-4o"},
+ wantProtocol: "",
+ wantModelID: "gpt-4o",
+ },
+ {
+ name: "nil config",
+ wantProtocol: "",
+ wantModelID: "",
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- protocol, modelID := ExtractProtocol(tt.model)
+ protocol, modelID := ExtractProtocol(tt.config)
if protocol != tt.wantProtocol {
- t.Errorf("ExtractProtocol(%q) protocol = %q, want %q", tt.model, protocol, tt.wantProtocol)
+ t.Errorf("ExtractProtocol() protocol = %q, want %q", protocol, tt.wantProtocol)
}
if modelID != tt.wantModelID {
- t.Errorf("ExtractProtocol(%q) modelID = %q, want %q", tt.model, modelID, tt.wantModelID)
+ t.Errorf("ExtractProtocol() modelID = %q, want %q", modelID, tt.wantModelID)
}
})
}
@@ -106,6 +129,29 @@ func TestCreateProviderFromConfig_OpenAI(t *testing.T) {
}
}
+func TestCreateProviderFromConfig_UsesExplicitProvider(t *testing.T) {
+ cfg := &config.ModelConfig{
+ ModelName: "test-explicit-provider",
+ Model: "z-ai/glm-5.1",
+ Provider: "nvidia",
+ }
+ 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 != "z-ai/glm-5.1" {
+ t.Fatalf("modelID = %q, want z-ai/glm-5.1", modelID)
+ }
+ if got := ResolveAPIBase(cfg); got != "https://integrate.api.nvidia.com/v1" {
+ t.Fatalf("ResolveAPIBase() = %q, want NVIDIA default API base", got)
+ }
+}
+
func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
tests := []struct {
name string
@@ -701,8 +747,9 @@ func TestCreateProviderFromConfig_QwenInternationalAlias(t *testing.T) {
if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider")
}
- if modelID != "qwen-max" {
- t.Errorf("modelID = %q, want %q", modelID, "qwen-max")
+ wantModelID := "qwen-max"
+ if modelID != wantModelID {
+ t.Errorf("modelID = %q, want %q", modelID, wantModelID)
}
if _, ok := provider.(*HTTPProvider); !ok {
t.Fatalf("expected *HTTPProvider, got %T", provider)
@@ -735,8 +782,9 @@ func TestCreateProviderFromConfig_QwenUSAlias(t *testing.T) {
if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider")
}
- if modelID != "qwen-max" {
- t.Errorf("modelID = %q, want %q", modelID, "qwen-max")
+ wantModelID := "qwen-max"
+ if modelID != wantModelID {
+ t.Errorf("modelID = %q, want %q", modelID, wantModelID)
}
if _, ok := provider.(*HTTPProvider); !ok {
t.Fatalf("expected *HTTPProvider, got %T", provider)
@@ -769,8 +817,9 @@ func TestCreateProviderFromConfig_CodingPlanAnthropic(t *testing.T) {
if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider")
}
- if modelID != "claude-sonnet-4-20250514" {
- t.Errorf("modelID = %q, want %q", modelID, "claude-sonnet-4-20250514")
+ wantModelID := "claude-sonnet-4-20250514"
+ if modelID != wantModelID {
+ t.Errorf("modelID = %q, want %q", modelID, wantModelID)
}
// coding-plan-anthropic uses Anthropic Messages provider
// Verify it's the anthropic messages provider by checking interface