@xynogen/pix-data 0.1.0 → 0.1.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 +9 -1
- package/package.json +42 -34
- package/src/data.test.ts +76 -23
- package/src/data.ts +31 -9
- package/src/index.ts +21 -1
package/README.md
CHANGED
|
@@ -19,13 +19,21 @@ Pi coding agent extension — shared model data layer. Fetches and caches [model
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
pi install
|
|
22
|
+
pi install npm:@xynogen/pix-data
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
## How it works
|
|
26
26
|
|
|
27
27
|
On session start the extension fires two parallel background fetches (`modelsDev.get()` + `benchmark.get()`). If the cache is fresh the fetches are skipped. Both cache files live in `~/.cache/pi/` — any Pi extension using the same `DataSource` + cache paths will share data automatically.
|
|
28
28
|
|
|
29
|
+
## Full distro
|
|
30
|
+
|
|
31
|
+
To install the complete pix suite (all packages + Pi itself):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
curl -fsSL https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.sh | sh
|
|
35
|
+
```
|
|
36
|
+
|
|
29
37
|
## License
|
|
30
38
|
|
|
31
39
|
MIT
|
package/package.json
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
2
|
+
"name": "@xynogen/pix-data",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Pi extension — shared model data layer (models.dev + BenchLM), cached at ~/.cache/pi",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "bun test"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"pi": {
|
|
16
|
+
"extensions": [
|
|
17
|
+
"src/index.ts"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"pi",
|
|
22
|
+
"pi-package",
|
|
23
|
+
"pi-extension",
|
|
24
|
+
"models.dev",
|
|
25
|
+
"benchlm"
|
|
26
|
+
],
|
|
27
|
+
"author": "xynogen",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/xynogen/pix-mono.git",
|
|
32
|
+
"directory": "packages/pix-data"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/xynogen/pix-mono/tree/main/packages/pix-data#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/xynogen/pix-mono/issues"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
43
|
+
}
|
|
36
44
|
}
|
package/src/data.test.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
type BenchmarkEntry,
|
|
4
|
+
benchmark,
|
|
3
5
|
buildModelsDevIndex,
|
|
6
|
+
lookupBenchmark,
|
|
4
7
|
lookupInIndex,
|
|
5
8
|
lookupModelsDev,
|
|
6
|
-
lookupBenchmark,
|
|
7
|
-
modelsDev,
|
|
8
|
-
benchmark,
|
|
9
9
|
type ModelsDevApi,
|
|
10
10
|
type ModelsDevModel,
|
|
11
|
-
|
|
11
|
+
modelsDev,
|
|
12
12
|
} from "./data.ts";
|
|
13
13
|
|
|
14
14
|
// ── buildModelsDevIndex ──────────────────────────────────────────────────────
|
|
@@ -17,13 +17,24 @@ describe("buildModelsDevIndex", () => {
|
|
|
17
17
|
const api: ModelsDevApi = {
|
|
18
18
|
anthropic: {
|
|
19
19
|
models: {
|
|
20
|
-
"claude-sonnet-4-5": {
|
|
21
|
-
|
|
20
|
+
"claude-sonnet-4-5": {
|
|
21
|
+
id: "claude-sonnet-4-5",
|
|
22
|
+
name: "Claude Sonnet 4.5",
|
|
23
|
+
},
|
|
24
|
+
"claude-opus-4": {
|
|
25
|
+
id: "claude-opus-4",
|
|
26
|
+
name: "Claude Opus 4",
|
|
27
|
+
reasoning: true,
|
|
28
|
+
},
|
|
22
29
|
},
|
|
23
30
|
},
|
|
24
31
|
openai: {
|
|
25
32
|
models: {
|
|
26
|
-
"gpt-4o": {
|
|
33
|
+
"gpt-4o": {
|
|
34
|
+
id: "gpt-4o",
|
|
35
|
+
name: "GPT-4o",
|
|
36
|
+
modalities: { input: ["text", "image"] },
|
|
37
|
+
},
|
|
27
38
|
},
|
|
28
39
|
},
|
|
29
40
|
};
|
|
@@ -39,7 +50,10 @@ describe("buildModelsDevIndex", () => {
|
|
|
39
50
|
const a: ModelsDevApi = {
|
|
40
51
|
anthropic: {
|
|
41
52
|
models: {
|
|
42
|
-
"claude-sonnet-4-5-20250514": {
|
|
53
|
+
"claude-sonnet-4-5-20250514": {
|
|
54
|
+
id: "claude-sonnet-4-5-20250514",
|
|
55
|
+
name: "Claude Sonnet 4.5",
|
|
56
|
+
},
|
|
43
57
|
},
|
|
44
58
|
},
|
|
45
59
|
};
|
|
@@ -73,7 +87,10 @@ describe("lookupInIndex", () => {
|
|
|
73
87
|
index = buildModelsDevIndex({
|
|
74
88
|
anthropic: {
|
|
75
89
|
models: {
|
|
76
|
-
"claude-sonnet-4-5": {
|
|
90
|
+
"claude-sonnet-4-5": {
|
|
91
|
+
id: "claude-sonnet-4-5",
|
|
92
|
+
name: "Claude Sonnet 4.5",
|
|
93
|
+
},
|
|
77
94
|
"claude-opus-4": { id: "claude-opus-4", name: "Claude Opus 4" },
|
|
78
95
|
},
|
|
79
96
|
},
|
|
@@ -87,23 +104,33 @@ describe("lookupInIndex", () => {
|
|
|
87
104
|
});
|
|
88
105
|
|
|
89
106
|
it("finds exact match", () => {
|
|
90
|
-
expect(lookupInIndex("claude-sonnet-4-5", index)?.name).toBe(
|
|
107
|
+
expect(lookupInIndex("claude-sonnet-4-5", index)?.name).toBe(
|
|
108
|
+
"Claude Sonnet 4.5",
|
|
109
|
+
);
|
|
91
110
|
});
|
|
92
111
|
|
|
93
112
|
it("strips provider prefix (provider/model)", () => {
|
|
94
|
-
expect(lookupInIndex("anthropic/claude-opus-4", index)?.name).toBe(
|
|
113
|
+
expect(lookupInIndex("anthropic/claude-opus-4", index)?.name).toBe(
|
|
114
|
+
"Claude Opus 4",
|
|
115
|
+
);
|
|
95
116
|
});
|
|
96
117
|
|
|
97
118
|
it("strips deep prefix (cc/model)", () => {
|
|
98
|
-
expect(lookupInIndex("cc/claude-opus-4", index)?.name).toBe(
|
|
119
|
+
expect(lookupInIndex("cc/claude-opus-4", index)?.name).toBe(
|
|
120
|
+
"Claude Opus 4",
|
|
121
|
+
);
|
|
99
122
|
});
|
|
100
123
|
|
|
101
124
|
it("strips date suffix", () => {
|
|
102
|
-
expect(lookupInIndex("claude-sonnet-4-5-20250514", index)?.name).toBe(
|
|
125
|
+
expect(lookupInIndex("claude-sonnet-4-5-20250514", index)?.name).toBe(
|
|
126
|
+
"Claude Sonnet 4.5",
|
|
127
|
+
);
|
|
103
128
|
});
|
|
104
129
|
|
|
105
130
|
it("strips provider prefix + date suffix", () => {
|
|
106
|
-
expect(
|
|
131
|
+
expect(
|
|
132
|
+
lookupInIndex("anthropic/claude-sonnet-4-5-20250514", index)?.name,
|
|
133
|
+
).toBe("Claude Sonnet 4.5");
|
|
107
134
|
});
|
|
108
135
|
|
|
109
136
|
it("returns undefined for unknown model", () => {
|
|
@@ -123,7 +150,10 @@ describe("lookupModelsDev", () => {
|
|
|
123
150
|
(modelsDev as any)._mem = {
|
|
124
151
|
anthropic: {
|
|
125
152
|
models: {
|
|
126
|
-
"claude-sonnet-4-5": {
|
|
153
|
+
"claude-sonnet-4-5": {
|
|
154
|
+
id: "claude-sonnet-4-5",
|
|
155
|
+
name: "Claude Sonnet 4.5",
|
|
156
|
+
},
|
|
127
157
|
},
|
|
128
158
|
},
|
|
129
159
|
openai: {
|
|
@@ -139,7 +169,9 @@ describe("lookupModelsDev", () => {
|
|
|
139
169
|
});
|
|
140
170
|
|
|
141
171
|
it("finds by exact provider + id", () => {
|
|
142
|
-
expect(lookupModelsDev("anthropic", "claude-sonnet-4-5")?.name).toBe(
|
|
172
|
+
expect(lookupModelsDev("anthropic", "claude-sonnet-4-5")?.name).toBe(
|
|
173
|
+
"Claude Sonnet 4.5",
|
|
174
|
+
);
|
|
143
175
|
});
|
|
144
176
|
|
|
145
177
|
it("falls back across providers when provider miss", () => {
|
|
@@ -147,9 +179,9 @@ describe("lookupModelsDev", () => {
|
|
|
147
179
|
});
|
|
148
180
|
|
|
149
181
|
it("strips path prefix from id", () => {
|
|
150
|
-
expect(
|
|
151
|
-
"
|
|
152
|
-
);
|
|
182
|
+
expect(
|
|
183
|
+
lookupModelsDev("anthropic", "anthropic/claude-sonnet-4-5")?.name,
|
|
184
|
+
).toBe("Claude Sonnet 4.5");
|
|
153
185
|
});
|
|
154
186
|
|
|
155
187
|
it("returns undefined for unknown model", () => {
|
|
@@ -161,9 +193,30 @@ describe("lookupModelsDev", () => {
|
|
|
161
193
|
|
|
162
194
|
describe("lookupBenchmark", () => {
|
|
163
195
|
const entries: BenchmarkEntry[] = [
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
196
|
+
{
|
|
197
|
+
rank: 1,
|
|
198
|
+
model: "Claude Sonnet 4.5",
|
|
199
|
+
creator: "Anthropic",
|
|
200
|
+
overallScore: 95,
|
|
201
|
+
inputPrice: 3,
|
|
202
|
+
outputPrice: 15,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
rank: 2,
|
|
206
|
+
model: "GPT-4o",
|
|
207
|
+
creator: "OpenAI",
|
|
208
|
+
overallScore: 90,
|
|
209
|
+
inputPrice: 5,
|
|
210
|
+
outputPrice: 15,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
rank: 3,
|
|
214
|
+
model: "Gemini 1.5 Pro",
|
|
215
|
+
creator: "Google",
|
|
216
|
+
overallScore: 88,
|
|
217
|
+
inputPrice: 3.5,
|
|
218
|
+
outputPrice: 10.5,
|
|
219
|
+
},
|
|
167
220
|
];
|
|
168
221
|
|
|
169
222
|
beforeEach(() => {
|
package/src/data.ts
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* import { lookupModelsDev, lookupBenchmark } from "./data.ts";
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
24
23
|
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
25
25
|
import { homedir } from "node:os";
|
|
26
26
|
import { dirname, join } from "node:path";
|
|
27
27
|
|
|
@@ -131,7 +131,11 @@ export class DataSource<T> {
|
|
|
131
131
|
try {
|
|
132
132
|
const url =
|
|
133
133
|
typeof this.opts.url === "function" ? this.opts.url() : this.opts.url;
|
|
134
|
-
const response = await fetchWithTimeout(
|
|
134
|
+
const response = await fetchWithTimeout(
|
|
135
|
+
url,
|
|
136
|
+
this.opts.timeoutMs,
|
|
137
|
+
this.opts.headers(),
|
|
138
|
+
);
|
|
135
139
|
if (!response.ok)
|
|
136
140
|
throw new Error(`${this.opts.label} fetch failed: ${response.status}`);
|
|
137
141
|
const raw = await response.json();
|
|
@@ -142,7 +146,9 @@ export class DataSource<T> {
|
|
|
142
146
|
} catch (error) {
|
|
143
147
|
const msg = error instanceof Error ? error.message : String(error);
|
|
144
148
|
if (cached !== undefined) {
|
|
145
|
-
console.warn(
|
|
149
|
+
console.warn(
|
|
150
|
+
`${this.opts.label} fetch failed, using stale cache: ${msg}`,
|
|
151
|
+
);
|
|
146
152
|
const val = this.opts.parseCache(cached.data);
|
|
147
153
|
this._mem = val;
|
|
148
154
|
return val;
|
|
@@ -152,7 +158,9 @@ export class DataSource<T> {
|
|
|
152
158
|
}
|
|
153
159
|
}
|
|
154
160
|
|
|
155
|
-
private async _readCache(): Promise<
|
|
161
|
+
private async _readCache(): Promise<
|
|
162
|
+
{ ts: number; data: unknown } | undefined
|
|
163
|
+
> {
|
|
156
164
|
try {
|
|
157
165
|
const raw = await readFile(this.opts.cachePath, "utf8");
|
|
158
166
|
const parsed = JSON.parse(raw) as { ts: number; data: unknown };
|
|
@@ -166,7 +174,10 @@ export class DataSource<T> {
|
|
|
166
174
|
private async _writeCache(data: unknown): Promise<void> {
|
|
167
175
|
try {
|
|
168
176
|
await mkdir(dirname(this.opts.cachePath), { recursive: true });
|
|
169
|
-
await writeFile(
|
|
177
|
+
await writeFile(
|
|
178
|
+
this.opts.cachePath,
|
|
179
|
+
JSON.stringify({ ts: Date.now(), data }),
|
|
180
|
+
);
|
|
170
181
|
} catch {
|
|
171
182
|
// Write failure is non-fatal — stale cache used on next run
|
|
172
183
|
}
|
|
@@ -226,7 +237,9 @@ function stripPrefix(id: string): string {
|
|
|
226
237
|
return i >= 0 ? id.slice(i + 1) : id;
|
|
227
238
|
}
|
|
228
239
|
|
|
229
|
-
export function buildModelsDevIndex(
|
|
240
|
+
export function buildModelsDevIndex(
|
|
241
|
+
api: ModelsDevApi,
|
|
242
|
+
): Map<string, ModelsDevModel> {
|
|
230
243
|
const index = new Map<string, ModelsDevModel>();
|
|
231
244
|
for (const provider of Object.values(api)) {
|
|
232
245
|
if (!provider?.models) continue;
|
|
@@ -254,7 +267,10 @@ export function lookupInIndex(
|
|
|
254
267
|
return undefined;
|
|
255
268
|
}
|
|
256
269
|
|
|
257
|
-
export function lookupModelsDev(
|
|
270
|
+
export function lookupModelsDev(
|
|
271
|
+
provider: string,
|
|
272
|
+
id: string,
|
|
273
|
+
): ModelsDevModel | undefined {
|
|
258
274
|
const data = modelsDev.getCached();
|
|
259
275
|
const canonical = id.includes("/") ? id.slice(id.lastIndexOf("/") + 1) : id;
|
|
260
276
|
const exact = data[provider]?.models?.[canonical];
|
|
@@ -266,12 +282,18 @@ export function lookupModelsDev(provider: string, id: string): ModelsDevModel |
|
|
|
266
282
|
return undefined;
|
|
267
283
|
}
|
|
268
284
|
|
|
269
|
-
export async function fetchModelsDevIndex(): Promise<
|
|
285
|
+
export async function fetchModelsDevIndex(): Promise<
|
|
286
|
+
Map<string, ModelsDevModel>
|
|
287
|
+
> {
|
|
270
288
|
return buildModelsDevIndex(await modelsDev.get());
|
|
271
289
|
}
|
|
272
290
|
|
|
273
291
|
function normBench(s: string): string {
|
|
274
|
-
return s
|
|
292
|
+
return s
|
|
293
|
+
.toLowerCase()
|
|
294
|
+
.replace(/[-_.]+/g, " ")
|
|
295
|
+
.replace(/\s+/g, " ")
|
|
296
|
+
.trim();
|
|
275
297
|
}
|
|
276
298
|
|
|
277
299
|
export function lookupBenchmark(modelName: string): BenchmarkEntry | undefined {
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,27 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import {
|
|
11
|
+
import { benchmark, modelsDev } from "./data.ts";
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
BenchmarkEntry,
|
|
15
|
+
ModelsDevApi,
|
|
16
|
+
ModelsDevModel,
|
|
17
|
+
} from "./data.ts";
|
|
18
|
+
// Public data API — single source of truth for the shared model data layer.
|
|
19
|
+
// Consumers (pix-core, pix-9router, …) import these instead of duplicating
|
|
20
|
+
// the DataSource implementation and models.dev/BenchLM lookups.
|
|
21
|
+
export {
|
|
22
|
+
benchmark,
|
|
23
|
+
buildModelsDevIndex,
|
|
24
|
+
CACHE_DIR,
|
|
25
|
+
DataSource,
|
|
26
|
+
fetchModelsDevIndex,
|
|
27
|
+
lookupBenchmark,
|
|
28
|
+
lookupInIndex,
|
|
29
|
+
lookupModelsDev,
|
|
30
|
+
modelsDev,
|
|
31
|
+
} from "./data.ts";
|
|
12
32
|
|
|
13
33
|
export default function (_pi: ExtensionAPI): void {
|
|
14
34
|
void modelsDev.get();
|