feat: add LiteLLM provider alias support (#930)

This commit is contained in:
Alfonso 2026-03-02 13:23:55 -08:00 committed by GitHub
parent 0150947e61
commit 946af6b53d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 131 additions and 7 deletions

View file

@ -925,7 +925,7 @@ This design also enables **multi-agent support** with flexible provider selectio
#### 📋 All Supported Vendors #### 📋 All Supported Vendors
| Vendor | `model` Prefix | Default API Base | Protocol | API Key | | Vendor | `model` Prefix | Default API Base | Protocol | API Key |
| ------------------- | ----------------- | --------------------------------------------------- | --------- | ---------------------------------------------------------------- | | ------------------- | ----------------- |-----------------------------------------------------| --------- | ---------------------------------------------------------------- |
| **OpenAI** | `openai/` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) | | **OpenAI** | `openai/` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) |
| **Anthropic** | `anthropic/` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) | | **Anthropic** | `anthropic/` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) |
| **智谱 AI (GLM)** | `zhipu/` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) | | **智谱 AI (GLM)** | `zhipu/` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
@ -937,6 +937,7 @@ This design also enables **multi-agent support** with flexible provider selectio
| **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [Get Key](https://build.nvidia.com) | | **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [Get Key](https://build.nvidia.com) |
| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | Local (no key needed) | | **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | Local (no key needed) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Get Key](https://openrouter.ai/keys) | | **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Get Key](https://openrouter.ai/keys) |
| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1 | OpenAI | Your LiteLLM proxy key |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local | | **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) | | **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) |
| **火山引擎** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://console.volcengine.com) | | **火山引擎** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://console.volcengine.com) |
@ -1038,6 +1039,19 @@ This design also enables **multi-agent support** with flexible provider selectio
} }
``` ```
**LiteLLM Proxy**
```json
{
"model_name": "lite-gpt4",
"model": "litellm/lite-gpt4",
"api_base": "http://localhost:4000/v1",
"api_key": "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`.
#### Load Balancing #### Load Balancing
Configure multiple endpoints for the same model name—PicoClaw will automatically round-robin between them: Configure multiple endpoints for the same model name—PicoClaw will automatically round-robin between them:

View file

