opencode-pollinations-plugin 6.1.0-beta.9 → 6.2.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/README.de.md +130 -0
- package/README.es.md +130 -0
- package/README.fr.md +130 -0
- package/README.it.md +130 -0
- package/README.md +87 -73
- package/dist/index.js +52 -161
- package/dist/locales/de.json +374 -0
- package/dist/locales/en.json +373 -0
- package/dist/locales/es.json +374 -0
- package/dist/locales/fr.json +373 -0
- package/dist/locales/index.d.ts +1 -0
- package/dist/locales/index.js +37 -0
- package/dist/locales/it.json +374 -0
- package/dist/server/commands.d.ts +6 -0
- package/dist/server/commands.js +394 -125
- package/dist/server/config.d.ts +34 -23
- package/dist/server/config.js +200 -108
- package/dist/server/connect-response.d.ts +2 -0
- package/dist/server/connect-response.js +59 -0
- package/dist/server/generate-config.d.ts +3 -30
- package/dist/server/generate-config.js +164 -106
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +124 -149
- package/dist/server/logger.d.ts +8 -0
- package/dist/server/logger.js +38 -0
- package/dist/server/models/cache.d.ts +35 -0
- package/dist/server/models/cache.js +160 -0
- package/dist/server/models/fetcher.d.ts +18 -0
- package/dist/server/models/fetcher.js +194 -0
- package/dist/server/models/index.d.ts +6 -0
- package/dist/server/models/index.js +5 -0
- package/dist/server/models/manual.d.ts +15 -0
- package/dist/server/models/manual.js +92 -0
- package/dist/server/models/types.d.ts +55 -0
- package/dist/server/models/types.js +7 -0
- package/dist/server/models/worker.d.ts +22 -0
- package/dist/server/models/worker.js +174 -0
- package/dist/server/pollinations-api.d.ts +11 -0
- package/dist/server/pollinations-api.js +21 -8
- package/dist/server/proxy.js +222 -307
- package/dist/server/quota.d.ts +2 -0
- package/dist/server/quota.js +89 -86
- package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
- package/dist/server/scripts/pollinations_pricing.js +246 -0
- package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
- package/dist/server/scripts/test_cost_endpoints.js +61 -0
- package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
- package/dist/server/scripts/test_dynamic_pricing.js +39 -0
- package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
- package/dist/server/scripts/test_freetier_audit.js +215 -0
- package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
- package/dist/server/scripts/test_parallel_cost.js +104 -0
- package/dist/server/toast.d.ts +7 -1
- package/dist/server/toast.js +43 -10
- package/dist/tools/design/gen_diagram.d.ts +2 -0
- package/dist/tools/design/gen_diagram.js +94 -0
- package/dist/tools/design/gen_palette.d.ts +2 -0
- package/dist/tools/design/gen_palette.js +182 -0
- package/dist/tools/design/gen_qrcode.d.ts +2 -0
- package/dist/tools/design/gen_qrcode.js +50 -0
- package/dist/tools/ffmpeg.d.ts +24 -0
- package/dist/tools/ffmpeg.js +54 -0
- package/dist/tools/index.d.ts +25 -0
- package/dist/tools/index.js +86 -0
- package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
- package/dist/tools/pollinations/beta_discovery.js +201 -0
- package/dist/tools/pollinations/cost-guard.d.ts +38 -0
- package/dist/tools/pollinations/cost-guard.js +136 -0
- package/dist/tools/pollinations/deepsearch.d.ts +7 -0
- package/dist/tools/pollinations/deepsearch.js +80 -0
- package/dist/tools/pollinations/gen_audio.d.ts +18 -0
- package/dist/tools/pollinations/gen_audio.js +220 -0
- package/dist/tools/pollinations/gen_image.d.ts +11 -0
- package/dist/tools/pollinations/gen_image.js +211 -0
- package/dist/tools/pollinations/gen_music.d.ts +14 -0
- package/dist/tools/pollinations/gen_music.js +157 -0
- package/dist/tools/pollinations/gen_video.d.ts +16 -0
- package/dist/tools/pollinations/gen_video.js +249 -0
- package/dist/tools/pollinations/polli_config.d.ts +2 -0
- package/dist/tools/pollinations/polli_config.js +95 -0
- package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
- package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
- package/dist/tools/pollinations/polli_status.d.ts +2 -0
- package/dist/tools/pollinations/polli_status.js +31 -0
- package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
- package/dist/tools/pollinations/polli_web_search.js +126 -0
- package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
- package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
- package/dist/tools/pollinations/shared.d.ts +181 -0
- package/dist/tools/pollinations/shared.js +758 -0
- package/dist/tools/pollinations/test_estimators.d.ts +1 -0
- package/dist/tools/pollinations/test_estimators.js +22 -0
- package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
- package/dist/tools/pollinations/transcribe_audio.js +171 -0
- package/dist/tools/power/extract_audio.d.ts +2 -0
- package/dist/tools/power/extract_audio.js +179 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +237 -0
- package/dist/tools/power/file_to_url.d.ts +2 -0
- package/dist/tools/power/file_to_url.js +217 -0
- package/dist/tools/power/remove_background.d.ts +2 -0
- package/dist/tools/power/remove_background.js +404 -0
- package/dist/tools/power/rmbg_keys.d.ts +2 -0
- package/dist/tools/power/rmbg_keys.js +79 -0
- package/dist/tools/shared.d.ts +30 -0
- package/dist/tools/shared.js +80 -0
- package/package.json +10 -4
- package/dist/server/models-seed.d.ts +0 -18
- package/dist/server/models-seed.js +0 -55
package/dist/server/quota.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DetailedUsageEntry } from './pollinations-api.js';
|
|
1
2
|
export interface QuotaStatus {
|
|
2
3
|
tierRemaining: number;
|
|
3
4
|
tierUsed: number;
|
|
@@ -12,5 +13,6 @@ export interface QuotaStatus {
|
|
|
12
13
|
tierEmoji: string;
|
|
13
14
|
errorType?: 'auth_limited' | 'network' | 'unknown';
|
|
14
15
|
}
|
|
16
|
+
export declare function fetchUsageForPeriod(apiKey: string, lastReset: Date): Promise<DetailedUsageEntry[]>;
|
|
15
17
|
export declare function getQuotaStatus(forceRefresh?: boolean): Promise<QuotaStatus>;
|
|
16
18
|
export declare function formatQuotaForToast(quota: QuotaStatus): string;
|
package/dist/server/quota.js
CHANGED
|
@@ -1,42 +1,65 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
1
|
import * as https from 'https'; // Use Native HTTPS
|
|
3
2
|
import { loadConfig } from './config.js';
|
|
4
|
-
// === CACHE ===
|
|
3
|
+
// === CACHE & CONSTANTS ===
|
|
5
4
|
const CACHE_TTL = 30000; // 30 secondes
|
|
6
5
|
let cachedQuota = null;
|
|
7
6
|
let lastQuotaFetch = 0;
|
|
7
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
const HISTORY_RETENTION_MS = 48 * 60 * 60 * 1000; // 48h history
|
|
8
9
|
// === TIER LIMITS ===
|
|
9
10
|
const TIER_LIMITS = {
|
|
11
|
+
microbe: { pollen: 0.1, emoji: '🦠' },
|
|
10
12
|
spore: { pollen: 1, emoji: '🦠' },
|
|
11
13
|
seed: { pollen: 3, emoji: '🌱' },
|
|
12
14
|
flower: { pollen: 10, emoji: '🌸' },
|
|
13
15
|
nectar: { pollen: 20, emoji: '🍯' },
|
|
14
16
|
};
|
|
15
17
|
// === LOGGING ===
|
|
18
|
+
import { logApi } from './logger.js';
|
|
16
19
|
function logQuota(msg) {
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
logApi(`[QUOTA] ${msg}`);
|
|
21
|
+
}
|
|
22
|
+
// === SMART FETCH API ===
|
|
23
|
+
export async function fetchUsageForPeriod(apiKey, lastReset) {
|
|
24
|
+
let allUsage = [];
|
|
25
|
+
let offset = 0;
|
|
26
|
+
const limit = 100; // Bulk fetch
|
|
27
|
+
while (true) {
|
|
28
|
+
let usageRes;
|
|
29
|
+
try {
|
|
30
|
+
usageRes = await fetchAPI(`/account/usage?limit=${limit}&offset=${offset}`, apiKey);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
logQuota(`SmartFetch failed at offset ${offset}: ${e}`);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (!usageRes.usage || usageRes.usage.length === 0) {
|
|
37
|
+
break; // No more records
|
|
38
|
+
}
|
|
39
|
+
let reachedCutoff = false;
|
|
40
|
+
for (const entry of usageRes.usage) {
|
|
41
|
+
const timestampStr = entry.timestamp.includes('Z') ? entry.timestamp : entry.timestamp.replace(' ', 'T') + 'Z';
|
|
42
|
+
const entryTime = new Date(timestampStr);
|
|
43
|
+
if (entryTime < lastReset) {
|
|
44
|
+
reachedCutoff = true;
|
|
45
|
+
break; // Entry is from previous period, stop.
|
|
46
|
+
}
|
|
47
|
+
allUsage.push(entry);
|
|
48
|
+
}
|
|
49
|
+
// If we found an entry older than lastReset, or if the page was not full, we reached the end.
|
|
50
|
+
if (reachedCutoff || usageRes.usage.length < limit) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
offset += limit;
|
|
19
54
|
}
|
|
20
|
-
|
|
55
|
+
logQuota(`SmartFetch: Retrieved ${allUsage.length} transactions for current period.`);
|
|
56
|
+
return allUsage;
|
|
21
57
|
}
|
|
22
|
-
// ===
|
|
58
|
+
// === MAIN QUOTA FUNCTION ===
|
|
23
59
|
export async function getQuotaStatus(forceRefresh = false) {
|
|
24
60
|
const config = loadConfig();
|
|
25
61
|
if (!config.apiKey) {
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
tierRemaining: 0,
|
|
29
|
-
tierUsed: 0,
|
|
30
|
-
tierLimit: 0,
|
|
31
|
-
walletBalance: 0,
|
|
32
|
-
nextResetAt: new Date(),
|
|
33
|
-
timeUntilReset: 0,
|
|
34
|
-
canUseEnterprise: false,
|
|
35
|
-
isUsingWallet: false,
|
|
36
|
-
needsAlert: false,
|
|
37
|
-
tier: 'none',
|
|
38
|
-
tierEmoji: '❌'
|
|
39
|
-
};
|
|
62
|
+
return createDefaultQuota('none', 0);
|
|
40
63
|
}
|
|
41
64
|
const now = Date.now();
|
|
42
65
|
if (!forceRefresh && cachedQuota && (now - lastQuotaFetch) < CACHE_TTL) {
|
|
@@ -44,27 +67,33 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
44
67
|
}
|
|
45
68
|
try {
|
|
46
69
|
logQuota("Fetching Quota Data...");
|
|
70
|
+
// 1. Fetch API
|
|
47
71
|
// SEQUENTIAL FETCH (Avoid Rate Limits)
|
|
48
|
-
// We fetch one by one. If one fails, we catch and return fallback.
|
|
49
72
|
const profileRes = await fetchAPI('/account/profile', config.apiKey);
|
|
50
73
|
const balanceRes = await fetchAPI('/account/balance', config.apiKey);
|
|
51
|
-
const usageRes = await fetchAPI('/account/usage', config.apiKey);
|
|
52
|
-
logQuota(`Fetch Success. Tier: ${profileRes.tier}, Balance: ${balanceRes.balance}`);
|
|
53
74
|
const profile = profileRes;
|
|
54
75
|
const balance = balanceRes.balance;
|
|
55
|
-
|
|
56
|
-
const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 1, emoji: '❓' }; // Default 1 (Spore)
|
|
57
|
-
const tierLimit = tierInfo.pollen;
|
|
58
|
-
// Calculer le reset
|
|
76
|
+
// 2. Convertir Timezone : Obtenir instant exact du Reset
|
|
59
77
|
const resetInfo = calculateResetInfo(profile.nextResetAt);
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
logQuota(`Fetch Success. Tier: ${profile.tier}, Balance: ${balance}, Next Reset: ${profile.nextResetAt}`);
|
|
79
|
+
// 3. Smart Fetch : Récupérer uniquement les dépenses du jour (depuis lastReset)
|
|
80
|
+
const periodUsage = await fetchUsageForPeriod(config.apiKey, resetInfo.lastReset);
|
|
81
|
+
const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 1, emoji: '❓' };
|
|
82
|
+
const tierLimit = tierInfo.pollen;
|
|
83
|
+
// 4. Calcul Strict FreeTier / Wallet
|
|
84
|
+
const { tierUsed } = calculateCurrentPeriodUsage(periodUsage, resetInfo);
|
|
85
|
+
// 4. Calculate Balances
|
|
62
86
|
const tierRemaining = Math.max(0, tierLimit - tierUsed);
|
|
63
87
|
// Fix rounding errors
|
|
64
88
|
const cleanTierRemaining = Math.max(0, parseFloat(tierRemaining.toFixed(4)));
|
|
65
89
|
// Le wallet c'est le reste (balance totale - ce qu'il reste du tier gratuit non consommé)
|
|
90
|
+
// Formula: Pollinations Balance = Wallet + TierRemaining.
|
|
66
91
|
const walletBalance = Math.max(0, balance - cleanTierRemaining);
|
|
67
92
|
const cleanWalletBalance = Math.max(0, parseFloat(walletBalance.toFixed(4)));
|
|
93
|
+
// needsAlert: check BOTH tier threshold AND wallet threshold
|
|
94
|
+
const tierAlertPercent = tierLimit > 0 ? (cleanTierRemaining / tierLimit * 100) : 0;
|
|
95
|
+
const tierNeedsAlert = tierLimit > 0 && tierAlertPercent <= config.thresholds.tier;
|
|
96
|
+
const walletNeedsAlert = cleanWalletBalance > 0 && cleanWalletBalance < (config.thresholds.wallet || 0.5);
|
|
68
97
|
cachedQuota = {
|
|
69
98
|
tierRemaining: cleanTierRemaining,
|
|
70
99
|
tierUsed,
|
|
@@ -74,7 +103,7 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
74
103
|
timeUntilReset: resetInfo.timeUntilReset,
|
|
75
104
|
canUseEnterprise: cleanTierRemaining > 0.05 || cleanWalletBalance > 0.05,
|
|
76
105
|
isUsingWallet: cleanTierRemaining <= 0.05 && cleanWalletBalance > 0.05,
|
|
77
|
-
needsAlert:
|
|
106
|
+
needsAlert: tierNeedsAlert || walletNeedsAlert,
|
|
78
107
|
tier: profile.tier,
|
|
79
108
|
tierEmoji: tierInfo.emoji
|
|
80
109
|
};
|
|
@@ -84,30 +113,29 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
84
113
|
catch (e) {
|
|
85
114
|
logQuota(`ERROR fetching quota: ${e.message}`);
|
|
86
115
|
let errorType = 'unknown';
|
|
87
|
-
if (e.message && e.message.includes('403'))
|
|
116
|
+
if (e.message && e.message.includes('403'))
|
|
88
117
|
errorType = 'auth_limited';
|
|
89
|
-
|
|
90
|
-
else if (e.message && e.message.includes('Network Error')) {
|
|
118
|
+
else if (e.message && e.message.includes('Network Error'))
|
|
91
119
|
errorType = 'network';
|
|
92
|
-
}
|
|
93
|
-
// Retourner le cache ou un état par défaut safe
|
|
94
|
-
return cachedQuota || {
|
|
95
|
-
tierRemaining: 0,
|
|
96
|
-
tierUsed: 0,
|
|
97
|
-
tierLimit: 1,
|
|
98
|
-
walletBalance: 0,
|
|
99
|
-
nextResetAt: new Date(),
|
|
100
|
-
timeUntilReset: 0,
|
|
101
|
-
canUseEnterprise: false,
|
|
102
|
-
isUsingWallet: false,
|
|
103
|
-
needsAlert: true,
|
|
104
|
-
tier: 'error',
|
|
105
|
-
tierEmoji: '⚠️',
|
|
106
|
-
errorType
|
|
107
|
-
};
|
|
120
|
+
return cachedQuota || { ...createDefaultQuota('error', 1), errorType };
|
|
108
121
|
}
|
|
109
122
|
}
|
|
110
|
-
|
|
123
|
+
function createDefaultQuota(tierName, limit) {
|
|
124
|
+
return {
|
|
125
|
+
tierRemaining: 0,
|
|
126
|
+
tierUsed: 0,
|
|
127
|
+
tierLimit: limit,
|
|
128
|
+
walletBalance: 0,
|
|
129
|
+
nextResetAt: new Date(),
|
|
130
|
+
timeUntilReset: 0,
|
|
131
|
+
canUseEnterprise: false,
|
|
132
|
+
isUsingWallet: false,
|
|
133
|
+
needsAlert: false,
|
|
134
|
+
tier: tierName,
|
|
135
|
+
tierEmoji: TIER_LIMITS[tierName]?.emoji || '❌'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// === HELPERS ===
|
|
111
139
|
function fetchAPI(endpoint, apiKey) {
|
|
112
140
|
return new Promise((resolve, reject) => {
|
|
113
141
|
const options = {
|
|
@@ -144,53 +172,30 @@ function fetchAPI(endpoint, apiKey) {
|
|
|
144
172
|
});
|
|
145
173
|
}
|
|
146
174
|
function calculateResetInfo(nextResetAt) {
|
|
147
|
-
const
|
|
175
|
+
const nextReset = new Date(nextResetAt);
|
|
176
|
+
const lastReset = new Date(nextReset.getTime() - ONE_DAY_MS);
|
|
148
177
|
const now = new Date();
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const resetSecond = nextResetFromAPI.getUTCSeconds();
|
|
153
|
-
// Calculer le reset d'aujourd'hui à cette heure
|
|
154
|
-
const todayResetUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), resetHour, resetMinute, resetSecond));
|
|
155
|
-
let lastReset;
|
|
156
|
-
let nextReset;
|
|
157
|
-
if (now >= todayResetUTC) {
|
|
158
|
-
// Le reset d'aujourd'hui est passé
|
|
159
|
-
lastReset = todayResetUTC;
|
|
160
|
-
nextReset = new Date(todayResetUTC.getTime() + 24 * 60 * 60 * 1000);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
// Le reset d'aujourd'hui n'est pas encore passé
|
|
164
|
-
lastReset = new Date(todayResetUTC.getTime() - 24 * 60 * 60 * 1000);
|
|
165
|
-
nextReset = todayResetUTC;
|
|
166
|
-
}
|
|
167
|
-
const timeUntilReset = nextReset.getTime() - now.getTime();
|
|
168
|
-
const timeSinceReset = now.getTime() - lastReset.getTime();
|
|
169
|
-
const cycleDuration = 24 * 60 * 60 * 1000;
|
|
170
|
-
const progressPercent = (timeSinceReset / cycleDuration) * 100;
|
|
178
|
+
const timeUntilReset = Math.max(0, nextReset.getTime() - now.getTime());
|
|
179
|
+
const timeSinceReset = Math.max(0, now.getTime() - lastReset.getTime());
|
|
180
|
+
const progressPercent = Math.min(100, (timeSinceReset / ONE_DAY_MS) * 100);
|
|
171
181
|
return {
|
|
172
182
|
nextReset,
|
|
173
183
|
lastReset,
|
|
174
184
|
timeUntilReset,
|
|
175
185
|
timeSinceReset,
|
|
176
|
-
resetHour,
|
|
177
|
-
resetMinute,
|
|
178
|
-
resetSecond,
|
|
186
|
+
resetHour: nextReset.getUTCHours(),
|
|
187
|
+
resetMinute: nextReset.getUTCMinutes(),
|
|
188
|
+
resetSecond: nextReset.getUTCSeconds(),
|
|
179
189
|
progressPercent
|
|
180
190
|
};
|
|
181
191
|
}
|
|
182
192
|
function calculateCurrentPeriodUsage(usage, resetInfo) {
|
|
183
193
|
let tierUsed = 0;
|
|
184
194
|
let packUsed = 0;
|
|
185
|
-
// Parser le timestamp de l'API avec Z pour UTC
|
|
186
|
-
function parseUsageTimestamp(timestamp) {
|
|
187
|
-
// Format: "2026-01-23 01:11:21"
|
|
188
|
-
const isoString = timestamp.replace(' ', 'T') + 'Z';
|
|
189
|
-
return new Date(isoString);
|
|
190
|
-
}
|
|
191
|
-
// FILTRER: Ne garder que les entrées APRÈS le dernier reset
|
|
192
195
|
const entriesAfterReset = usage.filter(entry => {
|
|
193
|
-
|
|
196
|
+
// Safe Parse
|
|
197
|
+
const timestamp = entry.timestamp.replace(' ', 'T') + 'Z';
|
|
198
|
+
const entryTime = new Date(timestamp);
|
|
194
199
|
return entryTime >= resetInfo.lastReset;
|
|
195
200
|
});
|
|
196
201
|
for (const entry of entriesAfterReset) {
|
|
@@ -203,7 +208,6 @@ function calculateCurrentPeriodUsage(usage, resetInfo) {
|
|
|
203
208
|
}
|
|
204
209
|
return { tierUsed, packUsed };
|
|
205
210
|
}
|
|
206
|
-
// === EXPORT POUR LES ALERTES ===
|
|
207
211
|
export function formatQuotaForToast(quota) {
|
|
208
212
|
if (quota.errorType === 'auth_limited') {
|
|
209
213
|
return `🔑 CLE LIMITÉE (Génération Seule) | 💎 Wallet: N/A | ⏰ Reset: N/A`;
|
|
@@ -211,7 +215,6 @@ export function formatQuotaForToast(quota) {
|
|
|
211
215
|
const tierPercent = quota.tierLimit > 0
|
|
212
216
|
? Math.round((quota.tierRemaining / quota.tierLimit) * 100)
|
|
213
217
|
: 0;
|
|
214
|
-
// Format compact: 1h23m
|
|
215
218
|
const ms = quota.timeUntilReset;
|
|
216
219
|
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
217
220
|
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* pollinations_pricing.ts — CLI Pricing Table for enter.pollinations.ai
|
|
4
|
+
* * Usage: npx tsx pollinations_pricing.ts [--api-key YOUR_KEY] [--debug] [--mode normal|dynamic]
|
|
5
|
+
* * - Mode 'normal' (défaut) : Affiche le miroir exact du dashboard marketing.
|
|
6
|
+
* - Mode 'dynamic' : Calcule le vrai coût token par token (Cost Guard Estimator).
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* pollinations_pricing.ts — CLI Pricing Table for enter.pollinations.ai
|
|
4
|
+
* * Usage: npx tsx pollinations_pricing.ts [--api-key YOUR_KEY] [--debug] [--mode normal|dynamic]
|
|
5
|
+
* * - Mode 'normal' (défaut) : Affiche le miroir exact du dashboard marketing.
|
|
6
|
+
* - Mode 'dynamic' : Calcule le vrai coût token par token (Cost Guard Estimator).
|
|
7
|
+
*/
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const API_KEY = args.includes("--api-key") ? args[args.indexOf("--api-key") + 1] : process.env.POLLINATIONS_API_KEY ?? "";
|
|
10
|
+
const DEBUG = args.includes("--debug");
|
|
11
|
+
const modeIndex = args.indexOf("--mode");
|
|
12
|
+
const MODE = modeIndex !== -1 ? args[modeIndex + 1] : "normal";
|
|
13
|
+
const H = API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {};
|
|
14
|
+
async function get(url) {
|
|
15
|
+
const r = await fetch(url, { headers: H });
|
|
16
|
+
if (!r.ok)
|
|
17
|
+
throw new Error(`HTTP ${r.status} on ${url}`);
|
|
18
|
+
return r.json();
|
|
19
|
+
}
|
|
20
|
+
// ─── COST GUARD ESTIMATOR (Pour le mode dynamic) ─────────────────────────────
|
|
21
|
+
const BENCH = {
|
|
22
|
+
TEXT_IN_TOK: 300,
|
|
23
|
+
TEXT_OUT_TOK: 800,
|
|
24
|
+
IMG_TOKENS_PER_STD: 1667,
|
|
25
|
+
VIDEO_DEFAULT_SEC: 6,
|
|
26
|
+
STT_CLIP_SEC: 30,
|
|
27
|
+
TTS_CHARS: 200,
|
|
28
|
+
MUSIC_SEC: 50,
|
|
29
|
+
VIDEO_DURATION: { veo: 6, "seedance-pro": 5, seedance: 5, "ltx-2": 6, wan: 5, "grok-video": 5 },
|
|
30
|
+
VIDEO_DEFAULT_TOKENS: 50_000,
|
|
31
|
+
};
|
|
32
|
+
let globalModelStats = [];
|
|
33
|
+
function n(v) {
|
|
34
|
+
const x = Number(v);
|
|
35
|
+
return v === undefined || v === null || isNaN(x) || x === 0 ? null : x;
|
|
36
|
+
}
|
|
37
|
+
function perM(v) {
|
|
38
|
+
const x = n(v);
|
|
39
|
+
if (!x)
|
|
40
|
+
return "—";
|
|
41
|
+
const m = x * 1_000_000;
|
|
42
|
+
return m >= 100 ? `${m.toFixed(1)}/M` : m >= 10 ? `${m.toFixed(2)}/M` : m >= 1 ? `${m.toFixed(3)}/M` : `${m.toFixed(4)}/M`;
|
|
43
|
+
}
|
|
44
|
+
function flat(v, unit) {
|
|
45
|
+
const x = n(v);
|
|
46
|
+
if (!x)
|
|
47
|
+
return "—";
|
|
48
|
+
return x >= 1 ? `${x.toFixed(3)}/${unit}` : x >= 0.01 ? `${x.toFixed(4)}/${unit}` : `${x.toFixed(5)}/${unit}`;
|
|
49
|
+
}
|
|
50
|
+
function per1pollen(cost) {
|
|
51
|
+
if (!cost || cost <= 0)
|
|
52
|
+
return "—";
|
|
53
|
+
const x = 1 / cost;
|
|
54
|
+
if (x >= 1_000_000)
|
|
55
|
+
return `${(x / 1_000_000).toFixed(1)}M`;
|
|
56
|
+
if (x >= 100_000)
|
|
57
|
+
return `${Math.round(x / 1000)}K`;
|
|
58
|
+
if (x >= 10_000)
|
|
59
|
+
return `${(x / 1000).toFixed(1)}K`.replace(/\.0K$/, "K");
|
|
60
|
+
if (x >= 1_000)
|
|
61
|
+
return `${Math.round(x / 100) * 100}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
62
|
+
if (x >= 100)
|
|
63
|
+
return `${Math.round(x)}`;
|
|
64
|
+
if (x >= 10)
|
|
65
|
+
return `${Math.round(x * 10) / 10}`;
|
|
66
|
+
return `${x.toFixed(1)}`;
|
|
67
|
+
}
|
|
68
|
+
function getCost(m, type) {
|
|
69
|
+
// 1. Priorité aux Stats Tinybird si on est en mode normal
|
|
70
|
+
if (MODE === "normal") {
|
|
71
|
+
// Tente de trouver la variable native à la racine (si l'API l'expose)
|
|
72
|
+
const rootCost = n(m.realAvgCost) ?? n(m.baseCost) ?? n(m.pollenCost) ?? n(m.averageCost);
|
|
73
|
+
if (rootCost)
|
|
74
|
+
return rootCost;
|
|
75
|
+
// Fallback dynamique sur les stats Tinybird
|
|
76
|
+
const stat = globalModelStats.find(s => s.model === m.name);
|
|
77
|
+
if (stat && stat.avg_cost_usd > 0)
|
|
78
|
+
return stat.avg_cost_usd;
|
|
79
|
+
}
|
|
80
|
+
// 2. Calcul mathématique (Mode Dynamic ou si Modèle inconnu)
|
|
81
|
+
const pr = m.pricing ?? {};
|
|
82
|
+
if (type === "text") {
|
|
83
|
+
return (n(pr.promptTextTokens) ?? 0) * BENCH.TEXT_IN_TOK + (n(pr.completionTextTokens) ?? 0) * BENCH.TEXT_OUT_TOK || null;
|
|
84
|
+
}
|
|
85
|
+
if (type === "image") {
|
|
86
|
+
const outPrice = n(pr.completionImageTokens);
|
|
87
|
+
if (!outPrice)
|
|
88
|
+
return null;
|
|
89
|
+
if (pr.promptTextTokens) {
|
|
90
|
+
return (n(pr.promptTextTokens) ?? 0) * 100 + (n(pr.promptImageTokens) ?? 0) * 100 + (outPrice * BENCH.IMG_TOKENS_PER_STD);
|
|
91
|
+
}
|
|
92
|
+
return outPrice;
|
|
93
|
+
}
|
|
94
|
+
if (type === "video") {
|
|
95
|
+
if (pr.completionVideoSeconds) {
|
|
96
|
+
return ((n(pr.completionVideoSeconds) ?? 0) + (n(pr.completionAudioSeconds) ?? 0)) * (BENCH.VIDEO_DURATION[m.name] ?? BENCH.VIDEO_DEFAULT_SEC);
|
|
97
|
+
}
|
|
98
|
+
return n(pr.completionVideoTokens) ? (n(pr.completionVideoTokens) ?? 0) * BENCH.VIDEO_DEFAULT_TOKENS : null;
|
|
99
|
+
}
|
|
100
|
+
if (type === "audio") {
|
|
101
|
+
if (n(pr.promptAudioSeconds))
|
|
102
|
+
return (n(pr.promptAudioSeconds) ?? 0) * BENCH.STT_CLIP_SEC;
|
|
103
|
+
if (n(pr.completionAudioSeconds))
|
|
104
|
+
return (n(pr.completionAudioSeconds) ?? 0) * BENCH.MUSIC_SEC;
|
|
105
|
+
if (n(pr.completionAudioTokens))
|
|
106
|
+
return (n(pr.completionAudioTokens) ?? 0) * BENCH.TTS_CHARS;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
// ─── RENDU VISUEL ────────────────────────────────────────────────────────────
|
|
111
|
+
function parseNameDesc(m) {
|
|
112
|
+
const fullDesc = m.description || m.name;
|
|
113
|
+
const parts = fullDesc.split(" - ");
|
|
114
|
+
if (parts.length > 1) {
|
|
115
|
+
return { nom: parts[0].trim(), desc: parts.slice(1).join(" - ").trim() };
|
|
116
|
+
}
|
|
117
|
+
return { nom: fullDesc, desc: "" };
|
|
118
|
+
}
|
|
119
|
+
function flags(m, overrides = []) {
|
|
120
|
+
const f = [];
|
|
121
|
+
if (m.paid_only)
|
|
122
|
+
f.push("💎");
|
|
123
|
+
const allFlags = [...(m.input_modalities || []), ...(m.output_modalities || []), ...overrides, m.name];
|
|
124
|
+
const str = allFlags.join(" ").toLowerCase();
|
|
125
|
+
if (str.includes("image") || str.includes("👁️"))
|
|
126
|
+
f.push("👁️");
|
|
127
|
+
if (m.reasoning || str.includes("reasoning"))
|
|
128
|
+
f.push("🧠");
|
|
129
|
+
if (str.includes("audio") && !overrides.includes("🔊") || str.includes("whisper") || str.includes("scribe") || str.includes("🎙️"))
|
|
130
|
+
f.push("🎙️");
|
|
131
|
+
if (str.includes("search") || str.includes("sonar") || str.includes("gemini"))
|
|
132
|
+
f.push("🔍");
|
|
133
|
+
if (m.output_modalities?.includes("audio") || overrides.includes("🔊") || str.includes("tts") || str.includes("music"))
|
|
134
|
+
f.push("🔊");
|
|
135
|
+
if (str.includes("coder") || str.includes("code") || str.includes("gemini"))
|
|
136
|
+
f.push("💻");
|
|
137
|
+
if ((m.context_window ?? 0) >= 100_000)
|
|
138
|
+
f.push(`📏${((m.context_window ?? 0) / 1000).toFixed(0)}k`);
|
|
139
|
+
return f.filter((v, i, a) => a.indexOf(v) === i).join(" ");
|
|
140
|
+
}
|
|
141
|
+
function renderTable(title, models, type, extraFlags = []) {
|
|
142
|
+
if (models.length === 0)
|
|
143
|
+
return "";
|
|
144
|
+
// Tri par coût (du moins cher au plus cher), puis ordre alphabétique
|
|
145
|
+
const sorted = [...models].sort((a, b) => {
|
|
146
|
+
const costA = getCost(a, type) ?? 0;
|
|
147
|
+
const costB = getCost(b, type) ?? 0;
|
|
148
|
+
const diff = costA - costB;
|
|
149
|
+
if (diff !== 0)
|
|
150
|
+
return diff;
|
|
151
|
+
return a.name.localeCompare(b.name);
|
|
152
|
+
});
|
|
153
|
+
const lines = [
|
|
154
|
+
`\n## ${title}\n`,
|
|
155
|
+
`| Nom | ID | Capabilities | 1 pollen ≈ | Pricing |`,
|
|
156
|
+
`|---|---|---|---|---|`
|
|
157
|
+
];
|
|
158
|
+
for (const m of sorted) {
|
|
159
|
+
const pr = m.pricing ?? {};
|
|
160
|
+
const parts = [];
|
|
161
|
+
// Génération de la colonne Pricing en fonction du type
|
|
162
|
+
if (type === "text") {
|
|
163
|
+
if (n(pr.promptTextTokens))
|
|
164
|
+
parts.push(`💬 ${perM(pr.promptTextTokens)}`);
|
|
165
|
+
if (n(pr.promptCachedTokens))
|
|
166
|
+
parts.push(`💾 ${perM(pr.promptCachedTokens)}`);
|
|
167
|
+
if (n(pr.completionTextTokens))
|
|
168
|
+
parts.push(`💬 ${perM(pr.completionTextTokens)}`);
|
|
169
|
+
}
|
|
170
|
+
else if (type === "image") {
|
|
171
|
+
if (pr.promptTextTokens)
|
|
172
|
+
parts.push(`💬 ${perM(pr.promptTextTokens)}`);
|
|
173
|
+
if (pr.completionImageTokens)
|
|
174
|
+
parts.push(`🖼️ ${pr.promptTextTokens ? perM(pr.completionImageTokens) : flat(pr.completionImageTokens, "img")}`);
|
|
175
|
+
}
|
|
176
|
+
else if (type === "video") {
|
|
177
|
+
if (pr.completionVideoSeconds)
|
|
178
|
+
parts.push(`🎬 ${flat(pr.completionVideoSeconds, "sec")}`);
|
|
179
|
+
if (pr.completionVideoTokens)
|
|
180
|
+
parts.push(`🎬 ${perM(pr.completionVideoTokens)}`);
|
|
181
|
+
}
|
|
182
|
+
else if (type === "audio") {
|
|
183
|
+
if (pr.promptAudioSeconds)
|
|
184
|
+
parts.push(`🎬 ${flat(pr.promptAudioSeconds, "sec")}`);
|
|
185
|
+
if (pr.completionAudioTokens)
|
|
186
|
+
parts.push(`🔊 ${((n(pr.completionAudioTokens) ?? 0) * 1000).toFixed(2)}/1K chars`);
|
|
187
|
+
if (pr.completionAudioSeconds)
|
|
188
|
+
parts.push(`🎬 ${flat(pr.completionAudioSeconds, "sec")}`);
|
|
189
|
+
}
|
|
190
|
+
const { nom } = parseNameDesc(m);
|
|
191
|
+
lines.push(`| ${nom} | \`${m.name}\` | ${flags(m, extraFlags)} | ${per1pollen(getCost(m, type))} | ${parts.join(" · ") || "—"} |`);
|
|
192
|
+
}
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
195
|
+
// ─── DEBUG ULTIME ────────────────────────────────────────────────────────────
|
|
196
|
+
function debugDump(label, models) {
|
|
197
|
+
if (!DEBUG)
|
|
198
|
+
return;
|
|
199
|
+
console.error(`\n[debug] ═══ ROOT OBJECT DUMP: ${label} (${models.length} models) ═══`);
|
|
200
|
+
for (const m of models) {
|
|
201
|
+
const clone = { ...m };
|
|
202
|
+
delete clone.description; // Pour garder le terminal lisible
|
|
203
|
+
console.error(JSON.stringify(clone, null, 2));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ─── EXECUTION MAIN ──────────────────────────────────────────────────────────
|
|
207
|
+
const BASE = "https://gen.pollinations.ai";
|
|
208
|
+
const STATS = "https://enter.pollinations.ai/api/model-stats";
|
|
209
|
+
const [text, imgRaw, audRaw, statsRaw] = await Promise.all([
|
|
210
|
+
get(`${BASE}/text/models`),
|
|
211
|
+
get(`${BASE}/image/models`),
|
|
212
|
+
get(`${BASE}/audio/models`),
|
|
213
|
+
get(STATS).catch(() => ({ data: [] }))
|
|
214
|
+
]);
|
|
215
|
+
globalModelStats = statsRaw.data;
|
|
216
|
+
// Séparation des modèles images et vidéos qui sont mixés sur le même endpoint
|
|
217
|
+
const isVid = (m) => (m.pricing?.completionVideoSeconds || m.pricing?.completionVideoTokens) !== undefined;
|
|
218
|
+
const img = imgRaw.filter(m => !isVid(m));
|
|
219
|
+
const vid = imgRaw.filter(m => isVid(m));
|
|
220
|
+
// Séparation Audio
|
|
221
|
+
const isSTT = (m) => !!m.pricing?.promptAudioSeconds;
|
|
222
|
+
const isMusic = (m) => m.name.includes("music");
|
|
223
|
+
const stt = audRaw.filter(m => isSTT(m));
|
|
224
|
+
const tts = audRaw.filter(m => !isSTT(m) && !isMusic(m));
|
|
225
|
+
const music = audRaw.filter(m => isMusic(m));
|
|
226
|
+
debugDump("IMAGE API", img);
|
|
227
|
+
debugDump("VIDEO API", vid);
|
|
228
|
+
debugDump("AUDIO API", audRaw);
|
|
229
|
+
debugDump("TEXT API", text);
|
|
230
|
+
console.log(`# 🌸 Pollinations — Live Model Pricing (Mode: ${MODE.toUpperCase()})`);
|
|
231
|
+
console.log(`> **${new Date().toISOString()}** · ${img.length} image · ${vid.length} video · ${audRaw.length} audio · ${text.length} text`);
|
|
232
|
+
console.log(renderTable("🖼️ Image", img, "image"));
|
|
233
|
+
console.log(renderTable("🎬 Video", vid, "video", ["👁️"]));
|
|
234
|
+
console.log(renderTable("🎙️ Speech-to-Text", stt, "audio", ["🎙️"]));
|
|
235
|
+
console.log(renderTable("🔊 Text-to-Speech", tts, "audio", ["🔊"]));
|
|
236
|
+
console.log(renderTable("🎵 Music", music, "audio", ["🔊"]));
|
|
237
|
+
console.log(renderTable("📝 Text", text, "text"));
|
|
238
|
+
console.log(`\n> **Capabilities** : 👁️ vision · 🧠 reasoning · 🎙️ audio in · 🔍 search · 🔊 audio out · 💻 code exec`);
|
|
239
|
+
console.log(`> **Token Types** : 💬 text · 🖼️ image · 💾 cached · 🎬 video · 🔊 audio`);
|
|
240
|
+
console.log(`> **Pricing Metrics** : /img = flat rate per image · /M = per million tokens · /sec = per second of video · /1K chars = per 1000 characters`);
|
|
241
|
+
console.log(`> **Other** : 💎 PAID ONLY (Wallet direct) · 📏 Contexte API max`);
|
|
242
|
+
console.log(`\n### 💡 How Pollen is Spent
|
|
243
|
+
1. Daily tier grants are used first
|
|
244
|
+
2. Purchased pollen is used after daily is depleted
|
|
245
|
+
⚠️ **Exception**: 💎 Paid Only models require purchased pollen only`);
|
|
246
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const API_KEY = process.env.POLLINATIONS_API_KEY || 'sk_eZbhgG1oJaaqSZKMvmy8nfVH9NNAGp0H';
|
|
2
|
+
async function logBalance(label) {
|
|
3
|
+
try {
|
|
4
|
+
const res = await fetch('https://gen.pollinations.ai/account/balance', {
|
|
5
|
+
headers: { 'Authorization': `Bearer ${API_KEY}` }
|
|
6
|
+
});
|
|
7
|
+
const bal = await res.json();
|
|
8
|
+
console.log(`\n[${label}] /account/balance :`);
|
|
9
|
+
console.log(JSON.stringify(bal, null, 2));
|
|
10
|
+
const resUsage = await fetch('https://gen.pollinations.ai/account/usage/daily', {
|
|
11
|
+
headers: { 'Authorization': `Bearer ${API_KEY}` }
|
|
12
|
+
});
|
|
13
|
+
const usg = await resUsage.json();
|
|
14
|
+
// Sum today stats
|
|
15
|
+
let costTier = 0;
|
|
16
|
+
let costPack = 0;
|
|
17
|
+
let req = 0;
|
|
18
|
+
if (usg.usage && usg.usage.length > 0) {
|
|
19
|
+
const todayDate = usg.usage[0].date;
|
|
20
|
+
for (const x of usg.usage) {
|
|
21
|
+
if (x.date === todayDate) {
|
|
22
|
+
req += x.requests;
|
|
23
|
+
if (x.meter_source === 'tier')
|
|
24
|
+
costTier += x.cost_usd;
|
|
25
|
+
else
|
|
26
|
+
costPack += x.cost_usd;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
console.log(`[${label}] /account/usage/daily (Today sum) : Requests = ${req}, TierCost = ${costTier}, PackCost = ${costPack}`);
|
|
31
|
+
return { bal, usage: { costTier, costPack, req } };
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error("Error fetching balance/usage:", e.message);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function run() {
|
|
39
|
+
console.log("=== COST ENDPOINTS PROBE ===");
|
|
40
|
+
// 1. Avant
|
|
41
|
+
const stateBefore = await logBalance("BEFORE GENERATION");
|
|
42
|
+
// 2. Générer Fake Image (Flux : censé être free tier ou cheap)
|
|
43
|
+
console.log("\n🎬 Génération d'une image (modèle 'flux')...");
|
|
44
|
+
const url = 'https://gen.pollinations.ai/image/a_beautiful_red_square_cost_test?model=flux&width=512&height=512&nologo=true';
|
|
45
|
+
const start = Date.now();
|
|
46
|
+
const res = await fetch(url, { headers: { 'Authorization': `Bearer ${API_KEY}` } });
|
|
47
|
+
const buffer = await res.arrayBuffer();
|
|
48
|
+
console.log(`✅ Image reçue en ${Date.now() - start}ms, taille: ${buffer.byteLength}B`);
|
|
49
|
+
console.log(`Headers Cost:`, {
|
|
50
|
+
'x-usage-cost-usd': res.headers.get('x-usage-cost-usd'),
|
|
51
|
+
'x-usage-completion-image-tokens': res.headers.get('x-usage-completion-image-tokens')
|
|
52
|
+
});
|
|
53
|
+
// 3. Immediatement Apres
|
|
54
|
+
await logBalance("IMMEDIATELY AFTER");
|
|
55
|
+
// 4. Attente 5 secondes
|
|
56
|
+
console.log("\n⏳ Attente 5 secondes pour propagation /usage...");
|
|
57
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
58
|
+
await logBalance("5 SECONDS AFTER");
|
|
59
|
+
}
|
|
60
|
+
run();
|
|
61
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as https from 'https';
|
|
2
|
+
function fetchJson(url) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
https.get(url, (res) => {
|
|
5
|
+
let data = '';
|
|
6
|
+
res.on('data', chunk => data += chunk);
|
|
7
|
+
res.on('end', () => resolve(JSON.parse(data)));
|
|
8
|
+
}).on('error', reject);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
async function auditDynamicPricing() {
|
|
12
|
+
console.log("=== AUDIT PRICING DYNAMIQUE (TINYBIRD) ===");
|
|
13
|
+
try {
|
|
14
|
+
console.log("Fetching /api/model-stats...");
|
|
15
|
+
const statsProxy = await fetchJson('https://enter.pollinations.ai/api/model-stats');
|
|
16
|
+
const stats = statsProxy.data || [];
|
|
17
|
+
console.log(`\nReçu ${stats.length} statistiques de prix moyens (average cost) depuis Tinybird:`);
|
|
18
|
+
// On affiche les 10 premiers pour l'exemple
|
|
19
|
+
console.log(stats.slice(0, 10).map((s) => `- ${s.model}: ${parseFloat(s.avg_cost_usd).toFixed(6)}$/req (${s.request_count} reqs)`).join('\n'));
|
|
20
|
+
console.log("\nFetching /image/models...");
|
|
21
|
+
const imageModels = await fetchJson('https://gen.pollinations.ai/image/models');
|
|
22
|
+
console.log("\nCroisement Models API vs Tinybird Stats:");
|
|
23
|
+
for (const m of imageModels.slice(0, 5)) {
|
|
24
|
+
const stat = stats.find((s) => s.model === m.name);
|
|
25
|
+
console.log(`\nModèle: ${m.name}`);
|
|
26
|
+
console.log(`- API Pricing Base:`, m.pricing);
|
|
27
|
+
if (stat) {
|
|
28
|
+
console.log(`- Tinybird Real Average Cost: ${stat.avg_cost_usd}$ (Basé sur le vrai usage!)`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(`- Tinybird Stats: Inconnu / Pas d'usage récent`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
console.error("Erreur lors de l'audit:", e.message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
auditDynamicPricing();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST FREETIER AUDIT — Quick & Dirty
|
|
3
|
+
* Objectif: Tester les modèles freetier Enter pour vérifier:
|
|
4
|
+
* - Latence de réponse
|
|
5
|
+
* - Headers de coût (x-usage-*)
|
|
6
|
+
* - Real cost via /account/balance diff
|
|
7
|
+
* - Erreurs et paramètres
|
|
8
|
+
*
|
|
9
|
+
* Usage: npx tsx src/server/scripts/test_freetier_audit.ts
|
|
10
|
+
*/
|
|
11
|
+
export {};
|