@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.
- package/README.md +14 -4
- package/package.json +2 -1
- 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 **
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
9
|
-
* which shares the
|
|
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 {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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.
|