@xynogen/pix-9router 0.2.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-9router",
3
- "version": "0.2.0",
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 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.
@@ -43,14 +43,14 @@ test("renderResult dims every line", () => {
43
43
  test("renderResult caps preview when not expanded", () => {
44
44
  const stub = stubComponent();
45
45
  const rr = makeRenderResult();
46
- const body = Array.from({ length: 45 }, (_, i) => `L${i}`).join("\n");
46
+ const body = Array.from({ length: 37 }, (_, i) => `L${i}`).join("\n");
47
47
  rr(
48
48
  { content: [{ type: "text", text: body }] } as never,
49
49
  { expanded: false, isPartial: false },
50
50
  theme,
51
51
  { lastComponent: stub.component as never, isError: false },
52
52
  );
53
- expect(stub.get()).toContain("… 5 more lines");
53
+ expect(stub.get()).toContain("… 5 more lines"); // 37 lines − 32 cap
54
54
  });
55
55
 
56
56
  test("renderResult renders empty body marker", () => {
package/src/render.ts CHANGED
@@ -33,7 +33,7 @@ type TextCtor = new (text?: string, padX?: number, padY?: number) => TextLike;
33
33
  // Preview cap is intentionally generous — dimming already de-emphasises the
34
34
  // body, so we only truncate to keep pathological multi-thousand-line dumps
35
35
  // from flooding the viewport. Expanding shows everything.
36
- const MAX_PREVIEW_LINES = 40;
36
+ const MAX_PREVIEW_LINES = 32;
37
37
 
38
38
  let TextComponent: TextCtor | undefined;
39
39
  try {