@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/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 SLOT_CEILING_FACTOR = 2;
39
- var slotCeiling = (slot) => slot.weight * SLOT_CEILING_FACTOR;
40
- var MAX_SLOTS = 4;
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
- 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 });
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, weight] = line.split(/\s+/);
95
- entries.push(weight === void 0 ? { model } : { model, weight });
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) return Math.min(seconds * 1e3, COOLDOWN_MAX_MS);
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)) return Math.min(Math.max(when - Date.now(), 1e3), COOLDOWN_MAX_MS);
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 prior = s.strikes.get(model);
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 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 {
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 = (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);
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, model) => counts.get(model) ?? 0;
628
- var underCeiling = (slot, counts) => loadOf(counts, slot.model) < slotCeiling(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.model) < slot.weight) return 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.model) < loadOf(snap.counts, best.model) ? slot : best, pool[0]);
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.model) < slot.weight)) return 1;
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
- SLOT_CEILING_FACTOR,
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,