@lawaty/lacode 1.1.0 → 1.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/CHANGELOG.md +47 -1
- package/README.md +22 -9
- package/dist/agents.js +8 -3
- package/dist/commands.js +9 -5
- package/dist/index.js +1017 -94
- package/dist/lacode-voice.js +2351 -0
- package/dist/pool.js +73 -38
- package/dist/tui.js +1282 -0
- package/docs/CONFIGURATION.md +17 -7
- package/docs/USER-STORIES.md +11 -11
- package/package.json +12 -4
- package/presets/README.md +17 -19
- package/presets/free-tier.json +11 -13
- package/scripts/score-models.mjs +353 -0
package/dist/pool.js
CHANGED
|
@@ -35,9 +35,9 @@ var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
|
35
35
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
36
36
|
var UNAVAILABLE_REASON = "endpoint-unavailable";
|
|
37
37
|
var MAX_WEIGHT = 100;
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
var
|
|
38
|
+
var slotCeiling = (slot) => slot.weight;
|
|
39
|
+
var slotBucket = (slot) => slot.bucket ?? slot.model;
|
|
40
|
+
var SLOTS_WARN_AT = 64;
|
|
41
41
|
var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
42
42
|
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
43
43
|
var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
|
|
@@ -47,8 +47,12 @@ var STATED_COOLDOWN_MAX_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
|
47
47
|
var isQuotaReason = (reason) => /quota|limit|rate|too-many|overloaded|429/i.test(reason);
|
|
48
48
|
var MODEL_MISSING_REASON = "model-missing";
|
|
49
49
|
var MODEL_MISSING_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
50
|
+
var AUTH_FAILURE_REASON = "auth-failed";
|
|
51
|
+
var AUTH_FAILURE_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
50
52
|
var MODEL_MISSING_PATTERN = /model\s+(?:not\s+found|does\s+not\s+exist|is\s+not\s+available)|(?:unknown|no\s+such)\s+model/i;
|
|
51
53
|
var isModelMissingText = (text) => MODEL_MISSING_PATTERN.test(text);
|
|
54
|
+
var AUTH_FAILURE_PATTERN = /\b401\b|\b403\b|unauthori[sz]ed|not\s+authori[sz]ed|invalid[\s_-]+(?:api[\s_-]?(?:key|token)|token|credential|bearer)|api[\s_-]?key\s+(?:is\s+)?(?:invalid|missing|expired|not\s+valid)|authentication\s+(?:failed|error|required)|incorrect[\s_-]+api[\s_-]?key/i;
|
|
55
|
+
var isAuthFailureText = (text) => AUTH_FAILURE_PATTERN.test(text);
|
|
52
56
|
var BASES = ["explore", "implement", "context"];
|
|
53
57
|
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
54
58
|
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
@@ -71,15 +75,18 @@ function parseSlots(parsed, origin) {
|
|
|
71
75
|
entries.forEach((entry, i) => {
|
|
72
76
|
const model = typeof entry?.model === "string" ? entry.model.trim() : "";
|
|
73
77
|
const at = `slot ${i + 1}`;
|
|
74
|
-
if (slots.length >= MAX_SLOTS) {
|
|
75
|
-
problems.push(`${at} (${model || "?"}): the pool is capped at ${MAX_SLOTS} slots (one variant agent per base per slot is declared); entry ignored`);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
78
|
if (!model.includes("/")) return problems.push(`${at}: model must be written provider/model-id, got "${model}"`);
|
|
79
79
|
const weight = readWeight(entry, at, model, problems);
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const rawBucket = entry?.bucket;
|
|
81
|
+
const bucket = typeof rawBucket === "string" ? rawBucket.trim() : void 0;
|
|
82
|
+
if (rawBucket !== void 0 && !bucket) {
|
|
83
|
+
problems.push(`${at} (${model}): bucket must be a non-empty name; using the model id`);
|
|
84
|
+
}
|
|
85
|
+
slots.push({ index: slots.length + 1, model, weight, ...bucket ? { bucket } : {} });
|
|
82
86
|
});
|
|
87
|
+
if (slots.length > SLOTS_WARN_AT) {
|
|
88
|
+
problems.push(`${origin}: ${slots.length} slots is unusually large, but every entry is kept (the pool has no slot cap)`);
|
|
89
|
+
}
|
|
83
90
|
if (slots.length === 0) {
|
|
84
91
|
problems.push(`${origin} yielded no usable slot; using built-in defaults`);
|
|
85
92
|
return { slots: FALLBACK_SLOTS, problems };
|
|
@@ -91,8 +98,17 @@ function parseTextSlots(text, origin) {
|
|
|
91
98
|
for (const raw of text.split(/\r?\n/)) {
|
|
92
99
|
const line = raw.split("#")[0].trim();
|
|
93
100
|
if (!line) continue;
|
|
94
|
-
const [model,
|
|
95
|
-
|
|
101
|
+
const [model, ...rest] = line.split(/\s+/);
|
|
102
|
+
let weight;
|
|
103
|
+
let bucket;
|
|
104
|
+
for (const token of rest) {
|
|
105
|
+
if (token.startsWith("b:")) {
|
|
106
|
+
if (bucket === void 0) bucket = token.slice(2);
|
|
107
|
+
} else if (weight === void 0) {
|
|
108
|
+
weight = token;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
entries.push({ model, ...weight === void 0 ? {} : { weight }, ...bucket === void 0 ? {} : { bucket } });
|
|
96
112
|
}
|
|
97
113
|
return parseSlots({ slots: entries }, origin);
|
|
98
114
|
}
|
|
@@ -236,9 +252,17 @@ var retryAfterMs = (headers) => {
|
|
|
236
252
|
for (const [key, value] of Object.entries(headers)) {
|
|
237
253
|
if (key.toLowerCase() !== "retry-after") continue;
|
|
238
254
|
const seconds = Number(value);
|
|
239
|
-
if (Number.isFinite(seconds) && seconds > 0)
|
|
255
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
256
|
+
const ms = seconds * 1e3;
|
|
257
|
+
if (ms > STATED_COOLDOWN_MAX_MS) return void 0;
|
|
258
|
+
return ms;
|
|
259
|
+
}
|
|
240
260
|
const when = Date.parse(value);
|
|
241
|
-
if (Number.isFinite(when))
|
|
261
|
+
if (Number.isFinite(when)) {
|
|
262
|
+
const delay = when - Date.now();
|
|
263
|
+
if (delay > STATED_COOLDOWN_MAX_MS) return void 0;
|
|
264
|
+
return Math.max(delay, 1e3);
|
|
265
|
+
}
|
|
242
266
|
}
|
|
243
267
|
return void 0;
|
|
244
268
|
};
|
|
@@ -288,7 +312,7 @@ function ensureSlots(s, onWarn) {
|
|
|
288
312
|
for (const problem of problems) onWarn(problem);
|
|
289
313
|
return slots;
|
|
290
314
|
}
|
|
291
|
-
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}`).join("|")}`;
|
|
315
|
+
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.bucket ?? ""}`).join("|")}`;
|
|
292
316
|
function reloadSlots(s, onWarn) {
|
|
293
317
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
294
318
|
const changed = slotSignature(origin, slots) !== slotSignature(s.origin, s.slots);
|
|
@@ -466,7 +490,7 @@ function readSiblings(s) {
|
|
|
466
490
|
try {
|
|
467
491
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
468
492
|
const claims = Object.values(parsed).filter(
|
|
469
|
-
(c) => c && typeof c.v === "string" && typeof c.m === "string" && now - (c.hb ?? c.t) <= CLAIM_TTL_MS
|
|
493
|
+
(c) => c && typeof c.v === "string" && typeof c.m === "string" && typeof c.k === "number" && Number.isFinite(c.k) && now - (c.hb ?? c.t) <= CLAIM_TTL_MS
|
|
470
494
|
);
|
|
471
495
|
siblings.push({ id: match[1], claims });
|
|
472
496
|
} catch {
|
|
@@ -516,20 +540,32 @@ function recordStrike(s, model, reason, stated) {
|
|
|
516
540
|
if (!s.dirReady) return;
|
|
517
541
|
if (reason === UNAVAILABLE_REASON) return coolUnavailable(s, model);
|
|
518
542
|
const now = Date.now();
|
|
519
|
-
const
|
|
543
|
+
const slotsForModel = s.slots.filter((candidate) => candidate.model === model);
|
|
544
|
+
const buckets = [...new Set(slotsForModel.map((slot) => slotBucket(slot)))];
|
|
545
|
+
const targets = buckets.length === 0 ? [model] : [...new Set(
|
|
546
|
+
s.slots.filter((candidate) => buckets.includes(slotBucket(candidate))).map((candidate) => candidate.model)
|
|
547
|
+
)];
|
|
548
|
+
if (targets.length === 0) targets.push(model);
|
|
549
|
+
const prior = targets.map((target) => s.strikes.get(target)).filter((candidate) => Boolean(candidate)).reduce((worst, candidate) => !worst || candidate.strikes > worst.strikes ? candidate : worst, void 0);
|
|
520
550
|
const strikes = prior && now - prior.last < STRIKE_DECAY_MS ? prior.strikes + 1 : 1;
|
|
521
551
|
const quota = isQuotaReason(reason);
|
|
522
552
|
const base = quota ? QUOTA_COOLDOWN_BASE_MS : COOLDOWN_BASE_MS;
|
|
523
553
|
const cap = quota ? QUOTA_COOLDOWN_MAX_MS : COOLDOWN_MAX_MS;
|
|
524
554
|
const backoff = Math.min(cap, base * 2 ** (strikes - 1));
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
555
|
+
const statedOk = typeof stated === "number" && Number.isFinite(stated) && stated > 0 && stated <= STATED_COOLDOWN_MAX_MS;
|
|
556
|
+
const until = now + (statedOk ? stated : backoff);
|
|
557
|
+
let strike;
|
|
558
|
+
for (const target of targets) {
|
|
559
|
+
const record = { model: target, until, strikes, reason, last: now };
|
|
560
|
+
s.cooling.set(target, record);
|
|
561
|
+
s.strikes.set(target, record);
|
|
562
|
+
const path = join2(s.dir, limitFile(target));
|
|
563
|
+
try {
|
|
564
|
+
writeFileSync(`${path}.tmp`, JSON.stringify(record));
|
|
565
|
+
renameSync(`${path}.tmp`, path);
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
if (target === model) strike = record;
|
|
533
569
|
}
|
|
534
570
|
return strike;
|
|
535
571
|
}
|
|
@@ -619,30 +655,26 @@ function snapshot(s) {
|
|
|
619
655
|
const siblings = readSiblings(s);
|
|
620
656
|
const cooling = readLimits(s);
|
|
621
657
|
const counts = /* @__PURE__ */ new Map();
|
|
622
|
-
const bump = (
|
|
623
|
-
for (const claim of s.own.values()) bump(claim.
|
|
624
|
-
for (const sibling of siblings) for (const claim of sibling.claims) bump(claim.
|
|
658
|
+
const bump = (k) => counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
659
|
+
for (const claim of s.own.values()) bump(claim.k);
|
|
660
|
+
for (const sibling of siblings) for (const claim of sibling.claims) bump(claim.k);
|
|
625
661
|
return { counts, cooling, siblings, own: s.own, dirReady: s.dirReady };
|
|
626
662
|
}
|
|
627
|
-
var loadOf = (counts,
|
|
628
|
-
var underCeiling = (slot, counts) => loadOf(counts, slot.
|
|
663
|
+
var loadOf = (counts, slot) => counts.get(slot) ?? 0;
|
|
664
|
+
var underCeiling = (slot, counts) => loadOf(counts, slot.index) < slotCeiling(slot);
|
|
629
665
|
function pickSlot(s, snap) {
|
|
630
666
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
631
667
|
const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
632
668
|
const pool = usable.length > 0 ? usable : slots;
|
|
633
669
|
for (const slot of usable) {
|
|
634
|
-
if (loadOf(snap.counts, slot.
|
|
635
|
-
}
|
|
636
|
-
for (const slot of usable) {
|
|
637
|
-
if (loadOf(snap.counts, slot.model) < slotCeiling(slot)) return slot;
|
|
670
|
+
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
638
671
|
}
|
|
639
|
-
return pool.reduce((best, slot) => loadOf(snap.counts, slot.
|
|
672
|
+
return pool.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, pool[0]);
|
|
640
673
|
}
|
|
641
674
|
function poolPass(s, snap) {
|
|
642
675
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
643
676
|
const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
644
|
-
if (usable.some((slot) => loadOf(snap.counts, slot.
|
|
645
|
-
if (usable.some((slot) => loadOf(snap.counts, slot.model) < slotCeiling(slot))) return 2;
|
|
677
|
+
if (usable.some((slot) => loadOf(snap.counts, slot.index) < slot.weight)) return 1;
|
|
646
678
|
return "overflow";
|
|
647
679
|
}
|
|
648
680
|
function agentFor(base, slot) {
|
|
@@ -749,6 +781,8 @@ function coolProvider(s, provider, until, reason) {
|
|
|
749
781
|
return affected;
|
|
750
782
|
}
|
|
751
783
|
export {
|
|
784
|
+
AUTH_FAILURE_COOLDOWN_MS,
|
|
785
|
+
AUTH_FAILURE_REASON,
|
|
752
786
|
BASES,
|
|
753
787
|
CATALOG_TTL_MS,
|
|
754
788
|
CLAIM_TTL_MS,
|
|
@@ -757,7 +791,6 @@ export {
|
|
|
757
791
|
FALLBACK_SLOTS,
|
|
758
792
|
HANG_COOLDOWN_MS,
|
|
759
793
|
MAX_CLAIM_AGE_MS,
|
|
760
|
-
MAX_SLOTS,
|
|
761
794
|
MAX_WEIGHT,
|
|
762
795
|
MODEL_MISSING_COOLDOWN_MS,
|
|
763
796
|
MODEL_MISSING_REASON,
|
|
@@ -766,7 +799,7 @@ export {
|
|
|
766
799
|
PRESETS_DIR,
|
|
767
800
|
QUOTA_COOLDOWN_BASE_MS,
|
|
768
801
|
QUOTA_COOLDOWN_MAX_MS,
|
|
769
|
-
|
|
802
|
+
SLOTS_WARN_AT,
|
|
770
803
|
STATED_COOLDOWN_MAX_MS,
|
|
771
804
|
STATE_SWEEP_INTERVAL_MS,
|
|
772
805
|
STUCK_IDLE_MS,
|
|
@@ -790,6 +823,7 @@ export {
|
|
|
790
823
|
findStuck,
|
|
791
824
|
idleFor,
|
|
792
825
|
init,
|
|
826
|
+
isAuthFailureText,
|
|
793
827
|
isAvailable,
|
|
794
828
|
isLimitText,
|
|
795
829
|
isModelMissingText,
|
|
@@ -821,6 +855,7 @@ export {
|
|
|
821
855
|
resolveModels,
|
|
822
856
|
retryAfterMs,
|
|
823
857
|
selfFile,
|
|
858
|
+
slotBucket,
|
|
824
859
|
slotCeiling,
|
|
825
860
|
slotFor,
|
|
826
861
|
slotSources,
|