mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 18:00:54 -10:00
`zero providers` had no way to list the models a provider actually serves, so a custom OpenAI-/Anthropic-compatible endpoint (e.g. a self-hosted proxy serving a dozen models) forced users to hand-write a config object per model. Live model discovery already existed for the TUI picker but had no headless CLI surface. Add `zero providers models [name] [--json]`: it probes the provider's live model-listing endpoint (an OpenAI-compatible /v1/models or the Anthropic /models) via providermodeldiscovery.Discover, using the profile's base URL and resolved credential (inline -> stored -> env). This uses the raw discovery primitive with no curated-catalog merge or coding-model filtering, so a custom provider's full model list is returned. Configure the provider once, run `zero providers models` to see what it serves, and use any id with `zero exec --model <id>` (Zero passes unknown model ids through to the provider) -- no per-model config needed. Wired through a new appDeps.discoverProviderModels hook (default: providermodeldiscovery.Discover) so the command is unit-tested hermetically. Fixes #381.
152 lines
5.3 KiB
Go
152 lines
5.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Gitlawb/zero/internal/config"
|
|
"github.com/Gitlawb/zero/internal/providermodeldiscovery"
|
|
)
|
|
|
|
func TestRunProvidersModelsListsDiscoveredModels(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
deps := commandCenterDeps(t)
|
|
var probed config.ProviderProfile
|
|
deps.discoverProviderModels = func(_ context.Context, profile config.ProviderProfile) ([]providermodeldiscovery.Model, error) {
|
|
probed = profile
|
|
return []providermodeldiscovery.Model{
|
|
{ID: "team/model-a", Description: "first"},
|
|
{ID: "team/model-b"},
|
|
}, nil
|
|
}
|
|
|
|
exitCode := runWithDeps([]string{"providers", "models"}, &stdout, &stderr, deps)
|
|
|
|
if exitCode != exitSuccess {
|
|
t.Fatalf("exit = %d, want %d: %s", exitCode, exitSuccess, stderr.String())
|
|
}
|
|
// No name given → the active provider is probed, with its resolved credential.
|
|
if probed.Name != "work" {
|
|
t.Fatalf("probed provider = %q, want active provider work", probed.Name)
|
|
}
|
|
out := stdout.String()
|
|
for _, want := range []string{"Provider models (work)", "team/model-a — first", "team/model-b", "2 models discovered", "--model team/model-a"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("output missing %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunProvidersModelsJSON(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
deps := commandCenterDeps(t)
|
|
deps.discoverProviderModels = func(_ context.Context, _ config.ProviderProfile) ([]providermodeldiscovery.Model, error) {
|
|
return []providermodeldiscovery.Model{{ID: "m1", Description: "d1"}, {ID: "m2"}}, nil
|
|
}
|
|
|
|
exitCode := runWithDeps([]string{"providers", "models", "--json"}, &stdout, &stderr, deps)
|
|
|
|
if exitCode != exitSuccess {
|
|
t.Fatalf("exit = %d, want %d: %s", exitCode, exitSuccess, stderr.String())
|
|
}
|
|
var payload struct {
|
|
Provider string `json:"provider"`
|
|
Count int `json:"count"`
|
|
Models []struct {
|
|
ID string `json:"id"`
|
|
Description string `json:"description"`
|
|
} `json:"models"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
|
|
t.Fatalf("decode json: %v\n%s", err, stdout.String())
|
|
}
|
|
if payload.Provider != "work" || payload.Count != 2 || len(payload.Models) != 2 {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
if payload.Models[0].ID != "m1" || payload.Models[0].Description != "d1" {
|
|
t.Fatalf("model[0] = %#v", payload.Models[0])
|
|
}
|
|
// A model with no description omits the field rather than emitting an empty string.
|
|
if payload.Models[1].ID != "m2" || payload.Models[1].Description != "" {
|
|
t.Fatalf("model[1] = %#v", payload.Models[1])
|
|
}
|
|
}
|
|
|
|
func TestRunProvidersModelsSelectsNamedProvider(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
deps := commandCenterDeps(t)
|
|
custom := config.ProviderProfile{
|
|
Name: "self-hosted",
|
|
ProviderKind: config.ProviderKindOpenAICompatible,
|
|
BaseURL: "https://models.internal/v1",
|
|
APIKey: "sk-internal",
|
|
Model: "house-model",
|
|
}
|
|
deps.resolveConfig = func(string, config.Overrides) (config.ResolvedConfig, error) {
|
|
work := config.ProviderProfile{Name: "work", ProviderKind: config.ProviderKindOpenAI, BaseURL: config.OpenAIBaseURL, APIKey: "sk", Model: "gpt-4.1"}
|
|
return config.ResolvedConfig{ActiveProvider: "work", Provider: work, Providers: []config.ProviderProfile{work, custom}}, nil
|
|
}
|
|
var probed config.ProviderProfile
|
|
deps.discoverProviderModels = func(_ context.Context, profile config.ProviderProfile) ([]providermodeldiscovery.Model, error) {
|
|
probed = profile
|
|
return []providermodeldiscovery.Model{{ID: "house-model"}}, nil
|
|
}
|
|
|
|
exitCode := runWithDeps([]string{"providers", "models", "self-hosted"}, &stdout, &stderr, deps)
|
|
|
|
if exitCode != exitSuccess {
|
|
t.Fatalf("exit = %d, want %d: %s", exitCode, exitSuccess, stderr.String())
|
|
}
|
|
if probed.Name != "self-hosted" || probed.BaseURL != "https://models.internal/v1" {
|
|
t.Fatalf("probed = %#v, want the named custom provider", probed)
|
|
}
|
|
}
|
|
|
|
func TestRunProvidersModelsUnknownProviderFails(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
deps := commandCenterDeps(t)
|
|
called := false
|
|
deps.discoverProviderModels = func(_ context.Context, _ config.ProviderProfile) ([]providermodeldiscovery.Model, error) {
|
|
called = true
|
|
return nil, nil
|
|
}
|
|
|
|
exitCode := runWithDeps([]string{"providers", "models", "ghost"}, &stdout, &stderr, deps)
|
|
|
|
if exitCode == exitSuccess {
|
|
t.Fatalf("exit = %d, want non-zero for unknown provider", exitCode)
|
|
}
|
|
if called {
|
|
t.Fatal("discovery must not run for an unknown provider")
|
|
}
|
|
if !strings.Contains(stderr.String(), `provider "ghost" not found`) {
|
|
t.Fatalf("stderr = %q, want provider-not-found", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProvidersModelsSurfacesDiscoveryError(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
deps := commandCenterDeps(t)
|
|
deps.discoverProviderModels = func(_ context.Context, _ config.ProviderProfile) ([]providermodeldiscovery.Model, error) {
|
|
return nil, errors.New("models endpoint returned 404")
|
|
}
|
|
|
|
exitCode := runWithDeps([]string{"providers", "models"}, &stdout, &stderr, deps)
|
|
|
|
if exitCode != exitProvider {
|
|
t.Fatalf("exit = %d, want exitProvider %d", exitCode, exitProvider)
|
|
}
|
|
if !strings.Contains(stderr.String(), "models endpoint returned 404") {
|
|
t.Fatalf("stderr = %q, want the discovery error", stderr.String())
|
|
}
|
|
}
|