pi-openrouter-provider-picker 0.1.0 → 0.1.2

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
@@ -17,6 +17,29 @@ pi install npm:pi-openrouter-provider-picker
17
17
 
18
18
  1. In Pi, use `/model` to select an OpenRouter model.
19
19
  2. Run `/or-provider` and choose one of the endpoints listed for that model.
20
+
21
+ Each entry shows the provider name, its routing tag, and that endpoint's
22
+ current price, for example:
23
+
24
+ ```text
25
+ OpenAI — openai · in $30/M · out $60/M · cache $7.50/M
26
+ ```
27
+
28
+ `in` and `out` are USD per million input and output tokens. `cache` is the
29
+ discounted rate for cached input tokens and only appears when it is cheaper
30
+ than the regular input rate. Endpoints that publish no price (for example
31
+ free tiers) show no price at all.
32
+
33
+ Endpoints that have recently stopped responding are flagged with their
34
+ uptime over the most recent window that carries traffic:
35
+
36
+ ```text
37
+ Chutes — chutes · in $0.05/M · out $0.1/M · ⚠ up 62%
38
+ ```
39
+
40
+ Endpoints at or above 95% uptime are not flagged. A `null` uptime means no
41
+ traffic was observed, not an outage, so it is never shown.
42
+
20
43
  3. The selected endpoint is pinned for that model; provider fallback is disabled.
21
44
  4. When you switch models, this extension clears its selection. The new model uses its normal routing.
22
45
 
@@ -5,10 +5,144 @@ const AUTO_LABEL = "Automatic — use model/OpenRouter routing settings";
5
5
  const CUSTOM_LABEL = "Enter a provider slug…";
6
6
  const STATUS_KEY = "openrouter-provider";
7
7
 
8
+ /** Endpoints at or above this uptime are not worth flagging in the menu. */
9
+ const UPTIME_WARNING_THRESHOLD = 95;
10
+
8
11
  function isRecord(value: unknown): value is Record<string, unknown> {
9
12
  return typeof value === "object" && value !== null && !Array.isArray(value);
10
13
  }
11
14
 
