feat: add LiteLLM provider alias support (#930)
This commit is contained in:
parent
0150947e61
commit
946af6b53d
10 changed files with 131 additions and 7 deletions
16
README.md
16
README.md
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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 == "" &&
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue