@xynogen/pix-9router 0.2.1 → 0.2.3

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.
Files changed (3) hide show
  1. package/README.md +14 -4
  2. package/package.json +2 -1
  3. package/src/data.ts +18 -211
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pix-9router
2
2
 
3
- Pi coding agent extension — **9Router provider** + **fetch** and **search** tools backed by an internal OpenAI-compatible router API.
3
+ Pi coding agent extension — **9Router provider** + **fetch**, **search**, and **transcribe** tools backed by an internal OpenAI-compatible router API.
4
4
 
5
5
  ## What's included
6
6
 
@@ -9,11 +9,12 @@ Pi coding agent extension — **9Router provider** + **fetch** and **search** to
9
9
  | `provider` | provider | Registers `9router` provider with live model list from the router API |
10
10
  | `fetch` | tool | `fetch(url, format, max_characters?)` — fetches web pages via exa through the router, falls back to curl |
11
11
  | `search` | tool | `search(query, search_type, max_results?)` — web/news search via exa through the router, falls back to curl |
12
+ | `transcribe` | tool | `transcribe(file, model?, language?)` — speech-to-text via `/audio/transcriptions` endpoint (default: `dg/nova-3`), falls back to curl |
12
13
 
13
14
  ## Install
14
15
 
15
16
  ```bash
16
- pi install git:github.com/xynogen/pix-9router
17
+ pi install npm:@xynogen/pix-9router
17
18
  ```
18
19
 
19
20
  ## Environment
@@ -32,8 +33,17 @@ export ROUTER_API_KEY="your-key-here"
32
33
 
33
34
  ## How it works
34
35
 
35
- - **Provider**: on load, fetches `/models` from the router and registers them with Pi. [models.dev](https://models.dev/api.json) is used internally to fill missing context window / modality fields where the router response omits them. Model list is cached at `~/.cache/pi/9router.json` (TTL 1h).
36
- - **fetch / search**: POST to `/web/fetch` and `/search` on the router (which proxies to exa). If the router is unreachable, falls back to raw `curl`.
36
+ - **Provider**: on load, fetches `/models` from the router and registers them with Pi. [models.dev](https://models.dev/api.json) is used internally to fill missing context window / modality fields where the router response omits them. Model list is cached at `~/.cache/pi/9router.json` (TTL 30 min).
37
+ - **fetch / search**: POST to `/web/fetch` and `/search` on the router (which proxies to exa). If the router is unreachable, falls back to raw `curl`. Tool output is rendered dimmed so fetched web content reads like faded context rather than primary output.
38
+ - **transcribe**: POST to `/audio/transcriptions` (Deepgram Nova 3 by default). Accepts any audio file path; falls back to curl.
39
+
40
+ ## Full distro
41
+
42
+ To install the complete pix suite (all packages + Pi itself):
43
+
44
+ ```bash
45
+ curl -fsSL https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.sh | sh
46
+ ```
37
47
 
38
48
  ## License
39
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-9router",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Pi extension — 9Router provider + fetch/search tools via router API",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -42,6 +42,7 @@
42
42
  "@earendil-works/pi-ai": "*"
43
43
  },
44
44
  "dependencies": {
45
+ "@xynogen/pix-data": "*",
45
46
  "typebox": "^1.1.38"
46
47
  }
47
48
  }
package/src/data.ts CHANGED
@@ -5,18 +5,27 @@
5
5
  * - routerBaseUrl() — resolved API base URL from env
6
6
  * - routerModels — DataSource<RouterModel[]>, cached 30m at ~/.cache/pi/9router.json
7
7
  *
8
- * models.dev + BenchLM data is provided by pix-data (github.com/xynogen/pix-data),
9
- * which shares the same ~/.cache/pi/ cache directory.
8
+ * models.dev + BenchLM data is provided by @xynogen/pix-data
9
+ * (github.com/xynogen/pix-mono/tree/main/packages/pix-data), which shares the
10
+ * same ~/.cache/pi/ cache directory. The DataSource implementation and the
11
+ * models.dev index helpers live there — imported here, not duplicated.
10
12
  *
11
13
  * Environment:
