# 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

- cmd/entire/cli/agent

- Mmodel_lister.go+6/-6

- pi

- Amodels.go+54

- Amodels_test.go+48

``` 
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
