@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-synthetic",
3
- "version": "0.5.1",
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",
@@ -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
+ });
@@ -25,7 +25,7 @@ export interface SyntheticModelConfig {
25
25
  }
26
26
 
27
27
  export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
28
- // models.dev: synthetic/hf:zai-org/GLM-4.7 → ctx=200000, out=64000
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: 64000,
41
+ maxTokens: 65536,
42
42
  },
43
- // models.dev: synthetic/hf:MiniMaxAI/MiniMax-M2.1 → ctx=204800, out=131072
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.55,
51
- output: 2.19,
52
- cacheRead: 0.55,
50
+ input: 0.3,
51
+ output: 1.2,
52
+ cacheRead: 0.3,
53
53
  cacheWrite: 0,
54
54
  },
55
55
  contextWindow: 196608,
56
- maxTokens: 131072,
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
- // models.dev: synthetic/hf:Qwen/Qwen3-Coder-480B-A35B-Instruct → ctx=256000, out=32000
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: false,
167
+ reasoning: true,
168
168
  input: ["text"],
169
169
  cost: {
170
- input: 0.45,
171
- output: 1.8,
172
- cacheRead: 0.45,
170
+ input: 2,
171
+ output: 2,
172
+ cacheRead: 2,
173
173
  cacheWrite: 0,
174
174
  },
175
175
  contextWindow: 262144,
176
- maxTokens: 32000,
176
+ maxTokens: 65536,
177
177
  },
178
- // models.dev: synthetic/hf:moonshotai/Kimi-K2.5 → ctx=262144, out=65536
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: 1.2,
186
- output: 1.2,
187
- cacheRead: 1.2,
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", "image"],
258
+ input: ["text"],
259
259
  cost: {
260
260
  input: 0.6,
261
261
  output: 3,