Live model enumeration for Pi via `pi --list-models` · Entire

Live model enumeration for Pi via pi --list-models

Pi is the only supported agent whose CLI can enumerate models, so wire a real ListModels that shells out to pi --list-models (output is on stderr, so capture combined output) and parses the provider/model table into "provider/model" ids. On failure it returns an error and the picker falls back to Default + Custom.

Other agents are unchanged: claude-code keeps its real aliases; codex and gemini have no enumeration command, so they show only Default + Custom. Refreshed the ModelLister doc comment to match.

Sessions

Changes

3

12 unmodified lines

}

// ModelLister is an optional capability for agents that can advertise the
// models usable with `entire review --model`.
// models usable with `entire scout --model`.
//
// Built-in agents whose CLI has no model-enumeration command (claude-code,
// codex, gemini) return a curated, intentionally non-exhaustive list of common
// models/aliases — `--model` ultimately accepts anything the agent CLI does.
// Agents whose CLI can enumerate models live (e.g. Pi's `pi --list-models`)
// may shell out instead.
// Pi enumerates models live by shelling out to `pi --list-models`. claude-code
// advertises a small curated list of real, valid aliases (opus/sonnet/haiku).
// Agents whose CLI has no enumeration command (codex, gemini) do not implement
// this interface at all; the picker then offers only Default + Custom, since
// `--model` ultimately accepts anything the agent CLI does.
type ModelLister interface {
    Agent
}

Mcmd/entire/cli/agent/model_lister.go+6/-6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

package pi

import (
    "context"
    "fmt"
    "os/exec"
    "strings"

"github.com/entireio/cli/cmd/entire/cli/agent"
)

var _ agent.ModelLister = (*PiAgent)(nil)

// ListModels returns the models Pi can run, fetched live from `pi
// --list-models`. Pi is the only supported agent whose CLI can enumerate
// models, so this is a real list rather than a curated/example one. The model
// picker always offers Default and Custom on top of this, and falls back to
// Default + Custom when the call fails (returned error).
func (a *PiAgent) ListModels(ctx context.Context) ([]agent.ModelInfo, error) {
    bin, err := exec.LookPath("pi")
    if err != nil {
        return nil, fmt.Errorf("pi not found on PATH: %w", err)
    }
    // `pi --list-models` prints the table to stderr, so capture combined output.
    out, err := exec.CommandContext(ctx, bin, "--list-models").CombinedOutput()
    if err != nil {
        return nil, fmt.Errorf("pi --list-models: %w", err)
    }
    return parsePiModelList(string(out)), nil
}

// parsePiModelList parses the tabular `pi --list-models` output into ModelInfo
// values. The output is a whitespace-aligned table:
//
//	provider   model                context  max-out  thinking  images
//	anthropic  claude-opus-4-5      200K     64K      yes       yes
//
// The first two columns (provider, model) become the "provider/model" id Pi
// accepts via --model; the header row and malformed lines are skipped.
func parsePiModelList(output string) []agent.ModelInfo {
    var models []agent.ModelInfo
    for _, line := range strings.Split(output, "\n") {
        fields := strings.Fields(line)
        if len(fields) < 2 {
            continue
        }
        provider, model := fields[0], fields[1]
        if provider == "provider" && model == "model" {
            continue // header row
        }
        models = append(models, agent.ModelInfo{ID: provider + "/" + model})
    }
    return models
}

Acmd/entire/cli/agent/pi/models.go+54

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

package pi

import (
    "testing"

"github.com/entireio/cli/cmd/entire/cli/agent"
)

// TestPiAgent_IsModelLister locks Pi in as the one agent with live enumeration.
func TestPiAgent_IsModelLister(t *testing.T) {
    t.Parallel()
    if _, ok := agent.AsModelLister(NewPiAgent()); !ok {
        t.Fatal("PiAgent should implement agent.ModelLister")
    }
}

func TestParsePiModelList(t *testing.T) {
    t.Parallel()
    output := `provider      model                         context  max-out  thinking  images
    anthropic     claude-opus-4-5               200K     64K      yes       yes
    anthropic     claude-sonnet-4-5             200K     64K      yes       yes
    openai        gpt-5                         400K     128K     yes       yes
`
    models := parsePiModelList(output)
    want := []string{
        "anthropic/claude-opus-4-5",
        "anthropic/claude-sonnet-4-5",
        "openai/gpt-5",
    }
    if len(models) != len(want) {
        t.Fatalf("got %d models, want %d: %+v", len(models), len(want), models)
    }
    for i, w := range want {
        if models[i].ID != w {
                                t.Errorf("models[%d].ID = %q, want %q", i, models[i].ID, w)
        }
    }
}

func TestParsePiModelList_SkipsHeaderAndBlanks(t *testing.T) {
    t.Parallel()
    if got := parsePiModelList("provider model context\n\n   \n"); len(got) != 0 {
        t.Fatalf("expected no models from header/blank-only output, got %+v", got)
    }
    if got := parsePiModelList(""); got != nil {
        t.Fatalf("expected nil for empty output, got %+v", got)
    }
}

Acmd/entire/cli/agent/pi/models_test.go+48