pi-openrouter-realtime 0.2.2 → 0.3.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 +88 -15
- package/extensions/openrouter-routing/api.ts +155 -0
- package/extensions/openrouter-routing/index.ts +358 -380
- 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.2
|
|
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,51 @@ Npm package:
|
|
|
10
10
|
|
|
11
11
|
- `pi-openrouter-realtime`
|
|
12
12
|
|
|
13
|
+
## What's New in v0.3.2
|
|
14
|
+
|
|
15
|
+
- **Context-safe info messages** — OpenRouter info panels still display in the UI, but are filtered out before LLM requests
|
|
16
|
+
- **Lower token waste** — `/openrouter-preview`, `/openrouter-balance`, and `/openrouter-status` no longer consume context window space unnecessarily
|
|
17
|
+
- **Less prompt contamination** — read-only extension output no longer gets echoed back into future model turns unless you explicitly include it
|
|
18
|
+
|
|
19
|
+
How it works:
|
|
20
|
+
|
|
21
|
+
- The extension still emits `openrouter-info` messages so you can see rich output in-session
|
|
22
|
+
- Before each LLM call, a `context` hook removes those `openrouter-info` custom messages from the message list
|
|
23
|
+
- Result: visible UX for humans, but no extra prompt baggage for the model
|
|
24
|
+
|
|
25
|
+
## What's New in v0.3.1
|
|
26
|
+
|
|
27
|
+
- **Fixed variant counting** — enriched variants are no longer presented as both base models and `+N variants`
|
|
28
|
+
- **Clearer totals** — status/output now distinguishes total registered models from variant count
|
|
29
|
+
- **Less intrusive account output** — removed the key label / redacted API-key style line from account/status output
|
|
30
|
+
|
|
31
|
+
## What's New in v0.3.0
|
|
32
|
+
|
|
33
|
+
- **Targeted enrichment** — enrich one model on demand without scanning the whole catalog
|
|
34
|
+
- **Interactive model picker** — run `/openrouter-enrich` without args → type a search query → pick from filtered results
|
|
35
|
+
- **Tab-completion** — autocomplete model IDs when typing commands
|
|
36
|
+
- **`/openrouter-preview`** — inspect provider variants and endpoint health without changing your model list
|
|
37
|
+
- **`/openrouter-balance`** — check your OpenRouter credit balance and usage
|
|
38
|
+
- **`/openrouter-status`** — see current extension state, active enrichments, cache age
|
|
39
|
+
- **Endpoint health data** — status, uptime, latency (TTFT), throughput per variant
|
|
40
|
+
- **Snapshot-based routing** — eliminates race conditions with stale route maps
|
|
41
|
+
- **Transactional sync** — state only updates on success, never left in a broken state
|
|
42
|
+
- **Fixed cost parsing** — missing pricing no longer shows as "free"
|
|
43
|
+
- **Auth detection fix** — works with both env vars and auth.json
|
|
44
|
+
- **Fetch timeouts** — 15s timeout prevents hanging on OpenRouter API issues
|
|
45
|
+
- **HTTP-Referer / X-Title headers** — proper app identification with OpenRouter
|
|
46
|
+
|
|
13
47
|
## Features
|
|
14
48
|
|
|
15
49
|
- Loads the latest OpenRouter model list into pi in real time
|
|
16
50
|
- Keeps startup behavior fast by default
|
|
17
51
|
- Adds provider-specific variants on demand
|
|
18
|
-
- Adds quantization-specific variants for
|
|
52
|
+
- Adds quantization-specific variants for chosen models
|
|
19
53
|
- Routes enriched selections through OpenRouter provider routing
|
|
20
|
-
-
|
|
54
|
+
- Shows endpoint health: status, uptime, latency, throughput, caching support
|
|
55
|
+
- Displays credit balance and usage statistics
|
|
56
|
+
- Interactive model selection with searchable picker
|
|
57
|
+
- Tab-completes model IDs for all commands
|
|
21
58
|
|
|
22
59
|
## Install
|
|
23
60
|
|
|
@@ -81,12 +118,7 @@ Using `~/.pi/agent/auth.json`:
|
|
|
81
118
|
}
|
|
82
119
|
```
|
|
83
120
|
|
|
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.
|
|
121
|
+
After the key is available, this extension automatically syncs the latest OpenRouter model list at session start.
|
|
90
122
|
|
|
91
123
|
### 3) Try without installing
|
|
92
124
|
|
|
@@ -102,10 +134,19 @@ pi -e git:github.com/olixis/pi-openrouter-plus
|
|
|
102
134
|
|
|
103
135
|
## Commands
|
|
104
136
|
|
|
105
|
-
|
|
106
|
-
|
|
137
|
+
| Command | Description |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `/openrouter-sync` | Fetch latest OpenRouter models and restore the plain model list |
|
|
140
|
+
| `/openrouter-enrich <model-id>` | Add provider/quantization variants for one model |
|
|
141
|
+
| `/openrouter-enrich` | Search → pick a model interactively (no args) |
|
|
142
|
+
| `/openrouter-preview <model-id>` | Preview endpoint variants with health data (read-only) |
|
|
143
|
+
| `/openrouter-preview` | Search → pick a model to preview (no args) |
|
|
144
|
+
| `/openrouter-balance` | Show credit balance, remaining funds, and usage breakdown |
|
|
145
|
+
| `/openrouter-status` | Show extension state: model count, enrichments, cache age |
|
|
107
146
|
|
|
108
|
-
##
|
|
147
|
+
## Examples
|
|
148
|
+
|
|
149
|
+
### Enrich a model
|
|
109
150
|
|
|
110
151
|
```bash
|
|
111
152
|
/openrouter-enrich kwaipilot/kat-coder-pro-v2
|
|
@@ -116,13 +157,45 @@ This keeps the normal OpenRouter catalog and adds variants like:
|
|
|
116
157
|
- `StreamLake — Kwaipilot: KAT-Coder-Pro V2`
|
|
117
158
|
- `AtlasCloud · fp8 — Kwaipilot: KAT-Coder-Pro V2`
|
|
118
159
|
|
|
160
|
+
### Preview endpoints before enriching
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
/openrouter-preview deepseek/deepseek-r1
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Shows provider variants with pricing and health data:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
DeepSeek: DeepSeek R1 (deepseek/deepseek-r1)
|
|
170
|
+
8 endpoints across 5 provider/quantization variants:
|
|
171
|
+
|
|
172
|
+
• DeepInfra — $0.55/M in · $2.19/M out · ✅ healthy · uptime: 99% · TTFT: 450ms · 85 tok/s
|
|
173
|
+
• DeepSeek — $0.55/M in · $2.19/M out · ✅ healthy · uptime: 100% · TTFT: 320ms · 120 tok/s · 📦 caching
|
|
174
|
+
• Fireworks · fp8 — $0.60/M in · $2.40/M out · ⚠️ degraded · uptime: 95% · TTFT: 600ms · 60 tok/s
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Check your balance
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
/openrouter-balance
|
|
181
|
+
```
|
|
182
|
+
|
|
119
183
|
## Behavior
|
|
120
184
|
|
|
121
|
-
- After the extension is installed and OpenRouter auth is configured, each new pi session syncs the latest
|
|
122
|
-
- Enrichment is
|
|
185
|
+
- After the extension is installed and OpenRouter auth is configured, each new pi session syncs the latest OpenRouter model list automatically
|
|
186
|
+
- Enrichment is intentionally simple: you enrich one selected model at a time
|
|
123
187
|
- Quantization variants are exposed as separate model choices when available
|
|
124
188
|
- Enriched variants are translated into OpenRouter provider routing fields at request time
|
|
125
189
|
- If you want to refresh manually or go back to the default list, run `/openrouter-sync`
|
|
190
|
+
- Preview output also includes search-related model info (id, name, terms, description) plus pricing and endpoint health
|
|
191
|
+
|
|
192
|
+
## Architecture (v0.3.x improvements)
|
|
193
|
+
|
|
194
|
+
- **Snapshot-based routing** — the stream factory captures a frozen route map at registration time, eliminating race conditions when syncing
|
|
195
|
+
- **Generation counter** — overlapping sync calls are safely discarded if a newer sync has started
|
|
196
|
+
- **Transactional state** — caches are not cleared before fetch; state only commits on success
|
|
197
|
+
- **Auth-keyed caching** — model cache invalidates when the API key changes
|
|
198
|
+
- **Fetch timeouts** — all OpenRouter API calls have a 15-second timeout via AbortController
|
|
126
199
|
|
|
127
200
|
## Development
|
|
128
201
|
|
|
@@ -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
|
+
}
|