refactor: support explicit model list providers

This commit is contained in:
lxowalle 2026-04-21 16:30:48 +08:00
parent 329e68e017
commit 8deb0a251a
29 changed files with 764 additions and 381 deletions

View file

@ -59,7 +59,7 @@ func authLoginOpenAI(useDeviceCode bool, noBrowser bool) error {
// Update or add openai in ModelList // Update or add openai in ModelList
foundOpenAI := false foundOpenAI := false
for i := range appCfg.ModelList { for i := range appCfg.ModelList {
if isOpenAIModel(appCfg.ModelList[i].Model) { if isOpenAIModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "oauth" appCfg.ModelList[i].AuthMethod = "oauth"
foundOpenAI = true foundOpenAI = true
break break
@ -130,7 +130,7 @@ func authLoginGoogleAntigravity(noBrowser bool) error {
// Update or add antigravity in ModelList // Update or add antigravity in ModelList
foundAntigravity := false foundAntigravity := false
for i := range appCfg.ModelList { for i := range appCfg.ModelList {
if isAntigravityModel(appCfg.ModelList[i].Model) { if isAntigravityModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "oauth" appCfg.ModelList[i].AuthMethod = "oauth"
foundAntigravity = true foundAntigravity = true
break break
@ -206,7 +206,7 @@ func authLoginAnthropicSetupToken() error {
if err == nil { if err == nil {
found := false found := false
for i := range appCfg.ModelList { for i := range appCfg.ModelList {
if isAnthropicModel(appCfg.ModelList[i].Model) { if isAnthropicModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "oauth" appCfg.ModelList[i].AuthMethod = "oauth"
found = true found = true
break break
@ -282,7 +282,7 @@ func authLoginPasteToken(provider string) error {
// Update ModelList // Update ModelList
found := false found := false
for i := range appCfg.ModelList { for i := range appCfg.ModelList {
if isAnthropicModel(appCfg.ModelList[i].Model) { if isAnthropicModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "token" appCfg.ModelList[i].AuthMethod = "token"
found = true found = true
break break
@ -300,7 +300,7 @@ func authLoginPasteToken(provider string) error {
// Update ModelList // Update ModelList
found := false found := false
for i := range appCfg.ModelList { for i := range appCfg.ModelList {
if isOpenAIModel(appCfg.ModelList[i].Model) { if isOpenAIModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "token" appCfg.ModelList[i].AuthMethod = "token"
found = true found = true
break break
@ -342,15 +342,15 @@ func authLogoutCmd(provider string) error {
for i := range appCfg.ModelList { for i := range appCfg.ModelList {
switch provider { switch provider {
case "openai": case "openai":
if isOpenAIModel(appCfg.ModelList[i].Model) { if isOpenAIModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "" appCfg.ModelList[i].AuthMethod = ""
} }
case "anthropic": case "anthropic":
if isAnthropicModel(appCfg.ModelList[i].Model) { if isAnthropicModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "" appCfg.ModelList[i].AuthMethod = ""
} }
case "google-antigravity", "antigravity": case "google-antigravity", "antigravity":
if isAntigravityModel(appCfg.ModelList[i].Model) { if isAntigravityModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "" appCfg.ModelList[i].AuthMethod = ""
} }
} }
@ -484,22 +484,20 @@ func authModelsCmd() error {
return nil return nil
} }
// isAntigravityModel checks if a model string belongs to antigravity provider // isAntigravityModel checks if a model config belongs to an Antigravity provider.
func isAntigravityModel(model string) bool { func isAntigravityModel(modelCfg *config.ModelConfig) bool {
return model == "antigravity" || protocol, _ := providers.ExtractProtocol(modelCfg)
model == "google-antigravity" || return protocol == "antigravity" || protocol == "google-antigravity"
strings.HasPrefix(model, "antigravity/") ||
strings.HasPrefix(model, "google-antigravity/")
} }
// isOpenAIModel checks if a model string belongs to openai provider // isOpenAIModel checks if a model config belongs to the OpenAI provider.
func isOpenAIModel(model string) bool { func isOpenAIModel(modelCfg *config.ModelConfig) bool {
return model == "openai" || protocol, _ := providers.ExtractProtocol(modelCfg)
strings.HasPrefix(model, "openai/") return protocol == "openai"
} }
// isAnthropicModel checks if a model string belongs to anthropic provider // isAnthropicModel checks if a model config belongs to the Anthropic provider.
func isAnthropicModel(model string) bool { func isAnthropicModel(modelCfg *config.ModelConfig) bool {
return model == "anthropic" || protocol, _ := providers.ExtractProtocol(modelCfg)
strings.HasPrefix(model, "anthropic/") return protocol == "anthropic"
} }

View file

@ -3,12 +3,12 @@ package status
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
"github.com/sipeed/picoclaw/pkg/auth" "github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/providers"
) )
func statusCmd() { func statusCmd() {
@ -44,12 +44,13 @@ func statusCmd() {
// not depend on a legacy cfg.Providers field (which may not exist under some // not depend on a legacy cfg.Providers field (which may not exist under some
// build tags). We infer provider availability from model_list entries. // build tags). We infer provider availability from model_list entries.
hasProtocolKey := func(protocol string) bool { hasProtocolKey := func(protocol string) bool {
prefix := protocol + "/" want := providers.NormalizeProvider(protocol)
for _, m := range cfg.ModelList { for _, m := range cfg.ModelList {
if m == nil { if m == nil {
continue continue
} }
if strings.HasPrefix(m.Model, prefix) && m.APIKey() != "" { got, _ := providers.ExtractProtocol(m)
if got == want && m.APIKey() != "" {
return true return true
} }
} }
@ -67,12 +68,13 @@ func statusCmd() {
return "", false return "", false
} }
findProtocolBase := func(protocol string) (string, bool) { findProtocolBase := func(protocol string) (string, bool) {
prefix := protocol + "/" want := providers.NormalizeProvider(protocol)
for _, m := range cfg.ModelList { for _, m := range cfg.ModelList {
if m == nil { if m == nil {
continue continue
} }
if strings.HasPrefix(m.Model, prefix) && m.APIBase != "" { got, _ := providers.ExtractProtocol(m)
if got == want && m.APIBase != "" {
return m.APIBase, true return m.APIBase, true
} }
} }

View file

@ -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)
}
}

View file

@ -493,7 +493,7 @@ The subagent has access to tools (message, web_search, etc.) and can communicate
### Model Configuration (model_list) ### 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: This design also enables **multi-agent support** with flexible provider selection:
@ -546,7 +546,8 @@ chmod 600 ~/.picoclaw/.security.yml
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4" "provider": "openai",
"model": "gpt-5.4"
// api_key loaded from .security.yml // api_key loaded from .security.yml
} }
], ],
@ -570,31 +571,31 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
#### All Supported Vendors #### 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) | | **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) | | **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) | | **智谱 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) | | **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) | | **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) | | **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) | | **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) | | **通义千问 (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) | | **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) | | **Ollama** | `ollama` | `http://localhost:11434/v1` | OpenAI | Local (no key needed) |
| **LM Studio** | `lmstudio/` | `http://localhost:1234/v1` | OpenAI | Optional (local default: no key) | | **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) | | **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 | | **LiteLLM Proxy** | `litellm` | `http://localhost:4000/v1` | OpenAI | Your LiteLLM proxy key |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local | | **VLLM** | `vllm` | `http://localhost:8000/v1` | OpenAI | Local |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) | | **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) | | **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 | — | | **神算云** | `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) | | **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) | | **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) | | **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) | | **ModelScope (魔搭)** | `modelscope` | `https://api-inference.modelscope.cn/v1` | OpenAI | [Get Token](https://modelscope.cn/my/tokens) |
| **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth only | | **Antigravity** | `antigravity` | Google Cloud | Custom | OAuth only |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | — | | **GitHub Copilot** | `github-copilot` | `localhost:4321` | gRPC | — |
#### Basic Configuration #### Basic Configuration
@ -603,22 +604,26 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
"model_list": [ "model_list": [
{ {
"model_name": "ark-code-latest", "model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest", "provider": "volcengine",
"model": "ark-code-latest",
"api_keys": ["sk-your-api-key"] "api_keys": ["sk-your-api-key"]
}, },
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-your-openai-key"] "api_keys": ["sk-your-openai-key"]
}, },
{ {
"model_name": "claude-sonnet-4.6", "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"] "api_keys": ["sk-ant-your-key"]
}, },
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "zhipu/glm-4.7", "provider": "zhipu",
"model": "glm-4.7",
"api_keys": ["your-zhipu-key"] "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. > **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 #### 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). > **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 ```json
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4" "provider": "openai",
"model": "gpt-5.4"
// api_key: set in .security.yml // api_key: set in .security.yml
} }
``` ```
@ -657,7 +670,8 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
```json ```json
{ {
"model_name": "ark-code-latest", "model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest" "provider": "volcengine",
"model": "ark-code-latest"
// api_key: set in .security.yml // api_key: set in .security.yml
} }
``` ```
@ -670,7 +684,8 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
```json ```json
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "zhipu/glm-4.7" "provider": "zhipu",
"model": "glm-4.7"
// api_key: set in .security.yml // api_key: set in .security.yml
} }
``` ```
@ -683,7 +698,8 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
```json ```json
{ {
"model_name": "deepseek-chat", "model_name": "deepseek-chat",
"model": "deepseek/deepseek-chat" "provider": "deepseek",
"model": "deepseek-chat"
// api_key: set in .security.yml // api_key: set in .security.yml
} }
``` ```
@ -696,7 +712,8 @@ For complete documentation, see [`../security/security_configuration.md`](../sec
```json ```json
{ {
"model_name": "claude-sonnet-4.6", "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 // api_key: set in .security.yml
} }
``` ```
@ -708,7 +725,8 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
```json ```json
{ {
"model_name": "claude-opus-4-6", "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_keys": ["sk-ant-your-key"],
"api_base": "https://api.anthropic.com" "api_base": "https://api.anthropic.com"
} }
@ -724,7 +742,8 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
```json ```json
{ {
"model_name": "llama3", "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 ```json
{ {
"model_name": "lmstudio-local", "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.<br/> `api_base` defaults to `http://localhost:1234/v1`. API key is optional unless your LM Studio server enables authentication.<br/>
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.
</details> </details>
@ -751,13 +771,14 @@ PicoClaw sends OpenAI-compatible requests to LM Studio, and strips the `lmstudio
```json ```json
{ {
"model_name": "my-custom-model", "model_name": "my-custom-model",
"model": "openai/custom-model", "provider": "openai",
"model": "custom-model",
"api_base": "https://my-proxy.com/v1" "api_base": "https://my-proxy.com/v1"
// api_key: set in .security.yml // 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.
</details> </details>
@ -782,7 +803,8 @@ model_list:
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_base": "https://api.openai.com/v1" "api_base": "https://api.openai.com/v1"
// api_keys loaded from .security.yml // api_keys loaded from .security.yml
} }
@ -797,13 +819,15 @@ model_list:
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_base": "https://api1.example.com/v1", "api_base": "https://api1.example.com/v1",
"api_keys": ["sk-key1"] "api_keys": ["sk-key1"]
}, },
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_base": "https://api2.example.com/v1", "api_base": "https://api2.example.com/v1",
"api_keys": ["sk-key2"] "api_keys": ["sk-key2"]
} }
@ -862,7 +886,7 @@ This keeps the runtime lightweight while making new OpenAI-compatible backends m
{ {
"agents": { "agents": {
"defaults": { "defaults": {
"model": "anthropic/claude-opus-4-5" "model_name": "claude-opus-4-5"
} }
}, },
"session": { "session": {

View file

@ -424,7 +424,7 @@ Agent 读取 HEARTBEAT.md
### 模型配置 (model_list) ### 模型配置 (model_list)
> **新特性:** PicoClaw 现在采用**以模型为中心**的配置方式。只需指定 `vendor/model` 格式(例如 `zhipu/glm-4.7`)即可接入新提供商——**无需修改任何代码!** > **新特性:** PicoClaw 现在优先推荐显式 `provider` + 原生 `model` 的配置方式,例如 `"provider": "zhipu", "model": "glm-4.7"`。如果未设置 `provider`,旧的单字段 `provider/model` 写法仍然兼容。
这一设计同时支持**多 Agent**场景,灵活选择提供商: 这一设计同时支持**多 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) | | **OpenAI** | `openai` | `https://api.openai.com/v1` | OpenAI | [获取](https://platform.openai.com) |
| **Anthropic** | `anthropic/` | `https://api.anthropic.com/v1` | Anthropic | [获取](https://console.anthropic.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) | | **智谱 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) | | **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) | | **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) | | **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) | | **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) | | **通义千问 (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) | | **NVIDIA** | `nvidia` | `https://integrate.api.nvidia.com/v1` | OpenAI | [获取](https://build.nvidia.com) |
| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | 本地(无需 Key | | **Ollama** | `ollama` | `http://localhost:11434/v1` | OpenAI | 本地(无需 Key |
| **LM Studio** | `lmstudio/` | `http://localhost:1234/v1` | OpenAI | 可选(本地默认无需密钥) | | **LM Studio** | `lmstudio` | `http://localhost:1234/v1` | OpenAI | 可选(本地默认无需密钥) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取](https://openrouter.ai/keys) | | **OpenRouter** | `openrouter` | `https://openrouter.ai/api/v1` | OpenAI | [获取](https://openrouter.ai/keys) |
| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | 你的 LiteLLM 代理 Key | | **LiteLLM Proxy** | `litellm` | `http://localhost:4000/v1` | OpenAI | 你的 LiteLLM 代理 Key |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 | | **VLLM** | `vllm` | `http://localhost:8000/v1` | OpenAI | 本地 |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [获取](https://cerebras.ai) | | **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) | | **火山引擎 (豆包)** | `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 | — | | **神算云** | `shengsuanyun` | `https://router.shengsuanyun.com/api/v1` | OpenAI | — |
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [获取](https://www.byteplus.com) | | **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) | | **Vivgrid** | `vivgrid` | `https://api.vivgrid.com/v1` | OpenAI | [获取](https://vivgrid.com) |
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [获取](https://longcat.chat/platform) | | **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) | | **ModelScope (魔搭)** | `modelscope` | `https://api-inference.modelscope.cn/v1` | OpenAI | [获取](https://modelscope.cn/my/tokens) |
| **Antigravity** | `antigravity/` | Google Cloud | Custom | 仅 OAuth | | **Antigravity** | `antigravity` | Google Cloud | Custom | 仅 OAuth |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | — | | **GitHub Copilot** | `github-copilot` | `localhost:4321` | gRPC | — |
#### 基础配置 #### 基础配置
@ -468,22 +468,26 @@ Agent 读取 HEARTBEAT.md
"model_list": [ "model_list": [
{ {
"model_name": "ark-code-latest", "model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest", "provider": "volcengine",
"model": "ark-code-latest",
"api_keys": ["sk-your-api-key"] "api_keys": ["sk-your-api-key"]
}, },
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-your-openai-key"] "api_keys": ["sk-your-openai-key"]
}, },
{ {
"model_name": "claude-sonnet-4.6", "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"] "api_keys": ["sk-ant-your-key"]
}, },
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "zhipu/glm-4.7", "provider": "zhipu",
"model": "glm-4.7",
"api_keys": ["your-zhipu-key"] "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。
#### 各厂商配置示例 #### 各厂商配置示例
<details> <details>
@ -503,7 +514,8 @@ Agent 读取 HEARTBEAT.md
```json ```json
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-..."] "api_keys": ["sk-..."]
} }
``` ```
@ -516,7 +528,8 @@ Agent 读取 HEARTBEAT.md
```json ```json
{ {
"model_name": "ark-code-latest", "model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest", "provider": "volcengine",
"model": "ark-code-latest",
"api_keys": ["sk-..."] "api_keys": ["sk-..."]
} }
``` ```
@ -529,7 +542,8 @@ Agent 读取 HEARTBEAT.md
```json ```json
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "zhipu/glm-4.7", "provider": "zhipu",
"model": "glm-4.7",
"api_keys": ["your-key"] "api_keys": ["your-key"]
} }
``` ```
@ -542,7 +556,8 @@ Agent 读取 HEARTBEAT.md
```json ```json
{ {
"model_name": "deepseek-chat", "model_name": "deepseek-chat",
"model": "deepseek/deepseek-chat", "provider": "deepseek",
"model": "deepseek-chat",
"api_keys": ["sk-..."] "api_keys": ["sk-..."]
} }
``` ```
@ -555,7 +570,8 @@ Agent 读取 HEARTBEAT.md
```json ```json
{ {
"model_name": "claude-sonnet-4.6", "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"] "api_keys": ["sk-ant-your-key"]
} }
``` ```
@ -567,7 +583,8 @@ Agent 读取 HEARTBEAT.md
```json ```json
{ {
"model_name": "claude-opus-4-6", "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_keys": ["sk-ant-your-key"],
"api_base": "https://api.anthropic.com" "api_base": "https://api.anthropic.com"
} }
@ -583,7 +600,8 @@ Agent 读取 HEARTBEAT.md
```json ```json
{ {
"model_name": "llama3", "model_name": "llama3",
"model": "ollama/llama3" "provider": "ollama",
"model": "llama3"
} }
``` ```
@ -595,12 +613,13 @@ Agent 读取 HEARTBEAT.md
```json ```json
{ {
"model_name": "lmstudio-local", "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。 `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
</details> </details>
@ -610,13 +629,14 @@ PicoClaw 向 LM Studio 的 OpenAI 兼容终结点发送请求,且将移除首
```json ```json
{ {
"model_name": "my-custom-model", "model_name": "my-custom-model",
"model": "openai/custom-model", "provider": "openai",
"model": "custom-model",
"api_base": "https://my-proxy.com/v1", "api_base": "https://my-proxy.com/v1",
"api_keys": ["sk-..."] "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` 时也会得到相同结果
</details> </details>
@ -629,13 +649,15 @@ PicoClaw 只剥离最外层的 `litellm/` 前缀再发送请求,因此 `litell
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_base": "https://api1.example.com/v1", "api_base": "https://api1.example.com/v1",
"api_keys": ["sk-key1"] "api_keys": ["sk-key1"]
}, },
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_base": "https://api2.example.com/v1", "api_base": "https://api2.example.com/v1",
"api_keys": ["sk-key2"] "api_keys": ["sk-key2"]
} }
@ -689,7 +711,7 @@ PicoClaw 按协议族路由提供商:
{ {
"agents": { "agents": {
"defaults": { "defaults": {
"model": "anthropic/claude-opus-4-5" "model_name": "claude-opus-4-5"
} }
}, },
"session": { "session": {

View file

@ -94,19 +94,22 @@ picoclaw onboard
"model_list": [ "model_list": [
{ {
"model_name": "ark-code-latest", "model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest", "provider": "volcengine",
"model": "ark-code-latest",
"api_keys": ["sk-your-api-key"], "api_keys": ["sk-your-api-key"],
"api_base":"https://ark.cn-beijing.volces.com/api/coding/v3" "api_base":"https://ark.cn-beijing.volces.com/api/coding/v3"
}, },
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["your-api-key"], "api_keys": ["your-api-key"],
"request_timeout": 300 "request_timeout": 300
}, },
{ {
"model_name": "claude-sonnet-4.6", "model_name": "claude-sonnet-4.6",
"model": "anthropic/claude-sonnet-4.6", "provider": "anthropic",
"model": "claude-sonnet-4.6",
"api_keys": ["your-anthropic-key"] "api_keys": ["your-anthropic-key"]
} }
], ],

View file

@ -93,19 +93,22 @@ picoclaw onboard
"model_list": [ "model_list": [
{ {
"model_name": "ark-code-latest", "model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest", "provider": "volcengine",
"model": "ark-code-latest",
"api_keys": ["sk-your-api-key"], "api_keys": ["sk-your-api-key"],
"api_base":"https://ark.cn-beijing.volces.com/api/coding/v3" "api_base":"https://ark.cn-beijing.volces.com/api/coding/v3"
}, },
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["your-api-key"], "api_keys": ["your-api-key"],
"request_timeout": 300 "request_timeout": 300
}, },
{ {
"model_name": "claude-sonnet-4.6", "model_name": "claude-sonnet-4.6",
"model": "anthropic/claude-sonnet-4.6", "provider": "anthropic",
"model": "claude-sonnet-4.6",
"api_keys": ["your-anthropic-key"] "api_keys": ["your-anthropic-key"]
} }
], ],

View file

@ -33,7 +33,7 @@
### Model Configuration (model_list) ### 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). 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 #### 📋 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) | | **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) | | **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) | | **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) | | **智谱 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) | | **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) | | **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) | | **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) | | **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) | | **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) | | **通义千问 (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) | | **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) | | **Ollama** | `ollama` | `http://localhost:11434/v1` | OpenAI | Local (no key needed) |
| **LM Studio** | `lmstudio/` | `http://localhost:1234/v1` | OpenAI | Optional (local default: no key) | | **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) | | **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 | | **LiteLLM Proxy** | `litellm` | `http://localhost:4000/v1` | OpenAI | Your LiteLLM proxy key |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local | | **VLLM** | `vllm` | `http://localhost:8000/v1` | OpenAI | Local |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) | | **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) | | **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 | - | | **神算云** | `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) | | **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) | | **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) | | **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) | | **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) | | **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) | | **Azure OpenAI** | `azure` | `https://{resource}.openai.azure.com` | Azure | [Get Key](https://portal.azure.com) |
| **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth only | | **Antigravity** | `antigravity` | Google Cloud | Custom | OAuth only |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - | | **GitHub Copilot** | `github-copilot` | `localhost:4321` | gRPC | - |
#### Basic Configuration #### Basic Configuration
@ -83,22 +83,26 @@ This design also enables **multi-agent support** with flexible provider selectio
"model_list": [ "model_list": [
{ {
"model_name": "ark-code-latest", "model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest", "provider": "volcengine",
"model": "ark-code-latest",
"api_keys": ["sk-your-api-key"] "api_keys": ["sk-your-api-key"]
}, },
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-your-openai-key"] "api_keys": ["sk-your-openai-key"]
}, },
{ {
"model_name": "claude-sonnet-4.6", "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"] "api_keys": ["sk-ant-your-key"]
}, },
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "zhipu/glm-4.7", "provider": "zhipu",
"model": "glm-4.7",
"api_keys": ["your-zhipu-key"] "api_keys": ["your-zhipu-key"]
} }
], ],
@ -115,7 +119,8 @@ This design also enables **multi-agent support** with flexible provider selectio
| Field | Type | Required | Description | | Field | Type | Required | Description |
|-------|------|----------|-------------| |-------|------|----------|-------------|
| `model_name` | string | Yes | Unique name used to reference this model in agent config | | `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_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 | | `api_base` | string | No | Override the default API endpoint URL |
| `proxy` | string | No | HTTP proxy URL for this model entry | | `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 | | `fallbacks` | string[] | No | Fallback model names for automatic failover |
| `enabled` | bool | No | Whether this model entry is active (default: `true`) | | `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 #### 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. 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_list": [
{ {
"model_name": "voice-gemini", "model_name": "voice-gemini",
"model": "gemini/gemini-2.5-flash", "provider": "gemini",
"model": "gemini-2.5-flash",
"api_keys": ["your-gemini-key"] "api_keys": ["your-gemini-key"]
} }
], ],
@ -163,7 +185,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json ```json
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-..."] "api_keys": ["sk-..."]
} }
``` ```
@ -173,7 +196,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json ```json
{ {
"model_name": "ark-code-latest", "model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest", "provider": "volcengine",
"model": "ark-code-latest",
"api_keys": ["sk-..."] "api_keys": ["sk-..."]
} }
``` ```
@ -183,7 +207,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json ```json
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "zhipu/glm-4.7", "provider": "zhipu",
"model": "glm-4.7",
"api_keys": ["your-key"] "api_keys": ["your-key"]
} }
``` ```
@ -193,7 +218,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json ```json
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "openai/glm-4.7", "provider": "openai",
"model": "glm-4.7",
"api_keys": ["your-z.ai-key"], "api_keys": ["your-z.ai-key"],
"api_base": "https://api.z.ai/api/coding/paas/v4" "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 ```json
{ {
"model_name": "deepseek-chat", "model_name": "deepseek-chat",
"model": "deepseek/deepseek-chat", "provider": "deepseek",
"model": "deepseek-chat",
"api_keys": ["sk-..."] "api_keys": ["sk-..."]
} }
``` ```
@ -214,7 +241,8 @@ If `voice.model_name` is not configured, PicoClaw will continue to fall back to
```json ```json
{ {
"model_name": "claude-sonnet-4.6", "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"] "api_keys": ["sk-ant-your-key"]
} }
``` ```
@ -228,7 +256,8 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
```json ```json
{ {
"model_name": "claude-opus-4-6", "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_keys": ["sk-ant-your-key"],
"api_base": "https://api.anthropic.com" "api_base": "https://api.anthropic.com"
} }
@ -246,7 +275,8 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
```json ```json
{ {
"model_name": "llama3", "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 ```json
{ {
"model_name": "lmstudio-local", "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.<br/> `api_base` defaults to `http://localhost:1234/v1`. API key is optional unless your LM Studio server enables authentication.<br/>
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** **Custom Proxy/API**
```json ```json
{ {
"model_name": "my-custom-model", "model_name": "my-custom-model",
"model": "openai/custom-model", "provider": "openai",
"model": "custom-model",
"api_base": "https://my-proxy.com/v1", "api_base": "https://my-proxy.com/v1",
"api_keys": ["sk-..."], "api_keys": ["sk-..."],
"user_agent": "MyApp/1.0", "user_agent": "MyApp/1.0",
@ -280,13 +312,14 @@ PicoClaw sends OpenAI-compatible requests to LM Studio, and strips the `lmstudio
```json ```json
{ {
"model_name": "lite-gpt4", "model_name": "lite-gpt4",
"model": "litellm/lite-gpt4", "provider": "litellm",
"model": "lite-gpt4",
"api_base": "http://localhost:4000/v1", "api_base": "http://localhost:4000/v1",
"api_keys": ["sk-..."] "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** **Z.AI Coding Plan**
@ -295,7 +328,8 @@ If the standard Zhipu endpoint (`https://open.bigmodel.cn/api/paas/v4`) returns
```json ```json
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "openai/glm-4.7", "provider": "openai",
"model": "glm-4.7",
"api_keys": ["your-zhipu-api-key"], "api_keys": ["your-zhipu-api-key"],
"api_base": "https://api.z.ai/api/coding/paas/v4" "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_list": [
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_base": "https://api1.example.com/v1", "api_base": "https://api1.example.com/v1",
"api_keys": ["sk-key1"] "api_keys": ["sk-key1"]
}, },
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_base": "https://api2.example.com/v1", "api_base": "https://api2.example.com/v1",
"api_keys": ["sk-key2"] "api_keys": ["sk-key2"]
} }
@ -337,18 +373,21 @@ It also applies cooldown tracking per candidate to avoid immediately retrying a
"model_list": [ "model_list": [
{ {
"model_name": "qwen-main", "model_name": "qwen-main",
"model": "openai/qwen3.5:cloud", "provider": "openai",
"model": "qwen3.5:cloud",
"api_base": "https://api.example.com/v1", "api_base": "https://api.example.com/v1",
"api_keys": ["sk-main"] "api_keys": ["sk-main"]
}, },
{ {
"model_name": "deepseek-backup", "model_name": "deepseek-backup",
"model": "deepseek/deepseek-chat", "provider": "deepseek",
"model": "deepseek-chat",
"api_keys": ["sk-backup-1"] "api_keys": ["sk-backup-1"]
}, },
{ {
"model_name": "gemini-backup", "model_name": "gemini-backup",
"model": "gemini/gemini-2.5-flash", "provider": "gemini",
"model": "gemini-2.5-flash",
"api_keys": ["sk-backup-2"] "api_keys": ["sk-backup-2"]
} }
], ],
@ -396,7 +435,8 @@ The old `providers` configuration is **deprecated** and has been removed in V2.
"model_list": [ "model_list": [
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "zhipu/glm-4.7", "provider": "zhipu",
"model": "glm-4.7",
"api_keys": ["your-key"] "api_keys": ["your-key"]
} }
], ],
@ -464,7 +504,7 @@ picoclaw agent -m "Hello"
{ {
"agents": { "agents": {
"defaults": { "defaults": {
"model_name": "anthropic/claude-opus-4-5" "model_name": "claude-opus-4-5"
} }
}, },
"session": { "session": {

View file

@ -32,7 +32,7 @@
<a id="模型配置-model_list"></a> <a id="模型配置-model_list"></a>
### 模型配置 (model_list) ### 模型配置 (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)。 如果你想看 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) | | **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) | | **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) | | **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) | | **智谱 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) | | **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) | | **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) | | **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) | | **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) | | **通义千问 (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) | | **NVIDIA** | `nvidia` | `https://integrate.api.nvidia.com/v1` | OpenAI | [获取密钥](https://build.nvidia.com) |
| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | 本地(无需密钥) | | **Ollama** | `ollama` | `http://localhost:11434/v1` | OpenAI | 本地(无需密钥) |
| **LM Studio** | `lmstudio/` | `http://localhost:1234/v1` | OpenAI | 可选(本地默认无需密钥) | | **LM Studio** | `lmstudio` | `http://localhost:1234/v1` | OpenAI | 可选(本地默认无需密钥) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) | | **OpenRouter** | `openrouter` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) |
| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | 你的 LiteLLM 代理密钥 | | **LiteLLM Proxy** | `litellm` | `http://localhost:4000/v1` | OpenAI | 你的 LiteLLM 代理密钥 |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 | | **VLLM** | `vllm` | `http://localhost:8000/v1` | OpenAI | 本地 |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [获取密钥](https://cerebras.ai) | | **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) | | **火山引擎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 | - | | **神算云** | `shengsuanyun` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [获取密钥](https://www.byteplus.com) | | **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) | | **Vivgrid** | `vivgrid` | `https://api.vivgrid.com/v1` | OpenAI | [获取密钥](https://vivgrid.com) |
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [获取密钥](https://longcat.chat/platform) | | **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) | | **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) | | **小米 MiMo** | `mimo` | `https://api.xiaomimimo.com/v1` | OpenAI | [获取密钥](https://platform.xiaomimimo.com) |
| **Antigravity** | `antigravity/` | Google Cloud | 自定义 | 仅 OAuth | | **Antigravity** | `antigravity` | Google Cloud | 自定义 | 仅 OAuth |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - | | **GitHub Copilot** | `github-copilot` | `localhost:4321` | gRPC | - |
#### 基础配置示例 #### 基础配置示例
@ -80,22 +80,26 @@
"model_list": [ "model_list": [
{ {
"model_name": "ark-code-latest", "model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest", "provider": "volcengine",
"model": "ark-code-latest",
"api_keys": ["sk-your-api-key"] "api_keys": ["sk-your-api-key"]
}, },
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-your-openai-key"] "api_keys": ["sk-your-openai-key"]
}, },
{ {
"model_name": "claude-sonnet-4.6", "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"] "api_keys": ["sk-ant-your-key"]
}, },
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "zhipu/glm-4.7", "provider": "zhipu",
"model": "glm-4.7",
"api_keys": ["your-zhipu-key"] "api_keys": ["your-zhipu-key"]
} }
], ],
@ -112,7 +116,8 @@
| 字段 | 类型 | 必填 | 说明 | | 字段 | 类型 | 必填 | 说明 |
|------|------|------|------| |------|------|------|------|
| `model_name` | string | 是 | 在 agent 配置中引用此模型的唯一名称 | | `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[] | 是* | 认证密钥。多个密钥可按请求轮换。本地 providerOllama、LM Studio、VLLM不需要 | | `api_keys` | string[] | 是* | 认证密钥。多个密钥可按请求轮换。本地 providerOllama、LM Studio、VLLM不需要 |
| `api_base` | string | 否 | 覆盖默认的 API 端点 URL | | `api_base` | string | 否 | 覆盖默认的 API 端点 URL |
| `proxy` | string | 否 | 此模型条目的 HTTP 代理 URL | | `proxy` | string | 否 | 此模型条目的 HTTP 代理 URL |
@ -126,6 +131,22 @@
| `fallbacks` | string[] | 否 | 自动故障转移的备用模型名称 | | `fallbacks` | string[] | 否 | 自动故障转移的备用模型名称 |
| `enabled` | bool | 否 | 是否启用此模型条目(默认:`true` | | `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。 你可以通过 `voice.model_name` 为语音转录指定一个专用模型。这样可以直接复用已经配置好的、支持音频输入的多模态 provider而不必只依赖 Groq。
@ -137,7 +158,8 @@
"model_list": [ "model_list": [
{ {
"model_name": "voice-gemini", "model_name": "voice-gemini",
"model": "gemini/gemini-2.5-flash", "provider": "gemini",
"model": "gemini-2.5-flash",
"api_keys": ["your-gemini-key"] "api_keys": ["your-gemini-key"]
} }
], ],
@ -160,7 +182,8 @@
```json ```json
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-..."] "api_keys": ["sk-..."]
} }
``` ```
@ -170,7 +193,8 @@
```json ```json
{ {
"model_name": "ark-code-latest", "model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest", "provider": "volcengine",
"model": "ark-code-latest",
"api_keys": ["sk-..."] "api_keys": ["sk-..."]
} }
``` ```
@ -180,7 +204,8 @@
```json ```json
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "zhipu/glm-4.7", "provider": "zhipu",
"model": "glm-4.7",
"api_keys": ["your-key"] "api_keys": ["your-key"]
} }
``` ```
@ -190,7 +215,8 @@
```json ```json
{ {
"model_name": "deepseek-chat", "model_name": "deepseek-chat",
"model": "deepseek/deepseek-chat", "provider": "deepseek",
"model": "deepseek-chat",
"api_keys": ["sk-..."] "api_keys": ["sk-..."]
} }
``` ```
@ -200,7 +226,8 @@
```json ```json
{ {
"model_name": "claude-sonnet-4.6", "model_name": "claude-sonnet-4.6",
"model": "anthropic/claude-sonnet-4.6", "provider": "anthropic",
"model": "claude-sonnet-4.6",
"auth_method": "oauth" "auth_method": "oauth"
} }
``` ```
@ -214,7 +241,8 @@
```json ```json
{ {
"model_name": "claude-opus-4-6", "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_keys": ["sk-ant-your-key"],
"api_base": "https://api.anthropic.com" "api_base": "https://api.anthropic.com"
} }
@ -232,7 +260,8 @@
```json ```json
{ {
"model_name": "llama3", "model_name": "llama3",
"model": "ollama/llama3" "provider": "ollama",
"model": "llama3"
} }
``` ```
@ -241,19 +270,21 @@
```json ```json
{ {
"model_name": "lmstudio-local", "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。 `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** **自定义代理/API**
```json ```json
{ {
"model_name": "my-custom-model", "model_name": "my-custom-model",
"model": "openai/custom-model", "provider": "openai",
"model": "custom-model",
"api_base": "https://my-proxy.com/v1", "api_base": "https://my-proxy.com/v1",
"api_keys": ["sk-..."], "api_keys": ["sk-..."],
"user_agent": "MyApp/1.0", "user_agent": "MyApp/1.0",
@ -266,13 +297,14 @@ PicoClaw 向 LM Studio 的 OpenAI 兼容终结点发送请求,且将移除首
```json ```json
{ {
"model_name": "lite-gpt4", "model_name": "lite-gpt4",
"model": "litellm/lite-gpt4", "provider": "litellm",
"model": "lite-gpt4",
"api_base": "http://localhost:4000/v1", "api_base": "http://localhost:4000/v1",
"api_keys": ["sk-..."] "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_list": [
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_base": "https://api1.example.com/v1", "api_base": "https://api1.example.com/v1",
"api_keys": ["sk-key1"] "api_keys": ["sk-key1"]
}, },
{ {
"model_name": "gpt-5.4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_base": "https://api2.example.com/v1", "api_base": "https://api2.example.com/v1",
"api_keys": ["sk-key2"] "api_keys": ["sk-key2"]
} }
@ -308,18 +342,21 @@ PicoClaw 在发送请求前仅去除外层 `litellm/` 前缀,因此 `litellm/l
"model_list": [ "model_list": [
{ {
"model_name": "qwen-main", "model_name": "qwen-main",
"model": "openai/qwen3.5:cloud", "provider": "openai",
"model": "qwen3.5:cloud",
"api_base": "https://api.example.com/v1", "api_base": "https://api.example.com/v1",
"api_keys": ["sk-main"] "api_keys": ["sk-main"]
}, },
{ {
"model_name": "deepseek-backup", "model_name": "deepseek-backup",
"model": "deepseek/deepseek-chat", "provider": "deepseek",
"model": "deepseek-chat",
"api_keys": ["sk-backup-1"] "api_keys": ["sk-backup-1"]
}, },
{ {
"model_name": "gemini-backup", "model_name": "gemini-backup",
"model": "gemini/gemini-2.5-flash", "provider": "gemini",
"model": "gemini-2.5-flash",
"api_keys": ["sk-backup-2"] "api_keys": ["sk-backup-2"]
} }
], ],
@ -367,7 +404,8 @@ PicoClaw 在发送请求前仅去除外层 `litellm/` 前缀,因此 `litellm/l
"model_list": [ "model_list": [
{ {
"model_name": "glm-4.7", "model_name": "glm-4.7",
"model": "zhipu/glm-4.7", "provider": "zhipu",
"model": "glm-4.7",
"api_keys": ["your-key"] "api_keys": ["your-key"]
} }
], ],
@ -435,7 +473,7 @@ picoclaw agent -m "你好"
{ {
"agents": { "agents": {
"defaults": { "defaults": {
"model_name": "anthropic/claude-opus-4-5" "model_name": "claude-opus-4-5"
} }
}, },
"session": { "session": {

View file

@ -69,12 +69,14 @@ This guide explains how to configure both for real deployments.
"model_list": [ "model_list": [
{ {
"model_name": "gpt-main", "model_name": "gpt-main",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-main"] "api_keys": ["sk-main"]
}, },
{ {
"model_name": "flash-light", "model_name": "flash-light",
"model": "gemini/gemini-2.0-flash-exp", "provider": "gemini",
"model": "gemini-2.0-flash-exp",
"api_keys": ["sk-light"] "api_keys": ["sk-light"]
} }
], ],

View file

@ -69,12 +69,14 @@ PicoClaw 里用户能直接感知到的“路由”主要有两部分:
"model_list": [ "model_list": [
{ {
"model_name": "gpt-main", "model_name": "gpt-main",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-main"] "api_keys": ["sk-main"]
}, },
{ {
"model_name": "flash-light", "model_name": "flash-light",
"model": "gemini/gemini-2.0-flash-exp", "provider": "gemini",
"model": "gemini-2.0-flash-exp",
"api_keys": ["sk-light"] "api_keys": ["sk-light"]
} }
], ],

View file

@ -8,7 +8,7 @@ The new `model_list` configuration offers several advantages:
- **Zero-code provider addition**: Add OpenAI-compatible providers with configuration only - **Zero-code provider addition**: Add OpenAI-compatible providers with configuration only
- **Load balancing**: Configure multiple endpoints for the same model - **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 - **Cleaner configuration**: Model-centric instead of vendor-centric
## Timeline ## Timeline
@ -54,18 +54,21 @@ The new `model_list` configuration offers several advantages:
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "gpt4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-your-openai-key"], "api_keys": ["sk-your-openai-key"],
"api_base": "https://api.openai.com/v1" "api_base": "https://api.openai.com/v1"
}, },
{ {
"model_name": "claude-sonnet-4.6", "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"] "api_keys": ["sk-ant-your-key"]
}, },
{ {
"model_name": "deepseek", "model_name": "deepseek",
"model": "deepseek/deepseek-chat", "provider": "deepseek",
"model": "deepseek-chat",
"api_keys": ["sk-your-deepseek-key"] "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. > **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 | ```json
|--------|-------------|---------| {
| `openai/` | OpenAI API (default) | `openai/gpt-5.4` | "provider": "openai",
| `anthropic/` | Anthropic API | `anthropic/claude-opus-4` | "model": "gpt-5.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` |
**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 ## ModelConfig Fields
| Field | Required | Description | | Field | Required | Description |
|-------|----------|-------------| |-------|----------|-------------|
| `model_name` | Yes | User-facing alias for the model | | `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_base` | No | API endpoint URL |
| `api_keys` | No | API authentication keys (array; supports multiple keys for load balancing) | | `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. | | `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_list": [
{ {
"model_name": "gpt4", "model_name": "gpt4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-key1", "sk-key2", "sk-key3"], "api_keys": ["sk-key1", "sk-key2", "sk-key3"],
"api_base": "https://api.openai.com/v1" "api_base": "https://api.openai.com/v1"
} }
@ -162,19 +172,22 @@ model_list:
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "gpt4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-key1"], "api_keys": ["sk-key1"],
"api_base": "https://api1.example.com/v1" "api_base": "https://api1.example.com/v1"
}, },
{ {
"model_name": "gpt4", "model_name": "gpt4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-key2"], "api_keys": ["sk-key2"],
"api_base": "https://api2.example.com/v1" "api_base": "https://api2.example.com/v1"
}, },
{ {
"model_name": "gpt4", "model_name": "gpt4",
"model": "openai/gpt-5.4", "provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-key3"], "api_keys": ["sk-key3"],
"api_base": "https://api3.example.com/v1" "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_list": [
{ {
"model_name": "my-custom-llm", "model_name": "my-custom-llm",
"model": "openai/my-model-v1", "provider": "openai",
"model": "my-model-v1",
"api_keys": ["your-api-key"], "api_keys": ["your-api-key"],
"api_base": "https://api.your-provider.com/v1" "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 ## 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 - [ ] Identify all providers you're currently using
- [ ] Create `model_list` entries for each provider - [ ] 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` - [ ] Update `agents.defaults.model_name` to reference the new `model_name`
- [ ] Test that all models work correctly - [ ] Test that all models work correctly
- [ ] Remove or comment out the old `providers` section - [ ] 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 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 ### Missing API key error

View file

@ -7,16 +7,22 @@
- `Error creating provider: model "openrouter/free" not found in model_list` - `Error creating provider: model "openrouter/free" not found in model_list`
- OpenRouter returns 400: `"free is not a valid model ID"` - 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. - If `provider` is set, the `model` field is sent to that provider unchanged.
- **Right:** `"model": "openrouter/free"` → OpenRouter receives `openrouter/free` (auto free-tier routing). - 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): **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"`). 1. **agents.defaults.model_name** must match a `model_name` in `model_list` (e.g. `"openrouter-free"`).
2. That entrys **model** must be a valid OpenRouter model ID, for example: 2. That entry should preferably set **provider** to `openrouter`, and **model** should be a valid OpenRouter model ID, for example:
- `"openrouter/free"` auto free-tier - `"free"` auto free-tier
- `"google/gemini-2.0-flash-exp:free"` - `"google/gemini-2.0-flash-exp:free"`
- `"meta-llama/llama-3.1-8b-instruct:free"` - `"meta-llama/llama-3.1-8b-instruct:free"`
@ -32,8 +38,9 @@ Example snippet:
"model_list": [ "model_list": [
{ {
"model_name": "openrouter-free", "model_name": "openrouter-free",
"model": "openrouter/free", "provider": "openrouter",
"api_key": "sk-or-v1-YOUR_OPENROUTER_KEY", "model": "free",
"api_keys": ["sk-or-v1-YOUR_OPENROUTER_KEY"],
"api_base": "https://openrouter.ai/api/v1" "api_base": "https://openrouter.ai/api/v1"
} }
] ]

View file

@ -9,16 +9,22 @@
- `Error creating provider: model "openrouter/free" not found in model_list` - `Error creating provider: model "openrouter/free" not found in model_list`
- OpenRouter 返回 400`"free is not a valid model ID"` - OpenRouter 返回 400`"free is not a valid model ID"`
**原因:** `model_list` 条目中的 `model` 字段是发送给 API 的内容。对于 OpenRouter你必须使用**完整的**模型 ID而不是简写。 **原因:** PicoClaw 现在按两步解析 provider 和 model
- **错误:** `"model": "free"` → OpenRouter 收到 `free` 并拒绝。 - 如果设置了 `provider`,则会把 `model` 原样发送给该 provider。
- **正确:** `"model": "openrouter/free"` → OpenRouter 收到 `openrouter/free`(自动免费层路由)。 - 如果未设置 `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`(或你的配置路径)中: **修复方法:** 在 `~/.picoclaw/config.json`(或你的配置路径)中:
1. **agents.defaults.model_name** 必须匹配 `model_list` 中的某个 `model_name`(例如 `"openrouter-free"`)。 1. **agents.defaults.model_name** 必须匹配 `model_list` 中的某个 `model_name`(例如 `"openrouter-free"`)。
2. 该条目的 **model** 必须是有效的 OpenRouter 模型 ID例如 2. 该条目推荐显式设置 **provider**`openrouter`,并在 **model** 中填写有效的 OpenRouter 模型 ID例如
- `"openrouter/free"` 自动免费层 - `"free"` 自动免费层
- `"google/gemini-2.0-flash-exp:free"` - `"google/gemini-2.0-flash-exp:free"`
- `"meta-llama/llama-3.1-8b-instruct:free"` - `"meta-llama/llama-3.1-8b-instruct:free"`
@ -34,8 +40,9 @@
"model_list": [ "model_list": [
{ {
"model_name": "openrouter-free", "model_name": "openrouter-free",
"model": "openrouter/free", "provider": "openrouter",
"api_key": "sk-or-v1-YOUR_OPENROUTER_KEY", "model": "free",
"api_keys": ["sk-or-v1-YOUR_OPENROUTER_KEY"],
"api_base": "https://openrouter.ai/api/v1" "api_base": "https://openrouter.ai/api/v1"
} }
] ]

View file

@ -39,20 +39,23 @@ Set `rpm` on any model in `model_list`:
```yaml ```yaml
model_list: model_list:
- model_name: gpt-4o-free - model_name: gpt-4o-free
model: openai/gpt-4o provider: openai
model: gpt-4o
api_base: https://api.openai.com/v1 api_base: https://api.openai.com/v1
rpm: 3 # max 3 requests per minute rpm: 3 # max 3 requests per minute
api_keys: api_keys:
- sk-... - sk-...
- model_name: claude-haiku - model_name: claude-haiku
model: anthropic/claude-haiku-4-5 provider: anthropic
model: claude-haiku-4-5
rpm: 60 # 60 rpm (Anthropic free tier) rpm: 60 # 60 rpm (Anthropic free tier)
api_keys: api_keys:
- sk-ant-... - sk-ant-...
- model_name: local-llm - model_name: local-llm
model: openai/llama3 provider: ollama
model: llama3
api_base: http://localhost:11434/v1 api_base: http://localhost:11434/v1
# no rpm → unrestricted # no rpm → unrestricted
``` ```
@ -68,7 +71,8 @@ When a model has fallbacks configured, each candidate is rate-limited **independ
```yaml ```yaml
model_list: model_list:
- model_name: gpt4-with-fallback - model_name: gpt4-with-fallback
model: openai/gpt-4o provider: openai
model: gpt-4o
rpm: 5 rpm: 5
fallbacks: fallbacks:
- gpt-4o-mini # must also be in model_list; its own rpm applies - gpt-4o-mini # must also be in model_list; its own rpm applies

View file

@ -128,7 +128,7 @@ func useTestSideQuestionProvider(al *AgentLoop, provider providers.LLMProvider)
al.providerFactory = func(mc *config.ModelConfig) (providers.LLMProvider, string, error) { al.providerFactory = func(mc *config.ModelConfig) (providers.LLMProvider, string, error) {
model := provider.GetDefaultModel() model := provider.GetDefaultModel()
if mc != nil { if mc != nil {
if _, modelID := providers.ExtractProtocol(mc.Model); modelID != "" { if _, modelID := providers.ExtractProtocol(mc); modelID != "" {
model = modelID model = modelID
} }
} }

View file

@ -881,7 +881,11 @@ func TestAgentLoop_HookRespond_InterruptSkipsRemaining(t *testing.T) {
resultCh <- result{resp: resp, err: err} 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 { if err := al.InterruptGraceful("stop now"); err != nil {
t.Fatalf("InterruptGraceful failed: %v", err) t.Fatalf("InterruptGraceful failed: %v", err)

View file

@ -270,8 +270,8 @@ func populateCandidateProvidersFromNames(
map[string]any{"name": name, "error": err.Error()}) map[string]any{"name": name, "error": err.Error()})
continue continue
} }
protocol, modelID := providers.ExtractProtocol(strings.TrimSpace(mc.Model)) protocol, modelID := providers.ExtractProtocol(mc)
key := providers.ModelKey(providers.NormalizeProvider(protocol), modelID) key := providers.ModelKey(protocol, modelID)
if _, exists := out[key]; exists { if _, exists := out[key]; exists {
continue continue
} }

View file

@ -104,6 +104,7 @@ func TestNewAgentInstance_ResolveCandidatesFromModelListAlias(t *testing.T) {
name string name string
aliasName string aliasName string
modelName string modelName string
provider string
apiBase string apiBase string
wantProvider string wantProvider string
wantModel string wantModel string
@ -124,6 +125,15 @@ func TestNewAgentInstance_ResolveCandidatesFromModelListAlias(t *testing.T) {
wantProvider: "openai", wantProvider: "openai",
wantModel: "glm-5", 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 { for _, tt := range tests {
@ -145,6 +155,7 @@ func TestNewAgentInstance_ResolveCandidatesFromModelListAlias(t *testing.T) {
{ {
ModelName: tt.aliasName, ModelName: tt.aliasName,
Model: tt.modelName, Model: tt.modelName,
Provider: tt.provider,
APIBase: tt.apiBase, APIBase: tt.apiBase,
}, },
}, },

View file

@ -37,14 +37,14 @@ func candidateFromModelConfig(
return providers.FallbackCandidate{}, false return providers.FallbackCandidate{}, false
} }
ref := providers.ParseModelRef(ensureProtocolModel(mc.Model), defaultProvider) protocol, modelID := providers.ExtractProtocol(mc)
if ref == nil { if strings.TrimSpace(modelID) == "" {
return providers.FallbackCandidate{}, false return providers.FallbackCandidate{}, false
} }
return providers.FallbackCandidate{ return providers.FallbackCandidate{
Provider: ref.Provider, Provider: protocol,
Model: ref.Model, Model: modelID,
RPM: mc.RPM, RPM: mc.RPM,
IdentityKey: modelConfigIdentityKey(mc), IdentityKey: modelConfigIdentityKey(mc),
}, true }, true
@ -72,7 +72,7 @@ func lookupModelConfigByRef(cfg *config.Config, raw string) *config.ModelConfig
if fullModel == raw { if fullModel == raw {
return mc return mc
} }
_, modelID := providers.ExtractProtocol(fullModel) _, modelID := providers.ExtractProtocol(mc)
if modelID == raw { if modelID == raw {
return mc return mc
} }

View file

@ -19,16 +19,16 @@ type TranscriptionResponse struct {
Duration float64 `json:"duration,omitempty"` Duration float64 `json:"duration,omitempty"`
} }
func supportsAudioTranscription(model string) bool { func supportsAudioTranscription(modelCfg *config.ModelConfig) bool {
protocol, _ := providers.ExtractProtocol(model) protocol, _ := providers.ExtractProtocol(modelCfg)
switch protocol { switch protocol {
case "openai", "azure", "azure-openai", case "openai", "azure", "azure-openai",
"litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia", "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras", "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", "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 // These protocols all go through the OpenAI-compatible or Azure provider path in
// providers.CreateProviderFromConfig, so they are the only ones that can supply // providers.CreateProviderFromConfig, so they are the only ones that can supply
// the audio media payload shape expected by NewAudioModelTranscriber. // the audio media payload shape expected by NewAudioModelTranscriber.
@ -41,15 +41,15 @@ func supportsAudioTranscription(model string) bool {
} }
} }
func supportsWhisperTranscription(model string) bool { func supportsWhisperTranscription(modelCfg *config.ModelConfig) bool {
protocol, _ := providers.ExtractProtocol(model) protocol, _ := providers.ExtractProtocol(modelCfg)
switch protocol { switch protocol {
case "openai", "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia", case "openai", "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras", "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", "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 return true
default: default:
return false return false
@ -61,11 +61,11 @@ func whisperModelID(modelCfg *config.ModelConfig) string {
return "" return ""
} }
if !supportsWhisperTranscription(modelCfg.Model) { if !supportsWhisperTranscription(modelCfg) {
return "" return ""
} }
_, modelID := providers.ExtractProtocol(strings.TrimSpace(modelCfg.Model)) _, modelID := providers.ExtractProtocol(modelCfg)
if strings.Contains(strings.ToLower(modelID), "whisper") { if strings.Contains(strings.ToLower(modelID), "whisper") {
return modelID return modelID
} }
@ -77,14 +77,14 @@ func transcriberFromModelConfig(modelCfg *config.ModelConfig) Transcriber {
return nil return nil
} }
protocol, _ := providers.ExtractProtocol(modelCfg.Model) protocol, _ := providers.ExtractProtocol(modelCfg)
if protocol == "elevenlabs" && modelCfg.APIKey() != "" { if protocol == "elevenlabs" && modelCfg.APIKey() != "" {
return NewElevenLabsTranscriber(modelCfg.APIKey(), modelCfg.APIBase) return NewElevenLabsTranscriber(modelCfg.APIKey(), modelCfg.APIBase)
} }
if modelID := whisperModelID(modelCfg); modelID != "" { if modelID := whisperModelID(modelCfg); modelID != "" {
return NewWhisperTranscriber(modelCfg) return NewWhisperTranscriber(modelCfg)
} }
if supportsAudioTranscription(modelCfg.Model) { if supportsAudioTranscription(modelCfg) {
return NewAudioModelTranscriber(modelCfg) return NewAudioModelTranscriber(modelCfg)
} }
return nil return nil
@ -95,7 +95,7 @@ func fallbackTranscriberFromModelConfig(modelCfg *config.ModelConfig) Transcribe
return nil return nil
} }
protocol, _ := providers.ExtractProtocol(modelCfg.Model) protocol, _ := providers.ExtractProtocol(modelCfg)
if protocol == "elevenlabs" && modelCfg.APIKey() != "" { if protocol == "elevenlabs" && modelCfg.APIKey() != "" {
return NewElevenLabsTranscriber(modelCfg.APIKey(), modelCfg.APIBase) return NewElevenLabsTranscriber(modelCfg.APIKey(), modelCfg.APIBase)
} }

View file

@ -32,7 +32,7 @@ func NewWhisperTranscriber(modelCfg *config.ModelConfig) *WhisperTranscriber {
return nil return nil
} }
protocol, modelID := providers.ExtractProtocol(modelCfg.Model) protocol, modelID := providers.ExtractProtocol(modelCfg)
if modelID == "" { if modelID == "" {
modelID = strings.TrimSpace(modelCfg.Model) modelID = strings.TrimSpace(modelCfg.Model)
} }

View file

@ -24,7 +24,7 @@ func providerFromModelConfig(mc *config.ModelConfig) TTSProvider {
return nil return nil
} }
protocol, modelID := providers.ExtractProtocol(mc.Model) protocol, modelID := providers.ExtractProtocol(mc)
if modelID == "" { if modelID == "" {
modelID = strings.TrimSpace(mc.Model) modelID = strings.TrimSpace(mc.Model)
} }

View file

@ -523,15 +523,16 @@ type VoiceConfig struct {
// ModelConfig represents a model-centric provider configuration. // ModelConfig represents a model-centric provider configuration.
// It allows adding new providers (especially OpenAI-compatible ones) via configuration only. // It allows adding new providers (especially OpenAI-compatible ones) via configuration only.
// The model field uses protocol prefix format: [protocol/]model-identifier // The Model field may be either a plain model identifier or a provider-prefixed
// Supported protocols include openai, anthropic, antigravity, claude-cli, // 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 // codex-cli, github-copilot, and named OpenAI-compatible protocols such as
// groq, deepseek, modelscope, and novita. // groq, deepseek, modelscope, and novita.
// Default protocol is "openai" if no prefix is specified.
type ModelConfig struct { type ModelConfig struct {
// Required fields // Required fields
ModelName string `json:"model_name"` // User-facing alias for the model 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 // HTTP-based providers
APIBase string `json:"api_base,omitempty"` // API endpoint URL 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 // Create a copy for the additional key
additionalEntry := &ModelConfig{ additionalEntry := &ModelConfig{
ModelName: expandedName, ModelName: expandedName,
Provider: m.Provider,
Model: m.Model, Model: m.Model,
APIBase: m.APIBase, APIBase: m.APIBase,
APIKeys: SimpleSecureStrings(keys[i]), APIKeys: SimpleSecureStrings(keys[i]),
@ -1434,6 +1436,7 @@ func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
// Create the primary entry with first key and fallbacks // Create the primary entry with first key and fallbacks
primaryEntry := &ModelConfig{ primaryEntry := &ModelConfig{
ModelName: originalName, ModelName: originalName,
Provider: m.Provider,
Model: m.Model, Model: m.Model,
APIBase: m.APIBase, APIBase: m.APIBase,
Proxy: m.Proxy, Proxy: m.Proxy,

View file

@ -1933,7 +1933,7 @@ func TestDefaultConfig_MinimaxExtraBody(t *testing.T) {
var minimaxCfg *ModelConfig var minimaxCfg *ModelConfig
for i := range cfg.ModelList { 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] minimaxCfg = cfg.ModelList[i]
break break
} }

View file

@ -61,129 +61,148 @@ func DefaultConfig() *Config {
// Zhipu AI (智谱) - https://open.bigmodel.cn/usercenter/apikeys // Zhipu AI (智谱) - https://open.bigmodel.cn/usercenter/apikeys
{ {
ModelName: "glm-4.7", ModelName: "glm-4.7",
Model: "zhipu/glm-4.7", Provider: "zhipu",
Model: "glm-4.7",
APIBase: "https://open.bigmodel.cn/api/paas/v4", APIBase: "https://open.bigmodel.cn/api/paas/v4",
}, },
// OpenAI - https://platform.openai.com/api-keys // OpenAI - https://platform.openai.com/api-keys
{ {
ModelName: "gpt-5.4", ModelName: "gpt-5.4",
Model: "openai/gpt-5.4", Provider: "openai",
Model: "gpt-5.4",
APIBase: "https://api.openai.com/v1", APIBase: "https://api.openai.com/v1",
}, },
// Anthropic Claude - https://console.anthropic.com/settings/keys // Anthropic Claude - https://console.anthropic.com/settings/keys
{ {
ModelName: "claude-sonnet-4.6", ModelName: "claude-sonnet-4.6",
Model: "anthropic/claude-sonnet-4.6", Provider: "anthropic",
Model: "claude-sonnet-4.6",
APIBase: "https://api.anthropic.com/v1", APIBase: "https://api.anthropic.com/v1",
}, },
// DeepSeek - https://platform.deepseek.com/ // DeepSeek - https://platform.deepseek.com/
{ {
ModelName: "deepseek-chat", ModelName: "deepseek-chat",
Model: "deepseek/deepseek-chat", Provider: "deepseek",
Model: "deepseek-chat",
APIBase: "https://api.deepseek.com/v1", APIBase: "https://api.deepseek.com/v1",
}, },
// Venice AI - https://venice.ai // Venice AI - https://venice.ai
{ {
ModelName: "venice-uncensored", ModelName: "venice-uncensored",
Model: "venice/venice-uncensored", Provider: "venice",
Model: "venice-uncensored",
APIBase: "https://api.venice.ai/api/v1", APIBase: "https://api.venice.ai/api/v1",
}, },
// Google Gemini - https://ai.google.dev/ // Google Gemini - https://ai.google.dev/
{ {
ModelName: "gemini-2.0-flash", 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", APIBase: "https://generativelanguage.googleapis.com/v1beta",
}, },
// Qwen (通义千问) - https://dashscope.console.aliyun.com/apiKey // Qwen (通义千问) - https://dashscope.console.aliyun.com/apiKey
{ {
ModelName: "qwen-plus", ModelName: "qwen-plus",
Model: "qwen/qwen-plus", Provider: "qwen",
Model: "qwen-plus",
APIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1", APIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1",
}, },
// Moonshot (月之暗面) - https://platform.moonshot.cn/console/api-keys // Moonshot (月之暗面) - https://platform.moonshot.cn/console/api-keys
{ {
ModelName: "moonshot-v1-8k", ModelName: "moonshot-v1-8k",
Model: "moonshot/moonshot-v1-8k", Provider: "moonshot",
Model: "moonshot-v1-8k",
APIBase: "https://api.moonshot.cn/v1", APIBase: "https://api.moonshot.cn/v1",
}, },
// Groq - https://console.groq.com/keys // Groq - https://console.groq.com/keys
{ {
ModelName: "llama-3.3-70b", 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", APIBase: "https://api.groq.com/openai/v1",
}, },
// OpenRouter (100+ models) - https://openrouter.ai/keys // OpenRouter (100+ models) - https://openrouter.ai/keys
{ {
ModelName: "openrouter-auto", ModelName: "openrouter-auto",
Model: "openrouter/auto", Provider: "openrouter",
Model: "auto",
APIBase: "https://openrouter.ai/api/v1", APIBase: "https://openrouter.ai/api/v1",
}, },
{ {
ModelName: "openrouter-gpt-5.4", ModelName: "openrouter-gpt-5.4",
Model: "openrouter/openai/gpt-5.4", Provider: "openrouter",
Model: "openai/gpt-5.4",
APIBase: "https://openrouter.ai/api/v1", APIBase: "https://openrouter.ai/api/v1",
}, },
// NVIDIA - https://build.nvidia.com/ // NVIDIA - https://build.nvidia.com/
{ {
ModelName: "nemotron-4-340b", ModelName: "nemotron-4-340b",
Model: "nvidia/nemotron-4-340b-instruct", Provider: "nvidia",
Model: "nemotron-4-340b-instruct",
APIBase: "https://integrate.api.nvidia.com/v1", APIBase: "https://integrate.api.nvidia.com/v1",
}, },
// Cerebras - https://inference.cerebras.ai/ // Cerebras - https://inference.cerebras.ai/
{ {
ModelName: "cerebras-llama-3.3-70b", 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", APIBase: "https://api.cerebras.ai/v1",
}, },
// Vivgrid - https://vivgrid.com // Vivgrid - https://vivgrid.com
{ {
ModelName: "vivgrid-auto", ModelName: "vivgrid-auto",
Model: "vivgrid/auto", Provider: "vivgrid",
Model: "auto",
APIBase: "https://api.vivgrid.com/v1", APIBase: "https://api.vivgrid.com/v1",
}, },
// Volcengine (火山引擎) - https://console.volcengine.com/ark // Volcengine (火山引擎) - https://console.volcengine.com/ark
{ {
ModelName: "ark-code-latest", ModelName: "ark-code-latest",
Model: "volcengine/ark-code-latest", Provider: "volcengine",
Model: "ark-code-latest",
APIBase: "https://ark.cn-beijing.volces.com/api/v3", APIBase: "https://ark.cn-beijing.volces.com/api/v3",
}, },
{ {
ModelName: "doubao-pro", ModelName: "doubao-pro",
Model: "volcengine/doubao-pro-32k", Provider: "volcengine",
Model: "doubao-pro-32k",
APIBase: "https://ark.cn-beijing.volces.com/api/v3", APIBase: "https://ark.cn-beijing.volces.com/api/v3",
}, },
// ShengsuanYun (神算云) // ShengsuanYun (神算云)
{ {
ModelName: "deepseek-v3", ModelName: "deepseek-v3",
Model: "shengsuanyun/deepseek-v3", Provider: "shengsuanyun",
Model: "deepseek-v3",
APIBase: "https://api.shengsuanyun.com/v1", APIBase: "https://api.shengsuanyun.com/v1",
}, },
// Antigravity (Google Cloud Code Assist) - OAuth only // Antigravity (Google Cloud Code Assist) - OAuth only
{ {
ModelName: "gemini-flash", ModelName: "gemini-flash",
Model: "antigravity/gemini-3-flash", Provider: "antigravity",
Model: "gemini-3-flash",
AuthMethod: "oauth", AuthMethod: "oauth",
}, },
// GitHub Copilot - https://github.com/settings/tokens // GitHub Copilot - https://github.com/settings/tokens
{ {
ModelName: "copilot-gpt-5.4", ModelName: "copilot-gpt-5.4",
Model: "github-copilot/gpt-5.4", Provider: "github-copilot",
Model: "gpt-5.4",
APIBase: "http://localhost:4321", APIBase: "http://localhost:4321",
AuthMethod: "oauth", AuthMethod: "oauth",
}, },
@ -191,33 +210,38 @@ func DefaultConfig() *Config {
// Ollama (local) - https://ollama.com // Ollama (local) - https://ollama.com
{ {
ModelName: "llama3", ModelName: "llama3",
Model: "ollama/llama3", Provider: "ollama",
Model: "llama3",
APIBase: "http://localhost:11434/v1", APIBase: "http://localhost:11434/v1",
}, },
// Mistral AI - https://console.mistral.ai/api-keys // Mistral AI - https://console.mistral.ai/api-keys
{ {
ModelName: "mistral-small", ModelName: "mistral-small",
Model: "mistral/mistral-small-latest", Provider: "mistral",
Model: "mistral-small-latest",
APIBase: "https://api.mistral.ai/v1", APIBase: "https://api.mistral.ai/v1",
}, },
// Avian - https://avian.io // Avian - https://avian.io
{ {
ModelName: "deepseek-v3.2", ModelName: "deepseek-v3.2",
Model: "avian/deepseek/deepseek-v3.2", Provider: "avian",
Model: "deepseek/deepseek-v3.2",
APIBase: "https://api.avian.io/v1", APIBase: "https://api.avian.io/v1",
}, },
{ {
ModelName: "kimi-k2.5", ModelName: "kimi-k2.5",
Model: "avian/moonshotai/kimi-k2.5", Provider: "avian",
Model: "moonshotai/kimi-k2.5",
APIBase: "https://api.avian.io/v1", APIBase: "https://api.avian.io/v1",
}, },
// Minimax - https://api.minimaxi.com/ // Minimax - https://api.minimaxi.com/
{ {
ModelName: "MiniMax-M2.5", ModelName: "MiniMax-M2.5",
Model: "minimax/MiniMax-M2.5", Provider: "minimax",
Model: "MiniMax-M2.5",
APIBase: "https://api.minimaxi.com/v1", APIBase: "https://api.minimaxi.com/v1",
ExtraBody: map[string]any{"reasoning_split": true}, ExtraBody: map[string]any{"reasoning_split": true},
}, },
@ -225,28 +249,32 @@ func DefaultConfig() *Config {
// LongCat - https://longcat.chat/platform // LongCat - https://longcat.chat/platform
{ {
ModelName: "LongCat-Flash-Thinking", ModelName: "LongCat-Flash-Thinking",
Model: "longcat/LongCat-Flash-Thinking", Provider: "longcat",
Model: "LongCat-Flash-Thinking",
APIBase: "https://api.longcat.chat/openai", APIBase: "https://api.longcat.chat/openai",
}, },
// ModelScope (魔搭社区) - https://modelscope.cn/my/tokens // ModelScope (魔搭社区) - https://modelscope.cn/my/tokens
{ {
ModelName: "modelscope-qwen", 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", APIBase: "https://api-inference.modelscope.cn/v1",
}, },
// VLLM (local) - http://localhost:8000 // VLLM (local) - http://localhost:8000
{ {
ModelName: "local-model", ModelName: "local-model",
Model: "vllm/custom-model", Provider: "vllm",
Model: "custom-model",
APIBase: "http://localhost:8000/v1", APIBase: "http://localhost:8000/v1",
}, },
// LM Studio (local) - http://localhost:1234 // LM Studio (local) - http://localhost:1234
{ {
ModelName: "lmstudio-local", ModelName: "lmstudio-local",
Model: "lmstudio/openai/gpt-oss-20b", Provider: "lmstudio",
Model: "openai/gpt-oss-20b",
APIBase: "http://localhost:1234/v1", 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 // model_name is a user-friendly alias; the model field's path after "azure/" is your deployment name
{ {
ModelName: "azure-gpt5", ModelName: "azure-gpt5",
Model: "azure/my-gpt5-deployment", Provider: "azure",
Model: "my-gpt5-deployment",
APIBase: "https://your-resource.openai.azure.com", APIBase: "https://your-resource.openai.azure.com",
}, },
}, },

View file

@ -188,6 +188,7 @@ func TestExpandMultiKeyModels_Deduplication(t *testing.T) {
func TestExpandMultiKeyModels_PreservesOtherFields(t *testing.T) { func TestExpandMultiKeyModels_PreservesOtherFields(t *testing.T) {
modelCfg := &ModelConfig{ modelCfg := &ModelConfig{
ModelName: "gpt-4", ModelName: "gpt-4",
Provider: "openrouter",
Model: "openai/gpt-4o", Model: "openai/gpt-4o",
APIBase: "https://api.example.com", APIBase: "https://api.example.com",
Proxy: "http://proxy:8080", Proxy: "http://proxy:8080",
@ -206,6 +207,9 @@ func TestExpandMultiKeyModels_PreservesOtherFields(t *testing.T) {
if primary.APIBase != "https://api.example.com" { if primary.APIBase != "https://api.example.com" {
t.Errorf("expected api_base preserved, got %q", primary.APIBase) 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" { if primary.Proxy != "http://proxy:8080" {
t.Errorf("expected proxy preserved, got %q", primary.Proxy) 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 // Check additional entry also preserves fields
additional := result[0] additional := result[0]
if additional.Provider != "openrouter" {
t.Errorf("expected additional provider preserved, got %q", additional.Provider)
}
if additional.APIBase != "https://api.example.com" { if additional.APIBase != "https://api.example.com" {
t.Errorf("expected additional api_base preserved, got %q", additional.APIBase) t.Errorf("expected additional api_base preserved, got %q", additional.APIBase)
} }

View file

@ -41,6 +41,7 @@ var protocolMetaByName = map[string]protocolMeta{
"vivgrid": {defaultAPIBase: "https://api.vivgrid.com/v1"}, "vivgrid": {defaultAPIBase: "https://api.vivgrid.com/v1"},
"volcengine": {defaultAPIBase: "https://ark.cn-beijing.volces.com/api/v3"}, "volcengine": {defaultAPIBase: "https://ark.cn-beijing.volces.com/api/v3"},
"qwen": {defaultAPIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1"}, "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-intl": {defaultAPIBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"},
"qwen-international": {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"}, "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"}, "qwen-coding": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/v1"},
"coding-plan-anthropic": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"}, "coding-plan-anthropic": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"},
"alibaba-coding-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}, "vllm": {defaultAPIBase: "http://localhost:8000/v1", emptyAPIKeyAllowed: true},
"mistral": {defaultAPIBase: "https://api.mistral.ai/v1"}, "mistral": {defaultAPIBase: "https://api.mistral.ai/v1"},
"avian": {defaultAPIBase: "https://api.avian.io/v1"}, "avian": {defaultAPIBase: "https://api.avian.io/v1"},
@ -84,19 +86,42 @@ func createCodexAuthProvider() (LLMProvider, error) {
return NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil return NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil
} }
// ExtractProtocol extracts the protocol prefix and model identifier from a model string. // ExtractProtocol extracts the effective protocol and model identifier from a
// If no prefix is specified, it defaults to "openai". // 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: // Examples:
// - "openai/gpt-4o" -> ("openai", "gpt-4o") // - Model "openai/gpt-4o" -> ("openai", "gpt-4o")
// - "anthropic/claude-sonnet-4.6" -> ("anthropic", "claude-sonnet-4.6") // - Model "nvidia/z-ai/glm-5.1" -> ("nvidia", "z-ai/glm-5.1")
// - "gpt-4o" -> ("openai", "gpt-4o") // default protocol // - Provider "nvidia", Model "z-ai/glm-5.1" -> ("nvidia", "z-ai/glm-5.1")
func ExtractProtocol(model string) (protocol, modelID string) { // - Model "gpt-4o" -> ("openai", "gpt-4o")
model = strings.TrimSpace(model) func ExtractProtocol(cfg *config.ModelConfig) (protocol, modelID string) {
protocol, modelID, found := strings.Cut(model, "/") 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 { if !found {
return "openai", model 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 // 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 != "" { if apiBase := strings.TrimSpace(cfg.APIBase); apiBase != "" {
return strings.TrimRight(apiBase, "/") return strings.TrimRight(apiBase, "/")
} }
protocol, _ := ExtractProtocol(cfg.Model) protocol, _ := ExtractProtocol(cfg)
return strings.TrimRight(getDefaultAPIBase(protocol), "/") return strings.TrimRight(getDefaultAPIBase(protocol), "/")
} }
// CreateProviderFromConfig creates a provider based on the ModelConfig. // 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), // Supported protocol families include OpenAI-compatible prefixes (e.g., openai, openrouter, groq),
// Azure OpenAI, Amazon Bedrock, Anthropic (including messages), and various CLI/compatibility shims. // Azure OpenAI, Amazon Bedrock, Anthropic (including messages), and various CLI/compatibility shims.
// See the switch on protocol in this function for the authoritative list. // 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) { func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) {
if cfg == nil { if cfg == nil {
return nil, "", fmt.Errorf("config is 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") return nil, "", fmt.Errorf("model is required")
} }
protocol, modelID := ExtractProtocol(cfg.Model) protocol, modelID := ExtractProtocol(cfg)
userAgent := cfg.UserAgent userAgent := cfg.UserAgent
if userAgent == "" { if userAgent == "" {
@ -220,9 +245,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
case "litellm", "lmstudio", "openrouter", "groq", "zhipu", "nvidia", "venice", case "litellm", "lmstudio", "openrouter", "groq", "zhipu", "nvidia", "venice",
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras", "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", "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 // All other OpenAI-compatible HTTP providers
if cfg.APIKey() == "" && cfg.APIBase == "" && !isEmptyAPIKeyAllowed(protocol) { if cfg.APIKey() == "" && cfg.APIBase == "" && !isEmptyAPIKeyAllowed(protocol) {
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol) return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)

View file

@ -19,68 +19,91 @@ import (
func TestExtractProtocol(t *testing.T) { func TestExtractProtocol(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
model string config *config.ModelConfig
wantProtocol string wantProtocol string
wantModelID string wantModelID string
}{ }{
{ {
name: "openai with prefix", name: "openai with prefix",
model: "openai/gpt-4o", config: &config.ModelConfig{Model: "openai/gpt-4o"},
wantProtocol: "openai", wantProtocol: "openai",
wantModelID: "gpt-4o", wantModelID: "gpt-4o",
}, },
{ {
name: "anthropic with prefix", name: "anthropic with prefix",
model: "anthropic/claude-sonnet-4.6", config: &config.ModelConfig{Model: "anthropic/claude-sonnet-4.6"},
wantProtocol: "anthropic", wantProtocol: "anthropic",
wantModelID: "claude-sonnet-4.6", wantModelID: "claude-sonnet-4.6",
}, },
{ {
name: "no prefix - defaults to openai", name: "no prefix - defaults to openai",
model: "gpt-4o", config: &config.ModelConfig{Model: "gpt-4o"},
wantProtocol: "openai", wantProtocol: "openai",
wantModelID: "gpt-4o", wantModelID: "gpt-4o",
}, },
{ {
name: "groq with prefix", name: "groq with prefix",
model: "groq/llama-3.1-70b", config: &config.ModelConfig{Model: "groq/llama-3.1-70b"},
wantProtocol: "groq", wantProtocol: "groq",
wantModelID: "llama-3.1-70b", wantModelID: "llama-3.1-70b",
}, },
{ {
name: "empty string", name: "empty string",
model: "", config: &config.ModelConfig{Model: ""},
wantProtocol: "openai", wantProtocol: "",
wantModelID: "", wantModelID: "",
}, },
{ {
name: "with whitespace", name: "with whitespace",
model: " openai/gpt-4 ", config: &config.ModelConfig{Model: " openai/gpt-4 "},
wantProtocol: "openai", wantProtocol: "openai",
wantModelID: "gpt-4", wantModelID: "gpt-4",
}, },
{ {
name: "multiple slashes", name: "multiple slashes",
model: "nvidia/meta/llama-3.1-8b", config: &config.ModelConfig{Model: "nvidia/meta/llama-3.1-8b"},
wantProtocol: "nvidia", wantProtocol: "nvidia",
wantModelID: "meta/llama-3.1-8b", 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", name: "azure with prefix",
model: "azure/my-gpt5-deployment", config: &config.ModelConfig{Model: "azure/my-gpt5-deployment"},
wantProtocol: "azure", wantProtocol: "azure",
wantModelID: "my-gpt5-deployment", 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
protocol, modelID := ExtractProtocol(tt.model) protocol, modelID := ExtractProtocol(tt.config)
if protocol != tt.wantProtocol { 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 { 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) { func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -701,8 +747,9 @@ func TestCreateProviderFromConfig_QwenInternationalAlias(t *testing.T) {
if provider == nil { if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider") t.Fatal("CreateProviderFromConfig() returned nil provider")
} }
if modelID != "qwen-max" { wantModelID := "qwen-max"
t.Errorf("modelID = %q, want %q", modelID, "qwen-max") if modelID != wantModelID {
t.Errorf("modelID = %q, want %q", modelID, wantModelID)
} }
if _, ok := provider.(*HTTPProvider); !ok { if _, ok := provider.(*HTTPProvider); !ok {
t.Fatalf("expected *HTTPProvider, got %T", provider) t.Fatalf("expected *HTTPProvider, got %T", provider)
@ -735,8 +782,9 @@ func TestCreateProviderFromConfig_QwenUSAlias(t *testing.T) {
if provider == nil { if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider") t.Fatal("CreateProviderFromConfig() returned nil provider")
} }
if modelID != "qwen-max" { wantModelID := "qwen-max"
t.Errorf("modelID = %q, want %q", modelID, "qwen-max") if modelID != wantModelID {
t.Errorf("modelID = %q, want %q", modelID, wantModelID)
} }
if _, ok := provider.(*HTTPProvider); !ok { if _, ok := provider.(*HTTPProvider); !ok {
t.Fatalf("expected *HTTPProvider, got %T", provider) t.Fatalf("expected *HTTPProvider, got %T", provider)
@ -769,8 +817,9 @@ func TestCreateProviderFromConfig_CodingPlanAnthropic(t *testing.T) {
if provider == nil { if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider") t.Fatal("CreateProviderFromConfig() returned nil provider")
} }
if modelID != "claude-sonnet-4-20250514" { wantModelID := "claude-sonnet-4-20250514"
t.Errorf("modelID = %q, want %q", modelID, "claude-sonnet-4-20250514") if modelID != wantModelID {
t.Errorf("modelID = %q, want %q", modelID, wantModelID)
} }
// coding-plan-anthropic uses Anthropic Messages provider // coding-plan-anthropic uses Anthropic Messages provider
// Verify it's the anthropic messages provider by checking interface // Verify it's the anthropic messages provider by checking interface