@xynogen/pix-9router 0.2.1 → 0.2.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/package.json +2 -1
- package/src/data.ts +18 -211
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-9router",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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.
|