@aliou/pi-synthetic 0.5.0 → 0.6.0
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 +5 -2
- package/src/providers/index.ts +4 -0
- package/src/providers/models.test.ts +203 -0
- package/src/providers/models.ts +36 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-synthetic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"@sinclair/typebox": "^0.34.48",
|
|
39
39
|
"@types/node": "^25.0.10",
|
|
40
40
|
"husky": "^9.1.7",
|
|
41
|
-
"typescript": "^5.9.3"
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
42
43
|
},
|
|
43
44
|
"peerDependenciesMeta": {
|
|
44
45
|
"@mariozechner/pi-coding-agent": {
|
|
@@ -52,6 +53,8 @@
|
|
|
52
53
|
"typecheck": "tsc --noEmit",
|
|
53
54
|
"lint": "biome check",
|
|
54
55
|
"format": "biome check --write",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
55
58
|
"check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
|
|
56
59
|
"changeset": "changeset",
|
|
57
60
|
"version": "changeset version",
|
package/src/providers/index.ts
CHANGED
|
@@ -6,6 +6,10 @@ export function registerSyntheticProvider(pi: ExtensionAPI): void {
|
|
|
6
6
|
baseUrl: "https://api.synthetic.new/openai/v1",
|
|
7
7
|
apiKey: "SYNTHETIC_API_KEY",
|
|
8
8
|
api: "openai-completions",
|
|
9
|
+
headers: {
|
|
10
|
+
Referer: "https://pi.dev",
|
|
11
|
+
"X-Title": "npm:@aliou/pi-synthetic",
|
|
12
|
+
},
|
|
9
13
|
models: SYNTHETIC_MODELS.map((model) => ({
|
|
10
14
|
id: model.id,
|
|
11
15
|
name: model.name,
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { SYNTHETIC_MODELS } from "./models";
|
|
3
|
+
|
|
4
|
+
interface ApiModel {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
input_modalities: string[];
|
|
8
|
+
output_modalities: string[];
|
|
9
|
+
context_length: number;
|
|
10
|
+
max_output_length: number;
|
|
11
|
+
pricing: {
|
|
12
|
+
prompt: string;
|
|
13
|
+
completion: string;
|
|
14
|
+
input_cache_reads: string;
|
|
15
|
+
input_cache_writes: string;
|
|
16
|
+
};
|
|
17
|
+
supported_features?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ApiResponse {
|
|
21
|
+
data: ApiModel[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Discrepancy {
|
|
25
|
+
model: string;
|
|
26
|
+
field: string;
|
|
27
|
+
hardcoded: unknown;
|
|
28
|
+
api: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function fetchApiModels(): Promise<ApiModel[]> {
|
|
32
|
+
// Making ourselves known
|
|
33
|
+
const response = await fetch("https://api.synthetic.new/openai/v1/models", {
|
|
34
|
+
headers: {
|
|
35
|
+
Referer: "https://github.com/aliou/pi-synthetic",
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`API request failed: ${response.status} ${response.statusText}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const data = (await response.json()) as ApiResponse;
|
|
46
|
+
return data.data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parsePrice(priceStr: string): number {
|
|
50
|
+
// Convert "$0.0000006" to 0.6 (dollars per million tokens)
|
|
51
|
+
const match = priceStr.match(/\$?(\d+\.?\d*)/);
|
|
52
|
+
if (!match) return 0;
|
|
53
|
+
const pricePerToken = Number.parseFloat(match[1]);
|
|
54
|
+
// API prices are per token, hardcoded prices are per million tokens
|
|
55
|
+
return pricePerToken * 1_000_000;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function compareModels(
|
|
59
|
+
apiModels: ApiModel[],
|
|
60
|
+
hardcodedModels: typeof SYNTHETIC_MODELS,
|
|
61
|
+
): Discrepancy[] {
|
|
62
|
+
const discrepancies: Discrepancy[] = [];
|
|
63
|
+
|
|
64
|
+
for (const hardcoded of hardcodedModels) {
|
|
65
|
+
const apiModel = apiModels.find((m) => m.id === hardcoded.id);
|
|
66
|
+
|
|
67
|
+
if (!apiModel) {
|
|
68
|
+
discrepancies.push({
|
|
69
|
+
model: hardcoded.id,
|
|
70
|
+
field: "exists",
|
|
71
|
+
hardcoded: true,
|
|
72
|
+
api: false,
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check input modalities (text vs image support)
|
|
78
|
+
const apiInputs = apiModel.input_modalities.sort();
|
|
79
|
+
const hardcodedInputs = [...hardcoded.input].sort();
|
|
80
|
+
if (JSON.stringify(apiInputs) !== JSON.stringify(hardcodedInputs)) {
|
|
81
|
+
discrepancies.push({
|
|
82
|
+
model: hardcoded.id,
|
|
83
|
+
field: "input",
|
|
84
|
+
hardcoded: hardcodedInputs,
|
|
85
|
+
api: apiInputs,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check context window
|
|
90
|
+
if (apiModel.context_length !== hardcoded.contextWindow) {
|
|
91
|
+
discrepancies.push({
|
|
92
|
+
model: hardcoded.id,
|
|
93
|
+
field: "contextWindow",
|
|
94
|
+
hardcoded: hardcoded.contextWindow,
|
|
95
|
+
api: apiModel.context_length,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check max output tokens (skip if API doesn't provide it)
|
|
100
|
+
if (
|
|
101
|
+
apiModel.max_output_length !== undefined &&
|
|
102
|
+
apiModel.max_output_length !== hardcoded.maxTokens
|
|
103
|
+
) {
|
|
104
|
+
discrepancies.push({
|
|
105
|
+
model: hardcoded.id,
|
|
106
|
+
field: "maxTokens",
|
|
107
|
+
hardcoded: hardcoded.maxTokens,
|
|
108
|
+
api: apiModel.max_output_length,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check input cost (convert API price to per-million rate)
|
|
113
|
+
const apiInputCost = parsePrice(apiModel.pricing.prompt);
|
|
114
|
+
const epsilon = 0.001; // Small tolerance for floating point
|
|
115
|
+
if (Math.abs(apiInputCost - hardcoded.cost.input) > epsilon) {
|
|
116
|
+
discrepancies.push({
|
|
117
|
+
model: hardcoded.id,
|
|
118
|
+
field: "cost.input",
|
|
119
|
+
hardcoded: hardcoded.cost.input,
|
|
120
|
+
api: apiInputCost,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check output cost
|
|
125
|
+
const apiOutputCost = parsePrice(apiModel.pricing.completion);
|
|
126
|
+
if (Math.abs(apiOutputCost - hardcoded.cost.output) > epsilon) {
|
|
127
|
+
discrepancies.push({
|
|
128
|
+
model: hardcoded.id,
|
|
129
|
+
field: "cost.output",
|
|
130
|
+
hardcoded: hardcoded.cost.output,
|
|
131
|
+
api: apiOutputCost,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check cache read cost
|
|
136
|
+
const apiCacheReadCost = parsePrice(apiModel.pricing.input_cache_reads);
|
|
137
|
+
if (Math.abs(apiCacheReadCost - hardcoded.cost.cacheRead) > epsilon) {
|
|
138
|
+
discrepancies.push({
|
|
139
|
+
model: hardcoded.id,
|
|
140
|
+
field: "cost.cacheRead",
|
|
141
|
+
hardcoded: hardcoded.cost.cacheRead,
|
|
142
|
+
api: apiCacheReadCost,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check reasoning capability from supported_features (skip if API doesn't provide it)
|
|
147
|
+
if (apiModel.supported_features !== undefined) {
|
|
148
|
+
const apiSupportsReasoning =
|
|
149
|
+
apiModel.supported_features.includes("reasoning");
|
|
150
|
+
if (apiSupportsReasoning !== hardcoded.reasoning) {
|
|
151
|
+
discrepancies.push({
|
|
152
|
+
model: hardcoded.id,
|
|
153
|
+
field: "reasoning",
|
|
154
|
+
hardcoded: hardcoded.reasoning,
|
|
155
|
+
api: apiSupportsReasoning,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check for API models not in hardcoded list
|
|
162
|
+
for (const apiModel of apiModels) {
|
|
163
|
+
const hardcoded = hardcodedModels.find((m) => m.id === apiModel.id);
|
|
164
|
+
if (!hardcoded) {
|
|
165
|
+
discrepancies.push({
|
|
166
|
+
model: apiModel.id,
|
|
167
|
+
field: "exists",
|
|
168
|
+
hardcoded: false,
|
|
169
|
+
api: true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return discrepancies;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
describe("Synthetic models", () => {
|
|
178
|
+
it("should match API model definitions", { timeout: 30000 }, async () => {
|
|
179
|
+
const apiModels = await fetchApiModels();
|
|
180
|
+
const discrepancies = compareModels(apiModels, SYNTHETIC_MODELS);
|
|
181
|
+
|
|
182
|
+
if (discrepancies.length > 0) {
|
|
183
|
+
console.error("\nModel discrepancies found:");
|
|
184
|
+
console.error("==========================");
|
|
185
|
+
for (const d of discrepancies) {
|
|
186
|
+
if (d.field === "exists") {
|
|
187
|
+
if (d.hardcoded) {
|
|
188
|
+
console.error(` ${d.model}: Missing from API`);
|
|
189
|
+
} else {
|
|
190
|
+
console.error(` ${d.model}: Missing from hardcoded models (NEW)`);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
console.error(` ${d.model}.${d.field}:`);
|
|
194
|
+
console.error(` hardcoded: ${JSON.stringify(d.hardcoded)}`);
|
|
195
|
+
console.error(` api: ${JSON.stringify(d.api)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
console.error("==========================\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
expect(discrepancies).toHaveLength(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
package/src/providers/models.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface SyntheticModelConfig {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
28
|
-
//
|
|
28
|
+
// API: hf:zai-org/GLM-4.7 → ctx=202752, out=65536
|
|
29
29
|
{
|
|
30
30
|
id: "hf:zai-org/GLM-4.7",
|
|
31
31
|
name: "zai-org/GLM-4.7",
|
|
@@ -38,22 +38,22 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
38
38
|
cacheWrite: 0,
|
|
39
39
|
},
|
|
40
40
|
contextWindow: 202752,
|
|
41
|
-
maxTokens:
|
|
41
|
+
maxTokens: 65536,
|
|
42
42
|
},
|
|
43
|
-
//
|
|
43
|
+
// API: hf:MiniMaxAI/MiniMax-M2.1 → ctx=196608, out=65536
|
|
44
44
|
{
|
|
45
45
|
id: "hf:MiniMaxAI/MiniMax-M2.1",
|
|
46
46
|
name: "MiniMaxAI/MiniMax-M2.1",
|
|
47
47
|
reasoning: true,
|
|
48
48
|
input: ["text"],
|
|
49
49
|
cost: {
|
|
50
|
-
input: 0.
|
|
51
|
-
output: 2
|
|
52
|
-
cacheRead: 0.
|
|
50
|
+
input: 0.3,
|
|
51
|
+
output: 1.2,
|
|
52
|
+
cacheRead: 0.3,
|
|
53
53
|
cacheWrite: 0,
|
|
54
54
|
},
|
|
55
55
|
contextWindow: 196608,
|
|
56
|
-
maxTokens:
|
|
56
|
+
maxTokens: 65536,
|
|
57
57
|
},
|
|
58
58
|
// models.dev: synthetic/hf:meta-llama/Llama-3.3-70B-Instruct → ctx=128000, out=32768
|
|
59
59
|
{
|
|
@@ -160,31 +160,31 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
160
160
|
contextWindow: 131072,
|
|
161
161
|
maxTokens: 32768,
|
|
162
162
|
},
|
|
163
|
-
//
|
|
163
|
+
// API: hf:Qwen/Qwen3-Coder-480B-A35B-Instruct → ctx=262144, out=65536
|
|
164
164
|
{
|
|
165
165
|
id: "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
|
166
166
|
name: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
|
167
|
-
reasoning:
|
|
167
|
+
reasoning: true,
|
|
168
168
|
input: ["text"],
|
|
169
169
|
cost: {
|
|
170
|
-
input:
|
|
171
|
-
output:
|
|
172
|
-
cacheRead:
|
|
170
|
+
input: 2,
|
|
171
|
+
output: 2,
|
|
172
|
+
cacheRead: 2,
|
|
173
173
|
cacheWrite: 0,
|
|
174
174
|
},
|
|
175
175
|
contextWindow: 262144,
|
|
176
|
-
maxTokens:
|
|
176
|
+
maxTokens: 65536,
|
|
177
177
|
},
|
|
178
|
-
//
|
|
178
|
+
// API: hf:moonshotai/Kimi-K2.5 → ctx=262144, out=65536
|
|
179
179
|
{
|
|
180
180
|
id: "hf:moonshotai/Kimi-K2.5",
|
|
181
181
|
name: "moonshotai/Kimi-K2.5",
|
|
182
182
|
reasoning: true,
|
|
183
183
|
input: ["text", "image"],
|
|
184
184
|
cost: {
|
|
185
|
-
input:
|
|
186
|
-
output:
|
|
187
|
-
cacheRead:
|
|
185
|
+
input: 0.6,
|
|
186
|
+
output: 3,
|
|
187
|
+
cacheRead: 0.6,
|
|
188
188
|
cacheWrite: 0,
|
|
189
189
|
},
|
|
190
190
|
contextWindow: 262144,
|
|
@@ -235,12 +235,12 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
235
235
|
contextWindow: 262144,
|
|
236
236
|
maxTokens: 32000,
|
|
237
237
|
},
|
|
238
|
-
// API: hf:Qwen/Qwen3.5-397B-A17B → ctx=262144, out=
|
|
238
|
+
// API: hf:Qwen/Qwen3.5-397B-A17B → ctx=262144, out=65536
|
|
239
239
|
{
|
|
240
240
|
id: "hf:Qwen/Qwen3.5-397B-A17B",
|
|
241
241
|
name: "Qwen/Qwen3.5-397B-A17B",
|
|
242
|
-
reasoning:
|
|
243
|
-
input: ["text"],
|
|
242
|
+
reasoning: true,
|
|
243
|
+
input: ["text", "image"],
|
|
244
244
|
cost: {
|
|
245
245
|
input: 0.6,
|
|
246
246
|
output: 3,
|
|
@@ -248,6 +248,21 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
248
248
|
cacheWrite: 0,
|
|
249
249
|
},
|
|
250
250
|
contextWindow: 262144,
|
|
251
|
-
maxTokens:
|
|
251
|
+
maxTokens: 65536,
|
|
252
|
+
},
|
|
253
|
+
// API: hf:MiniMaxAI/MiniMax-M2.5 → ctx=191488, out=65536
|
|
254
|
+
{
|
|
255
|
+
id: "hf:MiniMaxAI/MiniMax-M2.5",
|
|
256
|
+
name: "MiniMaxAI/MiniMax-M2.5",
|
|
257
|
+
reasoning: true,
|
|
258
|
+
input: ["text"],
|
|
259
|
+
cost: {
|
|
260
|
+
input: 0.6,
|
|
261
|
+
output: 3,
|
|
262
|
+
cacheRead: 0.6,
|
|
263
|
+
cacheWrite: 0,
|
|
264
|
+
},
|
|
265
|
+
contextWindow: 191488,
|
|
266
|
+
maxTokens: 65536,
|
|
252
267
|
},
|
|
253
268
|
];
|