opencode-pollinations-plugin 6.0.0 → 6.1.0-beta.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/dist/server/commands.js +36 -23
- package/dist/server/config.d.ts +23 -21
- package/dist/server/config.js +49 -20
- package/dist/server/proxy.js +84 -36
- package/package.json +1 -1
package/dist/server/commands.js
CHANGED
|
@@ -202,7 +202,7 @@ async function handleModeCommand(args) {
|
|
|
202
202
|
return { handled: true, error: `❌ Erreur de vérification: ${e.message}` };
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
-
// Allow switch (if
|
|
205
|
+
// Allow switch (if economy or manual, or verified pro)
|
|
206
206
|
saveConfig({ mode: mode });
|
|
207
207
|
const config = loadConfig();
|
|
208
208
|
if (config.gui.status !== 'none') {
|
|
@@ -264,32 +264,36 @@ async function handleUsageCommand(args) {
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
function handleFallbackCommand(args) {
|
|
267
|
-
const [
|
|
268
|
-
if (!
|
|
267
|
+
const [mode, model] = args;
|
|
268
|
+
if (!mode) {
|
|
269
269
|
const config = loadConfig();
|
|
270
|
-
const freeConfig = `Free: main=${config.fallbacks.free.main}, agent=${config.fallbacks.free.agent}`;
|
|
271
|
-
const enterConfig = `Enter: agent=${config.fallbacks.enter.agent}`;
|
|
272
270
|
return {
|
|
273
271
|
handled: true,
|
|
274
|
-
response: `Fallbacks actuels:\n${
|
|
272
|
+
response: `Fallbacks actuels:\n- Economy: ${config.fallbacks.economy}\n- Pro: ${config.fallbacks.pro}`
|
|
275
273
|
};
|
|
276
274
|
}
|
|
277
|
-
// Default behavior for "/poll fallback <model> <agent>" is setting FREE fallbacks
|
|
278
|
-
// User needs to use commands (maybe add /poll fallback enter ...) later
|
|
279
|
-
// For now, map to Free Fallback as it's the primary Safety Net
|
|
280
275
|
const config = loadConfig();
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
...config.fallbacks,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
276
|
+
if (mode === 'economy' && model) {
|
|
277
|
+
saveConfig({
|
|
278
|
+
fallbacks: { ...config.fallbacks, economy: model }
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
handled: true,
|
|
282
|
+
response: `✅ Fallback Economy configuré: ${model}`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (mode === 'pro' && model) {
|
|
286
|
+
saveConfig({
|
|
287
|
+
fallbacks: { ...config.fallbacks, pro: model }
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
handled: true,
|
|
291
|
+
response: `✅ Fallback Pro configuré: ${model}`
|
|
292
|
+
};
|
|
293
|
+
}
|
|
290
294
|
return {
|
|
291
295
|
handled: true,
|
|
292
|
-
|
|
296
|
+
error: `Usage: /pollinations fallback [economy|pro] <modèle>`
|
|
293
297
|
};
|
|
294
298
|
}
|
|
295
299
|
async function handleConnectCommand(args) {
|
|
@@ -411,14 +415,23 @@ function handleConfigCommand(args) {
|
|
|
411
415
|
saveConfig({ thresholds: { ...config.thresholds, tier: threshold } });
|
|
412
416
|
return { handled: true, response: `✅ threshold_tier = ${threshold}%` };
|
|
413
417
|
}
|
|
414
|
-
if (key === '
|
|
418
|
+
if (key === 'threshold_wallet_warn' && value) {
|
|
415
419
|
const threshold = parseInt(value);
|
|
416
420
|
if (isNaN(threshold) || threshold < 0 || threshold > 100) {
|
|
417
421
|
return { handled: true, error: 'Valeur entre 0 et 100 requise' };
|
|
418
422
|
}
|
|
419
423
|
const config = loadConfig();
|
|
420
|
-
saveConfig({ thresholds: { ...config.thresholds,
|
|
421
|
-
return { handled: true, response: `✅
|
|
424
|
+
saveConfig({ thresholds: { ...config.thresholds, wallet_warn: threshold } });
|
|
425
|
+
return { handled: true, response: `✅ threshold_wallet_warn = ${threshold}%` };
|
|
426
|
+
}
|
|
427
|
+
if (key === 'threshold_wallet_stop' && value) {
|
|
428
|
+
const stopValue = parseFloat(value);
|
|
429
|
+
if (isNaN(stopValue) || stopValue < 0) {
|
|
430
|
+
return { handled: true, error: 'Valeur $ positive requise' };
|
|
431
|
+
}
|
|
432
|
+
const config = loadConfig();
|
|
433
|
+
saveConfig({ thresholds: { ...config.thresholds, wallet_stop: stopValue } });
|
|
434
|
+
return { handled: true, response: `✅ threshold_wallet_stop = $${stopValue}` };
|
|
422
435
|
}
|
|
423
436
|
if (key === 'status_bar' && value) {
|
|
424
437
|
const enabled = value === 'true';
|
|
@@ -427,7 +440,7 @@ function handleConfigCommand(args) {
|
|
|
427
440
|
}
|
|
428
441
|
return {
|
|
429
442
|
handled: true,
|
|
430
|
-
error: `Clé inconnue: ${key}. Clés: status_gui, logs_gui, threshold_tier,
|
|
443
|
+
error: `Clé inconnue: ${key}. Clés: status_gui, logs_gui, threshold_tier, threshold_wallet_warn, threshold_wallet_stop, status_bar`
|
|
431
444
|
};
|
|
432
445
|
}
|
|
433
446
|
function handleHelpCommand() {
|
package/dist/server/config.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface PollinationsConfigV6 {
|
|
2
2
|
version: string | number;
|
|
3
|
-
mode: 'manual' | '
|
|
3
|
+
mode: 'manual' | 'economy' | 'pro';
|
|
4
4
|
apiKey?: string;
|
|
5
5
|
keyHasAccessToProfile?: boolean;
|
|
6
6
|
gui: {
|
|
@@ -9,24 +9,24 @@ export interface PollinationsConfigV5 {
|
|
|
9
9
|
};
|
|
10
10
|
thresholds: {
|
|
11
11
|
tier: number;
|
|
12
|
-
|
|
12
|
+
wallet_warn: number;
|
|
13
|
+
wallet_stop: number;
|
|
13
14
|
};
|
|
14
15
|
fallbacks: {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
16
|
+
economy: string;
|
|
17
|
+
pro: string;
|
|
18
|
+
};
|
|
19
|
+
session: {
|
|
20
|
+
wallet_initial?: number;
|
|
21
|
+
session_start?: string;
|
|
22
22
|
};
|
|
23
23
|
enablePaidTools: boolean;
|
|
24
24
|
statusBar: boolean;
|
|
25
25
|
}
|
|
26
|
-
export declare function loadConfig():
|
|
27
|
-
export declare function saveConfig(updates: Partial<
|
|
26
|
+
export declare function loadConfig(): PollinationsConfigV6;
|
|
27
|
+
export declare function saveConfig(updates: Partial<PollinationsConfigV6>): {
|
|
28
28
|
version: string;
|
|
29
|
-
mode: "manual" | "
|
|
29
|
+
mode: "manual" | "economy" | "pro";
|
|
30
30
|
apiKey?: string;
|
|
31
31
|
keyHasAccessToProfile?: boolean;
|
|
32
32
|
gui: {
|
|
@@ -35,17 +35,19 @@ export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
|
|
|
35
35
|
};
|
|
36
36
|
thresholds: {
|
|
37
37
|
tier: number;
|
|
38
|
-
|
|
38
|
+
wallet_warn: number;
|
|
39
|
+
wallet_stop: number;
|
|
39
40
|
};
|
|
40
41
|
fallbacks: {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
42
|
+
economy: string;
|
|
43
|
+
pro: string;
|
|
44
|
+
};
|
|
45
|
+
session: {
|
|
46
|
+
wallet_initial?: number;
|
|
47
|
+
session_start?: string;
|
|
48
48
|
};
|
|
49
49
|
enablePaidTools: boolean;
|
|
50
50
|
statusBar: boolean;
|
|
51
51
|
};
|
|
52
|
+
export declare function initSessionWallet(walletBalance: number): void;
|
|
53
|
+
export declare function getWalletWarnPercent(): number;
|
package/dist/server/config.js
CHANGED
|
@@ -9,7 +9,7 @@ const CONFIG_DIR_OPENCODE = path.join(HOMEDIR, '.config', 'opencode');
|
|
|
9
9
|
const OPENCODE_CONFIG_FILE = path.join(CONFIG_DIR_OPENCODE, 'opencode.json');
|
|
10
10
|
const AUTH_FILE = path.join(HOMEDIR, '.local', 'share', 'opencode', 'auth.json');
|
|
11
11
|
// LOAD PACKAGE VERSION
|
|
12
|
-
let PKG_VERSION = '
|
|
12
|
+
let PKG_VERSION = '6.1.0';
|
|
13
13
|
try {
|
|
14
14
|
const pkgPath = path.join(__dirname, '../../package.json');
|
|
15
15
|
if (fs.existsSync(pkgPath)) {
|
|
@@ -18,17 +18,22 @@ try {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
catch (e) { }
|
|
21
|
-
const
|
|
21
|
+
const DEFAULT_CONFIG_V6 = {
|
|
22
22
|
version: PKG_VERSION,
|
|
23
|
-
mode: '
|
|
23
|
+
mode: 'economy', // Défaut: economy (protège le wallet)
|
|
24
24
|
gui: { status: 'alert', logs: 'none' },
|
|
25
|
-
thresholds: {
|
|
25
|
+
thresholds: {
|
|
26
|
+
tier: 20, // 20% tier restant → alerte/fallback
|
|
27
|
+
wallet_warn: 20, // 20% wallet restant → alerte (pro)
|
|
28
|
+
wallet_stop: 0.50 // $0.50 → stop absolu (pro)
|
|
29
|
+
},
|
|
26
30
|
fallbacks: {
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
economy: 'nova-fast',
|
|
32
|
+
pro: 'qwen-coder'
|
|
29
33
|
},
|
|
34
|
+
session: {},
|
|
30
35
|
enablePaidTools: false,
|
|
31
|
-
keyHasAccessToProfile: true,
|
|
36
|
+
keyHasAccessToProfile: true,
|
|
32
37
|
statusBar: true
|
|
33
38
|
};
|
|
34
39
|
function logConfig(msg) {
|
|
@@ -40,17 +45,28 @@ function logConfig(msg) {
|
|
|
40
45
|
}
|
|
41
46
|
catch (e) { }
|
|
42
47
|
}
|
|
48
|
+
// MIGRATION: alwaysfree → economy
|
|
49
|
+
function migrateConfig(config) {
|
|
50
|
+
if (config.mode === 'alwaysfree') {
|
|
51
|
+
config.mode = 'economy';
|
|
52
|
+
logConfig('[Migration] alwaysfree → economy');
|
|
53
|
+
}
|
|
54
|
+
// Migrate old fallbacks structure
|
|
55
|
+
if (config.fallbacks?.free?.main) {
|
|
56
|
+
config.fallbacks.economy = config.fallbacks.free.main.replace('free/', '');
|
|
57
|
+
delete config.fallbacks.free;
|
|
58
|
+
logConfig('[Migration] fallbacks.free → fallbacks.economy');
|
|
59
|
+
}
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
43
62
|
// SIMPLE LOAD (Direct Disk Read - No Caching, No Watchers)
|
|
44
|
-
// This ensures the Proxy ALWAYS sees the latest state from auth.json
|
|
45
63
|
export function loadConfig() {
|
|
46
64
|
return readConfigFromDisk();
|
|
47
65
|
}
|
|
48
66
|
function readConfigFromDisk() {
|
|
49
|
-
let config = { ...
|
|
67
|
+
let config = { ...DEFAULT_CONFIG_V6 };
|
|
50
68
|
let finalKey = undefined;
|
|
51
69
|
let source = 'none';
|
|
52
|
-
// TIMESTAMP BASED PRIORITY LOGIC
|
|
53
|
-
// We want the most recently updated Valid Key to win.
|
|
54
70
|
let configTime = 0;
|
|
55
71
|
let authTime = 0;
|
|
56
72
|
try {
|
|
@@ -69,7 +85,7 @@ function readConfigFromDisk() {
|
|
|
69
85
|
try {
|
|
70
86
|
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
71
87
|
const custom = JSON.parse(raw);
|
|
72
|
-
config = { ...config, ...custom };
|
|
88
|
+
config = { ...config, ...migrateConfig(custom) };
|
|
73
89
|
if (custom.apiKey && custom.apiKey.length > 5)
|
|
74
90
|
configKey = custom.apiKey;
|
|
75
91
|
}
|
|
@@ -90,7 +106,6 @@ function readConfigFromDisk() {
|
|
|
90
106
|
catch (e) { }
|
|
91
107
|
}
|
|
92
108
|
// 2. DETERMINE WINNER
|
|
93
|
-
// If both exist, newest wins. If one exists, it wins.
|
|
94
109
|
if (configKey && authKey) {
|
|
95
110
|
if (configTime >= authTime) {
|
|
96
111
|
finalKey = configKey;
|
|
@@ -128,16 +143,9 @@ function readConfigFromDisk() {
|
|
|
128
143
|
// 4. APPLY
|
|
129
144
|
if (finalKey) {
|
|
130
145
|
config.apiKey = finalKey;
|
|
131
|
-
// config.mode = 'pro'; // REMOVED: Mode is decoupled from Key presence.
|
|
132
146
|
}
|
|
133
147
|
else {
|
|
134
|
-
// Ensure no phantom key remains
|
|
135
148
|
delete config.apiKey;
|
|
136
|
-
// if (config.mode === 'pro') config.mode = 'manual'; // OPTIONAL: Downgrade if no key? User says "No link".
|
|
137
|
-
// Actually, if I am in PRO mode and lose my key, I am broken. Falling back to manual is safer?
|
|
138
|
-
// User said "Manual mode is like standard API".
|
|
139
|
-
// Let's REMOVE this auto-downgrade too to be strictly "Decoupled".
|
|
140
|
-
// If user is in PRO without key, they get "Missing Key" error, which is correct.
|
|
141
149
|
}
|
|
142
150
|
return { ...config, version: PKG_VERSION };
|
|
143
151
|
}
|
|
@@ -149,6 +157,7 @@ export function saveConfig(updates) {
|
|
|
149
157
|
fs.mkdirSync(CONFIG_DIR_POLLI, { recursive: true });
|
|
150
158
|
}
|
|
151
159
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
|
|
160
|
+
logConfig(`[SaveConfig] Updated: ${Object.keys(updates).join(', ')}`);
|
|
152
161
|
return updated;
|
|
153
162
|
}
|
|
154
163
|
catch (e) {
|
|
@@ -156,3 +165,23 @@ export function saveConfig(updates) {
|
|
|
156
165
|
throw e;
|
|
157
166
|
}
|
|
158
167
|
}
|
|
168
|
+
// SESSION WALLET TRACKING
|
|
169
|
+
export function initSessionWallet(walletBalance) {
|
|
170
|
+
const config = loadConfig();
|
|
171
|
+
if (!config.session.wallet_initial) {
|
|
172
|
+
saveConfig({
|
|
173
|
+
session: {
|
|
174
|
+
wallet_initial: walletBalance,
|
|
175
|
+
session_start: new Date().toISOString()
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
logConfig(`[Session] Wallet initial stocké: $${walletBalance}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export function getWalletWarnPercent() {
|
|
182
|
+
const config = loadConfig();
|
|
183
|
+
const initial = config.session.wallet_initial;
|
|
184
|
+
if (!initial || initial <= 0)
|
|
185
|
+
return 100; // No reference = no warning
|
|
186
|
+
return config.thresholds.wallet_warn;
|
|
187
|
+
}
|
package/dist/server/proxy.js
CHANGED
|
@@ -245,13 +245,8 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
245
245
|
if (quota.walletBalance <= 0.001) { // Floating point safety
|
|
246
246
|
log(`[SafetyNet] Paid Only Model (${actualModel}) requested but Wallet is Empty ($${quota.walletBalance}). BLOCKING.`);
|
|
247
247
|
// Immediate Block or Fallback?
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
// Actually, Fallback to Free is usually better for UX if configured, BUT for specific "Paid Only" requests, the user explicitly chose a powerful model.
|
|
251
|
-
// Falling back to Mistral might be confusing if they asked for Gemini-Large.
|
|
252
|
-
// BUT we are failing gracefully.
|
|
253
|
-
// Let's Fallback to Free Default and Warn.
|
|
254
|
-
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
248
|
+
// Fallback based on current mode
|
|
249
|
+
actualModel = config.fallbacks.economy || 'nova-fast';
|
|
255
250
|
isEnterprise = false;
|
|
256
251
|
isFallbackActive = true;
|
|
257
252
|
fallbackReason = "Paid Only Model requires purchased credits";
|
|
@@ -277,62 +272,113 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
277
272
|
// WE DO NOT RETURN 403. WE ALLOW THE REQUEST.
|
|
278
273
|
// Since config.mode is now 'manual', the next checks (alwaysfree/pro) will be skipped.
|
|
279
274
|
}
|
|
280
|
-
|
|
275
|
+
// === MODE ECONOMY ===
|
|
276
|
+
// - Paid models BLOCKED (not fallback, BLOCK with message)
|
|
277
|
+
// - tier < threshold → fallback economy
|
|
278
|
+
// - tier = 0 → STOP (no wallet access)
|
|
279
|
+
if (config.mode === 'economy') {
|
|
281
280
|
if (isEnterprise) {
|
|
282
|
-
//
|
|
281
|
+
// 1. BLOCK Paid Models
|
|
283
282
|
try {
|
|
284
283
|
const homedir = process.env.HOME || '/tmp';
|
|
285
284
|
const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
|
|
286
285
|
if (fs.existsSync(standardPaidPath)) {
|
|
287
286
|
const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
|
|
288
287
|
if (paidModels.includes(actualModel)) {
|
|
289
|
-
log(`[
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
log(`[Economy] BLOCKED: Paid model ${actualModel} not allowed in Economy mode`);
|
|
289
|
+
emitStatusToast('error', `💎 Modèle payant interdit en mode Economy: ${actualModel}`, 'Mode Economy');
|
|
290
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
291
|
+
res.end(JSON.stringify({
|
|
292
|
+
error: {
|
|
293
|
+
message: `💎 Paid model "${actualModel}" is not allowed in Economy mode. Switch to Pro mode or use a free model.`,
|
|
294
|
+
code: 'ECONOMY_PAID_BLOCKED'
|
|
295
|
+
}
|
|
296
|
+
}));
|
|
297
|
+
return;
|
|
294
298
|
}
|
|
295
299
|
}
|
|
296
300
|
}
|
|
297
|
-
catch (e) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
catch (e) {
|
|
302
|
+
log(`[Economy] Error checking paid models: ${e}`);
|
|
303
|
+
}
|
|
304
|
+
// 2. Check Tier
|
|
305
|
+
if (quota.tier === 'error') {
|
|
306
|
+
log(`[Economy] Quota unreachable, switching to fallback`);
|
|
307
|
+
actualModel = config.fallbacks.economy || 'nova-fast';
|
|
303
308
|
isFallbackActive = true;
|
|
304
309
|
fallbackReason = "Quota Unreachable (Safety)";
|
|
305
310
|
}
|
|
306
311
|
else {
|
|
307
312
|
const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
|
|
313
|
+
// 3. STOP if tier = 0
|
|
314
|
+
if (quota.tierRemaining <= 0.01) {
|
|
315
|
+
log(`[Economy] STOP: Tier exhausted, no wallet access in Economy mode`);
|
|
316
|
+
emitStatusToast('error', `🛑 Quota épuisé! Attendez le reset ou passez en mode Pro.`, 'Mode Economy');
|
|
317
|
+
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
318
|
+
res.end(JSON.stringify({
|
|
319
|
+
error: {
|
|
320
|
+
message: `🛑 Daily quota exhausted. Wait for reset or switch to Pro mode to use wallet credits.`,
|
|
321
|
+
code: 'ECONOMY_TIER_EXHAUSTED'
|
|
322
|
+
}
|
|
323
|
+
}));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// 4. Fallback if tier < threshold
|
|
308
327
|
if (tierRatio <= (config.thresholds.tier / 100)) {
|
|
309
|
-
log(`[
|
|
310
|
-
actualModel = config.fallbacks.
|
|
311
|
-
isEnterprise = false;
|
|
328
|
+
log(`[Economy] Tier ${(tierRatio * 100).toFixed(1)}% <= ${config.thresholds.tier}%, switching to fallback`);
|
|
329
|
+
actualModel = config.fallbacks.economy || 'nova-fast';
|
|
312
330
|
isFallbackActive = true;
|
|
313
|
-
fallbackReason = `
|
|
331
|
+
fallbackReason = `Tier < ${config.thresholds.tier}% (Economie active)`;
|
|
314
332
|
}
|
|
315
333
|
}
|
|
316
334
|
}
|
|
317
335
|
}
|
|
336
|
+
// === MODE PRO ===
|
|
337
|
+
// - Paid models ALLOWED
|
|
338
|
+
// - wallet < wallet_stop $ → STOP
|
|
339
|
+
// - wallet < wallet_warn % → fallback pro
|
|
340
|
+
// - tier exhausted → info toast (continue on wallet)
|
|
318
341
|
else if (config.mode === 'pro') {
|
|
319
342
|
if (isEnterprise) {
|
|
343
|
+
// Init session wallet tracking (first request)
|
|
344
|
+
if (!config.session?.wallet_initial && quota.walletBalance > 0) {
|
|
345
|
+
const { initSessionWallet } = await import('./config.js');
|
|
346
|
+
initSessionWallet(quota.walletBalance);
|
|
347
|
+
}
|
|
320
348
|
if (quota.tier === 'error') {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
324
|
-
isEnterprise = false;
|
|
349
|
+
log(`[Pro] Quota unreachable, switching to fallback`);
|
|
350
|
+
actualModel = config.fallbacks.pro || 'qwen-coder';
|
|
325
351
|
isFallbackActive = true;
|
|
326
352
|
fallbackReason = "Quota Unreachable (Safety)";
|
|
327
353
|
}
|
|
328
354
|
else {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
355
|
+
const walletStop = config.thresholds.wallet_stop || 0.50;
|
|
356
|
+
const walletWarnPercent = config.thresholds.wallet_warn || 20;
|
|
357
|
+
const walletInitial = config.session?.wallet_initial || quota.walletBalance;
|
|
358
|
+
const walletPercent = walletInitial > 0 ? (quota.walletBalance / walletInitial) * 100 : 100;
|
|
359
|
+
// 1. STOP if wallet < wallet_stop $
|
|
360
|
+
if (quota.walletBalance < walletStop) {
|
|
361
|
+
log(`[Pro] STOP: Wallet $${quota.walletBalance} < limit $${walletStop}`);
|
|
362
|
+
emitStatusToast('error', `🛑 Wallet $${quota.walletBalance.toFixed(2)} sous limite $${walletStop}`, 'Mode Pro');
|
|
363
|
+
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
364
|
+
res.end(JSON.stringify({
|
|
365
|
+
error: {
|
|
366
|
+
message: `🛑 Wallet ($${quota.walletBalance.toFixed(2)}) below hard limit ($${walletStop}). Add credits or adjust wallet_stop threshold.`,
|
|
367
|
+
code: 'PRO_WALLET_LIMIT'
|
|
368
|
+
}
|
|
369
|
+
}));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// 2. Fallback if wallet% < wallet_warn%
|
|
373
|
+
if (walletPercent <= walletWarnPercent) {
|
|
374
|
+
log(`[Pro] Wallet ${walletPercent.toFixed(1)}% <= ${walletWarnPercent}%, switching to fallback`);
|
|
375
|
+
actualModel = config.fallbacks.pro || 'qwen-coder';
|
|
334
376
|
isFallbackActive = true;
|
|
335
|
-
fallbackReason = `Wallet
|
|
377
|
+
fallbackReason = `Wallet < ${walletWarnPercent}% (${quota.walletBalance.toFixed(2)}$)`;
|
|
378
|
+
}
|
|
379
|
+
// 3. Info if tier exhausted (continue on wallet)
|
|
380
|
+
if (quota.tierRemaining <= 0.01 && !isFallbackActive) {
|
|
381
|
+
emitStatusToast('info', `ℹ️ Tier épuisé, utilisation du wallet ($${quota.walletBalance.toFixed(2)})`, 'Mode Pro');
|
|
336
382
|
}
|
|
337
383
|
}
|
|
338
384
|
}
|
|
@@ -563,8 +609,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
563
609
|
if ((isEnterpriseFallback || isGeminiToolsFallback) && config.mode !== 'manual') {
|
|
564
610
|
log(`[SafetyNet] Upstream Rejection (${fetchRes.status}). Triggering Transparent Fallback.`);
|
|
565
611
|
if (isEnterpriseFallback) {
|
|
566
|
-
// 1a. Enterprise ->
|
|
567
|
-
actualModel = config.
|
|
612
|
+
// 1a. Enterprise -> Fallback based on mode
|
|
613
|
+
actualModel = config.mode === 'pro'
|
|
614
|
+
? (config.fallbacks.pro || 'qwen-coder')
|
|
615
|
+
: (config.fallbacks.economy || 'nova-fast');
|
|
568
616
|
isEnterprise = false;
|
|
569
617
|
isFallbackActive = true;
|
|
570
618
|
if (fetchRes.status === 402)
|
package/package.json
CHANGED