first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
+296
View File
@@ -0,0 +1,296 @@
package provider
// Tests for provider resolution moved from cmd/pigo (US-004, #361): ResolveProvider
// maps a model id to the right gateway (preset catalog first, then prefix rules,
// then OpenRouter default), and ResolveBaseURL applies the base-url override
// precedence. Environment lookups are injected via os.Getenv here.
import (
"os"
"strings"
"testing"
)
// TestResolveProviderPresetCatalog verifies a preset id resolves to its declared
// provider (NVIDIA and Ollama presets do not fall through to OpenRouter).
func TestResolveProviderPresetCatalog(t *testing.T) {
cases := []struct {
model string
wantName string
}{
{"meta/llama-3.3-70b-instruct", "nvidia"}, // NVIDIA preset
{"ollama/llama3.3", "ollama"}, // Ollama preset
{"openai/gpt-4o", "openrouter"}, // OpenRouter preset
{"anthropic/claude-3.5-sonnet", "openrouter"}, // OpenRouter preset
}
for _, c := range cases {
_, name, err := ResolveProvider(c.model, "", "", "", os.Getenv)
if err != nil {
t.Errorf("ResolveProvider(%q) error: %v", c.model, err)
continue
}
if name != c.wantName {
t.Errorf("ResolveProvider(%q) = %q, want %q", c.model, name, c.wantName)
}
}
}
// TestResolveProviderPrefixAndDefault verifies the prefix rules and the
// OpenRouter default for ids not in the catalog.
func TestResolveProviderPrefixAndDefault(t *testing.T) {
cases := []struct {
model string
baseURL string
wantName string
}{
{"ollama/some-local-model", "", "ollama"}, // ollama/ prefix
{"nvidia/some-nim-model", "", "nvidia"}, // nvidia/ prefix
{"some-unknown-model", "", "openrouter"}, // default
{"m", "http://host:11434/v1", "ollama"}, // ollama port
}
for _, c := range cases {
_, name, err := ResolveProvider(c.model, c.baseURL, "", "", os.Getenv)
if err != nil {
t.Errorf("ResolveProvider(%q) error: %v", c.model, err)
continue
}
if name != c.wantName {
t.Errorf("ResolveProvider(%q, %q) = %q, want %q", c.model, c.baseURL, name, c.wantName)
}
}
}
// TestResolveProviderExplicitProtocol verifies an explicit --protocol wins over
// model-id heuristics: openai (with base-url) and anthropic select the matching
// wire driver, an empty base-url for openai errors, and an unknown protocol
// errors instead of silently falling back.
func TestResolveProviderExplicitProtocol(t *testing.T) {
// openai protocol → "openai" provider name, requires base-url.
if _, name, err := ResolveProvider("any-model", "https://example.com/v1", "openai", "", os.Getenv); err != nil || name != "openai" {
t.Errorf("protocol=openai = (%q, %v), want (openai, nil)", name, err)
}
if _, _, err := ResolveProvider("any-model", "", "openai", "", os.Getenv); err == nil {
t.Error("protocol=openai with no base-url should error")
}
// anthropic protocol → "anthropic" provider name, base-url optional (defaults).
if _, name, err := ResolveProvider("claude-x", "", "anthropic", "", os.Getenv); err != nil || name != "anthropic" {
t.Errorf("protocol=anthropic = (%q, %v), want (anthropic, nil)", name, err)
}
// Unknown protocol errors rather than falling back to a heuristic.
if _, _, err := ResolveProvider("any-model", "", "grpc", "", os.Getenv); err == nil {
t.Error("unknown protocol should error")
}
}
// TestResolveProviderResponsesProtocol verifies the openai/resp_api selector
// routes to the Responses driver (against an explicit base-url), that the
// "openai/chat" alias resolves identically to "openai", and that resp_api with
// no base-url errors like the plain openai path (mirroring the base-url
// requirement rather than defaulting to a public endpoint).
func TestResolveProviderResponsesProtocol(t *testing.T) {
// openai/resp_api → "openai" provider name, backed by the Responses driver.
p, name, err := ResolveProvider("any-model", "https://example.com/v1", "openai/resp_api", "", os.Getenv)
if err != nil || name != "openai" {
t.Fatalf("protocol=openai/resp_api = (%q, %v), want (openai, nil)", name, err)
}
if _, ok := p.(*responsesDriver); !ok {
t.Errorf("protocol=openai/resp_api built %T, want *responsesDriver", p)
}
// resp_api with no base-url errors, mirroring the openai requirement.
if _, _, err := ResolveProvider("any-model", "", "openai/resp_api", "", os.Getenv); err == nil {
t.Error("protocol=openai/resp_api with no base-url should error")
}
// "openai/chat" is an alias of "openai": same driver, same base-url rule.
p, name, err = ResolveProvider("any-model", "https://example.com/v1", "openai/chat", "", os.Getenv)
if err != nil || name != "openai" {
t.Fatalf("protocol=openai/chat = (%q, %v), want (openai, nil)", name, err)
}
if _, ok := p.(*responsesDriver); ok {
t.Error("protocol=openai/chat should build the Chat Completions driver, not *responsesDriver")
}
if _, _, err := ResolveProvider("any-model", "", "openai/chat", "", os.Getenv); err == nil {
t.Error("protocol=openai/chat with no base-url should error")
}
}
// TestResolveProviderExplicitProvider verifies that --provider selects a
// built-in provider from the registry: the returned provider-name is the spec
// name (so key resolution reads the right env var), an OpenAI-protocol provider
// (deepseek) and an Anthropic-protocol provider (minimax) both resolve, an
// incompatible --protocol is a conflict error naming both flags, and an unknown
// provider name errors while listing the available names.
func TestResolveProviderExplicitProvider(t *testing.T) {
// OpenAI-protocol provider: returns its own name for key lookup.
if _, name, err := ResolveProvider("deepseek-chat", "", "", "deepseek", os.Getenv); err != nil || name != "deepseek" {
t.Errorf("provider=deepseek = (%q, %v), want (deepseek, nil)", name, err)
}
// Anthropic-protocol provider.
if _, name, err := ResolveProvider("MiniMax-M2", "", "", "minimax", os.Getenv); err != nil || name != "minimax" {
t.Errorf("provider=minimax = (%q, %v), want (minimax, nil)", name, err)
}
// A matching --protocol is not a conflict (deepseek speaks openai/resp_api).
if _, name, err := ResolveProvider("deepseek-chat", "", "openai/resp_api", "deepseek", os.Getenv); err != nil || name != "deepseek" {
t.Errorf("provider=deepseek + protocol=openai/resp_api = (%q, %v), want (deepseek, nil)", name, err)
}
// --provider wins over model-id heuristics: an ollama/-prefixed id still
// resolves to the named provider, not local Ollama.
if _, name, err := ResolveProvider("ollama/x", "", "", "deepseek", os.Getenv); err != nil || name != "deepseek" {
t.Errorf("provider=deepseek with ollama/ model = (%q, %v), want (deepseek, nil)", name, err)
}
// --base-url overrides the spec default without changing the provider name.
if _, name, err := ResolveProvider("deepseek-chat", "https://proxy.local/v1", "", "deepseek", os.Getenv); err != nil || name != "deepseek" {
t.Errorf("provider=deepseek + base-url = (%q, %v), want (deepseek, nil)", name, err)
}
// Conflict: minimax speaks anthropic; forcing --protocol openai errors and
// names both flags.
_, _, err := ResolveProvider("MiniMax-M2", "", "openai", "minimax", os.Getenv)
if err == nil {
t.Fatal("provider=minimax + protocol=openai should conflict")
}
if !strings.Contains(err.Error(), "--provider") || !strings.Contains(err.Error(), "--protocol") {
t.Errorf("conflict error should name both flags, got: %v", err)
}
// Unknown provider errors and lists available names.
_, _, err = ResolveProvider("m", "", "", "no-such-provider", os.Getenv)
if err == nil {
t.Fatal("unknown provider should error")
}
if !strings.Contains(err.Error(), "deepseek") {
t.Errorf("unknown-provider error should list available names, got: %v", err)
}
// An invalid --protocol paired with a named provider surfaces the clear
// "unknown --protocol" error (listing the accepted set) rather than a
// misleading conflict message.
_, _, err = ResolveProvider("deepseek-chat", "", "openai_api", "deepseek", os.Getenv)
if err == nil {
t.Fatal("provider=deepseek + protocol=openai_api should error")
}
if !strings.Contains(err.Error(), "unknown --protocol") {
t.Errorf("invalid --protocol should surface the unknown-protocol error, got: %v", err)
}
}
// TestResolveProviderCNPresets verifies the Chinese-cloud preset ids route to
// their own provider (not the OpenRouter default) via the LookupPreset branch.
func TestResolveProviderCNPresets(t *testing.T) {
cases := []struct {
model string
wantName string
}{
{"ernie-4.5-turbo-32k", "qianfan"},
{"doubao-seed-1-6", "volcengine"},
{"qwen-max", "dashscope"},
{"hunyuan-turbos-latest", "hunyuan"},
}
for _, c := range cases {
_, name, err := ResolveProvider(c.model, "", "", "", os.Getenv)
if err != nil {
t.Errorf("ResolveProvider(%q) error: %v", c.model, err)
continue
}
if name != c.wantName {
t.Errorf("ResolveProvider(%q) = %q, want %q", c.model, name, c.wantName)
}
}
}
// TestResolveProviderCNExplicit verifies --provider selects the CN providers
// directly and that --base-url overrides without changing the provider name.
func TestResolveProviderCNExplicit(t *testing.T) {
for _, name := range []string{"qianfan", "volcengine", "dashscope", "hunyuan"} {
if _, got, err := ResolveProvider("some-model", "", "", name, os.Getenv); err != nil || got != name {
t.Errorf("provider=%s = (%q, %v), want (%s, nil)", name, got, err, name)
}
if _, got, err := ResolveProvider("some-model", "https://proxy.local/v1", "", name, os.Getenv); err != nil || got != name {
t.Errorf("provider=%s + base-url = (%q, %v), want (%s, nil)", name, got, err, name)
}
}
}
// TestResolveProviderModelNameInference verifies model-name inference (Issue
// #235): with only --model given, a bare model name whose prefix identifies a
// single provider resolves to that provider — NOT the OpenRouter default.
func TestResolveProviderModelNameInference(t *testing.T) {
cases := []struct {
model string
wantName string
}{
{"claude-opus-4-8", "anthropic"},
{"deepseek-chat", "deepseek"},
{"gpt-4.1", "openai"},
{"gemini-3-pro", "google"},
{"grok-5", "xai"},
}
for _, c := range cases {
if _, name, err := ResolveProvider(c.model, "", "", "", os.Getenv); err != nil || name != c.wantName {
t.Errorf("ResolveProvider(%q) = (%q, %v), want (%q, nil)", c.model, name, err, c.wantName)
}
}
}
// TestResolveProviderInferencePrecedence verifies that model-name inference does
// not override explicit flags and does not fire when a --base-url is given, and
// that unknown/ambiguous names still fall back to OpenRouter.
func TestResolveProviderInferencePrecedence(t *testing.T) {
// Explicit --provider wins over an inferable model name.
if _, name, err := ResolveProvider("claude-opus-4-8", "", "", "deepseek", os.Getenv); err != nil || name != "deepseek" {
t.Errorf("provider=deepseek overrides inference = (%q, %v), want (deepseek, nil)", name, err)
}
// Explicit --protocol wins over an inferable model name.
if _, name, err := ResolveProvider("claude-opus-4-8", "https://example.com/v1", "openai", "", os.Getenv); err != nil || name != "openai" {
t.Errorf("protocol=openai overrides inference = (%q, %v), want (openai, nil)", name, err)
}
// A --base-url signals a custom endpoint: inference is skipped, default applies.
if _, name, err := ResolveProvider("claude-opus-4-8", "https://gw.local/v1", "", "", os.Getenv); err != nil || name != "openrouter" {
t.Errorf("inference skipped with base-url = (%q, %v), want (openrouter, nil)", name, err)
}
// Ambiguous/unknown names still default to OpenRouter.
for _, m := range []string{"llama-3.3-70b", "totally-unknown-model"} {
if _, name, err := ResolveProvider(m, "", "", "", os.Getenv); err != nil || name != "openrouter" {
t.Errorf("ResolveProvider(%q) = (%q, %v), want (openrouter, nil)", m, name, err)
}
}
}
// TestResolveBaseURLPrecedence exercises all four precedence levels for a
// hyphenated provider (zai-coding-cn → ZAI_CODING_CN_BASE_URL).
func TestResolveBaseURLPrecedence(t *testing.T) {
spec, ok := LookupProviderSpec("zai-coding-cn")
if !ok {
t.Fatal("expected zai-coding-cn in registry")
}
if got := ResolveBaseURL(spec, "", os.Getenv); got != spec.DefaultBaseURL {
t.Errorf("default: got %q, want %q", got, spec.DefaultBaseURL)
}
t.Setenv("ZAI_CODING_CN_BASE_URL", "https://generic.example/v4")
if got := ResolveBaseURL(spec, "", os.Getenv); got != "https://generic.example/v4" {
t.Errorf("generic env: got %q, want %q", got, "https://generic.example/v4")
}
if got := ResolveBaseURL(spec, "https://flag.example/v4", os.Getenv); got != "https://flag.example/v4" {
t.Errorf("flag over generic: got %q, want %q", got, "https://flag.example/v4")
}
}
// TestResolveBaseURLProviderSpecificEnv covers a provider that declares a
// provider-specific base-url env var (azure), asserting it sits between the flag
// and the generic convention in precedence.
func TestResolveBaseURLProviderSpecificEnv(t *testing.T) {
spec, ok := LookupProviderSpec("azure-openai-responses")
if !ok {
t.Fatal("expected azure-openai-responses in registry")
}
if len(spec.BaseURLEnvVars) == 0 {
t.Fatal("expected azure-openai-responses to declare BaseURLEnvVars")
}
t.Setenv("AZURE_OPENAI_BASE_URL", "https://specific.example")
if got := ResolveBaseURL(spec, "", os.Getenv); got != "https://specific.example" {
t.Errorf("provider-specific env: got %q, want %q", got, "https://specific.example")
}
t.Setenv("AZURE_OPENAI_RESPONSES_BASE_URL", "https://generic.example")
if got := ResolveBaseURL(spec, "", os.Getenv); got != "https://specific.example" {
t.Errorf("provider-specific beats generic: got %q, want %q", got, "https://specific.example")
}
if got := ResolveBaseURL(spec, "https://flag.example", os.Getenv); got != "https://flag.example" {
t.Errorf("flag beats provider-specific: got %q, want %q", got, "https://flag.example")
}
}