Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,21 +173,31 @@ ANTHROPIC_API_KEY=sk-... go run ./examples/basic/

## Supported Providers

12 setup gateways in `catalog/registry/providers.go` (hawk `/config` uses the same list):
22 provider gateways in `catalog/registry/providers.go` (hawk `/config` uses the same list), listed in registry `SortOrder`:

| Provider | ID | Env variable |
|---|---|---|
| **Anthropic** | `anthropic` | `ANTHROPIC_API_KEY` |
| **OpenAI** | `openai` | `OPENAI_API_KEY` |
| **Google Gemini** | `gemini` | `GEMINI_API_KEY` |
| **OpenRouter** | `openrouter` | `OPENROUTER_API_KEY` |
| **DeepSeek** | `deepseek` | `DEEPSEEK_API_KEY` |
| **xAI (Grok)** | `grok` | `XAI_API_KEY` |
| **Z.AI** | `z-ai` | `ZAI_API_KEY` |
| **CanopyWave** | `canopywave` | `CANOPYWAVE_API_KEY` |
| **OpenCode Go** | `opencodego` | `OPENCODEGO_API_KEY` |
| **Kimi (Moonshot)** | `kimi` | `MOONSHOT_API_KEY` |
| **Xiaomi (MiMo) Pay-as-you-go** | `xiaomi_mimo_payg` | `XIAOMI_MIMO_PAYG_API_KEY` |
| **Z.AI — Coding Plan** | `zai_coding` | `ZAI_CODING_API_KEY` |
| **Z.AI — Pay-as-you-go** | `zai_payg` | `ZAI_API_KEY` |
| **Xiaomi (MiMo) Token Plan** | `xiaomi_mimo_token_plan` | `XIAOMI_MIMO_TOKEN_PLAN_API_KEY` (+ region `cn` / `sgp` / `ams`) |
| **Xiaomi (MiMo) Pay-as-you-go** | `xiaomi_mimo_payg` | `XIAOMI_MIMO_PAYG_API_KEY` |
| **MiniMax — Token Plan** | `minimax_token_plan` | `MINIMAX_TOKEN_PLAN_API_KEY` |
| **MiniMax — Pay-as-you-go** | `minimax_payg` | `MINIMAX_PAYG_API_KEY` |
| **Azure OpenAI** | `azure` | `AZURE_OPENAI_API_KEY` (+ `AZURE_OPENAI_ENDPOINT`) |
| **Amazon Bedrock** | `bedrock` | `AWS_SECRET_ACCESS_KEY` (+ `AWS_ACCESS_KEY_ID`, `AWS_SESSION_TOKEN`) |
| **Vertex AI** | `vertex` | `VERTEX_ACCESS_TOKEN` (or `GOOGLE_OAUTH_ACCESS_TOKEN`) |
| **OpenRouter** | `openrouter` | `OPENROUTER_API_KEY` |
| **CanopyWave** | `canopywave` | `CANOPYWAVE_API_KEY` |
| **Poolside** | `poolside` | `POOLSIDE_API_KEY` |
| **Groq** | `groq` | `GROQ_API_KEY` |
| **ClinePass** | `clinepass` | `CLINE_API_KEY` |
| **OpenCode Go** | `opencodego` | `OPENCODEGO_API_KEY` |
| **Ollama** | `ollama` | `OLLAMA_BASE_URL` (local; no API key) |

