pi-openrouter-provider-picker 0.1.1 → 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 +23 -0
- package/extensions/openrouter-provider.ts +137 -3
- package/package.json +1 -1
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,
|
|
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,
|
|
176
|
+
byTag.set(tag, buildChoice(value, tag, providerName));
|
|
43
177
|
}
|
|
44
178
|
|
|
45
|
-
return [...byTag.
|
|
179
|
+
return alignChoices([...byTag.values()]);
|
|
46
180
|
}
|
|
47
181
|
|
|
48
182
|
export default function openRouterProviderExtension(pi: ExtensionAPI) {
|