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