pi-openrouter-realtime 0.2.2 → 0.3.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 +70 -15
- package/extensions/openrouter-routing/api.ts +155 -0
- package/extensions/openrouter-routing/index.ts +344 -381
- package/extensions/openrouter-routing/models.ts +274 -0
- package/extensions/openrouter-routing/picker.ts +333 -0
- package/extensions/openrouter-routing/routing.ts +63 -0
- package/extensions/openrouter-routing/state.ts +108 -0
- package/extensions/openrouter-routing/types.ts +159 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
|
-
# pi-openrouter-realtime
|
|
3
|
+
# pi-openrouter-realtime v0.3.0
|
|
4
4
|
|
|
5
|
-
Pi extension for OpenRouter that loads the latest models from OpenRouter in real time,
|
|
5
|
+
Pi extension for OpenRouter that loads the latest models from OpenRouter in real time, with provider/quantization enrichment, endpoint health indicators, credit balance display, interactive model picker, and tab-completion.
|
|
6
6
|
|
|
7
7
|
Once the extension is installed and your OpenRouter credential is configured in pi, each new pi session automatically fetches the latest OpenRouter model list.
|
|
8
8
|
|
|
@@ -10,14 +10,33 @@ Npm package:
|
|
|
10
10
|
|
|
11
11
|
- `pi-openrouter-realtime`
|
|
12
12
|
|
|
13
|
+
## What's New in v0.3.0
|
|
14
|
+
|
|
15
|
+
- **Targeted enrichment** — enrich one model on demand without scanning the whole catalog
|
|
16
|
+
- **Interactive model picker** — run `/openrouter-enrich` without args → type a search query → pick from filtered results
|
|
17
|
+
- **Tab-completion** — autocomplete model IDs when typing commands
|
|
18
|
+
- **`/openrouter-preview`** — inspect provider variants and endpoint health without changing your model list
|
|
19
|
+
- **`/openrouter-balance`** — check your OpenRouter credit balance and usage
|
|
20
|
+
- **`/openrouter-status`** — see current extension state, active enrichments, cache age
|
|
21
|
+
- **Endpoint health data** — status, uptime, latency (TTFT), throughput per variant
|
|
22
|
+
- **Snapshot-based routing** — eliminates race conditions with stale route maps
|
|
23
|
+
- **Transactional sync** — state only updates on success, never left in a broken state
|
|
24
|
+
- **Fixed cost parsing** — missing pricing no longer shows as "free"
|
|
25
|
+
- **Auth detection fix** — works with both env vars and auth.json
|
|
26
|
+
- **Fetch timeouts** — 15s timeout prevents hanging on OpenRouter API issues
|
|
27
|
+
- **HTTP-Referer / X-Title headers** — proper app identification with OpenRouter
|
|
28
|
+
|
|
13
29
|
## Features
|
|
14
30
|
|
|
15
31
|
- Loads the latest OpenRouter model list into pi in real time
|
|
16
32
|
- Keeps startup behavior fast by default
|
|
17
33
|
- Adds provider-specific variants on demand
|
|
18
|
-
- Adds quantization-specific variants for
|
|
34
|
+
- Adds quantization-specific variants for chosen models
|
|
19
35
|
- Routes enriched selections through OpenRouter provider routing
|
|
20
|
-
-
|
|
36
|
+
- Shows endpoint health: status, uptime, latency, throughput, caching support
|
|
37
|
+
- Displays credit balance and usage statistics
|
|
38
|
+
- Interactive model selection with searchable picker
|
|
39
|
+
- Tab-completes model IDs for all commands
|
|
21
40
|
|
|
22
41
|
## Install
|
|
23
42
|
|
|
@@ -81,12 +100,7 @@ Using `~/.pi/agent/auth.json`:
|
|
|
81
100
|
}
|
|
82
101
|
```
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- Environment variable: `OPENROUTER_API_KEY`
|
|
87
|
-
- `auth.json` key: `openrouter`
|
|
88
|
-
|
|
89
|
-
After the key is available, this extension automatically syncs the latest plain OpenRouter model list at session start.
|
|
103
|
+
After the key is available, this extension automatically syncs the latest OpenRouter model list at session start.
|
|
90
104
|
|
|
91
105
|
### 3) Try without installing
|
|
92
106
|
|
|
@@ -102,10 +116,19 @@ pi -e git:github.com/olixis/pi-openrouter-plus
|
|
|
102
116
|
|
|
103
117
|
## Commands
|
|
104
118
|
|
|
105
|
-
|
|
106
|
-
|
|
119
|
+
| Command | Description |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `/openrouter-sync` | Fetch latest OpenRouter models and restore the plain model list |
|
|
122
|
+
| `/openrouter-enrich <model-id>` | Add provider/quantization variants for one model |
|
|
123
|
+
| `/openrouter-enrich` | Search → pick a model interactively (no args) |
|
|
124
|
+
| `/openrouter-preview <model-id>` | Preview endpoint variants with health data (read-only) |
|
|
125
|
+
| `/openrouter-preview` | Search → pick a model to preview (no args) |
|
|
126
|
+
| `/openrouter-balance` | Show credit balance, remaining funds, and usage breakdown |
|
|
127
|
+
| `/openrouter-status` | Show extension state: model count, enrichments, cache age |
|
|
107
128
|
|
|
108
|
-
##
|
|
129
|
+
## Examples
|
|
130
|
+
|
|
131
|
+
### Enrich a model
|
|
109
132
|
|
|
110
133
|
```bash
|
|
111
134
|
/openrouter-enrich kwaipilot/kat-coder-pro-v2
|
|
@@ -116,13 +139,45 @@ This keeps the normal OpenRouter catalog and adds variants like:
|
|
|
116
139
|
- `StreamLake — Kwaipilot: KAT-Coder-Pro V2`
|
|
117
140
|
- `AtlasCloud · fp8 — Kwaipilot: KAT-Coder-Pro V2`
|
|
118
141
|
|
|
142
|
+
### Preview endpoints before enriching
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
/openrouter-preview deepseek/deepseek-r1
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Shows provider variants with pricing and health data:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
DeepSeek: DeepSeek R1 (deepseek/deepseek-r1)
|
|
152
|
+
8 endpoints across 5 provider/quantization variants:
|
|
153
|
+
|
|
154
|
+
• DeepInfra — $0.55/M in · $2.19/M out · ✅ healthy · uptime: 99% · TTFT: 450ms · 85 tok/s
|
|
155
|
+
• DeepSeek — $0.55/M in · $2.19/M out · ✅ healthy · uptime: 100% · TTFT: 320ms · 120 tok/s · 📦 caching
|
|
156
|
+
• Fireworks · fp8 — $0.60/M in · $2.40/M out · ⚠️ degraded · uptime: 95% · TTFT: 600ms · 60 tok/s
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Check your balance
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
/openrouter-balance
|
|
163
|
+
```
|
|
164
|
+
|
|
119
165
|
## Behavior
|
|
120
166
|
|
|
121
|
-
- After the extension is installed and OpenRouter auth is configured, each new pi session syncs the latest
|
|
122
|
-
- Enrichment is
|
|
167
|
+
- After the extension is installed and OpenRouter auth is configured, each new pi session syncs the latest OpenRouter model list automatically
|
|
168
|
+
- Enrichment is intentionally simple: you enrich one selected model at a time
|
|
123
169
|
- Quantization variants are exposed as separate model choices when available
|
|
124
170
|
- Enriched variants are translated into OpenRouter provider routing fields at request time
|
|
125
171
|
- If you want to refresh manually or go back to the default list, run `/openrouter-sync`
|
|
172
|
+
- Preview output also includes search-related model info (id, name, terms, description) plus pricing and endpoint health
|
|
173
|
+
|
|
174
|
+
## Architecture (v0.3.0 improvements)
|
|
175
|
+
|
|
176
|
+
- **Snapshot-based routing** — the stream factory captures a frozen route map at registration time, eliminating race conditions when syncing
|
|
177
|
+
- **Generation counter** — overlapping sync calls are safely discarded if a newer sync has started
|
|
178
|
+
- **Transactional state** — caches are not cleared before fetch; state only commits on success
|
|
179
|
+
- **Auth-keyed caching** — model cache invalidates when the API key changes
|
|
180
|
+
- **Fetch timeouts** — all OpenRouter API calls have a 15-second timeout via AbortController
|
|
126
181
|
|
|
127
182
|
## Development
|
|
128
183
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OPENROUTER_MODELS_URL,
|
|
3
|
+
OPENROUTER_BASE_URL,
|
|
4
|
+
CACHE_TTL_MS,
|
|
5
|
+
FETCH_TIMEOUT_MS,
|
|
6
|
+
type OpenRouterModel,
|
|
7
|
+
type OpenRouterEndpoint,
|
|
8
|
+
type OpenRouterEndpointsResponse,
|
|
9
|
+
type OpenRouterKeyInfo,
|
|
10
|
+
type OpenRouterCreditsInfo,
|
|
11
|
+
type EndpointCacheEntry,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
let cachedModels: OpenRouterModel[] | null = null;
|
|
15
|
+
let cacheTimestamp = 0;
|
|
16
|
+
let cachedApiKeyHash = "";
|
|
17
|
+
const endpointCache = new Map<string, EndpointCacheEntry>();
|
|
18
|
+
|
|
19
|
+
function hashKey(key?: string): string {
|
|
20
|
+
if (!key) return "";
|
|
21
|
+
return key.slice(0, 8) + key.slice(-4);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeHeaders(apiKey?: string): Record<string, string> {
|
|
25
|
+
const headers: Record<string, string> = {};
|
|
26
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
27
|
+
return headers;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function fetchWithTimeout(url: string, options: RequestInit = {}): Promise<Response> {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
33
|
+
try {
|
|
34
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
35
|
+
} finally {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatFetchError(res: Response, context: string): Error {
|
|
41
|
+
const status = res.status;
|
|
42
|
+
let hint = "";
|
|
43
|
+
if (status === 401 || status === 403) {
|
|
44
|
+
hint = " — check your OpenRouter API key";
|
|
45
|
+
} else if (status === 429) {
|
|
46
|
+
hint = " — rate limited, try again shortly";
|
|
47
|
+
} else if (status >= 500) {
|
|
48
|
+
hint = " — OpenRouter is having issues, try again later";
|
|
49
|
+
}
|
|
50
|
+
return new Error(`${context}: ${status} ${res.statusText}${hint}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function invalidateModelCache(): void {
|
|
54
|
+
cachedModels = null;
|
|
55
|
+
cacheTimestamp = 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function invalidateEndpointCache(modelId?: string): void {
|
|
59
|
+
if (modelId) {
|
|
60
|
+
endpointCache.delete(modelId);
|
|
61
|
+
} else {
|
|
62
|
+
endpointCache.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function invalidateAllCaches(): void {
|
|
67
|
+
invalidateModelCache();
|
|
68
|
+
invalidateEndpointCache();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function fetchModels(apiKey?: string, force = false): Promise<OpenRouterModel[]> {
|
|
72
|
+
const keyHash = hashKey(apiKey);
|
|
73
|
+
if (keyHash !== cachedApiKeyHash) {
|
|
74
|
+
cachedModels = null;
|
|
75
|
+
cacheTimestamp = 0;
|
|
76
|
+
cachedApiKeyHash = keyHash;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
if (!force && cachedModels && now - cacheTimestamp < CACHE_TTL_MS) {
|
|
81
|
+
return cachedModels;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const res = await fetchWithTimeout(OPENROUTER_MODELS_URL, {
|
|
85
|
+
headers: makeHeaders(apiKey),
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) throw formatFetchError(res, "OpenRouter models API");
|
|
88
|
+
|
|
89
|
+
const json = (await res.json()) as { data?: OpenRouterModel[] };
|
|
90
|
+
cachedModels = json.data || [];
|
|
91
|
+
cacheTimestamp = now;
|
|
92
|
+
return cachedModels;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildEndpointsUrl(modelId: string): string {
|
|
96
|
+
const path = modelId
|
|
97
|
+
.split("/")
|
|
98
|
+
.map((part) => encodeURIComponent(part))
|
|
99
|
+
.join("/");
|
|
100
|
+
return `${OPENROUTER_BASE_URL}/models/${path}/endpoints`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function fetchModelEndpoints(
|
|
104
|
+
modelId: string,
|
|
105
|
+
apiKey?: string,
|
|
106
|
+
force = false,
|
|
107
|
+
): Promise<OpenRouterEndpoint[]> {
|
|
108
|
+
const cached = endpointCache.get(modelId);
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
if (!force && cached && now - cached.timestamp < CACHE_TTL_MS) {
|
|
111
|
+
return cached.endpoints;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const res = await fetchWithTimeout(buildEndpointsUrl(modelId), {
|
|
115
|
+
headers: makeHeaders(apiKey),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (res.status === 404) {
|
|
119
|
+
endpointCache.set(modelId, { timestamp: now, endpoints: [] });
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
if (!res.ok) throw formatFetchError(res, "OpenRouter endpoints API");
|
|
123
|
+
|
|
124
|
+
const json = (await res.json()) as OpenRouterEndpointsResponse;
|
|
125
|
+
const endpoints = json.data?.endpoints || [];
|
|
126
|
+
endpointCache.set(modelId, { timestamp: now, endpoints });
|
|
127
|
+
return endpoints;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function fetchKeyInfo(apiKey: string): Promise<OpenRouterKeyInfo> {
|
|
131
|
+
const res = await fetchWithTimeout(`${OPENROUTER_BASE_URL}/key`, {
|
|
132
|
+
headers: makeHeaders(apiKey),
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok) throw formatFetchError(res, "OpenRouter key API");
|
|
135
|
+
|
|
136
|
+
const json = (await res.json()) as { data?: OpenRouterKeyInfo };
|
|
137
|
+
return json.data || {};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function fetchCredits(apiKey: string): Promise<OpenRouterCreditsInfo | null> {
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetchWithTimeout(`${OPENROUTER_BASE_URL}/credits`, {
|
|
143
|
+
headers: makeHeaders(apiKey),
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok) return null; // requires management key, may fail with regular key
|
|
146
|
+
const json = (await res.json()) as { data?: OpenRouterCreditsInfo };
|
|
147
|
+
return json.data || null;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function getCachedModels(): OpenRouterModel[] | null {
|
|
154
|
+
return cachedModels;
|
|
155
|
+
}
|