12
14
  * ROUTER_API_BASE — override base URL (default: https://9router.example.com/v1)
13
15
  * ROUTER_API_KEY — bearer token (required)
14
16
  */
15
17
 
16
- import { existsSync, readFileSync } from "node:fs";
17
- import { mkdir, readFile, writeFile } from "node:fs/promises";
18
- import { homedir } from "node:os";
19
- import { dirname, join } from "node:path";
18
+ import { join } from "node:path";
19
+ import { CACHE_DIR, DataSource } from "../../pix-data/src/index.ts";
20
+
21
+ export type { ModelsDevApi, ModelsDevModel } from "../../pix-data/src/index.ts";
22
+ // Re-export the shared models.dev API so existing consumers (provider.ts)
23
+ // can keep importing these names from "./data".
24
+ export {
25
+ buildModelsDevIndex,
26
+ fetchModelsDevIndex,
27
+ lookupInIndex,
28
+ } from "../../pix-data/src/index.ts";
20
29
 
21
30
  // ── Types ─────────────────────────────────────────────────────────────────────
22
31
 
@@ -30,160 +39,12 @@ export interface RouterModel {
30
39
  owned_by?: string;
31
40
  }
32
41
 
33
- // Re-export ModelsDevModel for provider.ts — sourced from pix-data at runtime
34
- // via shared cache; typed here to avoid a hard import dependency.
35
- export interface ModelsDevModel {
36
- id: string;
37
- name?: string;
38
- reasoning?: boolean;
39
- modalities?: { input?: string[]; output?: string[] };
40
- limit?: { context?: number; output?: number };
41
- cost?: {
42
- input?: number;
43
- output?: number;
44
- cache_read?: number;
45
- cache_write?: number;
46
- };
47
- }
48
-
49
- export type ModelsDevApi = Record<
50
- string,
51
- { models?: Record<string, ModelsDevModel> }
52
- >;
53
-
54
42
  interface RouterModelsResponse {
55
43
  data?: RouterModel[];
56
44
  }
57
45
 
58
- // ── DataSource (minimal, router-only) ────────────────────────────────────────
59
-
60
- interface DataSourceOptions<T> {
61
- url: string | (() => string);
62
- headers?: () => Record<string, string> | undefined;
63
- cachePath: string;
64
- ttlMs?: number;
65
- timeoutMs?: number;
66
- parse: (raw: unknown) => T;
67
- parseCache: (data: unknown) => T;
68
- empty: T;
69
- label: string;
70
- skip?: () => boolean;
71
- }
72
-
73
- class DataSource<T> {
74
- private _mem: T | null = null;
75
- private _inflight: Promise<T> | null = null;
76
- private readonly opts: Required<DataSourceOptions<T>>;
77
-
78
- constructor(opts: DataSourceOptions<T>) {
79
- this.opts = {
80
- ttlMs: 24 * 60 * 60 * 1000,
81
- timeoutMs: 10_000,
82
- headers: () => undefined,
83
- skip: () => false,
84
- ...opts,
85
- };
86
- }
87
-
88
- async get(): Promise<T> {
89
- if (this._inflight) return this._inflight;
90
- this._inflight = this._load().finally(() => {
91
- this._inflight = null;
92
- });
93
- return this._inflight;
94
- }
95
-
96
- getCached(): T {
97
- if (this._mem) return this._mem;
98
- try {
99
- if (existsSync(this.opts.cachePath)) {
100
- const raw = JSON.parse(readFileSync(this.opts.cachePath, "utf-8")) as {
101
- data: unknown;
102
- };
103
- this._mem = this.opts.parseCache(raw.data);
104
- return this._mem;
105
- }
106
- } catch {
107
- // No cache file or parse error — return empty
108
- }
109
- return this.opts.empty;
110
- }
111
-
112
- private async _load(): Promise<T> {
113
- if (this.opts.skip()) {
114
- this._mem = this.opts.empty;
115
- return this.opts.empty;
116
- }
117
- const cached = await this._readCache();
118
- if (cached !== undefined && Date.now() - cached.ts < this.opts.ttlMs) {
119
- const val = this.opts.parseCache(cached.data);
120
- this._mem = val;
121
- return val;
122
- }
123
- try {
124
- const url =
125
- typeof this.opts.url === "function" ? this.opts.url() : this.opts.url;
126
- const controller = new AbortController();
127
- const timer = setTimeout(() => controller.abort(), this.opts.timeoutMs);
128
- const response = await fetch(url, {
129
- signal: controller.signal,
130
- headers: this.opts.headers(),
131
- }).finally(() => clearTimeout(timer));
132
- if (!response.ok)
133
- throw new Error(`${this.opts.label} fetch failed: ${response.status}`);
134
- const raw = await response.json();
135
- const val = this.opts.parse(raw);
136
- this._mem = val;
137
- void this._writeCache(raw);
138
- return val;
139
- } catch (error) {
140
- const msg = error instanceof Error ? error.message : String(error);
141
- if (cached !== undefined) {
142
- console.warn(
143
- `${this.opts.label} fetch failed, using stale cache: ${msg}`,
144
- );
145
- const val = this.opts.parseCache(cached.data);
146
- this._mem = val;
147
- return val;
148
- }
149
- console.warn(`${this.opts.label} unavailable: ${msg}`);
150
- return this.opts.empty;
151
- }
152
- }
153
-
154
- private async _readCache(): Promise<
155
- { ts: number; data: unknown } | undefined
156
- > {
157
- try {
158
- const raw = await readFile(this.opts.cachePath, "utf8");
159
- const parsed = JSON.parse(raw) as { ts: number; data: unknown };
160
- if (typeof parsed.ts !== "number") return undefined;
161
- return parsed;
162
- } catch {
163
- return undefined;
164
- }
165
- }
166
-
167
- private async _writeCache(data: unknown): Promise<void> {
168
- try {
169
- await mkdir(dirname(this.opts.cachePath), { recursive: true });
170
- await writeFile(
171
- this.opts.cachePath,
172
- JSON.stringify({ ts: Date.now(), data }),
173
- );
174
- } catch {
175
- // Write failure is non-fatal
176
- }
177
- }
178
- }
179
-
180
46
  // ── Constants ──────────────────────────────────────────────────────────────────
181
47
 
182
- const CACHE_DIR = join(
183
- process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
184
- "pi",
185
- );
186
-
187
48
  const ROUTER_DEFAULT_BASE = "https://9router.example.com/v1";
188
49
 
189
50
  export function routerBaseUrl(): string {
@@ -215,60 +76,6 @@ export const routerModels = new DataSource<RouterModel[]>({
215
76
  });
216
77
 
217
78
  // ── models.dev index (read from pix-data shared cache) ────────────────────────
218
-
219
- const modelsDevCache = new DataSource<ModelsDevApi>({
220
- label: "models.dev",
221
- url: "https://models.dev/api.json",
222
- cachePath: join(CACHE_DIR, "models.json"),
223
- parse: (raw) => raw as ModelsDevApi,
224
- parseCache: (data) => (data as ModelsDevApi) ?? {},
225
- empty: {},
226
- });
227
-
228
- function normalize(id: string): string {
229
- return id
230
- .toLowerCase()
231
- .replace(/[:@].*$/, "")
232
- .replace(/-\d{8}$/, "");
233
- }
234
-
235
- function stripPrefix(id: string): string {
236
- const i = id.lastIndexOf("/");
237
- return i >= 0 ? id.slice(i + 1) : id;
238
- }
239
-
240
- export function buildModelsDevIndex(
241
- api: ModelsDevApi,
242
- ): Map<string, ModelsDevModel> {
243
- const index = new Map<string, ModelsDevModel>();
244
- for (const provider of Object.values(api)) {
245
- if (!provider?.models) continue;
246
- for (const [modelId, model] of Object.entries(provider.models)) {
247
- const m: ModelsDevModel = { ...model, id: modelId };
248
- if (!index.has(modelId)) index.set(modelId, m);
249
- const norm = normalize(modelId);
250
- if (!index.has(norm)) index.set(norm, m);
251
- }
252
- }
253
- return index;
254
- }
255
-
256
- export function lookupInIndex(
257
- id: string,
258
- index: Map<string, ModelsDevModel>,
259
- ): ModelsDevModel | undefined {
260
- const stripped = stripPrefix(id);
261
- const direct = index.get(stripped) ?? index.get(normalize(stripped));
262
- if (direct) return direct;
263
- const norm = normalize(stripped);
264
- for (const [key, model] of index) {
265
- if (key.startsWith(norm) || norm.startsWith(key)) return model;
266
- }
267
- return undefined;
268
- }
269
-
270
- export async function fetchModelsDevIndex(): Promise<
271
- Map<string, ModelsDevModel>
272
- > {
273
- return buildModelsDevIndex(await modelsDevCache.get());
274
- }
79
+ // fetchModelsDevIndex / buildModelsDevIndex / lookupInIndex are re-exported
80
+ // above directly from @xynogen/pix-data — they read the same shared
81
+ // ~/.cache/pi/models.json cache that pix-data warms.