mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 09:49:34 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e0fc6ee25 |
No files matched your search
+34
-28
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/Gitlawb/zero/internal/observability"
|
||||
"github.com/Gitlawb/zero/internal/plugins"
|
||||
"github.com/Gitlawb/zero/internal/providerhealth"
|
||||
"github.com/Gitlawb/zero/internal/providermodeldiscovery"
|
||||
"github.com/Gitlawb/zero/internal/provideronboarding"
|
||||
"github.com/Gitlawb/zero/internal/providers"
|
||||
"github.com/Gitlawb/zero/internal/redaction"
|
||||
@@ -53,32 +54,33 @@ type appDeps struct {
|
||||
// config.SetActiveProviderEnv, set in defaultAppDeps — deliberately NOT filled
|
||||
// by fillAppDeps, so tests never mutate the process environment unless they
|
||||
// inject it). nil ⇒ no export.
|
||||
exportActiveProvider func(providerName string)
|
||||
probeProviderHealth func(context.Context, providerhealth.Options) providerhealth.Result
|
||||
detectLocalRuntimes func(context.Context, provideronboarding.LocalDetectOptions) []provideronboarding.DetectedLocalRuntime
|
||||
newSessionStore func() *sessions.Store
|
||||
loadPlugins func(plugins.LoadOptions) (plugins.LoadResult, error)
|
||||
loadHooks func(hooks.LoadOptions) (hooks.LoadResult, error)
|
||||
skillsDir func() string
|
||||
pluginsDir func() string
|
||||
toolsDir func() string
|
||||
newMCPStore func() (*mcp.PermissionStore, error)
|
||||
newMCPTokenStore func() (*mcp.TokenStore, error)
|
||||
newSandboxStore func() (*sandbox.GrantStore, error)
|
||||
selectSandboxBackend func(sandbox.BackendOptions) sandbox.Backend
|
||||
runSandboxSetupHelper func(path string, args []string, stdout io.Writer, stderr io.Writer) error
|
||||
registerMCPTools func(context.Context, *tools.Registry, config.MCPConfig, mcp.RegisterOptions) (mcpToolRuntime, error)
|
||||
prepareWorktree func(context.Context, worktrees.Options) (worktrees.Result, error)
|
||||
detectVerifyPlan func(string) (verify.Plan, error)
|
||||
runVerify func(context.Context, verify.Plan, verify.RunOptions) verify.Report
|
||||
runSelfVerify func(context.Context, verify.Plan, selfverify.Options) selfverify.Report
|
||||
runAgentEval func(context.Context, agentEvalOptions) (agentEvalReport, error)
|
||||
inspectChanges func(context.Context, zerogit.InspectOptions) (zerogit.ChangeSummary, error)
|
||||
commitChanges func(context.Context, zerogit.CommitOptions) (zerogit.CommitResult, error)
|
||||
runTUI func(context.Context, tui.Options) int
|
||||
runEditor func(string) error
|
||||
checkUpdate func(context.Context, update.Options) (update.Result, error)
|
||||
now func() time.Time
|
||||
exportActiveProvider func(providerName string)
|
||||
probeProviderHealth func(context.Context, providerhealth.Options) providerhealth.Result
|
||||
discoverProviderModels func(context.Context, config.ProviderProfile) ([]providermodeldiscovery.Model, error)
|
||||
detectLocalRuntimes func(context.Context, provideronboarding.LocalDetectOptions) []provideronboarding.DetectedLocalRuntime
|
||||
newSessionStore func() *sessions.Store
|
||||
loadPlugins func(plugins.LoadOptions) (plugins.LoadResult, error)
|
||||
loadHooks func(hooks.LoadOptions) (hooks.LoadResult, error)
|
||||
skillsDir func() string
|
||||
pluginsDir func() string
|
||||
toolsDir func() string
|
||||
newMCPStore func() (*mcp.PermissionStore, error)
|
||||
newMCPTokenStore func() (*mcp.TokenStore, error)
|
||||
newSandboxStore func() (*sandbox.GrantStore, error)
|
||||
selectSandboxBackend func(sandbox.BackendOptions) sandbox.Backend
|
||||
runSandboxSetupHelper func(path string, args []string, stdout io.Writer, stderr io.Writer) error
|
||||
registerMCPTools func(context.Context, *tools.Registry, config.MCPConfig, mcp.RegisterOptions) (mcpToolRuntime, error)
|
||||
prepareWorktree func(context.Context, worktrees.Options) (worktrees.Result, error)
|
||||
detectVerifyPlan func(string) (verify.Plan, error)
|
||||
runVerify func(context.Context, verify.Plan, verify.RunOptions) verify.Report
|
||||
runSelfVerify func(context.Context, verify.Plan, selfverify.Options) selfverify.Report
|
||||
runAgentEval func(context.Context, agentEvalOptions) (agentEvalReport, error)
|
||||
inspectChanges func(context.Context, zerogit.InspectOptions) (zerogit.ChangeSummary, error)
|
||||
commitChanges func(context.Context, zerogit.CommitOptions) (zerogit.CommitResult, error)
|
||||
runTUI func(context.Context, tui.Options) int
|
||||
runEditor func(string) error
|
||||
checkUpdate func(context.Context, update.Options) (update.Result, error)
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type mcpToolRuntime interface {
|
||||
@@ -134,8 +136,9 @@ func defaultAppDeps() appDeps {
|
||||
OAuthLoginKey: loginKey,
|
||||
})
|
||||
},
|
||||
probeProviderHealth: providerhealth.Probe,
|
||||
detectLocalRuntimes: provideronboarding.DetectLocalRuntimes,
|
||||
probeProviderHealth: providerhealth.Probe,
|
||||
discoverProviderModels: defaultDiscoverProviderModels,
|
||||
detectLocalRuntimes: provideronboarding.DetectLocalRuntimes,
|
||||
newSessionStore: func() *sessions.Store {
|
||||
return sessions.NewStore(sessions.StoreOptions{})
|
||||
},
|
||||
@@ -432,6 +435,9 @@ func fillAppDeps(deps appDeps) appDeps {
|
||||
if deps.probeProviderHealth == nil {
|
||||
deps.probeProviderHealth = defaults.probeProviderHealth
|
||||
}
|
||||
if deps.discoverProviderModels == nil {
|
||||
deps.discoverProviderModels = defaults.discoverProviderModels
|
||||
}
|
||||
if deps.detectLocalRuntimes == nil {
|
||||
deps.detectLocalRuntimes = defaults.detectLocalRuntimes
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ func runProviders(args []string, stdout io.Writer, stderr io.Writer, deps appDep
|
||||
if command == "detect" {
|
||||
return runProvidersDetect(args, stdout, stderr, deps)
|
||||
}
|
||||
if command == "models" {
|
||||
return runProvidersModels(args, stdout, stderr, deps)
|
||||
}
|
||||
if command != "list" && command != "current" && command != "catalog" {
|
||||
return writeExecUsageError(stderr, fmt.Sprintf("unknown providers command %q", command))
|
||||
}
|
||||
@@ -465,9 +468,11 @@ func writeProvidersHelp(w io.Writer) error {
|
||||
zero providers use <name> [flags]
|
||||
zero providers setup <catalog-id> [flags]
|
||||
zero providers detect [flags]
|
||||
zero providers models [name] [flags]
|
||||
|
||||
Inspects resolved provider profiles and provider catalog descriptors without printing secrets.
|
||||
Detect probes for running local runtimes (Ollama, LM Studio) and prints adopt commands plus per-provider next steps.
|
||||
Models probes a provider's live model-listing endpoint (e.g. an OpenAI-compatible /v1/models) and lists the models it serves — including custom OpenAI-/Anthropic-compatible endpoints — so a self-hosted provider needs no per-model config.
|
||||
|
||||
Flags:
|
||||
--json Print JSON summary
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user