majordomo now ships qwen and kimi as built-ins that ARE the openai client at their own base URL, so "qwen/qwen3.8-max" works as a GADFLY_MODELS entry once the key reaches the container. This wires up the parts that key has to pass through. Two provider switches had to learn the names, not one. resolveModel's GADFLY_BASE_URL override was the obvious one; endpointProvider's GADFLY_ENDPOINT_* parser is its sibling, and I fixed the first and missed the second on the first pass — a config that resolves one way and errors the other for no reason a user could guess. TestOpenAICompatProvidersResolveOnBothPaths now asserts both from one table so the pair fails together; break-checked in both directions. QWEN_API_KEY (and KIMI_API_KEY) are declared as workflow_call secrets and forwarded to the container, with gadfly's own stub forwarding QWEN_API_KEY so a qwen entry can join the default swarm by editing GADFLY_DEFAULT_MODELS alone — no workflow edit, no re-release. The run.sh credential pre-flight is now a provider→variable table instead of an ollama-cloud special case. Without it a forgotten key surfaces as five identical per-lens agent failures naming no variable, and the operator reads a stack trace to find out which secret they missed. Google stays out of the table on purpose: it accepts either GOOGLE_API_KEY or GEMINI_API_KEY, and a one-var entry would wrongly skip a correctly-configured run. Verified across 17 provider x key-state combinations, including that a wrong-provider key never satisfies qwen (majordomo refuses cross-provider fallback) and that unkeyed providers are never blocked. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
123 lines
4.6 KiB
Go
123 lines
4.6 KiB
Go
package main
|
|
|
|
import "testing"
|
|
|
|
func TestEndpointProvider(t *testing.T) {
|
|
t.Run("ollama http endpoint registers under its name", func(t *testing.T) {
|
|
p, err := endpointProvider("bigbox", "ollama|http://192.168.1.50:11434")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if p.Name() != "bigbox" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "bigbox")
|
|
}
|
|
})
|
|
t.Run("openai compatible with key", func(t *testing.T) {
|
|
if _, err := endpointProvider("gpu", "openai|http://gpu.lan:8000/v1|sk-x"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
t.Run("foreman queue registers under its name", func(t *testing.T) {
|
|
p, err := endpointProvider("m1", "foreman|http://foreman-m1:8080|tok")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
// WithName(name) must win over the Foreman preset's default "foreman".
|
|
if p.Name() != "m1" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "m1")
|
|
}
|
|
})
|
|
t.Run("foreman without token", func(t *testing.T) {
|
|
if _, err := endpointProvider("m5", "foreman|http://foreman-m5:8080"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
t.Run("llamaswap registers under its name", func(t *testing.T) {
|
|
p, err := endpointProvider("ls", "llamaswap|http://swap.lan:8080|tok")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if p.Name() != "ls" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "ls")
|
|
}
|
|
})
|
|
t.Run("llamaswap without token", func(t *testing.T) {
|
|
if _, err := endpointProvider("ls2", "llamaswap|http://swap.lan:8080"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
// All llama-swap spellings (hyphenated/TLS variants mirror majordomo's DSN
|
|
// schemes) must resolve to the llamaswap provider.
|
|
for _, name := range []string{"llama-swap", "llama-swaps", "llamaswaps"} {
|
|
t.Run(name+" alias", func(t *testing.T) {
|
|
p, err := endpointProvider("ls", name+"|https://swap.lan:8080|tok")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if p.Name() != "ls" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "ls")
|
|
}
|
|
})
|
|
}
|
|
for _, bad := range []string{"", "ollama", "noprovider-no-pipe", "mystery|http://x"} {
|
|
t.Run("rejects "+bad, func(t *testing.T) {
|
|
if _, err := endpointProvider("n", bad); err == nil {
|
|
t.Errorf("endpointProvider(%q) = nil error, want error", bad)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOpenAICompatProvidersResolveOnBothPaths pins the two provider switches
|
|
// together. kimi and qwen are majordomo built-ins that ARE the openai client at
|
|
// a different base URL, and two independent places have to know it:
|
|
// resolveModel's GADFLY_BASE_URL override, and endpointProvider's
|
|
// GADFLY_ENDPOINT_* parser. Adding a name to one and not the other yields a
|
|
// provider that works when configured one way and errors the other, for no
|
|
// reason a user could guess — which is exactly what happened here on the first
|
|
// pass. Asserting both in one table is what makes the pair fail together.
|
|
func TestOpenAICompatProvidersResolveOnBothPaths(t *testing.T) {
|
|
for _, provider := range []string{"openai", "openai-compatible", "kimi", "qwen"} {
|
|
t.Run(provider+" via GADFLY_ENDPOINT_*", func(t *testing.T) {
|
|
p, err := endpointProvider("ep", provider+"|https://host.example/v1|sk-x")
|
|
if err != nil {
|
|
t.Fatalf("endpointProvider(%q): %v", provider, err)
|
|
}
|
|
if p.Name() != "ep" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "ep")
|
|
}
|
|
})
|
|
t.Run(provider+" via GADFLY_BASE_URL", func(t *testing.T) {
|
|
t.Setenv("GADFLY_PROVIDER", provider)
|
|
t.Setenv("GADFLY_BASE_URL", "https://host.example/v1")
|
|
t.Setenv("GADFLY_API_KEY", "sk-x")
|
|
t.Setenv("GADFLY_MODEL", "some-model")
|
|
if _, err := resolveModel(); err != nil {
|
|
t.Fatalf("resolveModel with GADFLY_PROVIDER=%q: %v", provider, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildSpec(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
provider string
|
|
model string
|
|
want string
|
|
}{
|
|
{"bare id gets provider prefix", "ollama-cloud", "qwen3-coder:480b-cloud", "ollama-cloud/qwen3-coder:480b-cloud"},
|
|
{"bare id local ollama", "ollama", "llama3.1", "ollama/llama3.1"},
|
|
{"already has provider passes through", "ollama-cloud", "openai/gpt-4o", "openai/gpt-4o"},
|
|
{"slashed model name passes through verbatim", "openai", "openai/meta-llama/Llama-3.1", "openai/meta-llama/Llama-3.1"},
|
|
{"failover chain passes through", "ollama-cloud", "anthropic/opus-4.8,ollama-cloud/qwen3-coder:480b-cloud", "anthropic/opus-4.8,ollama-cloud/qwen3-coder:480b-cloud"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := buildSpec(tt.provider, tt.model); got != tt.want {
|
|
t.Errorf("buildSpec(%q, %q) = %q, want %q", tt.provider, tt.model, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|