pi-model-costs 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,13 +11,16 @@ the real cost of each model before you switch.
11
11
  - context window and max output tokens
12
12
  - cache read/write rates and pricing tiers
13
13
  - reasoning support and modality (text / text+image)
14
+ - **`ctrl+s` cost sorting** — cycle through default, input ↑↓, output ↑↓
15
+ and total (input + output) ↑↓; the active mode is shown in the footer
14
16
  - same flow as `/model`: arrows to navigate, type to fuzzy-filter, `Tab`
15
17
  toggles all/scoped (when scoped models are configured), `Enter` selects,
16
18
  `Esc` cancels
17
19
  - **Footer status** — pricing of the currently active model in the footer
18
20
  (disable it by setting `SHOW_STATUS = false` in the source).
19
21
 
20
- The current model is marked with a ✓ and sorted to the top.
22
+ In default order the current model is marked with a ✓ and sorted to the top;
23
+ cost sorts rank models purely by price (ties broken by provider/id).
21
24
 
22
25
  ![model-cost picker](images/model-cost.png)
23
26
 
package/model-costs.ts CHANGED
@@ -7,7 +7,9 @@
7
7
  * window, cache rates, tiers and reasoning support.
8
8
  * Same flow as /model: arrows to navigate, type to
9
9
  * fuzzy-filter, Tab toggles all/scoped (when scoped
10
- * models are configured), Enter selects, Esc cancels.
10
+ * models are configured), Enter selects, Esc
11
+ * cancels. ctrl+s cycles cost sorting (input /
12
+ * output / total, ascending or descending).
11
13
  *
12
14
  * Footer status — pricing of the currently active model
13
15
  * (set SHOW_STATUS = false below to disable).
@@ -71,6 +73,22 @@ interface Picked {
71
73
  thinkingLevel?: ThinkingLevel;
72
74
  }
73
75
 
