@lawaty/lacode 1.1.0 → 1.3.0-beta.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 +87 -1
- package/README.md +25 -12
- package/dist/agents.js +10 -3
- package/dist/commands.js +11 -5
- package/dist/index.js +1254 -99
- package/dist/lacode-voice.js +2351 -0
- package/dist/lacode.js +2 -0
- package/dist/pool.js +102 -42
- package/dist/tui.js +1282 -0
- package/dist/tunnel.js +2 -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/lacode.js
CHANGED
|
@@ -43,9 +43,11 @@ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
|
43
43
|
|
|
44
44
|
// src/lib/pool.ts
|
|
45
45
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
46
|
+
var UNBOUND_CLAIM_MS = 2 * 60 * 1e3;
|
|
46
47
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
47
48
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
48
49
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
50
|
+
var PROVIDER_LIST_TTL_MS = 60 * 1e3;
|
|
49
51
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
50
52
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
51
53
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
package/dist/pool.js
CHANGED
|
@@ -25,9 +25,11 @@ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
|
25
25
|
|
|
26
26
|
// src/lib/pool.ts
|
|
27
27
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
28
|
+
var UNBOUND_CLAIM_MS = 2 * 60 * 1e3;
|
|
28
29
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
29
30
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
30
31
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
32
|
+
var PROVIDER_LIST_TTL_MS = 60 * 1e3;
|
|
31
33
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
32
34
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
33
35
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
@@ -35,9 +37,9 @@ var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
|
35
37
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
36
38
|
var UNAVAILABLE_REASON = "endpoint-unavailable";
|
|
37
39
|
var MAX_WEIGHT = 100;
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
var
|
|
40
|
+
var slotCeiling = (slot) => slot.weight;
|
|
41
|
+
var slotBucket = (slot) => slot.bucket ?? slot.model;
|
|
42
|
+
var SLOTS_WARN_AT = 64;
|
|
41
43
|
var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
42
44
|
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
43
45
|
var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
|
|
@@ -47,8 +49,12 @@ var STATED_COOLDOWN_MAX_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
|
47
49
|
var isQuotaReason = (reason) => /quota|limit|rate|too-many|overloaded|429/i.test(reason);
|
|
48
50
|
var MODEL_MISSING_REASON = "model-missing";
|
|
49
51
|
var MODEL_MISSING_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
52
|
+
var AUTH_FAILURE_REASON = "auth-failed";
|
|
53
|
+
var AUTH_FAILURE_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
50
54
|
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
55
|
var isModelMissingText = (text) => MODEL_MISSING_PATTERN.test(text);
|
|
56
|
+
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;
|
|
57
|
+
var isAuthFailureText = (text) => AUTH_FAILURE_PATTERN.test(text);
|
|
52
58
|
var BASES = ["explore", "implement", "context"];
|
|
53
59
|
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
54
60
|
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
@@ -71,15 +77,18 @@ function parseSlots(parsed, origin) {
|
|
|
71
77
|
entries.forEach((entry, i) => {
|
|
72
78
|
const model = typeof entry?.model === "string" ? entry.model.trim() : "";
|
|
73
79
|
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
80
|
if (!model.includes("/")) return problems.push(`${at}: model must be written provider/model-id, got "${model}"`);
|
|
79
81
|
const weight = readWeight(entry, at, model, problems);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
const rawBucket = entry?.bucket;
|
|
83
|
+
const bucket = typeof rawBucket === "string" ? rawBucket.trim() : void 0;
|
|
84
|
+
if (rawBucket !== void 0 && !bucket) {
|
|
85
|
+
problems.push(`${at} (${model}): bucket must be a non-empty name; using the model id`);
|
|
86
|
+
}
|
|
87
|
+
slots.push({ index: slots.length + 1, model, weight, ...bucket ? { bucket } : {} });
|
|
82
88
|
});
|
|
89
|
+
if (slots.length > SLOTS_WARN_AT) {
|
|
90
|
+
problems.push(`${origin}: ${slots.length} slots is unusually large, but every entry is kept (the pool has no slot cap)`);
|
|
91
|
+
}
|
|
83
92
|
if (slots.length === 0) {
|
|
84
93
|
problems.push(`${origin} yielded no usable slot; using built-in defaults`);
|
|
85
94
|
return { slots: FALLBACK_SLOTS, problems };
|
|
@@ -91,8 +100,17 @@ function parseTextSlots(text, origin) {
|
|
|
91
100
|
for (const raw of text.split(/\r?\n/)) {
|
|
92
101
|
const line = raw.split("#")[0].trim();
|
|
93
102
|
if (!line) continue;
|
|
94
|
-
const [model,
|
|
95
|
-
|
|
103
|
+
const [model, ...rest] = line.split(/\s+/);
|
|
104
|
+
let weight;
|
|
105
|
+
let bucket;
|
|
106
|
+
for (const token of rest) {
|
|
107
|
+
if (token.startsWith("b:")) {
|
|
108
|
+
if (bucket === void 0) bucket = token.slice(2);
|
|
109
|
+
} else if (weight === void 0) {
|
|
110
|
+
weight = token;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
entries.push({ model, ...weight === void 0 ? {} : { weight }, ...bucket === void 0 ? {} : { bucket } });
|
|
96
114
|
}
|
|
97
115
|
return parseSlots({ slots: entries }, origin);
|
|
98
116
|
}
|
|
@@ -236,9 +254,17 @@ var retryAfterMs = (headers) => {
|
|
|
236
254
|
for (const [key, value] of Object.entries(headers)) {
|
|
237
255
|
if (key.toLowerCase() !== "retry-after") continue;
|
|
238
256
|
const seconds = Number(value);
|
|
239
|
-
if (Number.isFinite(seconds) && seconds > 0)
|
|
257
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
258
|
+
const ms = seconds * 1e3;
|
|
259
|
+
if (ms > STATED_COOLDOWN_MAX_MS) return void 0;
|
|
260
|
+
return ms;
|
|
261
|
+
}
|
|
240
262
|
const when = Date.parse(value);
|
|
241
|
-
if (Number.isFinite(when))
|
|
263
|
+
if (Number.isFinite(when)) {
|
|
264
|
+
const delay = when - Date.now();
|
|
265
|
+
if (delay > STATED_COOLDOWN_MAX_MS) return void 0;
|
|
266
|
+
return Math.max(delay, 1e3);
|
|
267
|
+
}
|
|
242
268
|
}
|
|
243
269
|
return void 0;
|
|
244
270
|
};
|
|
@@ -270,6 +296,11 @@ function state(opts = {}) {
|
|
|
270
296
|
reaping: false,
|
|
271
297
|
lastSweep: 0,
|
|
272
298
|
lastDirSweep: 0,
|
|
299
|
+
connected: /* @__PURE__ */ new Set(),
|
|
300
|
+
connectedAt: 0,
|
|
301
|
+
connectedReady: false,
|
|
302
|
+
credentialed: /* @__PURE__ */ new Set(),
|
|
303
|
+
configCreds: /* @__PURE__ */ new Set(),
|
|
273
304
|
id,
|
|
274
305
|
dir,
|
|
275
306
|
modelsFile,
|
|
@@ -288,7 +319,7 @@ function ensureSlots(s, onWarn) {
|
|
|
288
319
|
for (const problem of problems) onWarn(problem);
|
|
289
320
|
return slots;
|
|
290
321
|
}
|
|
291
|
-
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}`).join("|")}`;
|
|
322
|
+
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.bucket ?? ""}`).join("|")}`;
|
|
292
323
|
function reloadSlots(s, onWarn) {
|
|
293
324
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
294
325
|
const changed = slotSignature(origin, slots) !== slotSignature(s.origin, s.slots);
|
|
@@ -377,9 +408,16 @@ function sweepStateDir(s) {
|
|
|
377
408
|
}
|
|
378
409
|
return removed;
|
|
379
410
|
}
|
|
411
|
+
var claimExpired = (claim, now) => now - (claim.hb ?? claim.t) > CLAIM_TTL_MS || !claim.g && now - claim.t > UNBOUND_CLAIM_MS;
|
|
380
412
|
function pruneOwn(s) {
|
|
381
413
|
const now = Date.now();
|
|
382
|
-
|
|
414
|
+
let removed = false;
|
|
415
|
+
for (const [key, claim] of s.own)
|
|
416
|
+
if (claimExpired(claim, now)) {
|
|
417
|
+
s.own.delete(key);
|
|
418
|
+
removed = true;
|
|
419
|
+
}
|
|
420
|
+
if (removed) publish(s);
|
|
383
421
|
}
|
|
384
422
|
function pruneActivity(s, now) {
|
|
385
423
|
if (s.activity.size === 0) return;
|
|
@@ -407,7 +445,7 @@ function publish(s) {
|
|
|
407
445
|
if (!s.dirReady) return;
|
|
408
446
|
const now = Date.now();
|
|
409
447
|
const payload = {};
|
|
410
|
-
for (const [key, claim] of s.own) if (
|
|
448
|
+
for (const [key, claim] of s.own) if (!claimExpired(claim, now)) payload[key] = claim;
|
|
411
449
|
try {
|
|
412
450
|
writeFileSync(join2(s.dir, `${selfFile(s.id)}.tmp`), JSON.stringify(payload));
|
|
413
451
|
renameSync(join2(s.dir, `${selfFile(s.id)}.tmp`), join2(s.dir, selfFile(s.id)));
|
|
@@ -466,7 +504,7 @@ function readSiblings(s) {
|
|
|
466
504
|
try {
|
|
467
505
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
468
506
|
const claims = Object.values(parsed).filter(
|
|
469
|
-
(c) => c && typeof c.v === "string" && typeof c.m === "string" &&
|
|
507
|
+
(c) => c && typeof c.v === "string" && typeof c.m === "string" && typeof c.k === "number" && Number.isFinite(c.k) && !claimExpired(c, now)
|
|
470
508
|
);
|
|
471
509
|
siblings.push({ id: match[1], claims });
|
|
472
510
|
} catch {
|
|
@@ -516,20 +554,32 @@ function recordStrike(s, model, reason, stated) {
|
|
|
516
554
|
if (!s.dirReady) return;
|
|
517
555
|
if (reason === UNAVAILABLE_REASON) return coolUnavailable(s, model);
|
|
518
556
|
const now = Date.now();
|
|
519
|
-
const
|
|
557
|
+
const slotsForModel = s.slots.filter((candidate) => candidate.model === model);
|
|
558
|
+
const buckets = [...new Set(slotsForModel.map((slot) => slotBucket(slot)))];
|
|
559
|
+
const targets = buckets.length === 0 ? [model] : [...new Set(
|
|
560
|
+
s.slots.filter((candidate) => buckets.includes(slotBucket(candidate))).map((candidate) => candidate.model)
|
|
561
|
+
)];
|
|
562
|
+
if (targets.length === 0) targets.push(model);
|
|
563
|
+
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
564
|
const strikes = prior && now - prior.last < STRIKE_DECAY_MS ? prior.strikes + 1 : 1;
|
|
521
565
|
const quota = isQuotaReason(reason);
|
|
522
566
|
const base = quota ? QUOTA_COOLDOWN_BASE_MS : COOLDOWN_BASE_MS;
|
|
523
567
|
const cap = quota ? QUOTA_COOLDOWN_MAX_MS : COOLDOWN_MAX_MS;
|
|
524
568
|
const backoff = Math.min(cap, base * 2 ** (strikes - 1));
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
569
|
+
const statedOk = typeof stated === "number" && Number.isFinite(stated) && stated > 0 && stated <= STATED_COOLDOWN_MAX_MS;
|
|
570
|
+
const until = now + (statedOk ? stated : backoff);
|
|
571
|
+
let strike;
|
|
572
|
+
for (const target of targets) {
|
|
573
|
+
const record = { model: target, until, strikes, reason, last: now };
|
|
574
|
+
s.cooling.set(target, record);
|
|
575
|
+
s.strikes.set(target, record);
|
|
576
|
+
const path = join2(s.dir, limitFile(target));
|
|
577
|
+
try {
|
|
578
|
+
writeFileSync(`${path}.tmp`, JSON.stringify(record));
|
|
579
|
+
renameSync(`${path}.tmp`, path);
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
if (target === model) strike = record;
|
|
533
583
|
}
|
|
534
584
|
return strike;
|
|
535
585
|
}
|
|
@@ -593,6 +643,12 @@ function clearCooldowns(s, models) {
|
|
|
593
643
|
var markProbe = (s, sessionID) => s.probes.add(sessionID);
|
|
594
644
|
var unmarkProbe = (s, sessionID) => s.probes.delete(sessionID);
|
|
595
645
|
var isProbe = (s, sessionID) => s.probes.has(sessionID);
|
|
646
|
+
function providerReady(s, model) {
|
|
647
|
+
if (!s.connectedReady) return true;
|
|
648
|
+
const provider = providerOf(model);
|
|
649
|
+
if (!s.connected.has(provider)) return false;
|
|
650
|
+
return s.credentialed.has(provider) || s.configCreds.has(provider) || provider === "opencode";
|
|
651
|
+
}
|
|
596
652
|
function refreshCatalog(s, serverUrl) {
|
|
597
653
|
if (!serverUrl || Date.now() - s.catalogAt < CATALOG_TTL_MS) return;
|
|
598
654
|
s.catalogAt = Date.now();
|
|
@@ -609,6 +665,7 @@ function refreshCatalog(s, serverUrl) {
|
|
|
609
665
|
})();
|
|
610
666
|
}
|
|
611
667
|
function isAvailable(s, model, cooling) {
|
|
668
|
+
if (!providerReady(s, model)) return false;
|
|
612
669
|
if (cooling.has(model)) return false;
|
|
613
670
|
const status = s.catalog.get(model);
|
|
614
671
|
return status === void 0 || status === "active" || status === "unknown";
|
|
@@ -619,30 +676,27 @@ function snapshot(s) {
|
|
|
619
676
|
const siblings = readSiblings(s);
|
|
620
677
|
const cooling = readLimits(s);
|
|
621
678
|
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.
|
|
679
|
+
const bump = (k) => counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
680
|
+
for (const claim of s.own.values()) bump(claim.k);
|
|
681
|
+
for (const sibling of siblings) for (const claim of sibling.claims) bump(claim.k);
|
|
625
682
|
return { counts, cooling, siblings, own: s.own, dirReady: s.dirReady };
|
|
626
683
|
}
|
|
627
|
-
var loadOf = (counts,
|
|
628
|
-
var underCeiling = (slot, counts) => loadOf(counts, slot.
|
|
684
|
+
var loadOf = (counts, slot) => counts.get(slot) ?? 0;
|
|
685
|
+
var underCeiling = (slot, counts) => loadOf(counts, slot.index) < slotCeiling(slot);
|
|
629
686
|
function pickSlot(s, snap) {
|
|
630
687
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
631
|
-
const
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
if (loadOf(snap.counts, slot.model) < slot.weight) return slot;
|
|
635
|
-
}
|
|
688
|
+
const configured = slots.filter((slot) => providerReady(s, slot.model));
|
|
689
|
+
const usable = configured.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
690
|
+
const pool = usable.length > 0 ? usable : configured.length > 0 ? configured : slots;
|
|
636
691
|
for (const slot of usable) {
|
|
637
|
-
if (loadOf(snap.counts, slot.
|
|
692
|
+
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
638
693
|
}
|
|
639
|
-
return pool.reduce((best, slot) => loadOf(snap.counts, slot.
|
|
694
|
+
return pool.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, pool[0]);
|
|
640
695
|
}
|
|
641
696
|
function poolPass(s, snap) {
|
|
642
697
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
643
698
|
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;
|
|
699
|
+
if (usable.some((slot) => loadOf(snap.counts, slot.index) < slot.weight)) return 1;
|
|
646
700
|
return "overflow";
|
|
647
701
|
}
|
|
648
702
|
function agentFor(base, slot) {
|
|
@@ -749,6 +803,8 @@ function coolProvider(s, provider, until, reason) {
|
|
|
749
803
|
return affected;
|
|
750
804
|
}
|
|
751
805
|
export {
|
|
806
|
+
AUTH_FAILURE_COOLDOWN_MS,
|
|
807
|
+
AUTH_FAILURE_REASON,
|
|
752
808
|
BASES,
|
|
753
809
|
CATALOG_TTL_MS,
|
|
754
810
|
CLAIM_TTL_MS,
|
|
@@ -757,22 +813,23 @@ export {
|
|
|
757
813
|
FALLBACK_SLOTS,
|
|
758
814
|
HANG_COOLDOWN_MS,
|
|
759
815
|
MAX_CLAIM_AGE_MS,
|
|
760
|
-
MAX_SLOTS,
|
|
761
816
|
MAX_WEIGHT,
|
|
762
817
|
MODEL_MISSING_COOLDOWN_MS,
|
|
763
818
|
MODEL_MISSING_REASON,
|
|
764
819
|
ORPHAN_TMP_MS,
|
|
765
820
|
PACKAGE_ROOT,
|
|
766
821
|
PRESETS_DIR,
|
|
822
|
+
PROVIDER_LIST_TTL_MS,
|
|
767
823
|
QUOTA_COOLDOWN_BASE_MS,
|
|
768
824
|
QUOTA_COOLDOWN_MAX_MS,
|
|
769
|
-
|
|
825
|
+
SLOTS_WARN_AT,
|
|
770
826
|
STATED_COOLDOWN_MAX_MS,
|
|
771
827
|
STATE_SWEEP_INTERVAL_MS,
|
|
772
828
|
STUCK_IDLE_MS,
|
|
773
829
|
STUCK_MIN_AGE_MS,
|
|
774
830
|
UNAVAILABLE_COOLDOWN_MS,
|
|
775
831
|
UNAVAILABLE_REASON,
|
|
832
|
+
UNBOUND_CLAIM_MS,
|
|
776
833
|
USER_CONFIG_DIR,
|
|
777
834
|
USER_MODELS_PATH,
|
|
778
835
|
USER_MODELS_TXT,
|
|
@@ -790,6 +847,7 @@ export {
|
|
|
790
847
|
findStuck,
|
|
791
848
|
idleFor,
|
|
792
849
|
init,
|
|
850
|
+
isAuthFailureText,
|
|
793
851
|
isAvailable,
|
|
794
852
|
isLimitText,
|
|
795
853
|
isModelMissingText,
|
|
@@ -807,6 +865,7 @@ export {
|
|
|
807
865
|
pickSlot,
|
|
808
866
|
poolPass,
|
|
809
867
|
providerOf,
|
|
868
|
+
providerReady,
|
|
810
869
|
pruneOwn,
|
|
811
870
|
pruneSessions,
|
|
812
871
|
publish,
|
|
@@ -821,6 +880,7 @@ export {
|
|
|
821
880
|
resolveModels,
|
|
822
881
|
retryAfterMs,
|
|
823
882
|
selfFile,
|
|
883
|
+
slotBucket,
|
|
824
884
|
slotCeiling,
|
|
825
885
|
slotFor,
|
|
826
886
|
slotSources,
|