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