opencode-pollinations-plugin 5.6.0-beta.2 → 5.6.0-beta.20
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/debug_check.js +36 -0
- package/dist/index.js +1 -1
- package/dist/server/commands.d.ts +6 -0
- package/dist/server/commands.js +106 -4
- package/dist/server/index.js +35 -8
- package/dist/server/proxy.js +16 -8
- package/dist/server/quota.d.ts +1 -0
- package/dist/server/quota.js +18 -8
- package/dist/test-require.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as https from 'https';
|
|
2
|
+
|
|
3
|
+
function checkEndpoint(ep, key) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
console.log(`Checking ${ep}...`);
|
|
6
|
+
const req = https.request({
|
|
7
|
+
hostname: 'gen.pollinations.ai',
|
|
8
|
+
path: ep,
|
|
9
|
+
method: 'GET',
|
|
10
|
+
headers: { 'Authorization': `Bearer ${key}` }
|
|
11
|
+
}, (res) => {
|
|
12
|
+
console.log(`Status Code: ${res.statusCode}`);
|
|
13
|
+
let data = '';
|
|
14
|
+
res.on('data', chunk => data += chunk);
|
|
15
|
+
res.on('end', () => {
|
|
16
|
+
console.log(`Headers:`, res.headers);
|
|
17
|
+
console.log(`Body Full: ${data}`);
|
|
18
|
+
if (res.statusCode === 200) resolve({ ok: true, body: data });
|
|
19
|
+
else resolve({ ok: false, status: res.statusCode, body: data });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
req.on('error', (e) => {
|
|
23
|
+
console.log(`Error: ${e.message}`);
|
|
24
|
+
resolve({ ok: false, status: e.message || 'Error' });
|
|
25
|
+
});
|
|
26
|
+
req.setTimeout(10000, () => req.destroy());
|
|
27
|
+
req.end();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const KEY = "plln_sk_F7a4RcBG4AVCeBSo6lnS36EKwm0nPn1O";
|
|
32
|
+
|
|
33
|
+
(async () => {
|
|
34
|
+
const res = await checkEndpoint('/account/profile', KEY);
|
|
35
|
+
console.log('Result:', res);
|
|
36
|
+
})();
|
package/dist/index.js
CHANGED
|
@@ -97,7 +97,7 @@ export const PollinationsPlugin = async (ctx) => {
|
|
|
97
97
|
if (!config.provider)
|
|
98
98
|
config.provider = {};
|
|
99
99
|
// Dynamic Provider Name
|
|
100
|
-
const version = require('
|
|
100
|
+
const version = require('../package.json').version;
|
|
101
101
|
config.provider['pollinations'] = {
|
|
102
102
|
id: 'pollinations',
|
|
103
103
|
name: `Pollinations AI (v${version})`,
|
package/dist/server/commands.js
CHANGED
|
@@ -1,8 +1,60 @@
|
|
|
1
|
+
import * as https from 'https';
|
|
1
2
|
import { loadConfig, saveConfig } from './config.js';
|
|
2
3
|
import { getQuotaStatus } from './quota.js';
|
|
3
4
|
import { emitStatusToast } from './toast.js';
|
|
4
5
|
import { getDetailedUsage } from './pollinations-api.js';
|
|
5
6
|
import { generatePollinationsConfig } from './generate-config.js';
|
|
7
|
+
function checkEndpoint(ep, key) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const req = https.request({
|
|
10
|
+
hostname: 'gen.pollinations.ai',
|
|
11
|
+
path: ep,
|
|
12
|
+
method: 'GET',
|
|
13
|
+
headers: {
|
|
14
|
+
'Authorization': `Bearer ${key}`,
|
|
15
|
+
'User-Agent': 'Pollinations-Plugin/5.6.0' // Identify cleanly
|
|
16
|
+
}
|
|
17
|
+
}, (res) => {
|
|
18
|
+
const isJson = res.headers['content-type']?.includes('application/json');
|
|
19
|
+
let data = '';
|
|
20
|
+
res.on('data', chunk => data += chunk);
|
|
21
|
+
res.on('end', () => {
|
|
22
|
+
if (res.statusCode === 200 && isJson) {
|
|
23
|
+
// Double Check Check Body for Logical Errors masked as 200
|
|
24
|
+
try {
|
|
25
|
+
const json = JSON.parse(data);
|
|
26
|
+
if (json.error || json.success === false) {
|
|
27
|
+
resolve({ ok: false, reason: "API Logical Error", status: 200 });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
resolve({ ok: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
resolve({ ok: false, reason: "Invalid JSON", status: 200 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
resolve({ ok: false, status: res.statusCode, reason: isJson ? "API Error" : "Not JSON (Cloudflare?)" });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
req.on('error', (e) => resolve({ ok: false, status: e.message || 'Error' }));
|
|
43
|
+
req.setTimeout(10000, () => req.destroy()); // 10s Timeout
|
|
44
|
+
req.end();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export async function checkKeyPermissions(key) {
|
|
48
|
+
// SEQUENTIAL CHECK (Avoid Rate Limits on Key Verification)
|
|
49
|
+
const endpoints = ['/account/profile', '/account/balance', '/account/usage'];
|
|
50
|
+
for (const ep of endpoints) {
|
|
51
|
+
const res = await checkEndpoint(ep, key);
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
return { ok: false, reason: `${ep} (${res.status})` };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { ok: true };
|
|
57
|
+
}
|
|
6
58
|
// === CONSTANTS & PRICING ===
|
|
7
59
|
const TIER_LIMITS = {
|
|
8
60
|
spore: { pollen: 1, emoji: '🦠' },
|
|
@@ -87,11 +139,11 @@ export async function handleCommand(command) {
|
|
|
87
139
|
const args = parts.slice(2);
|
|
88
140
|
switch (subCommand) {
|
|
89
141
|
case 'mode':
|
|
90
|
-
return handleModeCommand(args);
|
|
142
|
+
return await handleModeCommand(args);
|
|
91
143
|
case 'usage':
|
|
92
144
|
return await handleUsageCommand(args);
|
|
93
145
|
case 'connect':
|
|
94
|
-
return handleConnectCommand(args);
|
|
146
|
+
return await handleConnectCommand(args);
|
|
95
147
|
case 'fallback':
|
|
96
148
|
return handleFallbackCommand(args);
|
|
97
149
|
case 'config':
|
|
@@ -106,7 +158,7 @@ export async function handleCommand(command) {
|
|
|
106
158
|
}
|
|
107
159
|
}
|
|
108
160
|
// === SUB-COMMANDS ===
|
|
109
|
-
function handleModeCommand(args) {
|
|
161
|
+
async function handleModeCommand(args) {
|
|
110
162
|
const mode = args[0];
|
|
111
163
|
if (!mode) {
|
|
112
164
|
const config = loadConfig();
|
|
@@ -121,6 +173,32 @@ function handleModeCommand(args) {
|
|
|
121
173
|
error: `Mode invalide: ${mode}. Valeurs: manual, alwaysfree, pro`
|
|
122
174
|
};
|
|
123
175
|
}
|
|
176
|
+
const checkConfig = loadConfig();
|
|
177
|
+
// JIT VERIFICATION for PRO Mode
|
|
178
|
+
if (mode === 'pro') {
|
|
179
|
+
const key = checkConfig.apiKey;
|
|
180
|
+
if (!key) {
|
|
181
|
+
return { handled: true, error: "❌ Mode Pro nécessite une Clé API configurée." };
|
|
182
|
+
}
|
|
183
|
+
emitStatusToast('info', 'Vérification des droits...', 'Mode Pro');
|
|
184
|
+
try {
|
|
185
|
+
// Force verify permissions NOW
|
|
186
|
+
const check = await checkKeyPermissions(key);
|
|
187
|
+
if (!check.ok) {
|
|
188
|
+
saveConfig({ mode: 'manual', keyHasAccessToProfile: false });
|
|
189
|
+
return {
|
|
190
|
+
handled: true,
|
|
191
|
+
error: `❌ **Mode Refusé**\nVotre clé est limitée (Code ${check.status}: ${check.reason}).\nPassage en mode **manual**.`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Valid -> Ensure flag is true
|
|
195
|
+
saveConfig({ keyHasAccessToProfile: true });
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
return { handled: true, error: `❌ Erreur de vérification: ${e.message}` };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Allow switch (if alwaysfree or manual, or verified pro)
|
|
124
202
|
saveConfig({ mode: mode });
|
|
125
203
|
const config = loadConfig();
|
|
126
204
|
if (config.gui.status !== 'none') {
|
|
@@ -230,10 +308,34 @@ async function handleConnectCommand(args) {
|
|
|
230
308
|
const masked = key.substring(0, 6) + '...';
|
|
231
309
|
// Count Paid Only models found
|
|
232
310
|
const diamondCount = enterpriseModels.filter(m => m.name.includes('💎')).length;
|
|
311
|
+
// CHECK RESTRICTIONS: Strict Check (Usage + Profile + Balance)
|
|
312
|
+
let forcedModeMsg = "";
|
|
313
|
+
let isLimited = false;
|
|
314
|
+
let limitReason = "";
|
|
315
|
+
try {
|
|
316
|
+
// Strict Probe: Must be able to read ALL accounting data
|
|
317
|
+
const check = await checkKeyPermissions(key);
|
|
318
|
+
if (!check.ok) {
|
|
319
|
+
isLimited = true;
|
|
320
|
+
limitReason = check.reason || "Unknown";
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
isLimited = true;
|
|
325
|
+
limitReason = e.message;
|
|
326
|
+
}
|
|
327
|
+
// If Limited -> FORCE MANUAL
|
|
328
|
+
if (isLimited) {
|
|
329
|
+
saveConfig({ apiKey: key, mode: 'manual', keyHasAccessToProfile: false });
|
|
330
|
+
forcedModeMsg = `\n⚠️ **Clé Limitée** (Echec: ${limitReason}) -> Mode **MANUEL** forcé.\n*Requis pour mode Auto: Profile, Balance & Usage.*`;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
saveConfig({ apiKey: key, keyHasAccessToProfile: true }); // Let user keep current mode or default
|
|
334
|
+
}
|
|
233
335
|
emitStatusToast('success', `Clé Valide! (${enterpriseModels.length} modèles Pro débloqués)`, 'Pollinations Config');
|
|
234
336
|
return {
|
|
235
337
|
handled: true,
|
|
236
|
-
response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n-
|
|
338
|
+
response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n- Modèles Débloqués: ${enterpriseModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
|
|
237
339
|
};
|
|
238
340
|
}
|
|
239
341
|
else {
|
package/dist/server/index.js
CHANGED
|
@@ -145,12 +145,39 @@ process.on('exit', (code) => {
|
|
|
145
145
|
}
|
|
146
146
|
catch (e) { }
|
|
147
147
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
// STARTUP CHECK: Re-validate Key (in case of upgrade/config drift)
|
|
149
|
+
import { checkKeyPermissions } from './commands.js';
|
|
150
|
+
(async () => {
|
|
151
|
+
const config = loadConfig();
|
|
152
|
+
if (config.apiKey) {
|
|
153
|
+
try {
|
|
154
|
+
console.log('Pollinations Plugin: Verifying API Key on startup...');
|
|
155
|
+
const check = await checkKeyPermissions(config.apiKey);
|
|
156
|
+
if (!check.ok) {
|
|
157
|
+
console.warn(`Pollinations Plugin: Limited Key Detected on Startup (${check.reason}). Enforcing Manual Mode.`);
|
|
158
|
+
saveConfig({
|
|
159
|
+
apiKey: config.apiKey,
|
|
160
|
+
mode: 'manual',
|
|
161
|
+
keyHasAccessToProfile: false
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
if (config.keyHasAccessToProfile === false) {
|
|
166
|
+
saveConfig({ apiKey: config.apiKey, keyHasAccessToProfile: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
console.error('Pollinations Plugin: Startup Check Failed:', e);
|
|
172
|
+
}
|
|
153
173
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
174
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
175
|
+
const url = `http://127.0.0.1:${PORT}`;
|
|
176
|
+
log(`[SERVER] Started V3 Phase 3 (Auth Enabled) on port ${PORT}`);
|
|
177
|
+
try {
|
|
178
|
+
fs.appendFileSync(LIFE_LOG, `[${new Date().toISOString()}] [LISTEN] PID:${process.pid} Listening on ${PORT}\n`);
|
|
179
|
+
}
|
|
180
|
+
catch (e) { }
|
|
181
|
+
console.log(`POLLINATIONS_V3_URL=${url}`);
|
|
182
|
+
});
|
|
183
|
+
})();
|
package/dist/server/proxy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { loadConfig } from './config.js';
|
|
3
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
4
4
|
import { handleCommand } from './commands.js';
|
|
5
5
|
import { emitStatusToast, emitLogToast } from './toast.js';
|
|
6
6
|
// --- PERSISTENCE: SIGNATURE MAP (Multi-Round Support) ---
|
|
@@ -258,11 +258,22 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
// B. SAFETY NETS (The Core V5 Logic)
|
|
261
|
+
// 0. GLOBAL CHECK: Auth Limited (403 on Quota)
|
|
262
|
+
// If we can't read quota because of 403, we MUST STOP and force manual.
|
|
263
|
+
if (isEnterprise && quota.errorType === 'auth_limited') {
|
|
264
|
+
log(`[SafetyNet] CRITICAL: Limited Key Detected (403). Enforcing Manual Mode.`);
|
|
265
|
+
saveConfig({ mode: 'manual', keyHasAccessToProfile: false });
|
|
266
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
267
|
+
res.end(JSON.stringify({
|
|
268
|
+
error: "Clé Limitée - Accès Profil Refusé",
|
|
269
|
+
message: "Votre clé ne permet pas le suivi des quotas via /account/profile.\nLe mode automatique est désactivé.\nVeuillez passer en mode Manual ou utiliser une clé complète.",
|
|
270
|
+
code: "AUTH_LIMITED"
|
|
271
|
+
}));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
261
274
|
if (config.mode === 'alwaysfree') {
|
|
262
275
|
if (isEnterprise) {
|
|
263
276
|
// NEW: Paid Only Check for Always Free
|
|
264
|
-
// If the user asks for a 💎 Paid Only model while in Always Free, we BLOCK it to save wallet
|
|
265
|
-
// and fallback to free specific message.
|
|
266
277
|
try {
|
|
267
278
|
const homedir = process.env.HOME || '/tmp';
|
|
268
279
|
const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
|
|
@@ -279,6 +290,7 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
279
290
|
}
|
|
280
291
|
catch (e) { }
|
|
281
292
|
if (!isFallbackActive && quota.tier === 'error') {
|
|
293
|
+
// Network error or unknown error (but NOT auth_limited, handled above)
|
|
282
294
|
log(`[SafetyNet] AlwaysFree Mode: Quota Check Failed. Switching to Free Fallback.`);
|
|
283
295
|
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
284
296
|
isEnterprise = false;
|
|
@@ -300,6 +312,7 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
300
312
|
else if (config.mode === 'pro') {
|
|
301
313
|
if (isEnterprise) {
|
|
302
314
|
if (quota.tier === 'error') {
|
|
315
|
+
// Network error or unknown
|
|
303
316
|
log(`[SafetyNet] Pro Mode: Quota Unreachable. Switching to Free Fallback.`);
|
|
304
317
|
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
305
318
|
isEnterprise = false;
|
|
@@ -308,11 +321,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
308
321
|
}
|
|
309
322
|
else {
|
|
310
323
|
const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
|
|
311
|
-
// Logic: Fallback if Wallet is Low (< Threshold) AND Tier is Exhausted (< Threshold %)
|
|
312
|
-
// Wait, user wants priority to Free Tier.
|
|
313
|
-
// If Free Tier is available (Ratio > Threshold), we usage it (don't fallback).
|
|
314
|
-
// If Free Tier is exhausted (Ratio <= Threshold), THEN check Wallet.
|
|
315
|
-
// If Wallet also Low, THEN Fallback.
|
|
316
324
|
if (quota.walletBalance < config.thresholds.wallet && tierRatio <= (config.thresholds.tier / 100)) {
|
|
317
325
|
log(`[SafetyNet] Pro Mode: Wallet < $${config.thresholds.wallet} AND Tier < ${config.thresholds.tier}%. Switching.`);
|
|
318
326
|
actualModel = config.fallbacks.free.main.replace('free/', '');
|
package/dist/server/quota.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface QuotaStatus {
|
|
|
10
10
|
needsAlert: boolean;
|
|
11
11
|
tier: string;
|
|
12
12
|
tierEmoji: string;
|
|
13
|
+
errorType?: 'auth_limited' | 'network' | 'unknown';
|
|
13
14
|
}
|
|
14
15
|
export declare function getQuotaStatus(forceRefresh?: boolean): Promise<QuotaStatus>;
|
|
15
16
|
export declare function formatQuotaForToast(quota: QuotaStatus): string;
|
package/dist/server/quota.js
CHANGED
|
@@ -44,12 +44,11 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
44
44
|
}
|
|
45
45
|
try {
|
|
46
46
|
logQuota("Fetching Quota Data...");
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
]);
|
|
47
|
+
// SEQUENTIAL FETCH (Avoid Rate Limits)
|
|
48
|
+
// We fetch one by one. If one fails, we catch and return fallback.
|
|
49
|
+
const profileRes = await fetchAPI('/account/profile', config.apiKey);
|
|
50
|
+
const balanceRes = await fetchAPI('/account/balance', config.apiKey);
|
|
51
|
+
const usageRes = await fetchAPI('/account/usage', config.apiKey);
|
|
53
52
|
logQuota(`Fetch Success. Tier: ${profileRes.tier}, Balance: ${balanceRes.balance}`);
|
|
54
53
|
const profile = profileRes;
|
|
55
54
|
const balance = balanceRes.balance;
|
|
@@ -83,7 +82,14 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
83
82
|
return cachedQuota;
|
|
84
83
|
}
|
|
85
84
|
catch (e) {
|
|
86
|
-
logQuota(`ERROR fetching quota: ${e}`);
|
|
85
|
+
logQuota(`ERROR fetching quota: ${e.message}`);
|
|
86
|
+
let errorType = 'unknown';
|
|
87
|
+
if (e.message && e.message.includes('403')) {
|
|
88
|
+
errorType = 'auth_limited';
|
|
89
|
+
}
|
|
90
|
+
else if (e.message && e.message.includes('Network Error')) {
|
|
91
|
+
errorType = 'network';
|
|
92
|
+
}
|
|
87
93
|
// Retourner le cache ou un état par défaut safe
|
|
88
94
|
return cachedQuota || {
|
|
89
95
|
tierRemaining: 0,
|
|
@@ -96,7 +102,8 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
96
102
|
isUsingWallet: false,
|
|
97
103
|
needsAlert: true,
|
|
98
104
|
tier: 'error',
|
|
99
|
-
tierEmoji: '⚠️'
|
|
105
|
+
tierEmoji: '⚠️',
|
|
106
|
+
errorType
|
|
100
107
|
};
|
|
101
108
|
}
|
|
102
109
|
}
|
|
@@ -198,6 +205,9 @@ function calculateCurrentPeriodUsage(usage, resetInfo) {
|
|
|
198
205
|
}
|
|
199
206
|
// === EXPORT POUR LES ALERTES ===
|
|
200
207
|
export function formatQuotaForToast(quota) {
|
|
208
|
+
if (quota.errorType === 'auth_limited') {
|
|
209
|
+
return `🔑 CLE LIMITÉE (Génération Seule) | 💎 Wallet: N/A | ⏰ Reset: N/A`;
|
|
210
|
+
}
|
|
201
211
|
const tierPercent = quota.tierLimit > 0
|
|
202
212
|
? Math.round((quota.tierRemaining / quota.tierLimit) * 100)
|
|
203
213
|
: 0;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
try {
|
|
4
|
+
const pkg = require('../package.json');
|
|
5
|
+
console.log("SUCCESS: Loaded version " + pkg.version);
|
|
6
|
+
} catch (e) {
|
|
7
|
+
console.error("FAILURE:", e.message);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-pollinations-plugin",
|
|
3
3
|
"displayName": "Pollinations AI (V5.1)",
|
|
4
|
-
"version": "5.6.0-beta.
|
|
4
|
+
"version": "5.6.0-beta.20",
|
|
5
5
|
"description": "Native Pollinations.ai Provider Plugin for OpenCode",
|
|
6
6
|
"publisher": "pollinations",
|
|
7
7
|
"repository": {
|