15
+ function asNumber(value: unknown): number {
16
+ if (typeof value === "number") return value;
17
+ if (typeof value === "string") return value.trim() === "" ? Number.NaN : Number(value);
18
+ return Number.NaN;
19
+ }
20
+
21
+ /** Render a USD amount without trailing zeros (e.g. 30, 0.025, 0.001). */
22
+ function formatUsd(amount: number): string {
23
+ const text = amount >= 1 ? amount.toFixed(2) : amount.toPrecision(3);
24
+ return text.includes(".") ? text.replace(/\.?0+$/, "") : text;
25
+ }
26
+
27
+ /**
28
+ * OpenRouter prices `prompt`/`completion` per token (USD), while `request` is
29
+ * charged per request. Returns "" when the endpoint publishes no pricing.
30
+ */
31
+ function formatPriceSuffix(pricing: unknown): string {
32
+ if (!isRecord(pricing)) return "";
33
+
34
+ const parts: string[] = [];
35
+ const addPerToken = (label: string, raw: unknown): void => {
36
+ const perToken = asNumber(raw);
37
+ if (!Number.isFinite(perToken) || perToken <= 0) return;
38
+ parts.push(`${label} $${formatUsd(perToken * 1_000_000)}/M`);
39
+ };
40
+
41
+ addPerToken("in", pricing.prompt);
42
+ addPerToken("out", pricing.completion);
43
+
44
+ // Cached input is billed at its own (cheaper) rate; only mention a real discount.
45
+ const cache = asNumber(pricing.input_cache_read);
46
+ const input = asNumber(pricing.prompt);
47
+ const cheaperCache =
48
+ Number.isFinite(cache) &&
49
+ cache > 0 &&
50
+ !(Number.isFinite(input) && input > 0 && cache >= input);
51
+ if (cheaperCache) {
52
+ parts.push(`cache $${formatUsd(cache * 1_000_000)}/M`);
53
+ }
54
+
55
+ const perRequest = asNumber(pricing.request);
56
+ if (Number.isFinite(perRequest) && perRequest > 0) {
57
+ parts.push(`$${formatUsd(perRequest)}/req`);
58
+ }
59
+
60
+ return parts.length > 0 ? ` · ${parts.join(" · ")}` : "";
61
+ }
62
+
63
+ /**
64
+ * Prefer the freshest window that actually carries traffic. `null` means no
65
+ * traffic was observed, which is not the same as being down.
66
+ */
67
+ function firstUptime(endpoint: Record<string, unknown>): number | undefined {
68
+ for (const key of ["uptime_last_5m", "uptime_last_30m", "uptime_last_1d"]) {
69
+ const uptime = asNumber(endpoint[key]);
70
+ if (Number.isFinite(uptime)) return uptime;
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ /** Flag a degraded endpoint only; healthy ones stay out of the way. */
76
+ function formatUptimeWarning(endpoint: unknown): string {
77
+ if (!isRecord(endpoint)) return "";
78
+ const uptime = firstUptime(endpoint);
79
+ if (uptime === undefined || uptime >= UPTIME_WARNING_THRESHOLD) return "";
80
+ return ` · ⚠ up ${Math.floor(uptime)}%`;
81
+ }
82
+
83
+ /**
84
+ * East Asian wide/fullwidth ranges. Provider names are normally ASCII, so this
85
+ * only matters when a name carries CJK text.
86
+ */
87
+ const WIDE_RANGES: ReadonlyArray<readonly [number, number]> = [
88
+ [0x1100, 0x115f], [0x2e80, 0x303e], [0x3041, 0x33ff], [0x3400, 0x4dbf],
89
+ [0x4e00, 0x9fff], [0xa000, 0xa4cf], [0xac00, 0xd7a3], [0xf900, 0xfaff],
90
+ [0xfe10, 0xfe19], [0xfe30, 0xfe6f], [0xff00, 0xff60], [0xffe0, 0xffe6],
91
+ [0x1f300, 0x1f64f], [0x1f900, 0x1f9ff], [0x20000, 0x3fffd],
92
+ ];
93
+
94
+ /** Terminal columns a string occupies, so menu columns can be aligned. */
95
+ function displayWidth(text: string): number {
96
+ let width = 0;
97
+ for (const character of text) {
98
+ const codePoint = character.codePointAt(0) ?? 0;
99
+ width += WIDE_RANGES.some(([low, high]) => codePoint >= low && codePoint <= high)
100
+ ? 2
101
+ : 1;
102
+ }
103
+ return width;
104
+ }
105
+
106
+ function padEndWidth(text: string, target: number): string {
107
+ const missing = target - displayWidth(text);
108
+ return missing > 0 ? text + " ".repeat(missing) : text;
109
+ }
110
+
111
+ type EndpointChoice = {
112
+ slug: string;
113
+ base: string;
114
+ price: string;
115
+ health: string;
116
+ };
117
+
118
+ function buildChoice(endpoint: unknown, tag: string, providerName: string): EndpointChoice {
119
+ const value = isRecord(endpoint) ? endpoint : {};
120
+ return {
121
+ slug: tag,
122
+ base: `${providerName} — ${tag}`,
123
+ price: formatPriceSuffix(value.pricing),
124
+ health: formatUptimeWarning(value),
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Pad the name and price blocks to their widest entry so every price starts at
130
+ * the same column and the health flag lines up at the end.
131
+ */
132
+ function alignChoices(choices: EndpointChoice[]): Array<{ slug: string; label: string }> {
133
+ const baseWidth = Math.max(0, ...choices.map((c) => displayWidth(c.base)));
134
+ const priceWidth = Math.max(0, ...choices.map((c) => displayWidth(c.price)));
135
+
136
+ return choices.map((choice) => ({
137
+ slug: choice.slug,
138
+ label: (
139
+ padEndWidth(choice.base, baseWidth) +
140
+ padEndWidth(choice.price, priceWidth) +
141
+ choice.health
142
+ ).trimEnd(),
143
+ }));
144
+ }
145
+
12
146
  async function getEndpointChoices(modelId: string, apiKey: string) {
13
147
  const slash = modelId.indexOf("/");
14
148
  if (slash <= 0 || slash === modelId.length - 1) {
@@ -34,15 +168,15 @@ async function getEndpointChoices(modelId: string, apiKey: string) {
34
168
  const endpoints = Array.isArray(data?.endpoints) ? data.endpoints : undefined;
35
169
  if (!endpoints) throw new Error("OpenRouter returned an unexpected endpoint-list response.");
36
170
 
37
- const byTag = new Map<string, string>();
171
+ const byTag = new Map<string, EndpointChoice>();
38
172
  for (const value of endpoints) {
39
173
  if (!isRecord(value) || typeof value.tag !== "string" || !value.tag) continue;
40
174
  const tag = value.tag;
41
175
  const providerName = typeof value.provider_name === "string" ? value.provider_name : tag;
42
- byTag.set(tag, `${providerName} — ${tag}`);
176
+ byTag.set(tag, buildChoice(value, tag, providerName));
43
177
  }
44
178
 
45
- return [...byTag.entries()].map(([slug, label]) => ({ slug, label }));
179
+ return alignChoices([...byTag.values()]);
46
180
  }
47
181
 
48
182
  export default function openRouterProviderExtension(pi: ExtensionAPI) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openrouter-provider-picker",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Choose an upstream provider for OpenRouter models in Pi",
5
5
  "author": "soubell",
6
6
  "repository": {