diff --git a/README.md b/README.md index e1da9e5..7b49b02 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/catalog/registry/providers.go b/catalog/registry/providers.go index a010229..d9f88f2 100644 --- a/catalog/registry/providers.go +++ b/catalog/registry/providers.go @@ -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", @@ -42,7 +42,7 @@ 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", @@ -50,7 +50,7 @@ func providerSpecs() []ProviderSpec { { 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", @@ -58,7 +58,7 @@ func providerSpecs() []ProviderSpec { { 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", @@ -66,7 +66,7 @@ func providerSpecs() []ProviderSpec { { 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", @@ -74,7 +74,7 @@ func providerSpecs() []ProviderSpec { { 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", @@ -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", @@ -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", @@ -102,7 +102,7 @@ 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", @@ -110,7 +110,7 @@ func providerSpecs() []ProviderSpec { { 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", @@ -118,7 +118,7 @@ func providerSpecs() []ProviderSpec { { 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", @@ -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", @@ -169,7 +169,7 @@ 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", @@ -177,7 +177,7 @@ func providerSpecs() []ProviderSpec { { 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", @@ -185,7 +185,7 @@ func providerSpecs() []ProviderSpec { { 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", @@ -194,7 +194,7 @@ 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", @@ -202,7 +202,7 @@ func providerSpecs() []ProviderSpec { { 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", diff --git a/catalog/registry/registry_test.go b/catalog/registry/registry_test.go index e81f523..6667e75 100644 --- a/catalog/registry/registry_test.go +++ b/catalog/registry/registry_test.go @@ -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) { diff --git a/config/profiles.go b/config/profiles.go index b79cbcd..bb991bb 100644 --- a/config/profiles.go +++ b/config/profiles.go @@ -53,7 +53,7 @@ var ( Mode: "anthropic", DefaultBaseURL: DefaultAnthropicOpenAIBaseURL, DefaultModel: "claude-3-5-sonnet-latest", DetectionEnv: []string{"ANTHROPIC_API_KEY"}, ModelEnv: []string{"ANTHROPIC_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"ANTHROPIC_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"ANTHROPIC_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "ANTHROPIC_API_KEY", Source: "anthropic"}, {Env: "OPENAI_API_KEY", Source: "openai"}}, } OpenAIRuntimeProfile = RuntimeProviderProfile{ @@ -67,14 +67,14 @@ var ( Mode: "grok", DefaultBaseURL: DefaultGrokOpenAIBaseURL, DefaultModel: "grok-2", DetectionEnv: []string{"XAI_API_KEY"}, ModelEnv: []string{"XAI_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"XAI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"XAI_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "XAI_API_KEY", Source: "grok"}}, } GeminiRuntimeProfile = RuntimeProviderProfile{ Mode: "gemini", DefaultBaseURL: DefaultGeminiOpenAIBaseURL, DefaultModel: "gemini-2.0-flash", DetectionEnv: []string{"GEMINI_API_KEY"}, ModelEnv: []string{"GEMINI_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"GEMINI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"GEMINI_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "GEMINI_API_KEY", Source: "gemini"}, {Env: "OPENAI_API_KEY", Source: "openai"}}, } VertexRuntimeProfile = RuntimeProviderProfile{ @@ -102,35 +102,35 @@ var ( Mode: "openrouter", DefaultBaseURL: DefaultOpenRouterOpenAIBaseURL, DetectionEnv: []string{"OPENROUTER_API_KEY"}, ModelEnv: []string{"OPENROUTER_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"OPENROUTER_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"OPENROUTER_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "OPENROUTER_API_KEY", Source: "openrouter"}, {Env: "OPENAI_API_KEY", Source: "openai"}}, } ZAIPaygRuntimeProfile = RuntimeProviderProfile{ Mode: "openai", DefaultBaseURL: DefaultZAIOpenAIBaseURL, DetectionEnv: []string{"ZAI_API_KEY"}, ModelEnv: []string{"ZAI_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"ZAI_BASE_URL", "ZAI_API_BASE", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"ZAI_BASE_URL", "ZAI_API_BASE"}, APIKeys: []APIKeyDef{{Env: "ZAI_API_KEY", Source: "zai_payg"}}, } ZAICodingRuntimeProfile = RuntimeProviderProfile{ Mode: "openai", DefaultBaseURL: DefaultZAICodingOpenAIBaseURL, DetectionEnv: []string{"ZAI_CODING_API_KEY"}, ModelEnv: []string{"ZAI_CODING_MODEL", "ZAI_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"ZAI_CODING_BASE_URL", "ZAI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"ZAI_CODING_BASE_URL", "ZAI_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "ZAI_CODING_API_KEY", Source: "zai_coding"}}, } CanopyWaveRuntimeProfile = RuntimeProviderProfile{ Mode: "openai", DefaultBaseURL: DefaultCanopyWaveOpenAIBaseURL, DetectionEnv: []string{"CANOPYWAVE_API_KEY"}, ModelEnv: []string{"CANOPYWAVE_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"CANOPYWAVE_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"CANOPYWAVE_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "CANOPYWAVE_API_KEY", Source: "canopywave"}, {Env: "OPENAI_API_KEY", Source: "openai"}}, } DeepSeekRuntimeProfile = RuntimeProviderProfile{ Mode: "openai", DefaultBaseURL: "https://api.deepseek.com/v1", DetectionEnv: []string{"DEEPSEEK_API_KEY"}, ModelEnv: []string{"DEEPSEEK_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"DEEPSEEK_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"DEEPSEEK_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "DEEPSEEK_API_KEY", Source: "deepseek"}}, } OpenCodeGoRuntimeProfile = RuntimeProviderProfile{ @@ -144,35 +144,35 @@ var ( Mode: "openai", DefaultBaseURL: DefaultKimiOpenAIBaseURL, DetectionEnv: []string{"MOONSHOT_API_KEY"}, ModelEnv: []string{"MOONSHOT_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"MOONSHOT_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"MOONSHOT_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "MOONSHOT_API_KEY", Source: "kimi"}}, } XiaomiPaygRuntimeProfile = RuntimeProviderProfile{ Mode: "openai", DefaultBaseURL: DefaultXiaomiOpenAIBaseURL, DetectionEnv: []string{EnvXiaomiPaygAPIKey}, ModelEnv: []string{"XIAOMI_MIMO_PAYG_MODEL", "XIAOMI_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{EnvXiaomiPaygBaseURL, "XIAOMI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{EnvXiaomiPaygBaseURL, "XIAOMI_BASE_URL"}, APIKeys: []APIKeyDef{{Env: EnvXiaomiPaygAPIKey, Source: "xiaomi_mimo_payg"}}, } XiaomiTokenPlanRuntimeProfile = RuntimeProviderProfile{ Mode: "openai", DefaultBaseURL: "", DetectionEnv: []string{EnvXiaomiTokenPlanAPIKey}, ModelEnv: []string{"XIAOMI_MIMO_TOKEN_PLAN_MODEL", "XIAOMI_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{EnvXiaomiTokenPlanBaseURL, "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{EnvXiaomiTokenPlanBaseURL}, APIKeys: []APIKeyDef{{Env: EnvXiaomiTokenPlanAPIKey, Source: "xiaomi_mimo_token_plan"}}, } MiniMaxTokenPlanRuntimeProfile = RuntimeProviderProfile{ Mode: "openai", DefaultBaseURL: DefaultMiniMaxOpenAIBaseURL, DetectionEnv: []string{"MINIMAX_TOKEN_PLAN_API_KEY"}, ModelEnv: []string{"MINIMAX_TOKEN_PLAN_MODEL", "MINIMAX_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"MINIMAX_TOKEN_PLAN_BASE_URL", "MINIMAX_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"MINIMAX_TOKEN_PLAN_BASE_URL", "MINIMAX_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "MINIMAX_TOKEN_PLAN_API_KEY", Source: "minimax_token_plan"}}, } MiniMaxPaygRuntimeProfile = RuntimeProviderProfile{ Mode: "openai", DefaultBaseURL: DefaultMiniMaxOpenAIBaseURL, DetectionEnv: []string{"MINIMAX_PAYG_API_KEY"}, ModelEnv: []string{"MINIMAX_PAYG_MODEL", "MINIMAX_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"MINIMAX_PAYG_BASE_URL", "MINIMAX_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"MINIMAX_PAYG_BASE_URL", "MINIMAX_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "MINIMAX_PAYG_API_KEY", Source: "minimax_payg"}}, } OllamaRuntimeProfile = RuntimeProviderProfile{ @@ -186,21 +186,21 @@ var ( Mode: "openai", DefaultBaseURL: DefaultPoolsideOpenAIBaseURL, DetectionEnv: []string{"POOLSIDE_API_KEY"}, ModelEnv: []string{"POOLSIDE_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"POOLSIDE_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"POOLSIDE_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "POOLSIDE_API_KEY", Source: "poolside"}}, } GroqRuntimeProfile = RuntimeProviderProfile{ Mode: "openai", DefaultBaseURL: "https://api.groq.com/openai/v1", DetectionEnv: []string{"GROQ_API_KEY"}, ModelEnv: []string{"GROQ_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"GROQ_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"GROQ_BASE_URL"}, APIKeys: []APIKeyDef{{Env: "GROQ_API_KEY", Source: "groq"}}, } ClinePassRuntimeProfile = RuntimeProviderProfile{ Mode: "openai", DefaultBaseURL: DefaultClinePassOpenAIBaseURL, DetectionEnv: []string{"CLINE_API_KEY"}, ModelEnv: []string{"CLINE_MODEL", "OPENAI_MODEL"}, - BaseURLEnv: []string{"CLINE_API_BASE", "OPENAI_BASE_URL", "OPENAI_API_BASE"}, + BaseURLEnv: []string{"CLINE_API_BASE"}, APIKeys: []APIKeyDef{{Env: "CLINE_API_KEY", Source: "clinepass"}}, } ) diff --git a/errors/errors.go b/errors/errors.go deleted file mode 100644 index 4577901..0000000 --- a/errors/errors.go +++ /dev/null @@ -1,79 +0,0 @@ -package errors - -import ( - "regexp" - "strconv" - "strings" -) - -const ( - APIErrorMessagePrefix = "API Error" - PromptTooLongErrorMessage = "Prompt is too long" - CreditBalanceTooLowErrorMessage = "Credit balance is too low" - InvalidAPIKeyErrorMessage = "Not logged in · Please run /login" // #nosec G101 -- static error message text, not a secret value - InvalidAPIKeyErrorMessageExternal = "Invalid API key · Please check your credentials" // #nosec G101 -- static error message text, not a secret value - OrgDisabledErrorMessageEnvKeyWithOAuth = "Organization disabled" - OrgDisabledErrorMessageEnvKey = "Organization disabled" - TokenRevokedErrorMessage = "Token has been revoked" // #nosec G101 -- static error message text, not a secret value - CCRAuthErrorMessage = "Authentication error" - Repeated529ErrorMessage = "Repeated 529 Overloaded errors" - CustomOffSwitchMessage = "Service temporarily disabled" - APITimeoutErrorMessage = "Request timed out" - OAuthOrgNotAllowedErrorMessage = "Organization not allowed for OAuth" -) - -var promptTokensRe = regexp.MustCompile(`(?i)prompt is too long[^0-9]*(\d+)\s*tokens?\s*>\s*(\d+)`) - -func StartsWithApiErrorPrefix(text string) bool { - return strings.HasPrefix(text, APIErrorMessagePrefix) || - strings.HasPrefix(text, "Please run /login · "+APIErrorMessagePrefix) -} - -func ParsePromptTooLongTokenCounts(rawMessage string) (actualTokens *int, limitTokens *int) { - m := promptTokensRe.FindStringSubmatch(rawMessage) - if m == nil { - return nil, nil - } - a, err1 := strconv.Atoi(m[1]) - l, err2 := strconv.Atoi(m[2]) - if err1 != nil || err2 != nil { - return nil, nil - } - return &a, &l -} - -func IsMediaSizeError(raw string) bool { - lower := strings.ToLower(raw) - return strings.Contains(lower, "image is too large") || - strings.Contains(lower, "pdf is too large") || - strings.Contains(lower, "file is too large") || - strings.Contains(lower, "request is too large") -} - -func GetPdfTooLargeErrorMessage() string { - return "PDF is too large · Try splitting into smaller files or reducing quality" -} - -func GetPdfPasswordProtectedErrorMessage() string { - return "PDF is password protected · Please remove the password and try again" -} - -func GetPdfInvalidErrorMessage() string { - return "PDF is invalid · Please check the file and try again" -} - -func GetImageTooLargeErrorMessage() string { - return "Image is too large · Please reduce the file size and try again" -} - -func GetRequestTooLargeErrorMessage() string { - return "Request is too large · Please reduce the size and try again" -} - -func GetTokenRevokedErrorMessage() string { - return "Token has been revoked · Please run /login to reconnect" -} - -func GetOauthOrgNotAllowedErrorMessage() string { - return "Organization not allowed · Please ask IT to allowlist" -} diff --git a/errors/errors_test.go b/errors/errors_test.go deleted file mode 100644 index 0ca20ce..0000000 --- a/errors/errors_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package errors - -import "testing" - -func TestStartsWithApiErrorPrefix(t *testing.T) { - t.Parallel() - tests := []struct { - input string - want bool - }{ - {"API Error: something", true}, - {"Please run /login · API Error: auth", true}, - {"some other error", false}, - {"", false}, - } - for _, tt := range tests { - if got := StartsWithApiErrorPrefix(tt.input); got != tt.want { - t.Errorf("StartsWithApiErrorPrefix(%q) = %v, want %v", tt.input, got, tt.want) - } - } -} - -func TestParsePromptTooLongTokenCounts(t *testing.T) { - t.Parallel() - actual, limit := ParsePromptTooLongTokenCounts("prompt is too long: 137500 tokens > 135000 maximum") - if actual == nil || *actual != 137500 { - t.Errorf("expected actual=137500, got %v", actual) - } - if limit == nil || *limit != 135000 { - t.Errorf("expected limit=135000, got %v", limit) - } - - actual, limit = ParsePromptTooLongTokenCounts("no match here") - if actual != nil || limit != nil { - t.Error("expected nil for non-matching input") - } -} - -func TestIsMediaSizeError(t *testing.T) { - t.Parallel() - tests := []struct { - input string - want bool - }{ - {"image is too large", true}, - {"pdf is too large", true}, - {"file is too large", true}, - {"request is too large", true}, - {"something else", false}, - } - for _, tt := range tests { - if got := IsMediaSizeError(tt.input); got != tt.want { - t.Errorf("IsMediaSizeError(%q) = %v, want %v", tt.input, got, tt.want) - } - } -} - -func TestErrorMessageGetters(t *testing.T) { - t.Parallel() - if msg := GetPdfTooLargeErrorMessage(); msg == "" { - t.Error("expected non-empty message") - } - if msg := GetImageTooLargeErrorMessage(); msg == "" { - t.Error("expected non-empty message") - } - if msg := GetTokenRevokedErrorMessage(); msg == "" { - t.Error("expected non-empty message") - } -}