diff --git a/internal/harnesses/harnesses.go b/internal/harnesses/harnesses.go index d637b00..3417abf 100644 --- a/internal/harnesses/harnesses.go +++ b/internal/harnesses/harnesses.go @@ -13,8 +13,12 @@ type Status struct { } type ConfigureOptions struct { - Model string - Overwrite bool + Model string + // ContextWindow and MaxOutputTokens are what the model list reports for + // Model, zero when it does not say. + ContextWindow int + MaxOutputTokens int + Overwrite bool } type LaunchOptions struct { diff --git a/internal/harnesses/pi.go b/internal/harnesses/pi.go index f10bfcf..ecd672a 100644 --- a/internal/harnesses/pi.go +++ b/internal/harnesses/pi.go @@ -46,7 +46,9 @@ type piProviderConfig struct { } type piModelConfig struct { - ID string `json:"id"` + ID string `json:"id"` + ContextWindow int `json:"contextWindow"` + MaxTokens int `json:"maxTokens"` } type PiHarness struct { @@ -402,7 +404,7 @@ func (p *PiHarness) configureMerge(opts ConfigureOptions) error { "X-Title": "Pi", }, "models": []any{ - map[string]any{"id": opts.Model}, + piModelConfigFrom(opts), }, }, }, @@ -445,7 +447,7 @@ func (p *PiHarness) configureOverwrite(opts ConfigureOptions) error { "X-Title": "Pi", }, Models: []piModelConfig{ - {ID: opts.Model}, + piModelConfigFrom(opts), }, }, }, @@ -468,6 +470,22 @@ func (p *PiHarness) configureOverwrite(opts ConfigureOptions) error { return nil } +// piModelConfigFrom describes the configured model with its capacities, since +// Pi caps a model without them at 128k tokens. +func piModelConfigFrom(opts ConfigureOptions) piModelConfig { + entry := piCatalogModelFrom(client.Model{ + ID: opts.Model, + ContextWindow: opts.ContextWindow, + MaxOutputTokens: opts.MaxOutputTokens, + }) + + return piModelConfig{ + ID: entry.ID, + ContextWindow: entry.ContextWindow, + MaxTokens: entry.MaxTokens, + } +} + func (p *PiHarness) modelsPath() string { return filepath.Join(p.configDir, "models.json") } diff --git a/internal/harnesses/pi_test.go b/internal/harnesses/pi_test.go index 7fb400c..8a8405b 100644 --- a/internal/harnesses/pi_test.go +++ b/internal/harnesses/pi_test.go @@ -39,7 +39,9 @@ func TestPiHarnessRoundTrip(t *testing.T) { // Configure the machine. err = harness.Configure(ConfigureOptions{ - Model: "anthropic/claude-fable-5", + Model: "anthropic/claude-fable-5", + ContextWindow: 1_000_000, + MaxOutputTokens: 64_000, }) require.NoError(t, err) @@ -63,7 +65,7 @@ func TestPiHarnessRoundTrip(t *testing.T) { "X-Title": "Pi" }, "models": [ - {"id": "anthropic/claude-fable-5"} + {"id": "anthropic/claude-fable-5", "contextWindow": 1000000, "maxTokens": 64000} ] } } @@ -107,7 +109,7 @@ func TestPiHarnessConfigureCreatesMissingConfig(t *testing.T) { "X-Title": "Pi" }, "models": [ - {"id": "anthropic/claude-fable-5"} + {"id": "anthropic/claude-fable-5", "contextWindow": 200000, "maxTokens": 8192} ] } } diff --git a/internal/modelpicker/picker.go b/internal/modelpicker/picker.go index f204f3b..da9a348 100644 --- a/internal/modelpicker/picker.go +++ b/internal/modelpicker/picker.go @@ -45,7 +45,7 @@ type Options struct { // ChosenMsg reports that a model was chosen in a Picker. type ChosenMsg struct { - ID string + Model client.Model } // CancelledMsg reports that a Picker was closed without choosing. @@ -138,7 +138,7 @@ func (p program) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p.height = max(typedMsg.Height-2*framePadY, 0) return p, nil case ChosenMsg: - p.chosen = typedMsg.ID + p.chosen = typedMsg.Model.ID return p, tea.Quit case CancelledMsg: return p, tea.Quit @@ -285,8 +285,8 @@ func (m Picker) updateKey(msg tea.KeyPressMsg) (Picker, tea.Cmd) { } case "enter": if filtered := m.filtered(); len(filtered) > 0 { - chosen := filtered[m.cursor].ID - return m, func() tea.Msg { return ChosenMsg{ID: chosen} } + chosen := filtered[m.cursor] + return m, func() tea.Msg { return ChosenMsg{Model: chosen} } } default: previous := m.search.Value() diff --git a/internal/modelpicker/picker_test.go b/internal/modelpicker/picker_test.go index e0cd113..f437ffd 100644 --- a/internal/modelpicker/picker_test.go +++ b/internal/modelpicker/picker_test.go @@ -163,7 +163,7 @@ func TestEnterChoosesTheModelUnderTheCursor(t *testing.T) { _, cmd := update(m, key("enter")) require.NotNil(t, cmd) - assert.Equal(t, ChosenMsg{ID: "claude-sonnet-4-6"}, cmd()) + assert.Equal(t, "claude-sonnet-4-6", cmd().(ChosenMsg).Model.ID) } func TestFooterNamesTheAction(t *testing.T) { @@ -177,7 +177,7 @@ func TestFooterNamesTheAction(t *testing.T) { func TestProgramQuitsWithTheChosenModel(t *testing.T) { p := newProgram(context.Background(), Options{Client: newClient(t, lists{policies: policies, models: models})}) - next, cmd := p.Update(ChosenMsg{ID: "gpt-5.5"}) + next, cmd := p.Update(ChosenMsg{Model: client.Model{ID: "gpt-5.5"}}) assert.Equal(t, "gpt-5.5", next.(program).chosen) require.NotNil(t, cmd) diff --git a/internal/tui/pages/requesty/dashboard/integrations.go b/internal/tui/pages/requesty/dashboard/integrations.go index 83e4fa5..f570de1 100644 --- a/internal/tui/pages/requesty/dashboard/integrations.go +++ b/internal/tui/pages/requesty/dashboard/integrations.go @@ -151,7 +151,9 @@ func (m integrationState) update(msg tea.Msg) (integrationState, tea.Cmd) { case modelpicker.ChosenMsg: if m.wizard.open && m.wizard.step == integrationModelWizardStep { - m.wizard.options.Model = typedMsg.ID + m.wizard.options.Model = typedMsg.Model.ID + m.wizard.options.ContextWindow = typedMsg.Model.ContextWindow + m.wizard.options.MaxOutputTokens = typedMsg.Model.MaxOutputTokens m.wizard.step = integrationModeWizardStep m.wizard.modeCursor = 0 m.wizard.configureErr = nil @@ -239,6 +241,8 @@ func (m integrationState) updateModeStep(msg tea.KeyPressMsg) (integrationState, case "esc": m.wizard.step = integrationModelWizardStep m.wizard.options.Model = "" + m.wizard.options.ContextWindow = 0 + m.wizard.options.MaxOutputTokens = 0 m.wizard.modeCursor = 0 m.wizard.configureErr = nil case "up", "k": diff --git a/internal/tui/pages/requesty/dashboard/integrations_test.go b/internal/tui/pages/requesty/dashboard/integrations_test.go index 6743ce2..3617983 100644 --- a/internal/tui/pages/requesty/dashboard/integrations_test.go +++ b/internal/tui/pages/requesty/dashboard/integrations_test.go @@ -42,7 +42,7 @@ func newIntegrationTestState(t *testing.T) (integrationState, *fakeHarness) { if r.URL.Path == "/v1/models/managed" { id = "claude-sonnet-4-6" } - require.NoError(t, json.NewEncoder(w).Encode(map[string]any{"data": []client.Model{{ID: id}}})) + require.NoError(t, json.NewEncoder(w).Encode(map[string]any{"data": []client.Model{{ID: id, ContextWindow: 1_000_000, MaxOutputTokens: 64_000}}})) })) t.Cleanup(server.Close) @@ -133,7 +133,7 @@ func TestConfigureWithAManagedPolicy(t *testing.T) { m = run(m, key("enter")) - assert.Equal(t, []harnesses.ConfigureOptions{{Model: "claude-sonnet-4-6"}}, harness.configured) + assert.Equal(t, []harnesses.ConfigureOptions{{Model: "claude-sonnet-4-6", ContextWindow: 1_000_000, MaxOutputTokens: 64_000}}, harness.configured) assert.False(t, m.wizard.open) } @@ -145,7 +145,7 @@ func TestConfigureWithAModelFromTheModelsTab(t *testing.T) { m = run(m, key("enter")) m = run(m, key("enter")) - assert.Equal(t, []harnesses.ConfigureOptions{{Model: "anthropic/claude-sonnet-4-6"}}, harness.configured) + assert.Equal(t, []harnesses.ConfigureOptions{{Model: "anthropic/claude-sonnet-4-6", ContextWindow: 1_000_000, MaxOutputTokens: 64_000}}, harness.configured) } func TestEscapeFromModeStepReturnsToThePicker(t *testing.T) {