mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-12 08:41:32 -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.
149 lines
4.6 KiB
Go
149 lines
4.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/Gitlawb/zero/internal/config"
|
|
"github.com/Gitlawb/zero/internal/providermodeldiscovery"
|
|
)
|
|
|
|
type providerModelsOptions struct {
|
|
name string
|
|
json bool
|
|
}
|
|
|
|
// runProvidersModels lists the models a saved provider actually serves by probing
|
|
// its live model-discovery endpoint (e.g. an OpenAI-compatible `/v1/models`). It
|
|
// works for custom OpenAI-/Anthropic-compatible providers too: discovery runs off
|
|
// the profile's base URL + credentials, so a self-hosted endpoint serving a dozen
|
|
// models no longer needs a config object per model — configure the provider once,
|
|
// then run any listed model with `zero exec --model <id>` (Zero passes unknown
|
|
// model ids through to the provider).
|
|
func runProvidersModels(args []string, stdout io.Writer, stderr io.Writer, deps appDeps) int {
|
|
options, help, err := parseProviderModelsArgs(args)
|
|
if err != nil {
|
|
return writeExecUsageError(stderr, err.Error())
|
|
}
|
|
if help {
|
|
if err := writeProvidersHelp(stdout); err != nil {
|
|
return exitCrash
|
|
}
|
|
return exitSuccess
|
|
}
|
|
|
|
resolved, exitCode := resolveCommandCenterConfig(stderr, deps)
|
|
if exitCode != exitSuccess {
|
|
return exitCode
|
|
}
|
|
profile, err := selectProviderForCheck(resolved, options.name)
|
|
if err != nil {
|
|
return writeExecUsageError(stderr, err.Error())
|
|
}
|
|
|
|
ctx, stop := signalContext()
|
|
defer stop()
|
|
models, err := deps.discoverProviderModels(ctx, discoveryCredentialProfile(profile))
|
|
if err != nil {
|
|
return writeAppError(stderr, err.Error(), exitProvider)
|
|
}
|
|
|
|
if options.json {
|
|
items := make([]map[string]any, 0, len(models))
|
|
for _, model := range models {
|
|
entry := map[string]any{"id": model.ID}
|
|
if description := strings.TrimSpace(model.Description); description != "" {
|
|
entry["description"] = description
|
|
}
|
|
items = append(items, entry)
|
|
}
|
|
payload := map[string]any{
|
|
"provider": profile.Name,
|
|
"count": len(items),
|
|
"models": items,
|
|
}
|
|
if err := writePrettyJSON(stdout, payload); err != nil {
|
|
return exitCrash
|
|
}
|
|
return exitSuccess
|
|
}
|
|
|
|
name := strings.TrimSpace(profile.Name)
|
|
if name == "" {
|
|
name = "provider"
|
|
}
|
|
if _, err := fmt.Fprintf(stdout, "Provider models (%s)\n", name); err != nil {
|
|
return exitCrash
|
|
}
|
|
for _, model := range models {
|
|
line := strings.TrimSpace(model.ID)
|
|
if description := strings.TrimSpace(model.Description); description != "" {
|
|
line += " — " + description
|
|
}
|
|
if _, err := fmt.Fprintln(stdout, line); err != nil {
|
|
return exitCrash
|
|
}
|
|
}
|
|
suffix := "s"
|
|
if len(models) == 1 {
|
|
suffix = ""
|
|
}
|
|
if _, err := fmt.Fprintf(stdout, "%d model%s discovered\n", len(models), suffix); err != nil {
|
|
return exitCrash
|
|
}
|
|
if len(models) > 0 {
|
|
if _, err := fmt.Fprintf(stdout, "next: zero exec %q --model %s\n", "hello", setupCommandArg(models[0].ID)); err != nil {
|
|
return exitCrash
|
|
}
|
|
}
|
|
return exitSuccess
|
|
}
|
|
|
|
// discoveryCredentialProfile resolves the profile's API key the same way the
|
|
// runtime does — inline, then the stored credential, then the configured env var —
|
|
// so a `providers models` probe authenticates exactly like a real request. Mirrors
|
|
// discoveredModelContextWindow's credential resolution.
|
|
func discoveryCredentialProfile(profile config.ProviderProfile) config.ProviderProfile {
|
|
authed := profile
|
|
if strings.TrimSpace(authed.APIKey) == "" {
|
|
if store, err := config.ProviderKeyStore(); err == nil {
|
|
authed = config.ApplyStoredAPIKey(authed, store)
|
|
}
|
|
}
|
|
if strings.TrimSpace(authed.APIKey) == "" && strings.TrimSpace(authed.APIKeyEnv) != "" {
|
|
authed.APIKey = strings.TrimSpace(os.Getenv(authed.APIKeyEnv))
|
|
}
|
|
return authed
|
|
}
|
|
|
|
// defaultDiscoverProviderModels is the production discovery hook: a live probe of
|
|
// the provider's model-listing endpoint with no curated-catalog merge or
|
|
// coding-model filtering, so a custom provider's full model list is returned.
|
|
func defaultDiscoverProviderModels(ctx context.Context, profile config.ProviderProfile) ([]providermodeldiscovery.Model, error) {
|
|
return providermodeldiscovery.Discover(ctx, profile, providermodeldiscovery.Options{})
|
|
}
|
|
|
|
func parseProviderModelsArgs(args []string) (providerModelsOptions, bool, error) {
|
|
options := providerModelsOptions{}
|
|
for index := 0; index < len(args); index++ {
|
|
arg := args[index]
|
|
switch {
|
|
case arg == "-h" || arg == "--help" || arg == "help":
|
|
return options, true, nil
|
|
case arg == "--json":
|
|
options.json = true
|
|
case strings.HasPrefix(arg, "-"):
|
|
return options, false, execUsageError{fmt.Sprintf("unknown flag %q", arg)}
|
|
default:
|
|
if options.name != "" {
|
|
return options, false, execUsageError{fmt.Sprintf("unexpected argument %q", arg)}
|
|
}
|
|
options.name = arg
|
|
}
|
|
}
|
|
return options, false, nil
|
|
}
|