Runtime auto-detection uses a separate priority order for chat when no deployment is pinned; see `config` profiles.
Expand Down
34 changes: 17 additions & 17 deletions catalog/registry/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func providerSpecs() []ProviderSpec {
TransportKind: "anthropic",
RequiresKey: true, CredentialEnv: "ANTHROPIC_API_KEY",
CredentialAliases: []string{"CLAUDE_API_KEY"},
BaseURLEnv: []string{"ANTHROPIC_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"ANTHROPIC_BASE_URL"},
ProbeKind: ProbeAnthropic,
LiveFetcherKey: "anthropic", LiveCatalogKey: "anthropic",
ProtocolID: "anthropic-messages", AdapterID: "anthropic", RuntimeProfileKey: "anthropic",
Expand All @@ -42,39 +42,39 @@ func providerSpecs() []ProviderSpec {
RuntimeBaseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
RequiresKey: true, CredentialEnv: "GEMINI_API_KEY",
CredentialAliases: []string{"GOOGLE_API_KEY"},
BaseURLEnv: []string{"GEMINI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"GEMINI_BASE_URL"},
ProbeKind: ProbeGemini,
LiveFetcherKey: "gemini", LiveCatalogKey: "gemini",
ProtocolID: "gemini-generate-content", AdapterID: "gemini", RuntimeProfileKey: "gemini",
},
{
ProviderID: "deepseek", DisplayName: "DeepSeek", DeploymentID: "deepseek-direct", SortOrder: 4, ChatPreference: 11,
RequiresKey: true, CredentialEnv: "DEEPSEEK_API_KEY",
BaseURLEnv: []string{"DEEPSEEK_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"DEEPSEEK_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.deepseek.com/v1",
LiveFetcherKey: "deepseek", LiveCatalogKey: "deepseek",
ProtocolID: "openai-chat-completions", AdapterID: "deepseek", RuntimeProfileKey: "deepseek",
},
{
ProviderID: "grok", DisplayName: "xAI", DeploymentID: "grok-direct", SortOrder: 5, ChatPreference: 4,
RequiresKey: true, CredentialEnv: "XAI_API_KEY",
BaseURLEnv: []string{"XAI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"XAI_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.x.ai/v1",
LiveFetcherKey: "grok", LiveCatalogKey: "grok",
ProtocolID: "openai-chat-completions", AdapterID: "grok", RuntimeProfileKey: "grok",
},
{
ProviderID: "kimi", DisplayName: "Kimi", DeploymentID: "kimi-direct", SortOrder: 6, ChatPreference: 14,
RequiresKey: true, CredentialEnv: "MOONSHOT_API_KEY",
BaseURLEnv: []string{"MOONSHOT_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"MOONSHOT_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.moonshot.ai/v1",
LiveFetcherKey: "kimi", LiveCatalogKey: "kimi",
ProtocolID: "openai-chat-completions", AdapterID: "kimi", RuntimeProfileKey: "kimi",
},
{
ProviderID: "zai_coding", DisplayName: "Z.AI — Coding Plan", DeploymentID: "zai_coding-direct", SortOrder: 7, ChatPreference: 8,
RequiresKey: true, CredentialEnv: "ZAI_CODING_API_KEY",
BaseURLEnv: []string{"ZAI_CODING_BASE_URL", "ZAI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"ZAI_CODING_BASE_URL", "ZAI_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.z.ai/api/coding/paas/v4",
LiveFetcherKey: "zai_coding", LiveCatalogKey: "zai_coding",
ProtocolID: "openai-chat-completions", AdapterID: "zai_coding", RuntimeProfileKey: "zai_coding",
Expand All @@ -83,7 +83,7 @@ func providerSpecs() []ProviderSpec {
{
ProviderID: "zai_payg", DisplayName: "Z.AI — Pay-as-you-go", DeploymentID: "zai_payg-direct", SortOrder: 8, ChatPreference: 9,
RequiresKey: true, CredentialEnv: "ZAI_API_KEY",
BaseURLEnv: []string{"ZAI_BASE_URL", "ZAI_API_BASE", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"ZAI_BASE_URL", "ZAI_API_BASE"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.z.ai/api/paas/v4",
LiveFetcherKey: "zai_payg", LiveCatalogKey: "zai_payg",
ProtocolID: "openai-chat-completions", AdapterID: "zai_payg", RuntimeProfileKey: "zai_payg",
Expand All @@ -92,7 +92,7 @@ func providerSpecs() []ProviderSpec {
{
ProviderID: "xiaomi_mimo_token_plan", DisplayName: "Xiaomi MiMo — Token Plan", DeploymentID: "xiaomi_mimo_token_plan-direct", SortOrder: 9, ChatPreference: 16,
RequiresKey: true, CredentialEnv: "XIAOMI_MIMO_TOKEN_PLAN_API_KEY",
BaseURLEnv: []string{"XIAOMI_MIMO_TOKEN_PLAN_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"XIAOMI_MIMO_TOKEN_PLAN_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "",
LiveFetcherKey: "xiaomi_mimo_token_plan", LiveCatalogKey: "xiaomi_mimo_token_plan",
ProtocolID: "openai-chat-completions", AdapterID: "xiaomi_mimo", RuntimeProfileKey: "xiaomi_mimo_token_plan",
Expand All @@ -102,23 +102,23 @@ func providerSpecs() []ProviderSpec {
ProviderID: "xiaomi_mimo_payg", DisplayName: "Xiaomi MiMo — Pay-as-you-go", DeploymentID: "xiaomi_mimo_payg-direct", SortOrder: 10, ChatPreference: 15,
RequiresKey: true, CredentialEnv: "XIAOMI_MIMO_PAYG_API_KEY",
CredentialAliases: []string{"XIAOMI_MIMO_API_KEY"},
BaseURLEnv: []string{"XIAOMI_MIMO_PAYG_BASE_URL", "XIAOMI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"XIAOMI_MIMO_PAYG_BASE_URL", "XIAOMI_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.xiaomimimo.com/v1",
LiveFetcherKey: "xiaomi_mimo_payg", LiveCatalogKey: "xiaomi_mimo_payg",
ProtocolID: "openai-chat-completions", AdapterID: "xiaomi_mimo", RuntimeProfileKey: "xiaomi_mimo_payg",
},
{
ProviderID: "minimax_token_plan", DisplayName: "MiniMax — Token Plan", DeploymentID: "minimax_token_plan-direct", SortOrder: 11, ChatPreference: 17,
RequiresKey: true, CredentialEnv: "MINIMAX_TOKEN_PLAN_API_KEY",
BaseURLEnv: []string{"MINIMAX_TOKEN_PLAN_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"MINIMAX_TOKEN_PLAN_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.minimax.io/v1",
LiveFetcherKey: "minimax_token_plan", LiveCatalogKey: "minimax_token_plan",
ProtocolID: "openai-chat-completions", AdapterID: "openai", RuntimeProfileKey: "minimax_token_plan",
},
{
ProviderID: "minimax_payg", DisplayName: "MiniMax — Pay-as-you-go", DeploymentID: "minimax_payg-direct", SortOrder: 12, ChatPreference: 18,
RequiresKey: true, CredentialEnv: "MINIMAX_PAYG_API_KEY",
BaseURLEnv: []string{"MINIMAX_PAYG_BASE_URL", "MINIMAX_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"MINIMAX_PAYG_BASE_URL", "MINIMAX_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.minimax.io/v1",
LiveFetcherKey: "minimax_payg", LiveCatalogKey: "minimax_payg",
ProtocolID: "openai-chat-completions", AdapterID: "openai", RuntimeProfileKey: "minimax_payg",
Expand Down Expand Up @@ -159,7 +159,7 @@ func providerSpecs() []ProviderSpec {
{
ProviderID: "openrouter", DisplayName: "OpenRouter", DeploymentID: "openrouter", SortOrder: 16, ChatPreference: 3,
RequiresKey: true, CredentialEnv: "OPENROUTER_API_KEY",
BaseURLEnv: []string{"OPENROUTER_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"OPENROUTER_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://openrouter.ai/api/v1",
LiveFetcherKey: "openrouter", LiveCatalogKey: "openrouter",
ProtocolID: "openai-chat-completions", AdapterID: "openrouter", RuntimeProfileKey: "openrouter",
Expand All @@ -169,23 +169,23 @@ func providerSpecs() []ProviderSpec {
{
ProviderID: "canopywave", DisplayName: "CanopyWave", DeploymentID: "canopywave", SortOrder: 17, ChatPreference: 10,
RequiresKey: true, CredentialEnv: "CANOPYWAVE_API_KEY",
BaseURLEnv: []string{"CANOPYWAVE_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"CANOPYWAVE_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://inference.canopywave.io/v1",
LiveFetcherKey: "canopywave", LiveCatalogKey: "canopywave",
ProtocolID: "openai-chat-completions", AdapterID: "canopywave", RuntimeProfileKey: "canopywave",
},
{
ProviderID: "poolside", DisplayName: "Poolside", DeploymentID: "poolside", SortOrder: 18, ChatPreference: 20,
RequiresKey: true, CredentialEnv: "POOLSIDE_API_KEY",
BaseURLEnv: []string{"POOLSIDE_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"POOLSIDE_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://inference.poolside.ai/v1",
LiveFetcherKey: "poolside", LiveCatalogKey: "poolside",
ProtocolID: "openai-chat-completions", AdapterID: "poolside", RuntimeProfileKey: "poolside",
},
{
ProviderID: "groq", DisplayName: "Groq", DeploymentID: "groq-direct", SortOrder: 19, ChatPreference: 21,
RequiresKey: true, CredentialEnv: "GROQ_API_KEY",
BaseURLEnv: []string{"GROQ_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"GROQ_BASE_URL"},
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.groq.com/openai/v1",
LiveFetcherKey: "groq", LiveCatalogKey: "groq",
ProtocolID: "openai-chat-completions", AdapterID: "groq", RuntimeProfileKey: "groq",
Expand All @@ -194,15 +194,15 @@ func providerSpecs() []ProviderSpec {
ProviderID: "clinepass", DisplayName: "ClinePass", DeploymentID: "clinepass", SortOrder: 20, ChatPreference: 22,
RuntimeBaseURL: "https://api.cline.bot/api/v1",
RequiresKey: true, CredentialEnv: "CLINE_API_KEY",
BaseURLEnv: []string{"CLINE_API_BASE", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"CLINE_API_BASE"},
ProbeKind: ProbeNone,
LiveFetcherKey: "clinepass", LiveCatalogKey: "clinepass",
ProtocolID: "openai-chat-completions", AdapterID: "clinepass", RuntimeProfileKey: "clinepass",
},
{
ProviderID: "opencodego", DisplayName: "OpenCode Go", DeploymentID: "opencodego", SortOrder: 21, ChatPreference: 13,
RequiresKey: true, CredentialEnv: "OPENCODEGO_API_KEY",
BaseURLEnv: []string{"OPENCODEGO_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
BaseURLEnv: []string{"OPENCODEGO_BASE_URL"},
ProbeKind: ProbeOpenAIModels,
ProbeBaseURL: opencodego.DefaultBaseURL,
LiveFetcherKey: "opencodego", LiveCatalogKey: "opencodego",
Expand Down
26 changes: 26 additions & 0 deletions catalog/registry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,32 @@ func TestProviderRegistry_DeploymentEnvFallbacks_BaseURL(t *testing.T) {
}
}

// TestProviderRegistry_AnthropicBaseURL_NoOpenAIFallback guards against a
// credential-hijack regression: a provider's base-URL must only ever come from
// its OWN env vars. A stray OPENAI_BASE_URL must not redirect anthropic's
// API key to an attacker-controlled host, so the anthropic provider's base_url
// fallback must not include OPENAI_BASE_URL / OPENAI_API_BASE.
func TestProviderRegistry_AnthropicBaseURL_NoOpenAIFallback(t *testing.T) {
t.Parallel()
var spec ProviderSpec
found := false
for _, s := range All() {
if s.ProviderID == "anthropic" {
spec = s
found = true
break
}
}
if !found {
t.Fatal("anthropic provider not registered")
}
for _, env := range spec.BaseURLEnv {
if env == "OPENAI_BASE_URL" || env == "OPENAI_API_BASE" {
t.Fatalf("anthropic BaseURLEnv must not fall back to %s (got %v)", env, spec.BaseURLEnv)
}
}
}

// --- CredentialPresent tests ---

func TestCredentialPresent(t *testing.T) {
Expand Down
Loading
Loading