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 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.2
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,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 a chosen model
52
+ - Adds quantization-specific variants for chosen models
19
53
  - Routes enriched selections through OpenRouter provider routing
20
- - Enriches one model at a time to avoid slow full-catalog endpoint scans
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
- 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.
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
- - `/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
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
- ## Example
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 plain OpenRouter model list from OpenRouter automatically
122
- - Enrichment is manual and targeted to one model at a time
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
+ }