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 +4 -1
- package/model-costs.ts +59 -9
- package/package.json +2 -2
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
|
-
|
|
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
|

|
|
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
|
|
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.
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
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",
|