76
+ type SortMode = "default" | "input-asc" | "input-desc" | "output-asc" | "output-desc" | "total-asc" | "total-desc";
77
+
78
+ const SORT_ORDER: SortMode[] = ["default", "input-asc", "input-desc", "output-asc", "output-desc", "total-asc", "total-desc"];
79
+
80
+ function sortLabel(mode: SortMode): string {
81
+ switch (mode) {
82
+ case "input-asc": return "in↑";
83
+ case "input-desc": return "in↓";
84
+ case "output-asc": return "out↑";
85
+ case "output-desc": return "out↓";
86
+ case "total-asc": return "total↑";
87
+ case "total-desc": return "total↓";
88
+ default: return "default";
89
+ }
90
+ }
91
+
74
92
  class ModelCostPicker extends Container implements Focusable {
75
93
  private searchInput = new Input();
76
94
  private listContainer = new Container();
@@ -93,6 +111,8 @@ class ModelCostPicker extends Container implements Focusable {
93
111
  private selectedIndex = 0;
94
112
  private scope: "all" | "scoped";
95
113
  private scopeText: Text | undefined;
114
+ private sortMode: SortMode = "default";
115
+ private footerText!: Text;
96
116
 
97
117
  constructor(
98
118
  private tui: import("@earendil-works/pi-tui").TUI,
@@ -135,7 +155,8 @@ class ModelCostPicker extends Container implements Focusable {
135
155
  this.addChild(this.detailContainer);
136
156
 
137
157
  this.addChild(new Spacer(1));
138
- this.addChild(new Text(theme.fg("dim", " ↑↓ navigate · Enter select · Esc cancel · prices per 1M tokens"), 0, 0));
158
+ this.footerText = new Text(this.renderFooter(), 0, 0);
159
+ this.addChild(this.footerText);
139
160
  this.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
140
161
 
141
162
  const currentIndex = this.activeItems.findIndex((item) => sameModel(this.currentModel, item.model));
@@ -143,17 +164,44 @@ class ModelCostPicker extends Container implements Focusable {
143
164
  this.applyFilter(initialQuery ?? "");
144
165
  }
145
166
 
146
- private sortItems(items: ModelItem[]): ModelItem[] {
147
- const sorted = [...items];
148
- sorted.sort((a, b) => {
167
+ private compareItems(a: ModelItem, b: ModelItem): number {
168
+ // Pin current model on top only in default mode; cost sorts rank it purely
169
+ if (this.sortMode === "default") {
149
170
  const aCurrent = sameModel(this.currentModel, a.model);
150
171
  const bCurrent = sameModel(this.currentModel, b.model);
151
172
  if (aCurrent && !bCurrent) return -1;
152
173
  if (!aCurrent && bCurrent) return 1;
153
- const byProvider = a.model.provider.localeCompare(b.model.provider);
154
- return byProvider !== 0 ? byProvider : a.model.id.localeCompare(b.model.id);
155
- });
156
- return sorted;
174
+ }
175
+ const dir = this.sortMode.endsWith("desc") ? -1 : 1;
176
+ let diff = 0;
177
+ if (this.sortMode.startsWith("input")) diff = a.model.cost.input - b.model.cost.input;
178
+ else if (this.sortMode.startsWith("output")) diff = a.model.cost.output - b.model.cost.output;
179
+ else if (this.sortMode.startsWith("total")) diff = (a.model.cost.input + a.model.cost.output) - (b.model.cost.input + b.model.cost.output);
180
+ if (diff !== 0) return diff * dir;
181
+ const byProvider = a.model.provider.localeCompare(b.model.provider);
182
+ return byProvider !== 0 ? byProvider : a.model.id.localeCompare(b.model.id);
183
+ }
184
+
185
+ private sortItems(items: ModelItem[]): ModelItem[] {
186
+ return [...items].sort((a, b) => this.compareItems(a, b));
187
+ }
188
+
189
+ private renderFooter(): string {
190
+ return this.theme.fg("dim", ` ↑↓ navigate · Enter select · Esc cancel · ctrl+s sort (${sortLabel(this.sortMode)}) · prices per 1M tokens`);
191
+ }
192
+
193
+ private cycleSort(): void {
194
+ this.sortMode = SORT_ORDER[(SORT_ORDER.indexOf(this.sortMode) + 1) % SORT_ORDER.length] as SortMode;
195
+ // Re-sort cached lists in place, full re-query if model count grows large
196
+ this.allItems = this.sortItems(this.allItems);
197
+ this.scopedItems = this.sortItems(this.scopedItems);
198
+ this.activeItems = this.scope === "scoped" ? this.scopedItems : this.allItems;
199
+ this.footerText.setText(this.renderFooter());
200
+ this.applyFilter(this.searchInput.getValue());
201
+ // Jump selection to current model so it stays visible after re-sort
202
+ const curIdx = this.filtered.findIndex((item) => sameModel(this.currentModel, item.model));
203
+ this.selectedIndex = curIdx >= 0 ? curIdx : 0;
204
+ this.updateList();
157
205
  }
158
206
 
159
207
  private renderScopeText(): string {
@@ -276,6 +324,8 @@ class ModelCostPicker extends Container implements Focusable {
276
324
  this.selectedIndex = currentIndex >= 0 ? currentIndex : 0;
277
325
  if (this.scopeText) this.scopeText.setText(this.renderScopeText());
278
326
  this.applyFilter(this.searchInput.getValue());
327
+ } else if (matchesKey(data, "ctrl+s")) {
328
+ this.cycleSort();
279
329
  } else if (matchesKey(data, "up")) {
280
330
  if (this.filtered.length === 0) return;
281
331
  this.selectedIndex = this.selectedIndex === 0 ? this.filtered.length - 1 : this.selectedIndex - 1;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pi-model-costs",
3
3
  "displayName": "model-costs",
4
- "version": "1.0.0",
5
- "description": "Pricing-aware model picker for pi: select models with per-1M-token cost, context window, cache rates, tiers and reasoning support.",
4
+ "version": "1.1.0",
5
+ "description": "Pricing-aware model picker for pi: select models with per-1M-token cost, context window, cache rates, tiers and reasoning support. Sort models by input/output/total cost with ctrl+s.",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "pi-package",