pi-free 2.0.15 → 2.1.1
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/CHANGELOG.md +100 -3
- package/README.md +64 -79
- package/banner.svg +21 -36
- package/config.ts +123 -9
- package/constants.ts +3 -9
- package/index.ts +14 -15
- package/lib/built-in-toggle.ts +29 -56
- package/lib/json-persistence.ts +90 -22
- package/lib/logger.ts +21 -12
- package/lib/model-detection.ts +2 -12
- package/lib/model-enhancer.ts +11 -2
- package/lib/model-metadata.ts +387 -0
- package/lib/open-browser.ts +74 -24
- package/lib/paths.ts +90 -0
- package/lib/probe-cache.ts +19 -19
- package/lib/provider-cache.ts +74 -28
- package/lib/provider-compat.ts +53 -37
- package/lib/provider-probe.ts +188 -0
- package/lib/registry.ts +1 -5
- package/lib/session-start-metrics.ts +46 -0
- package/lib/telemetry.ts +115 -86
- package/lib/types.ts +22 -2
- package/lib/util.ts +80 -21
- package/package.json +7 -2
- package/provider-failover/benchmark-lookup.ts +17 -5
- package/provider-helper.ts +12 -27
- package/providers/cline/cline-models.ts +7 -1
- package/providers/cline/cline-xml-bridge.ts +1471 -0
- package/providers/cline/cline.ts +67 -199
- package/providers/codestral/codestral.ts +0 -11
- package/providers/crofai/crofai.ts +6 -1
- package/providers/deepinfra/deepinfra.ts +69 -2
- package/providers/dynamic-built-in/index.ts +237 -22
- package/providers/kilo/kilo-models.ts +3 -1
- package/providers/kilo/kilo.ts +270 -60
- package/providers/model-fetcher.ts +18 -55
- package/providers/novita/novita.ts +69 -2
- package/providers/ollama/ollama.ts +47 -36
- package/providers/opencode-session.ts +67 -2
- package/providers/routeway/routeway.ts +25 -17
- package/providers/sambanova/sambanova.ts +67 -1
- package/providers/together/together.ts +69 -2
- package/providers/tokenrouter/tokenrouter.ts +634 -0
- package/providers/zenmux/zenmux.ts +6 -1
- package/scripts/check-extensions.mjs +32 -16
- package/providers/nvidia/nvidia.ts +0 -510
package/lib/telemetry.ts
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
* Provides a real-world performance signal alongside static CI benchmarks.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import { join } from "node:path";
|
|
13
10
|
import { createLogger } from "./logger.ts";
|
|
11
|
+
import { resolveSafeDataFile } from "./paths.ts";
|
|
12
|
+
import { createJSONStore } from "./json-persistence.ts";
|
|
14
13
|
|
|
15
14
|
const _logger = createLogger("telemetry");
|
|
16
15
|
|
|
@@ -71,55 +70,44 @@ export interface TelemetryStore {
|
|
|
71
70
|
// Constants
|
|
72
71
|
// =============================================================================
|
|
73
72
|
|
|
74
|
-
const
|
|
75
|
-
|
|
73
|
+
const TELEMETRY_FILE = resolveSafeDataFile(
|
|
74
|
+
process.env.PI_FREE_TELEMETRY_FILE,
|
|
75
|
+
"free-telemetry.json",
|
|
76
|
+
);
|
|
76
77
|
const MAX_RECENT_CALLS = 50;
|
|
77
78
|
|
|
78
|
-
// In-flight tracking: keyed by "provider/model", value is start timestamp
|
|
79
|
+
// In-flight tracking: keyed by "provider/model", value is start timestamp.
|
|
80
|
+
// TTL: 1 hour — anything older is stale (the matching recordModelCall
|
|
81
|
+
// never fired, e.g. the agent was killed mid-call) and gets reaped
|
|
82
|
+
// on the next startModelCall/recordModelCall.
|
|
79
83
|
const _inFlight = new Map<string, number>();
|
|
84
|
+
const _IN_FLIGHT_TTL_MS = 60 * 60 * 1000;
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
function ensureDir(): void {
|
|
86
|
-
if (!existsSync(TELEMETRY_DIR)) {
|
|
87
|
-
mkdirSync(TELEMETRY_DIR, { recursive: true });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function loadStore(): TelemetryStore {
|
|
92
|
-
try {
|
|
93
|
-
if (!existsSync(TELEMETRY_FILE)) {
|
|
94
|
-
return { models: {}, lastUpdated: Date.now() };
|
|
86
|
+
function reapStaleInFlight(now: number): void {
|
|
87
|
+
for (const [key, start] of _inFlight) {
|
|
88
|
+
if (now - start > _IN_FLIGHT_TTL_MS) {
|
|
89
|
+
_inFlight.delete(key);
|
|
95
90
|
}
|
|
96
|
-
const raw = readFileSync(TELEMETRY_FILE, "utf-8");
|
|
97
|
-
return JSON.parse(raw) as TelemetryStore;
|
|
98
|
-
} catch (err) {
|
|
99
|
-
_logger.warn("Failed to load telemetry store, resetting", {
|
|
100
|
-
error: String(err),
|
|
101
|
-
});
|
|
102
|
-
return { models: {}, lastUpdated: Date.now() };
|
|
103
91
|
}
|
|
104
92
|
}
|
|
105
93
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// Storage
|
|
96
|
+
// =============================================================================
|
|
97
|
+
|
|
98
|
+
const _store = createJSONStore<TelemetryStore>(TELEMETRY_FILE, {
|
|
99
|
+
models: {},
|
|
100
|
+
lastUpdated: Date.now(),
|
|
101
|
+
});
|
|
117
102
|
|
|
118
103
|
// =============================================================================
|
|
119
104
|
// Entry management
|
|
120
105
|
// =============================================================================
|
|
121
106
|
|
|
122
|
-
function deriveModelTelemetry(
|
|
107
|
+
function deriveModelTelemetry(
|
|
108
|
+
_modelKey: string,
|
|
109
|
+
entries: TelemetryEntry[],
|
|
110
|
+
): ModelTelemetry {
|
|
123
111
|
const recent = entries.slice(-MAX_RECENT_CALLS);
|
|
124
112
|
const totalCalls = entries.length;
|
|
125
113
|
const successCalls = entries.filter((e) => e.success).length;
|
|
@@ -134,12 +122,24 @@ function deriveModelTelemetry(modelKey: string, entries: TelemetryEntry[]): Mode
|
|
|
134
122
|
acc.totalCost += e.cost;
|
|
135
123
|
return acc;
|
|
136
124
|
},
|
|
137
|
-
{
|
|
125
|
+
{
|
|
126
|
+
totalTokens: 0,
|
|
127
|
+
totalPromptTokens: 0,
|
|
128
|
+
totalCompletionTokens: 0,
|
|
129
|
+
totalLatencyMs: 0,
|
|
130
|
+
totalCost: 0,
|
|
131
|
+
},
|
|
138
132
|
);
|
|
139
133
|
|
|
140
134
|
const totalSuccessEntries = entries.filter((e) => e.success);
|
|
141
|
-
const totalTokensFromSuccessful = totalSuccessEntries.reduce(
|
|
142
|
-
|
|
135
|
+
const totalTokensFromSuccessful = totalSuccessEntries.reduce(
|
|
136
|
+
(s, e) => s + e.totalTokens,
|
|
137
|
+
0,
|
|
138
|
+
);
|
|
139
|
+
const totalLatencyFromSuccessful = totalSuccessEntries.reduce(
|
|
140
|
+
(s, e) => s + e.latencyMs,
|
|
141
|
+
0,
|
|
142
|
+
);
|
|
143
143
|
|
|
144
144
|
return {
|
|
145
145
|
totalCalls,
|
|
@@ -150,31 +150,47 @@ function deriveModelTelemetry(modelKey: string, entries: TelemetryEntry[]): Mode
|
|
|
150
150
|
totalCompletionTokens: stats.totalCompletionTokens,
|
|
151
151
|
totalLatencyMs: stats.totalLatencyMs,
|
|
152
152
|
totalCost: stats.totalCost,
|
|
153
|
-
avgLatencyMs:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
153
|
+
avgLatencyMs:
|
|
154
|
+
totalSuccessEntries.length > 0
|
|
155
|
+
? Math.round(totalLatencyFromSuccessful / totalSuccessEntries.length)
|
|
156
|
+
: 0,
|
|
157
|
+
avgTokensPerSecond:
|
|
158
|
+
totalLatencyFromSuccessful > 0
|
|
159
|
+
? parseFloat(
|
|
160
|
+
(
|
|
161
|
+
totalTokensFromSuccessful /
|
|
162
|
+
(totalLatencyFromSuccessful / 1000)
|
|
163
|
+
).toFixed(1),
|
|
164
|
+
)
|
|
165
|
+
: 0,
|
|
166
|
+
successRate:
|
|
167
|
+
totalCalls > 0
|
|
168
|
+
? parseFloat(((successCalls / totalCalls) * 100).toFixed(1))
|
|
169
|
+
: 0,
|
|
162
170
|
recentCalls: recent,
|
|
163
171
|
};
|
|
164
172
|
}
|
|
165
173
|
|
|
166
|
-
function addEntry(entry: TelemetryEntry): void {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
async function addEntry(entry: TelemetryEntry): Promise<void> {
|
|
175
|
+
await _store.update((store) => {
|
|
176
|
+
const modelKey = `${entry.provider}/${entry.model}`;
|
|
177
|
+
|
|
178
|
+
const existing: TelemetryEntry[] =
|
|
179
|
+
store.models[modelKey]?.recentCalls ?? [];
|
|
180
|
+
existing.push(entry);
|
|
181
|
+
|
|
182
|
+
// Keep only last MAX_RECENT_CALLS * 2 in raw storage (we derive stats from these)
|
|
183
|
+
const pruned = existing.slice(-MAX_RECENT_CALLS * 2);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...store,
|
|
187
|
+
models: {
|
|
188
|
+
...store.models,
|
|
189
|
+
[modelKey]: deriveModelTelemetry(modelKey, pruned),
|
|
190
|
+
},
|
|
191
|
+
lastUpdated: Date.now(),
|
|
192
|
+
};
|
|
193
|
+
});
|
|
178
194
|
}
|
|
179
195
|
|
|
180
196
|
// =============================================================================
|
|
@@ -185,23 +201,27 @@ function addEntry(entry: TelemetryEntry): void {
|
|
|
185
201
|
* Get telemetry for all tracked models.
|
|
186
202
|
*/
|
|
187
203
|
export function getAllTelemetry(): Record<string, ModelTelemetry> {
|
|
188
|
-
|
|
189
|
-
return store.models;
|
|
204
|
+
return _store.load().models;
|
|
190
205
|
}
|
|
191
206
|
|
|
192
207
|
/**
|
|
193
208
|
* Get telemetry for a specific provider/model combination.
|
|
194
209
|
*/
|
|
195
|
-
export function getModelTelemetry(
|
|
196
|
-
|
|
197
|
-
|
|
210
|
+
export function getModelTelemetry(
|
|
211
|
+
provider: string,
|
|
212
|
+
model: string,
|
|
213
|
+
): ModelTelemetry | null {
|
|
214
|
+
return _store.load().models[`${provider}/${model}`] ?? null;
|
|
198
215
|
}
|
|
199
216
|
|
|
200
217
|
/**
|
|
201
218
|
* Format a model's telemetry as a human-readable string (for status bar / /model list).
|
|
202
219
|
* Returns undefined if no telemetry data is available.
|
|
203
220
|
*/
|
|
204
|
-
export function formatModelTelemetry(
|
|
221
|
+
export function formatModelTelemetry(
|
|
222
|
+
provider: string,
|
|
223
|
+
model: string,
|
|
224
|
+
): string | undefined {
|
|
205
225
|
const telemetry = getModelTelemetry(provider, model);
|
|
206
226
|
if (!telemetry || telemetry.totalCalls === 0) return undefined;
|
|
207
227
|
|
|
@@ -230,7 +250,7 @@ export function getProviderTelemetry(provider: string): {
|
|
|
230
250
|
totalCost: number;
|
|
231
251
|
models: number;
|
|
232
252
|
} {
|
|
233
|
-
const store =
|
|
253
|
+
const store = _store.load();
|
|
234
254
|
let totalCalls = 0;
|
|
235
255
|
let totalCost = 0;
|
|
236
256
|
let models = 0;
|
|
@@ -252,7 +272,16 @@ export function getProviderTelemetry(provider: string): {
|
|
|
252
272
|
*/
|
|
253
273
|
export function startModelCall(provider: string, model: string): void {
|
|
254
274
|
const key = `${provider}/${model}`;
|
|
255
|
-
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
reapStaleInFlight(now);
|
|
277
|
+
_inFlight.set(key, now);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Options for {@link recordModelCall} */
|
|
281
|
+
export interface RecordModelCallOptions {
|
|
282
|
+
success: boolean;
|
|
283
|
+
stopReason?: string;
|
|
284
|
+
errorMessage?: string;
|
|
256
285
|
}
|
|
257
286
|
|
|
258
287
|
/**
|
|
@@ -263,28 +292,26 @@ export function startModelCall(provider: string, model: string): void {
|
|
|
263
292
|
* @param model - The model ID
|
|
264
293
|
* @param usage - Token usage { input, output, totalTokens }
|
|
265
294
|
* @param cost - Cost in USD
|
|
266
|
-
* @param
|
|
267
|
-
* @param stopReason - The stop reason (e.g. "stop", "error")
|
|
268
|
-
* @param errorMessage - Error message if failed
|
|
295
|
+
* @param options - Options object ({@link RecordModelCallOptions})
|
|
269
296
|
*/
|
|
270
|
-
export function recordModelCall(
|
|
297
|
+
export async function recordModelCall(
|
|
271
298
|
provider: string,
|
|
272
299
|
model: string,
|
|
273
300
|
usage: { input: number; output: number; totalTokens: number },
|
|
274
301
|
cost: number,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
errorMessage
|
|
278
|
-
): void {
|
|
302
|
+
options: RecordModelCallOptions,
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
const { success, stopReason, errorMessage } = options;
|
|
279
305
|
const key = `${provider}/${model}`;
|
|
280
306
|
const startTime = _inFlight.get(key) ?? Date.now();
|
|
281
307
|
const latencyMs = Date.now() - startTime;
|
|
282
308
|
_inFlight.delete(key);
|
|
283
309
|
|
|
284
310
|
const totalTokens = usage.totalTokens || usage.input + usage.output;
|
|
285
|
-
const tokensPerSecond =
|
|
286
|
-
|
|
287
|
-
|
|
311
|
+
const tokensPerSecond =
|
|
312
|
+
latencyMs > 0
|
|
313
|
+
? parseFloat((totalTokens / (latencyMs / 1000)).toFixed(1))
|
|
314
|
+
: 0;
|
|
288
315
|
|
|
289
316
|
const entry: TelemetryEntry = {
|
|
290
317
|
timestamp: Date.now(),
|
|
@@ -301,7 +328,7 @@ export function recordModelCall(
|
|
|
301
328
|
...(errorMessage ? { error: errorMessage } : {}),
|
|
302
329
|
};
|
|
303
330
|
|
|
304
|
-
addEntry(entry);
|
|
331
|
+
await addEntry(entry);
|
|
305
332
|
|
|
306
333
|
_logger.info(`Telemetry: ${provider}/${model}`, {
|
|
307
334
|
latencyMs,
|
|
@@ -315,9 +342,11 @@ export function recordModelCall(
|
|
|
315
342
|
/**
|
|
316
343
|
* Clear all telemetry data.
|
|
317
344
|
*/
|
|
318
|
-
export function clearTelemetry(): void {
|
|
319
|
-
|
|
320
|
-
|
|
345
|
+
export async function clearTelemetry(): Promise<void> {
|
|
346
|
+
await _store.update(() => ({
|
|
347
|
+
models: {},
|
|
348
|
+
lastUpdated: Date.now(),
|
|
349
|
+
}));
|
|
321
350
|
}
|
|
322
351
|
|
|
323
352
|
/**
|
package/lib/types.ts
CHANGED
|
@@ -14,6 +14,19 @@ export interface CostConfig {
|
|
|
14
14
|
cacheWrite: number;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface ModelIdentity {
|
|
18
|
+
id: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
family?: string;
|
|
21
|
+
provider?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ModelMatchHints = Partial<ModelIdentity>;
|
|
25
|
+
|
|
26
|
+
export interface ModelsDevEnrichedMetadata {
|
|
27
|
+
modelsDev?: ModelMatchHints;
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
export interface ProviderModelConfig {
|
|
18
31
|
id: string;
|
|
19
32
|
name: string;
|
|
@@ -35,6 +48,13 @@ export interface ModelsDevCost {
|
|
|
35
48
|
cache_write?: number;
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
export interface ModelsDevReasoningOption {
|
|
52
|
+
type: "effort" | "toggle" | "budget_tokens";
|
|
53
|
+
values?: string[];
|
|
54
|
+
min?: number;
|
|
55
|
+
max?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
export interface ModelsDevLimit {
|
|
39
59
|
context: number;
|
|
40
60
|
output: number;
|
|
@@ -45,10 +65,10 @@ export interface ModelsDevModalities {
|
|
|
45
65
|
output?: string[];
|
|
46
66
|
}
|
|
47
67
|
|
|
48
|
-
export interface ModelsDevModel {
|
|
49
|
-
id: string;
|
|
68
|
+
export interface ModelsDevModel extends ModelIdentity {
|
|
50
69
|
name: string;
|
|
51
70
|
reasoning: boolean;
|
|
71
|
+
reasoning_options?: ModelsDevReasoningOption[];
|
|
52
72
|
cost?: ModelsDevCost;
|
|
53
73
|
limit: ModelsDevLimit;
|
|
54
74
|
modalities?: ModelsDevModalities;
|
package/lib/util.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "./logger.ts";
|
|
2
|
+
import { safeEnrichModelsWithModelsDev } from "./model-metadata.ts";
|
|
2
3
|
import {
|
|
3
4
|
getProxyModelCompat,
|
|
4
5
|
isLikelyReasoningModel,
|
|
@@ -6,12 +7,40 @@ import {
|
|
|
6
7
|
import type { ProviderModelConfig as PiProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import type { ProviderModelConfig } from "./types.ts";
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Optional callbacks that providers can pass to
|
|
12
|
+
* `fetchOpenAICompatibleModels` to override default reasoning/compat
|
|
13
|
+
* detection logic. Keeping these as injected dependencies (rather
|
|
14
|
+
* than hard-coding `isLikelyReasoningModel` / `getProxyModelCompat`)
|
|
15
|
+
* lets `lib/util.ts` stay decoupled from `lib/provider-compat.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export interface OpenAIModelCallbacks {
|
|
18
|
+
/**
|
|
19
|
+
* Determine whether a model is a reasoning model.
|
|
20
|
+
* If omitted, defaults to `isLikelyReasoningModel` from provider-compat.
|
|
21
|
+
*/
|
|
22
|
+
detectReasoning?: (model: { id: string; name?: string }) => boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Determine proxy-compat overrides for a model.
|
|
25
|
+
* If omitted, defaults to `getProxyModelCompat` from provider-compat.
|
|
26
|
+
*/
|
|
27
|
+
getProxyCompat?: (model: {
|
|
28
|
+
id: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
}) => PiProviderModelConfig["compat"] | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
9
33
|
const _logger = createLogger("util");
|
|
10
34
|
|
|
11
35
|
// =============================================================================
|
|
12
36
|
// Shared Utilities
|
|
13
37
|
// =============================================================================
|
|
14
38
|
|
|
39
|
+
/** Async sleep helper — avoids creating anonymous functions in loops */
|
|
40
|
+
export function sleep(ms: number): Promise<void> {
|
|
41
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
/**
|
|
16
45
|
* Log a warning message for provider operations
|
|
17
46
|
*/
|
|
@@ -74,7 +103,7 @@ export async function fetchWithRetry(
|
|
|
74
103
|
if (response.status >= 500) {
|
|
75
104
|
lastError = new Error(`Server error ${response.status}`);
|
|
76
105
|
if (i < retries - 1) {
|
|
77
|
-
await
|
|
106
|
+
await sleep(delayMs * (i + 1));
|
|
78
107
|
continue;
|
|
79
108
|
}
|
|
80
109
|
// Last retry exhausted - throw the error
|
|
@@ -85,7 +114,7 @@ export async function fetchWithRetry(
|
|
|
85
114
|
} catch (error) {
|
|
86
115
|
lastError = error;
|
|
87
116
|
if (i < retries - 1) {
|
|
88
|
-
await
|
|
117
|
+
await sleep(delayMs * (i + 1));
|
|
89
118
|
}
|
|
90
119
|
}
|
|
91
120
|
}
|
|
@@ -310,16 +339,22 @@ export function cleanModelName(name: string): string {
|
|
|
310
339
|
// Handle patterns like "Provider : Model Name" or "Provider / Model Name"
|
|
311
340
|
const colonIdx = name.indexOf(":");
|
|
312
341
|
const slashIdx = name.indexOf("/");
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
? colonIdx
|
|
318
|
-
: Math.min(colonIdx, slashIdx);
|
|
319
|
-
if (idx > 0) {
|
|
320
|
-
return name.slice(idx + 1).trim();
|
|
342
|
+
let idx = -1;
|
|
343
|
+
if (colonIdx === -1 && slashIdx === -1) {
|
|
344
|
+
// Neither found — return trimmed name as-is
|
|
345
|
+
return name.trim();
|
|
321
346
|
}
|
|
322
|
-
|
|
347
|
+
if (colonIdx === -1) {
|
|
348
|
+
// Only slash found
|
|
349
|
+
idx = slashIdx;
|
|
350
|
+
} else if (slashIdx === -1) {
|
|
351
|
+
// Only colon found
|
|
352
|
+
idx = colonIdx;
|
|
353
|
+
} else {
|
|
354
|
+
// Both found — use the earliest
|
|
355
|
+
idx = Math.min(colonIdx, slashIdx);
|
|
356
|
+
}
|
|
357
|
+
return name.slice(idx + 1).trim();
|
|
323
358
|
}
|
|
324
359
|
|
|
325
360
|
// =============================================================================
|
|
@@ -335,31 +370,51 @@ export function mapOpenRouterModel(m: {
|
|
|
335
370
|
name: string;
|
|
336
371
|
context_length?: number;
|
|
337
372
|
max_completion_tokens?: number | null;
|
|
338
|
-
top_provider?: {
|
|
339
|
-
|
|
373
|
+
top_provider?: {
|
|
374
|
+
context_length?: number | null;
|
|
375
|
+
max_completion_tokens?: number | null;
|
|
376
|
+
};
|
|
377
|
+
pricing?: {
|
|
378
|
+
prompt?: string | null;
|
|
379
|
+
completion?: string | null;
|
|
380
|
+
input_cache_read?: string | null;
|
|
381
|
+
input_cache_write?: string | null;
|
|
382
|
+
};
|
|
340
383
|
architecture?: {
|
|
341
384
|
input_modalities?: string[] | null;
|
|
342
385
|
output_modalities?: string[] | null;
|
|
343
386
|
};
|
|
387
|
+
supported_parameters?: string[] | null;
|
|
344
388
|
isFree?: boolean;
|
|
345
389
|
}): ProviderModelConfig {
|
|
346
390
|
const promptPrice = Number.parseFloat(m.pricing?.prompt ?? "0");
|
|
347
391
|
const completionPrice = Number.parseFloat(m.pricing?.completion ?? "0");
|
|
392
|
+
const cacheReadPrice = Number.parseFloat(
|
|
393
|
+
m.pricing?.input_cache_read ?? "0",
|
|
394
|
+
);
|
|
395
|
+
const cacheWritePrice = Number.parseFloat(
|
|
396
|
+
m.pricing?.input_cache_write ?? "0",
|
|
397
|
+
);
|
|
398
|
+
const supportedParameters = m.supported_parameters ?? [];
|
|
399
|
+
const reasoning =
|
|
400
|
+
supportedParameters.includes("reasoning") ||
|
|
401
|
+
supportedParameters.includes("reasoning_effort");
|
|
348
402
|
|
|
349
403
|
return {
|
|
350
404
|
id: m.id,
|
|
351
405
|
name: cleanModelName(m.name),
|
|
352
|
-
reasoning
|
|
406
|
+
reasoning,
|
|
407
|
+
...(reasoning && { thinkingLevelMap: { off: "none" } }),
|
|
353
408
|
input: m.architecture?.input_modalities?.includes("image")
|
|
354
409
|
? (["text", "image"] as const)
|
|
355
410
|
: (["text"] as const),
|
|
356
411
|
cost: {
|
|
357
412
|
input: promptPrice,
|
|
358
413
|
output: completionPrice,
|
|
359
|
-
cacheRead:
|
|
360
|
-
cacheWrite:
|
|
414
|
+
cacheRead: cacheReadPrice,
|
|
415
|
+
cacheWrite: cacheWritePrice,
|
|
361
416
|
},
|
|
362
|
-
contextWindow: m.context_length ?? 4096,
|
|
417
|
+
contextWindow: m.context_length ?? m.top_provider?.context_length ?? 4096,
|
|
363
418
|
maxTokens:
|
|
364
419
|
m.max_completion_tokens ?? m.top_provider?.max_completion_tokens ?? 4096,
|
|
365
420
|
_pricingKnown: true,
|
|
@@ -433,8 +488,11 @@ export async function fetchOpenAICompatibleModels(
|
|
|
433
488
|
baseUrl: string,
|
|
434
489
|
apiKey: string,
|
|
435
490
|
defaults: OpenAIModelDefaults = {},
|
|
491
|
+
callbacks: OpenAIModelCallbacks = {},
|
|
436
492
|
): Promise<PiProviderModelConfig[]> {
|
|
437
493
|
const logger = createLogger(providerId);
|
|
494
|
+
const detectReasoning = callbacks.detectReasoning ?? isLikelyReasoningModel;
|
|
495
|
+
const getCompat = callbacks.getProxyCompat ?? getProxyModelCompat;
|
|
438
496
|
|
|
439
497
|
logger.info(`[${providerId}] Fetching models...`);
|
|
440
498
|
|
|
@@ -463,7 +521,7 @@ export async function fetchOpenAICompatibleModels(
|
|
|
463
521
|
|
|
464
522
|
logger.info(`[${providerId}] Fetched ${models.length} models`);
|
|
465
523
|
|
|
466
|
-
|
|
524
|
+
const mapped = models
|
|
467
525
|
.filter((m) => m.id)
|
|
468
526
|
.map((m): PiProviderModelConfig => {
|
|
469
527
|
const name = m.id.split("/").pop() || m.id;
|
|
@@ -484,8 +542,7 @@ export async function fetchOpenAICompatibleModels(
|
|
|
484
542
|
4_096;
|
|
485
543
|
|
|
486
544
|
// Use per-model reasoning flag if the API provides it
|
|
487
|
-
const reasoning =
|
|
488
|
-
m.reasoning ?? isLikelyReasoningModel({ id: m.id, name });
|
|
545
|
+
const reasoning = m.reasoning ?? detectReasoning({ id: m.id, name });
|
|
489
546
|
|
|
490
547
|
// Use per-model input_modalities if the API provides it
|
|
491
548
|
const hasVision = m.input_modalities?.includes("image") ?? false;
|
|
@@ -521,10 +578,12 @@ export async function fetchOpenAICompatibleModels(
|
|
|
521
578
|
},
|
|
522
579
|
contextWindow,
|
|
523
580
|
maxTokens,
|
|
524
|
-
compat:
|
|
581
|
+
compat: getCompat({ id: m.id, name }),
|
|
525
582
|
_pricingKnown: hasApiPricing,
|
|
526
583
|
} as PiProviderModelConfig & { _pricingKnown?: boolean };
|
|
527
584
|
});
|
|
585
|
+
|
|
586
|
+
return await safeEnrichModelsWithModelsDev(mapped, { providerId });
|
|
528
587
|
} catch (error) {
|
|
529
588
|
logger.error(`[${providerId}] Failed to fetch models:`, {
|
|
530
589
|
error: error instanceof Error ? error.message : String(error),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-free",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI model providers for Pi with free model filtering and dynamic model fetching",
|
|
6
6
|
"keywords": [
|
|
@@ -44,10 +44,15 @@
|
|
|
44
44
|
"scripts/check-extensions.mjs"
|
|
45
45
|
],
|
|
46
46
|
"scripts": {
|
|
47
|
+
"audit:prod": "npm audit --omit=dev --audit-level=high",
|
|
47
48
|
"check": "node scripts/check-extensions.mjs",
|
|
49
|
+
"check:lockfile": "node scripts/check-lockfile-sync.mjs",
|
|
50
|
+
"check:tarball": "node scripts/check-tarball.mjs",
|
|
51
|
+
"lint": "tsc --noEmit",
|
|
48
52
|
"test": "vitest",
|
|
49
53
|
"test:ui": "vitest --ui",
|
|
50
|
-
"test:run": "vitest run"
|
|
54
|
+
"test:run": "vitest run",
|
|
55
|
+
"smoke:cline": "tsx scripts/smoke-cline-xml-bridge.ts"
|
|
51
56
|
},
|
|
52
57
|
"peerDependencies": {
|
|
53
58
|
"@earendil-works/pi-ai": "*",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
+
import type { ModelMatchHints } from "../lib/types.ts";
|
|
15
16
|
import {
|
|
16
17
|
HARDCODED_BENCHMARKS,
|
|
17
18
|
type HardcodedBenchmark,
|
|
@@ -677,6 +678,7 @@ export function findHardcodedBenchmark(
|
|
|
677
678
|
modelName: string,
|
|
678
679
|
modelId: string,
|
|
679
680
|
provider?: string,
|
|
681
|
+
hints?: ModelMatchHints,
|
|
680
682
|
): HardcodedBenchmark | null {
|
|
681
683
|
// Normalize: convert colons to dashes (Ollama model:tag format)
|
|
682
684
|
const search = `${modelName} ${modelId}`.toLowerCase().replaceAll(":", "-");
|
|
@@ -699,9 +701,17 @@ export function findHardcodedBenchmark(
|
|
|
699
701
|
);
|
|
700
702
|
if (normalizedResult) return normalizedResult;
|
|
701
703
|
|
|
702
|
-
// 4. Prefix fallback with base model extraction
|
|
703
|
-
|
|
704
|
-
|
|
704
|
+
// 4. Prefix fallback with base model extraction. Also try models.dev
|
|
705
|
+
// canonical IDs/names when available for opaque gateway model IDs.
|
|
706
|
+
const prefixCandidates = [normalized, hints?.id, hints?.name]
|
|
707
|
+
.map((candidate) =>
|
|
708
|
+
(candidate?.trim() ?? "").toLowerCase().replaceAll(/[\s_:]+/g, "-"),
|
|
709
|
+
)
|
|
710
|
+
.filter(Boolean);
|
|
711
|
+
for (const candidate of prefixCandidates) {
|
|
712
|
+
const prefix = tryPrefixFallback(candidate, provider, modelId, modelName);
|
|
713
|
+
if (prefix) return prefix;
|
|
714
|
+
}
|
|
705
715
|
|
|
706
716
|
// No match found
|
|
707
717
|
logDebug({
|
|
@@ -724,8 +734,9 @@ export function getHardcodedScore(
|
|
|
724
734
|
modelName: string,
|
|
725
735
|
modelId: string,
|
|
726
736
|
provider?: string,
|
|
737
|
+
hints?: ModelMatchHints,
|
|
727
738
|
): number | null {
|
|
728
|
-
const benchmark = findHardcodedBenchmark(modelName, modelId, provider);
|
|
739
|
+
const benchmark = findHardcodedBenchmark(modelName, modelId, provider, hints);
|
|
729
740
|
return benchmark?.codingIndex ?? null;
|
|
730
741
|
}
|
|
731
742
|
|
|
@@ -737,8 +748,9 @@ export function enhanceModelNameWithCodingIndex(
|
|
|
737
748
|
modelName: string,
|
|
738
749
|
modelId: string,
|
|
739
750
|
provider?: string,
|
|
751
|
+
hints?: ModelMatchHints,
|
|
740
752
|
): string {
|
|
741
|
-
const benchmark = findHardcodedBenchmark(modelName, modelId, provider);
|
|
753
|
+
const benchmark = findHardcodedBenchmark(modelName, modelId, provider, hints);
|
|
742
754
|
if (benchmark?.codingIndex !== undefined) {
|
|
743
755
|
return `${modelName} [CI: ${benchmark.codingIndex.toFixed(1)}]`;
|
|
744
756
|
}
|