@aliou/pi-synthetic 0.5.1 → 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 +18 -18
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,
|
|
@@ -255,7 +255,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
255
255
|
id: "hf:MiniMaxAI/MiniMax-M2.5",
|
|
256
256
|
name: "MiniMaxAI/MiniMax-M2.5",
|
|
257
257
|
reasoning: true,
|
|
258
|
-
input: ["text"
|
|
258
|
+
input: ["text"],
|
|
259
259
|
cost: {
|
|
260
260
|
input: 0.6,
|
|
261
261
|
output: 3,
|