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 CHANGED
@@ -1,8 +1,8 @@
1
1
  ![Preview](https://raw.githubusercontent.com/olixis/pi-openrouter-plus/main/assets/preview.png)
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, keeps the default model list simple, and lets you enrich a specific model with provider and quantization variants.
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 a chosen model
34
+ - Adds quantization-specific variants for chosen models
19
35
  - Routes enriched selections through OpenRouter provider routing
20
- - Enriches one model at a time to avoid slow full-catalog endpoint scans
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
- Pi's official provider docs use:
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
- - `/openrouter-sync` fetch the latest OpenRouter model list in real time
106
- - `/openrouter-enrich <model-id>` — add provider and quantization variants for one specific OpenRouter model
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
- ## Example
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 plain OpenRouter model list from OpenRouter automatically
122
- - Enrichment is manual and targeted to one model at a time
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
+ }