pi-free 2.0.7 → 2.0.9
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 +96 -10
- package/README.md +572 -495
- package/config.ts +58 -11
- package/constants.ts +12 -0
- package/index.ts +67 -3
- package/lib/built-in-toggle.ts +2 -2
- package/lib/model-detection.ts +2 -1
- package/lib/model-enhancer.ts +1 -1
- package/lib/open-browser.ts +1 -1
- package/lib/provider-compat.ts +1 -1
- package/lib/quota-monitor.ts +123 -0
- package/lib/registry.ts +1 -1
- package/lib/types.ts +101 -101
- package/lib/util.ts +460 -351
- package/package.json +4 -4
- package/provider-failover/benchmark-lookup.ts +743 -702
- package/provider-failover/benchmarks-chunk-0.ts +48 -48
- package/provider-failover/benchmarks-chunk-1.ts +44 -44
- package/provider-failover/benchmarks-chunk-2.ts +39 -39
- package/provider-failover/benchmarks-chunk-3.ts +41 -41
- package/provider-failover/benchmarks-chunk-4.ts +33 -33
- package/provider-helper.ts +1 -1
- package/providers/cline/cline-auth.ts +473 -473
- package/providers/cline/cline-models.ts +2 -2
- package/providers/cline/cline.ts +3 -3
- package/providers/codestral/codestral.ts +139 -0
- package/providers/crofai/crofai.ts +14 -85
- package/providers/deepinfra/deepinfra.ts +109 -0
- package/providers/dynamic-built-in/index.ts +1 -1
- package/providers/kilo/kilo-auth.ts +155 -155
- package/providers/kilo/kilo.ts +3 -3
- package/providers/llm7/llm7.ts +156 -0
- package/providers/model-fetcher.ts +2 -2
- package/providers/nvidia/nvidia.ts +5 -5
- package/providers/ollama/ollama.ts +2 -2
- package/providers/opencode-session.ts +1 -1
- package/providers/qwen/qwen-auth.ts +1 -1
- package/providers/qwen/qwen-models.ts +1 -1
- package/providers/qwen/qwen.ts +3 -3
- package/providers/sambanova/sambanova.ts +109 -0
- package/providers/zenmux/zenmux.ts +6 -3
- package/scripts/check-extensions.mjs +6 -4
package/config.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
|
|
14
|
+
export {
|
|
15
15
|
PROVIDER_CLINE,
|
|
16
16
|
PROVIDER_KILO,
|
|
17
17
|
PROVIDER_MODAL,
|
|
@@ -27,7 +27,11 @@ interface PiFreeConfig {
|
|
|
27
27
|
ollama_api_key?: string;
|
|
28
28
|
zenmux_api_key?: string;
|
|
29
29
|
crofai_api_key?: string;
|
|
30
|
+
codestral_api_key?: string;
|
|
30
31
|
mistral_api_key?: string;
|
|
32
|
+
llm7_api_key?: string;
|
|
33
|
+
deepinfra_api_key?: string;
|
|
34
|
+
sambanova_api_key?: string;
|
|
31
35
|
groq_api_key?: string;
|
|
32
36
|
cerebras_api_key?: string;
|
|
33
37
|
xai_api_key?: string;
|
|
@@ -40,6 +44,10 @@ interface PiFreeConfig {
|
|
|
40
44
|
cline_show_paid?: boolean;
|
|
41
45
|
zenmux_show_paid?: boolean;
|
|
42
46
|
crofai_show_paid?: boolean;
|
|
47
|
+
codestral_show_paid?: boolean;
|
|
48
|
+
llm7_show_paid?: boolean;
|
|
49
|
+
deepinfra_show_paid?: boolean;
|
|
50
|
+
sambanova_show_paid?: boolean;
|
|
43
51
|
openrouter_show_paid?: boolean;
|
|
44
52
|
opencode_show_paid?: boolean;
|
|
45
53
|
}
|
|
@@ -49,7 +57,11 @@ const CONFIG_TEMPLATE: PiFreeConfig = {
|
|
|
49
57
|
ollama_api_key: "",
|
|
50
58
|
zenmux_api_key: "",
|
|
51
59
|
crofai_api_key: "",
|
|
60
|
+
codestral_api_key: "",
|
|
52
61
|
mistral_api_key: "",
|
|
62
|
+
llm7_api_key: "",
|
|
63
|
+
deepinfra_api_key: "",
|
|
64
|
+
sambanova_api_key: "",
|
|
53
65
|
groq_api_key: "",
|
|
54
66
|
cerebras_api_key: "",
|
|
55
67
|
xai_api_key: "",
|
|
@@ -63,6 +75,10 @@ const CONFIG_TEMPLATE: PiFreeConfig = {
|
|
|
63
75
|
cline_show_paid: false,
|
|
64
76
|
zenmux_show_paid: false,
|
|
65
77
|
crofai_show_paid: false,
|
|
78
|
+
codestral_show_paid: false,
|
|
79
|
+
llm7_show_paid: false,
|
|
80
|
+
deepinfra_show_paid: false,
|
|
81
|
+
sambanova_show_paid: false,
|
|
66
82
|
openrouter_show_paid: false,
|
|
67
83
|
opencode_show_paid: false,
|
|
68
84
|
};
|
|
@@ -147,6 +163,31 @@ export function getCrofaiShowPaid(): boolean {
|
|
|
147
163
|
return resolveBool("CROFAI_SHOW_PAID", loadConfigFile().crofai_show_paid);
|
|
148
164
|
}
|
|
149
165
|
|
|
166
|
+
export function getCodestralShowPaid(): boolean {
|
|
167
|
+
return resolveBool(
|
|
168
|
+
"CODESTRAL_SHOW_PAID",
|
|
169
|
+
loadConfigFile().codestral_show_paid,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getLlm7ShowPaid(): boolean {
|
|
174
|
+
return resolveBool("LLM7_SHOW_PAID", loadConfigFile().llm7_show_paid);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getDeepinfraShowPaid(): boolean {
|
|
178
|
+
return resolveBool(
|
|
179
|
+
"DEEPINFRA_SHOW_PAID",
|
|
180
|
+
loadConfigFile().deepinfra_show_paid,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function getSambanovaShowPaid(): boolean {
|
|
185
|
+
return resolveBool(
|
|
186
|
+
"SAMBANOVA_SHOW_PAID",
|
|
187
|
+
loadConfigFile().sambanova_show_paid,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
150
191
|
export function getOllamaShowPaid(): boolean {
|
|
151
192
|
return resolveBool("OLLAMA_SHOW_PAID", loadConfigFile().ollama_show_paid);
|
|
152
193
|
}
|
|
@@ -190,6 +231,22 @@ export function getCrofaiApiKey(): string | undefined {
|
|
|
190
231
|
return resolve("CROFAI_API_KEY", loadConfigFile().crofai_api_key);
|
|
191
232
|
}
|
|
192
233
|
|
|
234
|
+
export function getCodestralApiKey(): string | undefined {
|
|
235
|
+
return resolve("CODESTRAL_API_KEY", loadConfigFile().codestral_api_key);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function getLlm7ApiKey(): string | undefined {
|
|
239
|
+
return resolve("LLM7_API_KEY", loadConfigFile().llm7_api_key);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function getDeepinfraApiKey(): string | undefined {
|
|
243
|
+
return resolve("DEEPINFRA_TOKEN", loadConfigFile().deepinfra_api_key);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function getSambanovaApiKey(): string | undefined {
|
|
247
|
+
return resolve("SAMBANOVA_API_KEY", loadConfigFile().sambanova_api_key);
|
|
248
|
+
}
|
|
249
|
+
|
|
193
250
|
export function getOllamaApiKey(): string | undefined {
|
|
194
251
|
return resolve("OLLAMA_API_KEY", loadConfigFile().ollama_api_key);
|
|
195
252
|
}
|
|
@@ -278,13 +335,3 @@ export function getConfig(): PiFreeConfig {
|
|
|
278
335
|
}
|
|
279
336
|
|
|
280
337
|
// =============================================================================
|
|
281
|
-
// Re-export provider names for consistency
|
|
282
|
-
// =============================================================================
|
|
283
|
-
|
|
284
|
-
export {
|
|
285
|
-
PROVIDER_CLINE,
|
|
286
|
-
PROVIDER_KILO,
|
|
287
|
-
PROVIDER_MODAL,
|
|
288
|
-
PROVIDER_NVIDIA,
|
|
289
|
-
PROVIDER_QWEN,
|
|
290
|
-
};
|
package/constants.ts
CHANGED
|
@@ -17,6 +17,10 @@ export const PROVIDER_QWEN = "qwen";
|
|
|
17
17
|
export const PROVIDER_MODAL = "modal";
|
|
18
18
|
export const PROVIDER_ZENMUX = "zenmux";
|
|
19
19
|
export const PROVIDER_CROFAI = "crofai";
|
|
20
|
+
export const PROVIDER_CODESTRAL = "codestral";
|
|
21
|
+
export const PROVIDER_LLM7 = "llm7";
|
|
22
|
+
export const PROVIDER_DEEPINFRA = "deepinfra";
|
|
23
|
+
export const PROVIDER_SAMBANOVA = "sambanova";
|
|
20
24
|
|
|
21
25
|
export const ALL_UNIQUE_PROVIDERS = [
|
|
22
26
|
PROVIDER_KILO,
|
|
@@ -28,6 +32,10 @@ export const ALL_UNIQUE_PROVIDERS = [
|
|
|
28
32
|
PROVIDER_OLLAMA,
|
|
29
33
|
PROVIDER_ZENMUX,
|
|
30
34
|
PROVIDER_CROFAI,
|
|
35
|
+
PROVIDER_CODESTRAL,
|
|
36
|
+
PROVIDER_LLM7,
|
|
37
|
+
PROVIDER_DEEPINFRA,
|
|
38
|
+
PROVIDER_SAMBANOVA,
|
|
31
39
|
] as const;
|
|
32
40
|
|
|
33
41
|
// =============================================================================
|
|
@@ -44,6 +52,10 @@ export const BASE_URL_QWEN =
|
|
|
44
52
|
"https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
45
53
|
export const BASE_URL_ZENMUX = "https://zenmux.ai/api/v1";
|
|
46
54
|
export const BASE_URL_CROFAI = "https://crof.ai/v1";
|
|
55
|
+
export const BASE_URL_CODESTRAL = "https://codestral.mistral.ai/v1";
|
|
56
|
+
export const BASE_URL_LLM7 = "https://api.llm7.io/v1";
|
|
57
|
+
export const BASE_URL_DEEPINFRA = "https://api.deepinfra.com/v1/openai";
|
|
58
|
+
export const BASE_URL_SAMBANOVA = "https://api.sambanova.ai/v1";
|
|
47
59
|
|
|
48
60
|
/** Cline fetches free models from OpenRouter */
|
|
49
61
|
export const BASE_URL_OPENROUTER = "https://openrouter.ai/api/v1";
|
package/index.ts
CHANGED
|
@@ -10,11 +10,19 @@
|
|
|
10
10
|
* - NVIDIA: NVIDIA NIM hosting (free tier available)
|
|
11
11
|
* - Ollama Cloud: Ollama's cloud-hosted models with usage-based free tier
|
|
12
12
|
* - ZenMux: Unified AI API gateway with 200+ models
|
|
13
|
+
* - Codestral: Mistral's code-focused model via codestral.mistral.ai (free tier)
|
|
14
|
+
* - DeepInfra: AI inference cloud ($5 trial credit)
|
|
15
|
+
* - SambaNova: Fast inference on RDU hardware (free tier, no credit card)
|
|
16
|
+
* - LLM7: AI gateway (free default/fast selectors)
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
import type { ExtensionAPI } from "@
|
|
19
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
20
|
import { setupBuiltInProviderToggles } from "./lib/built-in-toggle.ts";
|
|
17
21
|
import { createLogger } from "./lib/logger.ts";
|
|
22
|
+
import {
|
|
23
|
+
processQuotaResponse,
|
|
24
|
+
formatQuotaStatus,
|
|
25
|
+
} from "./lib/quota-monitor.ts";
|
|
18
26
|
import {
|
|
19
27
|
applyGlobalFilter,
|
|
20
28
|
getGlobalFreeOnly,
|
|
@@ -24,8 +32,12 @@ import {
|
|
|
24
32
|
} from "./lib/registry.ts";
|
|
25
33
|
// Import unique provider extensions (only providers NOT built into pi)
|
|
26
34
|
import cline from "./providers/cline/cline.ts";
|
|
35
|
+
import codestral from "./providers/codestral/codestral.ts";
|
|
27
36
|
import crofai from "./providers/crofai/crofai.ts";
|
|
28
37
|
import kilo from "./providers/kilo/kilo.ts";
|
|
38
|
+
import llm7 from "./providers/llm7/llm7.ts";
|
|
39
|
+
import deepinfra from "./providers/deepinfra/deepinfra.ts";
|
|
40
|
+
import sambanova from "./providers/sambanova/sambanova.ts";
|
|
29
41
|
import nvidia from "./providers/nvidia/nvidia.ts";
|
|
30
42
|
import ollama from "./providers/ollama/ollama.ts";
|
|
31
43
|
import zenmux from "./providers/zenmux/zenmux.ts";
|
|
@@ -87,7 +99,13 @@ function setupGlobalCommands(pi: ExtensionAPI) {
|
|
|
87
99
|
"cerebras",
|
|
88
100
|
]);
|
|
89
101
|
// Freemium providers - all models share a free tier quota
|
|
90
|
-
const freemiumProviders = new Set([
|
|
102
|
+
const freemiumProviders = new Set([
|
|
103
|
+
"nvidia",
|
|
104
|
+
"sambanova",
|
|
105
|
+
"ollama-cloud",
|
|
106
|
+
]);
|
|
107
|
+
// Trial credit providers - one-time credits, otherwise paid
|
|
108
|
+
const trialCreditProviders = new Set(["deepinfra"]);
|
|
91
109
|
|
|
92
110
|
for (const [id, entry] of registry) {
|
|
93
111
|
const free = entry.stored.free.length;
|
|
@@ -98,6 +116,9 @@ function setupGlobalCommands(pi: ExtensionAPI) {
|
|
|
98
116
|
if (freemiumProviders.has(id)) {
|
|
99
117
|
// Freemium: all models share a free tier (e.g., 1,000 reqs/month)
|
|
100
118
|
lines.push(`${indicator} ${id}: ${all} models (freemium)`);
|
|
119
|
+
} else if (trialCreditProviders.has(id)) {
|
|
120
|
+
// Trial credit: one-time credits, otherwise paid
|
|
121
|
+
lines.push(`${indicator} ${id}: ${all} models ($5 trial credit)`);
|
|
101
122
|
} else if (noPricingApi.has(id)) {
|
|
102
123
|
// Provider doesn't expose pricing - can't determine free vs paid
|
|
103
124
|
lines.push(
|
|
@@ -123,17 +144,56 @@ function setupGlobalCommands(pi: ExtensionAPI) {
|
|
|
123
144
|
});
|
|
124
145
|
}
|
|
125
146
|
|
|
147
|
+
// =============================================================================
|
|
148
|
+
// Quota Monitoring
|
|
149
|
+
// =============================================================================
|
|
150
|
+
|
|
151
|
+
function setupQuotaMonitoring(pi: ExtensionAPI) {
|
|
152
|
+
// Capture rate-limit headers from every provider response
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
154
|
+
(pi as any).on(
|
|
155
|
+
"after_provider_response",
|
|
156
|
+
(event: { status: number; headers: Record<string, string> }, ctx: any) => {
|
|
157
|
+
const providerId = ctx.model?.provider;
|
|
158
|
+
if (!providerId) return;
|
|
159
|
+
|
|
160
|
+
processQuotaResponse(providerId, event.headers);
|
|
161
|
+
|
|
162
|
+
// Update status bar with quota for the active provider
|
|
163
|
+
const status = formatQuotaStatus(providerId);
|
|
164
|
+
if (status) {
|
|
165
|
+
ctx.ui.setStatus("quota", status);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Clear quota status when switching away from a provider
|
|
171
|
+
pi.on("model_select", (_event, ctx) => {
|
|
172
|
+
const providerId = ctx.model?.provider;
|
|
173
|
+
if (!providerId) {
|
|
174
|
+
ctx.ui.setStatus("quota", undefined);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Show cached quota on provider switch (if still fresh)
|
|
178
|
+
const status = formatQuotaStatus(providerId);
|
|
179
|
+
ctx.ui.setStatus("quota", status);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
126
183
|
// =============================================================================
|
|
127
184
|
// Main Entry Point
|
|
128
185
|
// =============================================================================
|
|
129
186
|
|
|
130
|
-
export default async function (pi: ExtensionAPI) {
|
|
187
|
+
export default async function piFreeEntry(pi: ExtensionAPI) {
|
|
131
188
|
const globalFreeOnly = getGlobalFreeOnly();
|
|
132
189
|
_logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
|
|
133
190
|
|
|
134
191
|
// Setup global commands first
|
|
135
192
|
setupGlobalCommands(pi);
|
|
136
193
|
|
|
194
|
+
// Setup quota monitoring (passive, no extra API calls)
|
|
195
|
+
setupQuotaMonitoring(pi);
|
|
196
|
+
|
|
137
197
|
// Load all unique providers
|
|
138
198
|
// Each provider will register itself with the global toggle system
|
|
139
199
|
await Promise.allSettled([
|
|
@@ -143,6 +203,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
143
203
|
cline(pi),
|
|
144
204
|
zenmux(pi),
|
|
145
205
|
crofai(pi),
|
|
206
|
+
codestral(pi),
|
|
207
|
+
llm7(pi),
|
|
208
|
+
deepinfra(pi),
|
|
209
|
+
sambanova(pi),
|
|
146
210
|
]);
|
|
147
211
|
|
|
148
212
|
// Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face)
|
package/lib/built-in-toggle.ts
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* Usage: /toggle-opencode
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { Api, Model } from "@
|
|
14
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
15
15
|
import type {
|
|
16
16
|
ExtensionAPI,
|
|
17
17
|
ProviderModelConfig,
|
|
18
|
-
} from "@
|
|
18
|
+
} from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { getOpencodeShowPaid, getOpenrouterShowPaid } from "../config.ts";
|
|
20
20
|
import { createLogger } from "./logger.ts";
|
|
21
21
|
import { isFreeModel, registerWithGlobalToggle } from "./registry.ts";
|
package/lib/model-detection.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Used for failover when providers hit rate limits.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Model } from "@
|
|
7
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
8
8
|
import type { ProviderModelConfig } from "./types.ts";
|
|
9
9
|
|
|
10
10
|
export interface ModelInfo {
|
|
@@ -206,6 +206,7 @@ export function normalizeModelName(name: string): string {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
// CI score suffix — regex with disjoint char classes (linear)
|
|
209
|
+
// Anchored with $, matches at most once → .replace() is correct (S4144 N/A)
|
|
209
210
|
normalized = normalized.replace(/\(ci:\s*[\d.]+\)$/, "").trimEnd();
|
|
210
211
|
normalized = normalized.replace(/\[ci:\s*[\d.]+\]$/, "").trimEnd();
|
|
211
212
|
|
package/lib/model-enhancer.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Adds Coding Index scores to model names for display in /model
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { ProviderModelConfig } from "@
|
|
6
|
+
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { enhanceModelNameWithCodingIndex } from "../provider-failover/benchmark-lookup.ts";
|
|
8
8
|
|
|
9
9
|
/**
|
package/lib/open-browser.ts
CHANGED
|
@@ -50,7 +50,7 @@ export function openBrowser(url: string): void {
|
|
|
50
50
|
"-NoProfile",
|
|
51
51
|
"-NonInteractive",
|
|
52
52
|
"-Command",
|
|
53
|
-
`Start-Process "${url.
|
|
53
|
+
`Start-Process "${url.replaceAll(/[\\"]/g, "\\$&")}"`,
|
|
54
54
|
],
|
|
55
55
|
{ detached: true, shell: false, windowsHide: true },
|
|
56
56
|
).unref();
|
package/lib/provider-compat.ts
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota Monitoring for pi-free providers.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to pi's `after_provider_response` event to extract rate-limit
|
|
5
|
+
* headers from provider responses and track remaining quota per provider.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by free-coding-models' extractQuotaPercent and provider-quota-fetchers.
|
|
8
|
+
*
|
|
9
|
+
* Supported header formats (tried in order):
|
|
10
|
+
* 1. x-ratelimit-remaining-requests / x-ratelimit-limit-requests (SambaNova)
|
|
11
|
+
* 2. x-ratelimit-remaining / x-ratelimit-limit (Mistral, others)
|
|
12
|
+
* 3. ratelimit-remaining-requests / ratelimit-limit-requests
|
|
13
|
+
* 4. ratelimit-remaining / ratelimit-limit
|
|
14
|
+
* 5. x-ratelimit-remaining-requests-day / x-ratelimit-limit-requests-day (SambaNova daily)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const _quotaState = new Map<string, QuotaSnapshot>();
|
|
18
|
+
|
|
19
|
+
/** Snapshot of quota state for a single provider. */
|
|
20
|
+
export interface QuotaSnapshot {
|
|
21
|
+
/** Requests remaining in the current window. */
|
|
22
|
+
remaining: number;
|
|
23
|
+
/** Total requests allowed in the current window. */
|
|
24
|
+
limit: number;
|
|
25
|
+
/** Remaining as percentage 0–100. */
|
|
26
|
+
percent: number;
|
|
27
|
+
/** Timestamp (Date.now()) when this snapshot was captured. */
|
|
28
|
+
lastUpdated: number;
|
|
29
|
+
/** Which header variant was matched (for debugging). */
|
|
30
|
+
source: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Header key pairs to try, in priority order.
|
|
34
|
+
// Each pair is [remaining, limit].
|
|
35
|
+
const HEADER_PAIRS: [string, string][] = [
|
|
36
|
+
// Per-minute (most common)
|
|
37
|
+
["x-ratelimit-remaining-requests", "x-ratelimit-limit-requests"],
|
|
38
|
+
["x-ratelimit-remaining", "x-ratelimit-limit"],
|
|
39
|
+
["ratelimit-remaining-requests", "ratelimit-limit-requests"],
|
|
40
|
+
["ratelimit-remaining", "ratelimit-limit"],
|
|
41
|
+
// Per-day
|
|
42
|
+
["x-ratelimit-remaining-requests-day", "x-ratelimit-limit-requests-day"],
|
|
43
|
+
["x-ratelimit-remaining-day", "x-ratelimit-limit-day"],
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Attempt to extract quota from response headers.
|
|
48
|
+
* Returns { remaining, limit, source } or null if no quota headers found.
|
|
49
|
+
*/
|
|
50
|
+
export function extractQuota(
|
|
51
|
+
headers: Record<string, string>,
|
|
52
|
+
): { remaining: number; limit: number; source: string } | null {
|
|
53
|
+
// Normalize keys to lowercase for case-insensitive matching.
|
|
54
|
+
// Some proxies/servers vary header casing.
|
|
55
|
+
const normalized: Record<string, string> = {};
|
|
56
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
57
|
+
normalized[key.toLowerCase()] = value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [remainingKey, limitKey] of HEADER_PAIRS) {
|
|
61
|
+
const remaining = Number.parseFloat(normalized[remainingKey]);
|
|
62
|
+
const limit = Number.parseFloat(normalized[limitKey]);
|
|
63
|
+
if (Number.isFinite(remaining) && Number.isFinite(limit) && limit > 0) {
|
|
64
|
+
return { remaining, limit, source: remainingKey };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Process an after_provider_response event, updating quota state.
|
|
73
|
+
* Call from the event handler in index.ts.
|
|
74
|
+
*/
|
|
75
|
+
export function processQuotaResponse(
|
|
76
|
+
providerId: string,
|
|
77
|
+
headers: Record<string, string>,
|
|
78
|
+
): void {
|
|
79
|
+
const extracted = extractQuota(headers);
|
|
80
|
+
if (!extracted) return;
|
|
81
|
+
|
|
82
|
+
const percent = Math.round((extracted.remaining / extracted.limit) * 100);
|
|
83
|
+
|
|
84
|
+
_quotaState.set(providerId, {
|
|
85
|
+
remaining: extracted.remaining,
|
|
86
|
+
limit: extracted.limit,
|
|
87
|
+
percent: Math.max(0, Math.min(100, percent)),
|
|
88
|
+
lastUpdated: Date.now(),
|
|
89
|
+
source: extracted.source,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the latest quota snapshot for a provider, or null if unknown.
|
|
95
|
+
*/
|
|
96
|
+
export function getQuota(providerId: string): QuotaSnapshot | null {
|
|
97
|
+
return _quotaState.get(providerId) ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get all tracked quotas.
|
|
102
|
+
*/
|
|
103
|
+
export function getAllQuotas(): ReadonlyMap<string, QuotaSnapshot> {
|
|
104
|
+
return _quotaState;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build a human-readable status bar line for a provider's quota.
|
|
109
|
+
* Returns undefined if no quota data is available.
|
|
110
|
+
*/
|
|
111
|
+
export function formatQuotaStatus(providerId: string): string | undefined {
|
|
112
|
+
const q = _quotaState.get(providerId);
|
|
113
|
+
if (!q) return undefined;
|
|
114
|
+
|
|
115
|
+
// Stale after 5 minutes
|
|
116
|
+
if (Date.now() - q.lastUpdated > 5 * 60 * 1000) return undefined;
|
|
117
|
+
|
|
118
|
+
if (q.percent <= 10)
|
|
119
|
+
return `⚠️ ${providerId}: ${q.remaining}/${q.limit} (${q.percent}%)`;
|
|
120
|
+
if (q.percent <= 25)
|
|
121
|
+
return `⚡ ${providerId}: ${q.remaining}/${q.limit} (${q.percent}%)`;
|
|
122
|
+
return `${providerId}: ${q.remaining}/${q.limit} (${q.percent}%)`;
|
|
123
|
+
}
|
package/lib/registry.ts
CHANGED
package/lib/types.ts
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types for pi-free-providers.
|
|
3
|
-
* Interfaces duplicated across providers consolidated here.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// =============================================================================
|
|
7
|
-
// Provider model configuration (matches Pi's ProviderModelConfig)
|
|
8
|
-
// =============================================================================
|
|
9
|
-
|
|
10
|
-
export interface CostConfig {
|
|
11
|
-
input: number;
|
|
12
|
-
output: number;
|
|
13
|
-
cacheRead: number;
|
|
14
|
-
cacheWrite: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ProviderModelConfig {
|
|
18
|
-
id: string;
|
|
19
|
-
name: string;
|
|
20
|
-
reasoning: boolean;
|
|
21
|
-
input: ("text" | "image")[];
|
|
22
|
-
cost: CostConfig;
|
|
23
|
-
contextWindow: number;
|
|
24
|
-
maxTokens: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// =============================================================================
|
|
28
|
-
// models.dev schema types
|
|
29
|
-
// =============================================================================
|
|
30
|
-
|
|
31
|
-
export interface ModelsDevCost {
|
|
32
|
-
input: number;
|
|
33
|
-
output: number;
|
|
34
|
-
cache_read?: number;
|
|
35
|
-
cache_write?: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ModelsDevLimit {
|
|
39
|
-
context: number;
|
|
40
|
-
output: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface ModelsDevModalities {
|
|
44
|
-
input?: string[];
|
|
45
|
-
output?: string[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface ModelsDevModel {
|
|
49
|
-
id: string;
|
|
50
|
-
name: string;
|
|
51
|
-
reasoning: boolean;
|
|
52
|
-
cost?: ModelsDevCost;
|
|
53
|
-
limit: ModelsDevLimit;
|
|
54
|
-
modalities?: ModelsDevModalities;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface ModelsDevProvider {
|
|
58
|
-
id: string;
|
|
59
|
-
api: string;
|
|
60
|
-
models: Record<string, ModelsDevModel>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// =============================================================================
|
|
64
|
-
// OpenRouter API response types
|
|
65
|
-
// =============================================================================
|
|
66
|
-
|
|
67
|
-
export interface OpenRouterPricing {
|
|
68
|
-
prompt?: string | null;
|
|
69
|
-
completion?: string | null;
|
|
70
|
-
input_cache_write?: string | null;
|
|
71
|
-
input_cache_read?: string | null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface OpenRouterArchitecture {
|
|
75
|
-
input_modalities?: string[] | null;
|
|
76
|
-
output_modalities?: string[] | null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface OpenRouterTopProvider {
|
|
80
|
-
max_completion_tokens?: number | null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface OpenRouterModel {
|
|
84
|
-
id: string;
|
|
85
|
-
name: string;
|
|
86
|
-
context_length: number;
|
|
87
|
-
max_completion_tokens?: number | null;
|
|
88
|
-
pricing?: OpenRouterPricing;
|
|
89
|
-
architecture?: OpenRouterArchitecture;
|
|
90
|
-
top_provider?: OpenRouterTopProvider;
|
|
91
|
-
supported_parameters?: string[];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// =============================================================================
|
|
95
|
-
// Zen gateway types
|
|
96
|
-
// =============================================================================
|
|
97
|
-
|
|
98
|
-
export interface ZenGatewayModel {
|
|
99
|
-
id: string;
|
|
100
|
-
object?: string;
|
|
101
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for pi-free-providers.
|
|
3
|
+
* Interfaces duplicated across providers consolidated here.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Provider model configuration (matches Pi's ProviderModelConfig)
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
export interface CostConfig {
|
|
11
|
+
input: number;
|
|
12
|
+
output: number;
|
|
13
|
+
cacheRead: number;
|
|
14
|
+
cacheWrite: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ProviderModelConfig {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
reasoning: boolean;
|
|
21
|
+
input: ("text" | "image")[];
|
|
22
|
+
cost: CostConfig;
|
|
23
|
+
contextWindow: number;
|
|
24
|
+
maxTokens: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// models.dev schema types
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
export interface ModelsDevCost {
|
|
32
|
+
input: number;
|
|
33
|
+
output: number;
|
|
34
|
+
cache_read?: number;
|
|
35
|
+
cache_write?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ModelsDevLimit {
|
|
39
|
+
context: number;
|
|
40
|
+
output: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ModelsDevModalities {
|
|
44
|
+
input?: string[];
|
|
45
|
+
output?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ModelsDevModel {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
reasoning: boolean;
|
|
52
|
+
cost?: ModelsDevCost;
|
|
53
|
+
limit: ModelsDevLimit;
|
|
54
|
+
modalities?: ModelsDevModalities;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ModelsDevProvider {
|
|
58
|
+
id: string;
|
|
59
|
+
api: string;
|
|
60
|
+
models: Record<string, ModelsDevModel>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// OpenRouter API response types
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
export interface OpenRouterPricing {
|
|
68
|
+
prompt?: string | null;
|
|
69
|
+
completion?: string | null;
|
|
70
|
+
input_cache_write?: string | null;
|
|
71
|
+
input_cache_read?: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface OpenRouterArchitecture {
|
|
75
|
+
input_modalities?: string[] | null;
|
|
76
|
+
output_modalities?: string[] | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface OpenRouterTopProvider {
|
|
80
|
+
max_completion_tokens?: number | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface OpenRouterModel {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
context_length: number;
|
|
87
|
+
max_completion_tokens?: number | null;
|
|
88
|
+
pricing?: OpenRouterPricing;
|
|
89
|
+
architecture?: OpenRouterArchitecture;
|
|
90
|
+
top_provider?: OpenRouterTopProvider;
|
|
91
|
+
supported_parameters?: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// Zen gateway types
|
|
96
|
+
// =============================================================================
|
|
97
|
+
|
|
98
|
+
export interface ZenGatewayModel {
|
|
99
|
+
id: string;
|
|
100
|
+
object?: string;
|
|
101
|
+
}
|