oc-auth-switcher 0.6.0 → 0.7.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/dist/cli.js +333 -17
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,12 @@ var STATE_FILE = path.join(configDir, "auth-switcher-state.json");
|
|
|
14
14
|
var DEFAULT_THRESHOLD = 0.95;
|
|
15
15
|
var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
|
|
16
16
|
var REJECTION_FALLBACK_SECONDS = 60 * 60;
|
|
17
|
+
var METRIC_MODEL_FAMILY = {
|
|
18
|
+
session5h: null,
|
|
19
|
+
weekly7d: null,
|
|
20
|
+
weekly7dSonnet: "sonnet",
|
|
21
|
+
weekly7dFable: "fable"
|
|
22
|
+
};
|
|
17
23
|
|
|
18
24
|
// node_modules/@ex-machina/opencode-anthropic-auth/dist/constants.js
|
|
19
25
|
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
@@ -257,10 +263,252 @@ function ensureAccountsInState(state, accountNames) {
|
|
|
257
263
|
}
|
|
258
264
|
|
|
259
265
|
// src/rotation.ts
|
|
266
|
+
function isTemporarilyUnavailable(state, accountName) {
|
|
267
|
+
const cooldownUntil = state.authFailures[accountName];
|
|
268
|
+
if (!cooldownUntil)
|
|
269
|
+
return false;
|
|
270
|
+
if (Date.now() > cooldownUntil) {
|
|
271
|
+
delete state.authFailures[accountName];
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
function isOverThreshold(usage, state, modelFamily) {
|
|
277
|
+
if (!usage)
|
|
278
|
+
return false;
|
|
279
|
+
const thresholds = getThresholds(state.config);
|
|
280
|
+
if (usage.rejected.status?.toLowerCase() === "rejected" && isRejectionRelevant(usage.rejected.prefix, modelFamily))
|
|
281
|
+
return true;
|
|
282
|
+
return metricEntries(usage, thresholds, modelFamily).some((metric) => metric.threshold > 0 && metric.util >= metric.threshold);
|
|
283
|
+
}
|
|
284
|
+
function getUtilizationScore(usage, state, modelFamily) {
|
|
285
|
+
if (!usage)
|
|
286
|
+
return 0;
|
|
287
|
+
const thresholds = getThresholds(state.config);
|
|
288
|
+
if (usage.rejected.status?.toLowerCase() === "rejected" && isRejectionRelevant(usage.rejected.prefix, modelFamily))
|
|
289
|
+
return Infinity;
|
|
290
|
+
const scores = metricEntries(usage, thresholds, modelFamily).filter((metric) => metric.threshold > 0).map((metric) => metric.util / metric.threshold);
|
|
291
|
+
return scores.length > 0 ? Math.max(...scores) : 0;
|
|
292
|
+
}
|
|
293
|
+
function getModelFamily(model) {
|
|
294
|
+
if (!model)
|
|
295
|
+
return;
|
|
296
|
+
const normalized = model.toLowerCase();
|
|
297
|
+
return ["fable", "sonnet", "opus"].find((family) => normalized.includes(family));
|
|
298
|
+
}
|
|
299
|
+
function isMetricRelevant(metric, modelFamily) {
|
|
300
|
+
const metricFamily = METRIC_MODEL_FAMILY[metric];
|
|
301
|
+
return modelFamily === undefined || metricFamily === null || metricFamily === modelFamily;
|
|
302
|
+
}
|
|
303
|
+
function rejectionFamily(prefix) {
|
|
304
|
+
if (!prefix)
|
|
305
|
+
return;
|
|
306
|
+
const normalized = prefix.toLowerCase();
|
|
307
|
+
return ["fable", "sonnet", "opus"].find((family) => normalized === `7d_${family}` || normalized === `anthropic-ratelimit-unified-7d_${family}`);
|
|
308
|
+
}
|
|
309
|
+
function isRejectionRelevant(prefix, modelFamily) {
|
|
310
|
+
const rejectedFamily = rejectionFamily(prefix);
|
|
311
|
+
return modelFamily === undefined || rejectedFamily === undefined || rejectedFamily === modelFamily;
|
|
312
|
+
}
|
|
313
|
+
function metricEntries(usage, thresholds, modelFamily) {
|
|
314
|
+
return Object.keys(METRIC_MODEL_FAMILY).filter((key) => isMetricRelevant(key, modelFamily)).map((key) => ({
|
|
315
|
+
name: key,
|
|
316
|
+
util: usage[key].utilization,
|
|
317
|
+
threshold: thresholds[key]
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
function purgeExpiredCooldowns(state) {
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
for (const name of Object.keys(state.authFailures)) {
|
|
323
|
+
if (state.authFailures[name] <= now) {
|
|
324
|
+
delete state.authFailures[name];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function findBestAvailable(candidates, state, exclude, modelFamily) {
|
|
329
|
+
let best = null;
|
|
330
|
+
let bestScore = Infinity;
|
|
331
|
+
for (const acct of candidates) {
|
|
332
|
+
if (exclude.has(acct.name))
|
|
333
|
+
continue;
|
|
334
|
+
if (isTemporarilyUnavailable(state, acct.name))
|
|
335
|
+
continue;
|
|
336
|
+
if (!isOverThreshold(state.usage[acct.name], state, modelFamily)) {
|
|
337
|
+
const score = getUtilizationScore(state.usage[acct.name], state, modelFamily);
|
|
338
|
+
if (!best || score < bestScore) {
|
|
339
|
+
bestScore = score;
|
|
340
|
+
best = acct;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (best)
|
|
345
|
+
return best;
|
|
346
|
+
best = null;
|
|
347
|
+
bestScore = Infinity;
|
|
348
|
+
for (const acct of candidates) {
|
|
349
|
+
if (exclude.has(acct.name))
|
|
350
|
+
continue;
|
|
351
|
+
if (isTemporarilyUnavailable(state, acct.name))
|
|
352
|
+
continue;
|
|
353
|
+
const score = getUtilizationScore(state.usage[acct.name], state, modelFamily);
|
|
354
|
+
if (!best || score < bestScore) {
|
|
355
|
+
bestScore = score;
|
|
356
|
+
best = acct;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return best;
|
|
360
|
+
}
|
|
361
|
+
function selectAccount(accounts, state, model) {
|
|
362
|
+
if (accounts.length === 0) {
|
|
363
|
+
throw new Error("No accounts available");
|
|
364
|
+
}
|
|
365
|
+
purgeExpiredCooldowns(state);
|
|
366
|
+
const modelFamily = getModelFamily(model);
|
|
367
|
+
const currentIdx = accounts.findIndex((a) => a.name === state.currentAccount);
|
|
368
|
+
if (currentIdx < 0) {
|
|
369
|
+
const best = findBestAvailable(accounts, state, new Set, modelFamily);
|
|
370
|
+
const selected = best ?? accounts[0];
|
|
371
|
+
return {
|
|
372
|
+
account: selected,
|
|
373
|
+
switched: true,
|
|
374
|
+
reason: `Selecting account ${selected.name}`
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const current = accounts[currentIdx];
|
|
378
|
+
const currentUsage = state.usage[current.name];
|
|
379
|
+
const currentOverThreshold = isOverThreshold(currentUsage, state, modelFamily);
|
|
380
|
+
const currentInCooldown = isTemporarilyUnavailable(state, current.name);
|
|
381
|
+
if (currentOverThreshold || currentInCooldown) {
|
|
382
|
+
const reason = currentInCooldown ? `${current.name} in auth-failure cooldown` : `${current.name} exceeded threshold`;
|
|
383
|
+
const best = findBestAvailable(accounts, state, new Set([current.name]), modelFamily);
|
|
384
|
+
if (best && best.name !== current.name) {
|
|
385
|
+
return {
|
|
386
|
+
account: best,
|
|
387
|
+
switched: true,
|
|
388
|
+
reason: `${reason} \u2014 switching to ${best.name}`
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return { account: current, switched: false };
|
|
392
|
+
}
|
|
393
|
+
return { account: current, switched: false };
|
|
394
|
+
}
|
|
260
395
|
function clearAuthFailure(state, accountName) {
|
|
261
396
|
delete state.authFailures[accountName];
|
|
262
397
|
}
|
|
263
398
|
|
|
399
|
+
// src/status.ts
|
|
400
|
+
var MODEL_FAMILIES = ["opus", "sonnet", "fable"];
|
|
401
|
+
function percentage(value) {
|
|
402
|
+
return `${(value * 100).toFixed(0)}%`;
|
|
403
|
+
}
|
|
404
|
+
function formatRelativeDuration(targetMs, nowMs = Date.now()) {
|
|
405
|
+
const difference = targetMs - nowMs;
|
|
406
|
+
const absoluteSeconds = Math.abs(difference) / 1000;
|
|
407
|
+
if (absoluteSeconds < 30)
|
|
408
|
+
return "now";
|
|
409
|
+
const units = [
|
|
410
|
+
["d", 86400],
|
|
411
|
+
["h", 3600],
|
|
412
|
+
["m", 60]
|
|
413
|
+
];
|
|
414
|
+
const parts = [];
|
|
415
|
+
let remaining = absoluteSeconds;
|
|
416
|
+
for (const [suffix, seconds] of units) {
|
|
417
|
+
const amount = Math.floor(remaining / seconds);
|
|
418
|
+
if (amount > 0) {
|
|
419
|
+
parts.push(`${amount}${suffix}`);
|
|
420
|
+
remaining -= amount * seconds;
|
|
421
|
+
}
|
|
422
|
+
if (parts.length === 2)
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
if (parts.length === 0)
|
|
426
|
+
parts.push(`${Math.max(1, Math.round(remaining))}s`);
|
|
427
|
+
return difference >= 0 ? `in ${parts.join(" ")}` : `${parts.join(" ")} ago`;
|
|
428
|
+
}
|
|
429
|
+
function displayRejectionPrefix(prefix) {
|
|
430
|
+
return prefix?.replace(/^anthropic-ratelimit-unified-/i, "");
|
|
431
|
+
}
|
|
432
|
+
function rejectionFamily2(prefix) {
|
|
433
|
+
if (!prefix)
|
|
434
|
+
return;
|
|
435
|
+
const normalized = prefix.toLowerCase();
|
|
436
|
+
return MODEL_FAMILIES.find((family) => normalized === `7d_${family}` || normalized === `anthropic-ratelimit-unified-7d_${family}`);
|
|
437
|
+
}
|
|
438
|
+
function tokenExpiryDescription(account, nowMs) {
|
|
439
|
+
if (account.expires > nowMs) {
|
|
440
|
+
return `expires ${formatRelativeDuration(account.expires, nowMs)}`;
|
|
441
|
+
}
|
|
442
|
+
if (account.refresh) {
|
|
443
|
+
return account.expires > 0 ? `expired ${formatRelativeDuration(account.expires, nowMs)}; refresh available` : "expired; refresh available";
|
|
444
|
+
}
|
|
445
|
+
return account.expires > 0 ? `expired/unrefreshable (${formatRelativeDuration(account.expires, nowMs)})` : "expired/unrefreshable";
|
|
446
|
+
}
|
|
447
|
+
function deriveAccountHealth(account, usage, thresholds, cooldownUntil, nowMs = Date.now()) {
|
|
448
|
+
const availability = Object.fromEntries(MODEL_FAMILIES.map((family) => [family, true]));
|
|
449
|
+
const reasons = [];
|
|
450
|
+
const metrics = usage ? Object.keys(METRIC_MODEL_FAMILY).filter((metric) => thresholds[metric] > 0).map((metric) => ({
|
|
451
|
+
metric,
|
|
452
|
+
utilization: usage[metric].utilization,
|
|
453
|
+
threshold: thresholds[metric],
|
|
454
|
+
family: METRIC_MODEL_FAMILY[metric]
|
|
455
|
+
})) : [];
|
|
456
|
+
for (const metric of metrics) {
|
|
457
|
+
if (metric.utilization < metric.threshold)
|
|
458
|
+
continue;
|
|
459
|
+
const families = metric.family ? [metric.family] : [...MODEL_FAMILIES];
|
|
460
|
+
for (const family of families)
|
|
461
|
+
availability[family] = false;
|
|
462
|
+
reasons.push({
|
|
463
|
+
kind: "metric",
|
|
464
|
+
message: `${metric.metric} ${percentage(metric.utilization)} >= ${percentage(metric.threshold)}${metric.family ? ` \u2014 ${metric.family} only` : ""}`,
|
|
465
|
+
families,
|
|
466
|
+
reset: usage?.[metric.metric].reset
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
if (usage?.rejected.status?.toLowerCase() === "rejected") {
|
|
470
|
+
const family = rejectionFamily2(usage.rejected.prefix);
|
|
471
|
+
const families = family ? [family] : [...MODEL_FAMILIES];
|
|
472
|
+
for (const blockedFamily of families)
|
|
473
|
+
availability[blockedFamily] = false;
|
|
474
|
+
const prefix = displayRejectionPrefix(usage.rejected.prefix);
|
|
475
|
+
reasons.push({
|
|
476
|
+
kind: "rejection",
|
|
477
|
+
message: `rejected${prefix ? ` (${prefix})` : ""}${family ? ` \u2014 ${family} only` : ""}`,
|
|
478
|
+
families,
|
|
479
|
+
reset: usage.rejected.reset
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (cooldownUntil && cooldownUntil > nowMs) {
|
|
483
|
+
for (const family of MODEL_FAMILIES)
|
|
484
|
+
availability[family] = false;
|
|
485
|
+
reasons.push({
|
|
486
|
+
kind: "cooldown",
|
|
487
|
+
message: `auth-failure cooldown \u2014 ${formatRelativeDuration(cooldownUntil, nowMs)}`,
|
|
488
|
+
families: [...MODEL_FAMILIES]
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
const hasUsableToken = !!account.access && account.expires > nowMs || !!account.refresh;
|
|
492
|
+
if (!hasUsableToken) {
|
|
493
|
+
for (const family of MODEL_FAMILIES)
|
|
494
|
+
availability[family] = false;
|
|
495
|
+
reasons.push({
|
|
496
|
+
kind: "token",
|
|
497
|
+
message: "expired/unrefreshable token",
|
|
498
|
+
families: [...MODEL_FAMILIES]
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const highestUtilization = metrics.sort((a, b) => b.utilization / b.threshold - a.utilization / a.threshold)[0] ?? null;
|
|
502
|
+
const futureResets = reasons.map((reason) => reason.reset).filter((reset) => !!reset && reset * 1000 > nowMs);
|
|
503
|
+
return {
|
|
504
|
+
availability,
|
|
505
|
+
highestUtilization,
|
|
506
|
+
reasons,
|
|
507
|
+
earliestReset: futureResets.length > 0 ? Math.min(...futureResets) : undefined,
|
|
508
|
+
tokenExpiry: tokenExpiryDescription(account, nowMs)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
264
512
|
// node_modules/@ex-machina/opencode-anthropic-auth/dist/pkce.js
|
|
265
513
|
function base64UrlEncode(bytes) {
|
|
266
514
|
let bin = "";
|
|
@@ -372,14 +620,15 @@ async function exchange(input, verifier, redirectUri, expectedState) {
|
|
|
372
620
|
|
|
373
621
|
// src/cli.ts
|
|
374
622
|
import { spawn } from "child_process";
|
|
375
|
-
var
|
|
376
|
-
var
|
|
377
|
-
var
|
|
378
|
-
var
|
|
379
|
-
var
|
|
380
|
-
var
|
|
381
|
-
var
|
|
382
|
-
var
|
|
623
|
+
var COLOR = !!process.stdout.isTTY;
|
|
624
|
+
var RESET = COLOR ? "\x1B[0m" : "";
|
|
625
|
+
var BOLD = COLOR ? "\x1B[1m" : "";
|
|
626
|
+
var DIM = COLOR ? "\x1B[2m" : "";
|
|
627
|
+
var RED = COLOR ? "\x1B[31m" : "";
|
|
628
|
+
var GREEN = COLOR ? "\x1B[32m" : "";
|
|
629
|
+
var YELLOW = COLOR ? "\x1B[33m" : "";
|
|
630
|
+
var BLUE = COLOR ? "\x1B[34m" : "";
|
|
631
|
+
var CYAN = COLOR ? "\x1B[36m" : "";
|
|
383
632
|
function progressBar(value, threshold, width = 30) {
|
|
384
633
|
const pct = Math.min(value, 1);
|
|
385
634
|
const filled = Math.round(pct * width);
|
|
@@ -704,23 +953,90 @@ ${RED}Usage: oc-auth-switcher switch <account-name>${RESET}`);
|
|
|
704
953
|
saveState(state);
|
|
705
954
|
console.log(`${GREEN}Switched to "${name}". Will take effect on the next API request.${RESET}`);
|
|
706
955
|
}
|
|
956
|
+
function healthLabel(health) {
|
|
957
|
+
const usable = MODEL_FAMILIES.filter((family) => health.availability[family]);
|
|
958
|
+
const unavailable = MODEL_FAMILIES.filter((family) => !health.availability[family]);
|
|
959
|
+
if (unavailable.length === 0)
|
|
960
|
+
return `${GREEN}healthy \u2014 all models usable${RESET}`;
|
|
961
|
+
if (usable.length === 0)
|
|
962
|
+
return `${RED}unavailable \u2014 all models${RESET}`;
|
|
963
|
+
return `${YELLOW}partial \u2014 usable: ${usable.join(", ")}; unavailable: ${unavailable.join(", ")}${RESET}`;
|
|
964
|
+
}
|
|
965
|
+
function utilizationLabel(health) {
|
|
966
|
+
const peak = health.highestUtilization;
|
|
967
|
+
if (!peak)
|
|
968
|
+
return `${DIM}no usage data${RESET}`;
|
|
969
|
+
const utilization = `${(peak.utilization * 100).toFixed(0)}%`;
|
|
970
|
+
const threshold = `${(peak.threshold * 100).toFixed(0)}%`;
|
|
971
|
+
const family = peak.family ? ` \u2014 ${peak.family} only` : "";
|
|
972
|
+
if (peak.utilization >= peak.threshold) {
|
|
973
|
+
return `${RED}${peak.metric} ${utilization} >= ${threshold}${family}${RESET}`;
|
|
974
|
+
}
|
|
975
|
+
const color = peak.utilization >= peak.threshold * 0.8 ? YELLOW : GREEN;
|
|
976
|
+
return `${color}${peak.metric} ${utilization} / ${threshold} threshold${family}${RESET}`;
|
|
977
|
+
}
|
|
978
|
+
function nextAccountSummary(accounts, state, activeName) {
|
|
979
|
+
if (accounts.length === 0 || activeName && accounts.length === 1) {
|
|
980
|
+
return "(none \u2014 no alternate account)";
|
|
981
|
+
}
|
|
982
|
+
const selections = MODEL_FAMILIES.map((family) => {
|
|
983
|
+
const candidateState = structuredClone(state);
|
|
984
|
+
candidateState.currentAccount = activeName;
|
|
985
|
+
if (activeName)
|
|
986
|
+
candidateState.authFailures[activeName] = Date.now() + 60000;
|
|
987
|
+
const selected = selectAccount(accounts, candidateState, `claude-${family}`);
|
|
988
|
+
return [
|
|
989
|
+
family,
|
|
990
|
+
selected.account.name === activeName ? null : selected.account.name
|
|
991
|
+
];
|
|
992
|
+
});
|
|
993
|
+
const names = new Set(selections.map(([_, name]) => name));
|
|
994
|
+
if (names.size === 1) {
|
|
995
|
+
return selections[0][1] ? `${selections[0][1]} (all models)` : "(none \u2014 no usable alternate account)";
|
|
996
|
+
}
|
|
997
|
+
return selections.map(([family, name]) => `${family}: ${name ?? "none"}`).join(", ");
|
|
998
|
+
}
|
|
707
999
|
function cmdStatus() {
|
|
708
1000
|
const data = loadAccounts();
|
|
709
1001
|
const state = loadState();
|
|
710
1002
|
resolveStaleMetrics(state);
|
|
1003
|
+
const thresholds = getThresholds(state.config);
|
|
1004
|
+
const now = Date.now();
|
|
1005
|
+
const configuredActive = data.accounts.find((account) => account.name === state.currentAccount);
|
|
1006
|
+
const healthByAccount = new Map(data.accounts.map((account) => [
|
|
1007
|
+
account.name,
|
|
1008
|
+
deriveAccountHealth(account, state.usage[account.name], thresholds, state.authFailures[account.name], now)
|
|
1009
|
+
]));
|
|
711
1010
|
console.log(`
|
|
712
1011
|
${BOLD}${CYAN}=== Auth Switcher Status ===${RESET}
|
|
713
1012
|
`);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1013
|
+
if (configuredActive) {
|
|
1014
|
+
console.log(` Active account: ${BOLD}${configuredActive.name}${RESET} \u2014 ${healthLabel(healthByAccount.get(configuredActive.name))}`);
|
|
1015
|
+
} else {
|
|
1016
|
+
console.log(` Active account: ${BOLD}(none)${RESET}`);
|
|
1017
|
+
}
|
|
1018
|
+
console.log(` Next if unavailable: ${BOLD}${nextAccountSummary(data.accounts, state, configuredActive?.name ?? null)}${RESET}`);
|
|
1019
|
+
console.log(` Accounts: ${data.accounts.length} Requests: ${state.requestCount}`);
|
|
717
1020
|
console.log();
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
console.log(
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1021
|
+
if (data.accounts.length === 0) {
|
|
1022
|
+
console.log(` ${DIM}No accounts configured. Run 'oc-auth-switcher add' to add one.${RESET}`);
|
|
1023
|
+
console.log();
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log(` ${BOLD}Account health${RESET}`);
|
|
1026
|
+
for (const account of data.accounts) {
|
|
1027
|
+
const health = healthByAccount.get(account.name);
|
|
1028
|
+
const active = account.name === configuredActive?.name ? ` ${GREEN}[ACTIVE]${RESET}` : "";
|
|
1029
|
+
console.log(` ${BOLD}${account.name}${RESET}${active} \u2014 ${healthLabel(health)}`);
|
|
1030
|
+
console.log(` Peak: ${utilizationLabel(health)}`);
|
|
1031
|
+
for (const reason of health.reasons) {
|
|
1032
|
+
console.log(` ${RED}Reason: ${reason.message}${RESET}`);
|
|
1033
|
+
}
|
|
1034
|
+
if (health.earliestReset) {
|
|
1035
|
+
const resetMs = health.earliestReset * 1000;
|
|
1036
|
+
console.log(` Reset: ${new Date(resetMs).toLocaleString()} (${formatRelativeDuration(resetMs, now)})`);
|
|
1037
|
+
}
|
|
1038
|
+
const tokenColor = health.reasons.some((reason) => reason.kind === "token") ? RED : DIM;
|
|
1039
|
+
console.log(` ${tokenColor}Token: ${health.tokenExpiry}${RESET}`);
|
|
724
1040
|
}
|
|
725
1041
|
console.log();
|
|
726
1042
|
}
|