@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/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 SLOT_CEILING_FACTOR = 2;
39
- var slotCeiling = (slot) => slot.weight * SLOT_CEILING_FACTOR;
40
- var MAX_SLOTS = 4;
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
- if (slots.some((s) => s.model === model)) return problems.push(`${at}: ${model} is already used by another slot; slots must be distinct models`);
81
- slots.push({ index: slots.length + 1, model, weight });
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, weight] = line.split(/\s+/);
95
- entries.push(weight === void 0 ? { model } : { model, weight });
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) return Math.min(seconds * 1e3, COOLDOWN_MAX_MS);
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)) return Math.min(Math.max(when - Date.now(), 1e3), COOLDOWN_MAX_MS);
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
- for (const [key, claim] of s.own) if (now - (claim.hb ?? claim.t) > CLAIM_TTL_MS) s.own.delete(key);
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 (now - (claim.hb ?? claim.t) <= CLAIM_TTL_MS) payload[key] = claim;
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" && now - (c.hb ?? c.t) <= CLAIM_TTL_MS
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 prior = s.strikes.get(model);
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 strike = { model, until: now + (stated ?? backoff), strikes, reason, last: now };
526
- s.cooling.set(model, strike);
527
- s.strikes.set(model, strike);
528
- const path = join2(s.dir, limitFile(model));
529
- try {
530
- writeFileSync(`${path}.tmp`, JSON.stringify(strike));
531
- renameSync(`${path}.tmp`, path);
532
- } catch {
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 = (m) => counts.set(m, (counts.get(m) ?? 0) + 1);
623
- for (const claim of s.own.values()) bump(claim.m);
624
- for (const sibling of siblings) for (const claim of sibling.claims) bump(claim.m);
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, model) => counts.get(model) ?? 0;
628
- var underCeiling = (slot, counts) => loadOf(counts, slot.model) < slotCeiling(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 usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
632
- const pool = usable.length > 0 ? usable : slots;
633
- for (const slot of usable) {
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.model) < slotCeiling(slot)) return slot;
692
+ if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
638
693
  }
639
- return pool.reduce((best, slot) => loadOf(snap.counts, slot.model) < loadOf(snap.counts, best.model) ? slot : best, pool[0]);
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.model) < slot.weight)) return 1;
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
- SLOT_CEILING_FACTOR,
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,