152 lines
6.4 KiB
Go
152 lines
6.4 KiB
Go
// Node #186: end-to-end wiring test for every OpenAI-protocol built-in provider.
|
|
//
|
|
// For each provider reachable via --provider (US-005) this asserts three things:
|
|
// (a) the registry spec reports Protocol "openai" and the PRD-mandated default
|
|
// base URL,
|
|
// (b) its primary API-key env var resolves through envAPIKey (the same path
|
|
// auth.go uses at request time), and
|
|
// (c) the generic OpenAI-compatible construction path used by main.go's
|
|
// resolveNamedProvider builds a non-nil driver bound to the spec's model.
|
|
//
|
|
// It also pins the restored legacy key aliases (CLAUDE_API_KEY, GOOGLE_API_KEY,
|
|
// NVIDIA_NIM_API_KEY) and the non-standard HF_TOKEN key name.
|
|
//
|
|
// This file is intentionally separate from providers_test.go: a sibling node
|
|
// edits that file concurrently.
|
|
package provider
|
|
|
|
import "testing"
|
|
|
|
// openAIWiringCase describes one OpenAI-protocol provider's expected registry
|
|
// metadata and primary key env var.
|
|
type openAIWiringCase struct {
|
|
name string
|
|
baseURL string
|
|
primaryEnv string
|
|
}
|
|
|
|
// openAIProviderCases lists every OpenAI-protocol provider that must work
|
|
// end-to-end via --provider (US-005). Base URLs mirror the PRD's Technical
|
|
// Considerations table.
|
|
var openAIProviderCases = []openAIWiringCase{
|
|
{"groq", "https://api.groq.com/openai/v1", "GROQ_API_KEY"},
|
|
{"xai", "https://api.x.ai/v1", "XAI_API_KEY"},
|
|
{"cerebras", "https://api.cerebras.ai/v1", "CEREBRAS_API_KEY"},
|
|
{"mistral", "https://api.mistral.ai", "MISTRAL_API_KEY"},
|
|
{"moonshotai", "https://api.moonshot.ai/v1", "MOONSHOT_API_KEY"},
|
|
{"moonshotai-cn", "https://api.moonshot.cn/v1", "MOONSHOT_API_KEY"},
|
|
{"fireworks", "https://api.fireworks.ai/inference", "FIREWORKS_API_KEY"},
|
|
{"together", "https://api.together.ai/v1", "TOGETHER_API_KEY"},
|
|
{"openrouter", "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"},
|
|
{"nvidia", "https://integrate.api.nvidia.com/v1", "NVIDIA_API_KEY"},
|
|
{"zai", "https://api.z.ai/api/coding/paas/v4", "ZAI_API_KEY"},
|
|
{"zai-coding-cn", "https://open.bigmodel.cn/api/coding/paas/v4", "ZAI_CODING_CN_API_KEY"},
|
|
{"kimi-coding", "https://api.kimi.com/coding", "KIMI_API_KEY"},
|
|
{"opencode", "https://opencode.ai/zen", "OPENCODE_API_KEY"},
|
|
{"opencode-go", "https://opencode.ai/zen/go", "OPENCODE_API_KEY"},
|
|
{"huggingface", "https://router.huggingface.co/v1", "HF_TOKEN"},
|
|
{"ant-ling", "https://api.ant-ling.com/v1", "ANT_LING_API_KEY"},
|
|
{"vercel-ai-gateway", "https://ai-gateway.vercel.sh", "AI_GATEWAY_API_KEY"},
|
|
{"xiaomi", "https://api.xiaomimimo.com/v1", "XIAOMI_API_KEY"},
|
|
{"xiaomi-token-plan-cn", "https://token-plan-cn.xiaomimimo.com/v1", "XIAOMI_TOKEN_PLAN_CN_API_KEY"},
|
|
{"xiaomi-token-plan-ams", "https://token-plan-ams.xiaomimimo.com/v1", "XIAOMI_TOKEN_PLAN_AMS_API_KEY"},
|
|
{"xiaomi-token-plan-sgp", "https://token-plan-sgp.xiaomimimo.com/v1", "XIAOMI_TOKEN_PLAN_SGP_API_KEY"},
|
|
}
|
|
|
|
func TestOpenAIProviderWiring(t *testing.T) {
|
|
for _, tc := range openAIProviderCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// (a) registry spec: protocol + default base URL + primary env var.
|
|
spec, ok := LookupProviderSpec(tc.name)
|
|
if !ok {
|
|
t.Fatalf("LookupProviderSpec(%q): not found in registry", tc.name)
|
|
}
|
|
if spec.Protocol != ProtocolOpenAI {
|
|
t.Errorf("Protocol = %q, want %q", spec.Protocol, ProtocolOpenAI)
|
|
}
|
|
if spec.DefaultBaseURL != tc.baseURL {
|
|
t.Errorf("DefaultBaseURL = %q, want %q", spec.DefaultBaseURL, tc.baseURL)
|
|
}
|
|
if len(spec.EnvVars) == 0 || spec.EnvVars[0] != tc.primaryEnv {
|
|
t.Errorf("primary EnvVar = %v, want first = %q", spec.EnvVars, tc.primaryEnv)
|
|
}
|
|
|
|
// (b) key resolution via the primary env var, using the same
|
|
// envAPIKey path auth.go relies on at request time.
|
|
t.Setenv(tc.primaryEnv, "sk-"+tc.name)
|
|
if got := envAPIKey(tc.name); got != "sk-"+tc.name {
|
|
t.Errorf("envAPIKey(%q) = %q, want %q", tc.name, got, "sk-"+tc.name)
|
|
}
|
|
|
|
// (c) construction path equivalent to main.go's resolveNamedProvider
|
|
// for an openai-protocol spec: build a generic OpenAI-compatible
|
|
// driver against the spec's base URL, bound to the spec's model.
|
|
models := []Model{{Provider: spec.Name, ID: "test-model", SupportsImages: true}}
|
|
drv := NewOpenAICompatibleProvider(spec.DefaultBaseURL, models)
|
|
if drv == nil {
|
|
t.Fatalf("NewOpenAICompatibleProvider(%q) returned nil", spec.DefaultBaseURL)
|
|
}
|
|
got := drv.Models()
|
|
if len(got) != 1 || got[0].Provider != spec.Name || got[0].ID != "test-model" {
|
|
t.Errorf("driver Models() = %+v, want one model bound to provider %q", got, spec.Name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLegacyKeyAliases pins the secondary env vars restored to the registry so
|
|
// credentials set under older names still resolve.
|
|
func TestLegacyKeyAliases(t *testing.T) {
|
|
aliases := []struct {
|
|
provider string
|
|
primary string // primary env var (must NOT be set for the alias to be exercised)
|
|
alias string // legacy alias env var under test
|
|
}{
|
|
{"anthropic", "ANTHROPIC_API_KEY", "CLAUDE_API_KEY"},
|
|
{"google", "GEMINI_API_KEY", "GOOGLE_API_KEY"},
|
|
{"nvidia", "NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY"},
|
|
}
|
|
for _, a := range aliases {
|
|
t.Run(a.provider, func(t *testing.T) {
|
|
// Ensure the alias is listed in the registry's EnvVars.
|
|
spec, ok := LookupProviderSpec(a.provider)
|
|
if !ok {
|
|
t.Fatalf("LookupProviderSpec(%q): not found", a.provider)
|
|
}
|
|
found := false
|
|
for _, e := range spec.EnvVars {
|
|
if e == a.alias {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("EnvVars %v missing legacy alias %q", spec.EnvVars, a.alias)
|
|
}
|
|
// Clear the primary so only the alias can satisfy resolution, then
|
|
// assert the alias resolves.
|
|
t.Setenv(a.primary, "")
|
|
t.Setenv(a.alias, "legacy-"+a.provider)
|
|
if got := envAPIKey(a.provider); got != "legacy-"+a.provider {
|
|
t.Errorf("envAPIKey(%q) via %s = %q, want %q", a.provider, a.alias, got, "legacy-"+a.provider)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHuggingFaceTokenEnv asserts the non-standard HF_TOKEN key name resolves
|
|
// for huggingface (it does not follow the <PROVIDER>_API_KEY convention).
|
|
func TestHuggingFaceTokenEnv(t *testing.T) {
|
|
spec, ok := LookupProviderSpec("huggingface")
|
|
if !ok {
|
|
t.Fatal("LookupProviderSpec(\"huggingface\"): not found")
|
|
}
|
|
if len(spec.EnvVars) == 0 || spec.EnvVars[0] != "HF_TOKEN" {
|
|
t.Fatalf("huggingface EnvVars = %v, want first = HF_TOKEN", spec.EnvVars)
|
|
}
|
|
t.Setenv("HF_TOKEN", "hf-secret")
|
|
if got := envAPIKey("huggingface"); got != "hf-secret" {
|
|
t.Errorf("envAPIKey(\"huggingface\") = %q, want %q", got, "hf-secret")
|
|
}
|
|
}
|