@ -399,6 +399,7 @@ type DevicesConfig struct {
type ProvidersConfig struct { type ProvidersConfig struct {
Anthropic ProviderConfig `json:"anthropic"` Anthropic ProviderConfig `json:"anthropic"`
OpenAI OpenAIProviderConfig `json:"openai"` OpenAI OpenAIProviderConfig `json:"openai"`
LiteLLM ProviderConfig `json:"litellm"`
OpenRouter ProviderConfig `json:"openrouter"` OpenRouter ProviderConfig `json:"openrouter"`
Groq ProviderConfig `json:"groq"` Groq ProviderConfig `json:"groq"`
Zhipu ProviderConfig `json:"zhipu"` Zhipu ProviderConfig `json:"zhipu"`
@ -422,6 +423,7 @@ type ProvidersConfig struct {
func (p ProvidersConfig) IsEmpty() bool { func (p ProvidersConfig) IsEmpty() bool {
return p.Anthropic.APIKey == "" && p.Anthropic.APIBase == "" && return p.Anthropic.APIKey == "" && p.Anthropic.APIBase == "" &&
p.OpenAI.APIKey == "" && p.OpenAI.APIBase == "" && p.OpenAI.APIKey == "" && p.OpenAI.APIBase == "" &&
p.LiteLLM.APIKey == "" && p.LiteLLM.APIBase == "" &&
p.OpenRouter.APIKey == "" && p.OpenRouter.APIBase == "" && p.OpenRouter.APIKey == "" && p.OpenRouter.APIBase == "" &&
p.Groq.APIKey == "" && p.Groq.APIBase == "" && p.Groq.APIKey == "" && p.Groq.APIBase == "" &&
p.Zhipu.APIKey == "" && p.Zhipu.APIBase == "" && p.Zhipu.APIKey == "" && p.Zhipu.APIBase == "" &&

View file

@ -88,6 +88,23 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
}, true }, true
}, },
}, },
{
providerNames: []string{"litellm"},
protocol: "litellm",
buildConfig: func(p ProvidersConfig) (ModelConfig, bool) {
if p.LiteLLM.APIKey == "" && p.LiteLLM.APIBase == "" {
return ModelConfig{}, false
}
return ModelConfig{
ModelName: "litellm",
Model: "litellm/auto",
APIKey: p.LiteLLM.APIKey,
APIBase: p.LiteLLM.APIBase,
Proxy: p.LiteLLM.Proxy,
RequestTimeout: p.LiteLLM.RequestTimeout,
}, true
},
},
{ {
providerNames: []string{"openrouter"}, providerNames: []string{"openrouter"},
protocol: "openrouter", protocol: "openrouter",

View file

@ -63,6 +63,33 @@ func TestConvertProvidersToModelList_Anthropic(t *testing.T) {
} }
} }
func TestConvertProvidersToModelList_LiteLLM(t *testing.T) {
cfg := &Config{
Providers: ProvidersConfig{
LiteLLM: ProviderConfig{
APIKey: "litellm-key",
APIBase: "http://localhost:4000/v1",
},
},
}
result := ConvertProvidersToModelList(cfg)
if len(result) != 1 {
t.Fatalf("len(result) = %d, want 1", len(result))
}
if result[0].ModelName != "litellm" {
t.Errorf("ModelName = %q, want %q", result[0].ModelName, "litellm")
}
if result[0].Model != "litellm/auto" {
t.Errorf("Model = %q, want %q", result[0].Model, "litellm/auto")
}
if result[0].APIBase != "http://localhost:4000/v1" {
t.Errorf("APIBase = %q, want %q", result[0].APIBase, "http://localhost:4000/v1")
}
}
func TestConvertProvidersToModelList_Multiple(t *testing.T) { func TestConvertProvidersToModelList_Multiple(t *testing.T) {
cfg := &Config{ cfg := &Config{
Providers: ProvidersConfig{ Providers: ProvidersConfig{
@ -115,6 +142,7 @@ func TestConvertProvidersToModelList_AllProviders(t *testing.T) {
cfg := &Config{ cfg := &Config{
Providers: ProvidersConfig{ Providers: ProvidersConfig{
OpenAI: OpenAIProviderConfig{ProviderConfig: ProviderConfig{APIKey: "key1"}}, OpenAI: OpenAIProviderConfig{ProviderConfig: ProviderConfig{APIKey: "key1"}},
LiteLLM: ProviderConfig{APIKey: "key-litellm", APIBase: "http://localhost:4000/v1"},
Anthropic: ProviderConfig{APIKey: "key2"}, Anthropic: ProviderConfig{APIKey: "key2"},
OpenRouter: ProviderConfig{APIKey: "key3"}, OpenRouter: ProviderConfig{APIKey: "key3"},
Groq: ProviderConfig{APIKey: "key4"}, Groq: ProviderConfig{APIKey: "key4"},
@ -137,9 +165,9 @@ func TestConvertProvidersToModelList_AllProviders(t *testing.T) {
result := ConvertProvidersToModelList(cfg) result := ConvertProvidersToModelList(cfg)
// All 18 providers should be converted // All 19 providers should be converted
if len(result) != 18 { if len(result) != 19 {
t.Errorf("len(result) = %d, want 18", len(result)) t.Errorf("len(result) = %d, want 19", len(result))
} }
} }

View file

@ -102,6 +102,15 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
sel.apiBase = "https://openrouter.ai/api/v1" sel.apiBase = "https://openrouter.ai/api/v1"
} }
} }
case "litellm":
if cfg.Providers.LiteLLM.APIKey != "" || cfg.Providers.LiteLLM.APIBase != "" {
sel.apiKey = cfg.Providers.LiteLLM.APIKey
sel.apiBase = cfg.Providers.LiteLLM.APIBase
sel.proxy = cfg.Providers.LiteLLM.Proxy
if sel.apiBase == "" {
sel.apiBase = "http://localhost:4000/v1"
}
}
case "zhipu", "glm": case "zhipu", "glm":
if cfg.Providers.Zhipu.APIKey != "" { if cfg.Providers.Zhipu.APIKey != "" {
sel.apiKey = cfg.Providers.Zhipu.APIKey sel.apiKey = cfg.Providers.Zhipu.APIKey

View file

@ -53,7 +53,7 @@ func ExtractProtocol(model string) (protocol, modelID string) {
// CreateProviderFromConfig creates a provider based on the ModelConfig. // CreateProviderFromConfig creates a provider based on the ModelConfig.
// It uses the protocol prefix in the Model field to determine which provider to create. // It uses the protocol prefix in the Model field to determine which provider to create.
// Supported protocols: openai, anthropic, antigravity, claude-cli, codex-cli, github-copilot // Supported protocols: openai, litellm, anthropic, antigravity, claude-cli, codex-cli, github-copilot
// Returns the provider, the model ID (without protocol prefix), and any error. // Returns the provider, the model ID (without protocol prefix), and any error.
func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) { func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) {
if cfg == nil { if cfg == nil {
@ -92,7 +92,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
cfg.RequestTimeout, cfg.RequestTimeout,
), modelID, nil ), modelID, nil
case "openrouter", "groq", "zhipu", "gemini", "nvidia", case "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras", "ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
"volcengine", "vllm", "qwen", "mistral": "volcengine", "vllm", "qwen", "mistral":
// All other OpenAI-compatible HTTP providers // All other OpenAI-compatible HTTP providers
@ -180,6 +180,8 @@ func getDefaultAPIBase(protocol string) string {
return "https://api.openai.com/v1" return "https://api.openai.com/v1"
case "openrouter": case "openrouter":
return "https://openrouter.ai/api/v1" return "https://openrouter.ai/api/v1"
case "litellm":
return "http://localhost:4000/v1"
case "groq": case "groq":
return "https://api.groq.com/openai/v1" return "https://api.groq.com/openai/v1"
case "zhipu": case "zhipu":

View file

@ -135,6 +135,32 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
} }
} }
func TestGetDefaultAPIBase_LiteLLM(t *testing.T) {
if got := getDefaultAPIBase("litellm"); got != "http://localhost:4000/v1" {
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "litellm", got, "http://localhost:4000/v1")
}
}
func TestCreateProviderFromConfig_LiteLLM(t *testing.T) {
cfg := &config.ModelConfig{
ModelName: "test-litellm",
Model: "litellm/my-proxy-alias",
APIKey: "test-key",
APIBase: "http://localhost:4000/v1",
}
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 != "my-proxy-alias" {
t.Errorf("modelID = %q, want %q", modelID, "my-proxy-alias")
}
}
func TestCreateProviderFromConfig_Anthropic(t *testing.T) { func TestCreateProviderFromConfig_Anthropic(t *testing.T) {
cfg := &config.ModelConfig{ cfg := &config.ModelConfig{
ModelName: "test-anthropic", ModelName: "test-anthropic",

View file

@ -17,6 +17,27 @@ func TestResolveProviderSelection(t *testing.T) {
wantProxy string wantProxy string
wantErrSubstr string wantErrSubstr string
}{ }{
{
name: "explicit litellm provider uses configured base",
setup: func(cfg *config.Config) {
cfg.Agents.Defaults.Provider = "litellm"
cfg.Providers.LiteLLM.APIKey = "litellm-key"
cfg.Providers.LiteLLM.APIBase = "http://localhost:4000/v1"
cfg.Providers.LiteLLM.Proxy = "http://127.0.0.1:7890"
},
wantType: providerTypeHTTPCompat,
wantAPIBase: "http://localhost:4000/v1",
wantProxy: "http://127.0.0.1:7890",
},
{
name: "explicit litellm provider defaults base when only key is configured",
setup: func(cfg *config.Config) {
cfg.Agents.Defaults.Provider = "litellm"
cfg.Providers.LiteLLM.APIKey = "litellm-key"
},
wantType: providerTypeHTTPCompat,
wantAPIBase: "http://localhost:4000/v1",
},
{ {
name: "explicit claude-cli provider routes to cli provider type", name: "explicit claude-cli provider routes to cli provider type",
setup: func(cfg *config.Config) { setup: func(cfg *config.Config) {

View file

@ -325,7 +325,7 @@ func normalizeModel(model, apiBase string) string {
prefix := strings.ToLower(before) prefix := strings.ToLower(before)
switch prefix { switch prefix {
case "moonshot", "nvidia", "groq", "ollama", "deepseek", "google", "openrouter", "zhipu", "mistral": case "litellm", "moonshot", "nvidia", "groq", "ollama", "deepseek", "google", "openrouter", "zhipu", "mistral":
return after return after
default: default:
return model return model

View file

@ -256,6 +256,11 @@ func TestProviderChat_StripsGroqAndOllamaPrefixes(t *testing.T) {
input string input string
wantModel string wantModel string
}{ }{
{
name: "strips litellm prefix and preserves proxy model name",
input: "litellm/my-proxy-alias",
wantModel: "my-proxy-alias",
},
{ {
name: "strips groq prefix and keeps nested model", name: "strips groq prefix and keeps nested model",
input: "groq/openai/gpt-oss-120b", input: "groq/openai/gpt-oss-120b",