@lawaty/lacode 2.3.0-beta.5 → 2.3.0-beta.6
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 +25 -0
- package/README.md +13 -5
- package/dist/agents.js +1 -0
- package/dist/commands.js +6 -3
- package/dist/index.js +329 -111
- package/dist/lacode.js +1 -0
- package/dist/pool.js +149 -67
- package/dist/tunnel.js +1 -0
- package/docs/CONFIGURATION.md +15 -4
- package/docs/USER-STORIES.md +1 -1
- package/package.json +1 -1
- package/presets/README.md +35 -16
- package/presets/free-tier.json +17 -9
- package/scripts/pool-selection.mjs +161 -19
- package/scripts/refresh-pool.mjs +62 -20
- package/scripts/score-models.mjs +58 -21
package/dist/index.js
CHANGED
|
@@ -45,9 +45,10 @@ var PROVIDER_DEGRADED_MIN_MODELS = 2;
|
|
|
45
45
|
var TRANSIENT_DEGRADATION_REASONS = /^(?:no answer|no-answer|hang|timeout|timed out|endpoint-unavailable|provider-degraded|empty response|network|server)$/i;
|
|
46
46
|
var FLAT_OUTAGE_REASONS = /* @__PURE__ */ new Set([UNAVAILABLE_REASON, PROVIDER_DEGRADED_REASON]);
|
|
47
47
|
var MAX_WEIGHT = 100;
|
|
48
|
+
var MAX_CEILING = MAX_WEIGHT;
|
|
48
49
|
var MIN_SLOT_CONTEXT = 1e5;
|
|
49
50
|
var MIN_SLOT_TPM = 2e4;
|
|
50
|
-
var slotCeiling = (slot) => slot.weight;
|
|
51
|
+
var slotCeiling = (slot) => slot.ceiling ?? slot.weight;
|
|
51
52
|
var slotBucket = (slot) => slot.bucket ?? slot.model;
|
|
52
53
|
var NOTIFY_DEFAULT_ON_PROVIDERS = /* @__PURE__ */ new Set(["opencode", "opencode-go"]);
|
|
53
54
|
var notifyDefault = (model) => {
|
|
@@ -67,7 +68,7 @@ var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
|
|
|
67
68
|
var QUOTA_COOLDOWN_MAX_MS = 6 * 60 * 60 * 1e3;
|
|
68
69
|
var STRIKE_DECAY_MS = 30 * 60 * 1e3;
|
|
69
70
|
var STATED_COOLDOWN_MAX_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
70
|
-
var isQuotaReason = (reason) => /quota|limit|rate|too-many|overloaded|429/i.test(reason);
|
|
71
|
+
var isQuotaReason = (reason) => /quota|limit|rate|too-many|overloaded|budget|429/i.test(reason);
|
|
71
72
|
var MODEL_MISSING_REASON = "model-missing";
|
|
72
73
|
var MODEL_MISSING_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
73
74
|
var AUTH_FAILURE_REASON = "auth-failed";
|
|
@@ -76,6 +77,16 @@ var MODEL_MISSING_PATTERN = /model\s+(?:not\s+found|does\s+not\s+exist|is\s+not\
|
|
|
76
77
|
var isModelMissingText = (text) => MODEL_MISSING_PATTERN.test(text);
|
|
77
78
|
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;
|
|
78
79
|
var isAuthFailureText = (text) => AUTH_FAILURE_PATTERN.test(text);
|
|
80
|
+
var ABORT_PATTERN = /MessageAbortedError|"name"\s*:\s*"[^"]*Abort/i;
|
|
81
|
+
var isAbortText = (text) => ABORT_PATTERN.test(text);
|
|
82
|
+
var INVALID_REQUEST_REASON = "invalid-request";
|
|
83
|
+
var INVALID_REQUEST_COOLDOWN_MS = MODEL_MISSING_COOLDOWN_MS;
|
|
84
|
+
var UNSUPPORTED_PARAMETER_PATTERN = /not\s+a\s+supported\s+value|unsupported\s+(?:value|parameter|field)|(?:invalid|unknown)\s+(?:value|parameter)\s+for|is\s+not\s+supported/i;
|
|
85
|
+
var isUnsupportedParameterText = (text) => UNSUPPORTED_PARAMETER_PATTERN.test(text);
|
|
86
|
+
var TRANSIENT_UPSTREAM_REASON = "upstream-transient";
|
|
87
|
+
var TRANSIENT_UPSTREAM_COOLDOWN_MS = 60 * 1e3;
|
|
88
|
+
var TRANSIENT_UPSTREAM_PATTERN = /reasoning_display/i;
|
|
89
|
+
var isTransientUpstreamText = (text) => TRANSIENT_UPSTREAM_PATTERN.test(text);
|
|
79
90
|
var BASES = ["explore", "implement", "context"];
|
|
80
91
|
var LEARNED_TPM_FILE = "learned-tpm.json";
|
|
81
92
|
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
@@ -102,64 +113,100 @@ var coerceNotify = (raw, at, model, problems) => {
|
|
|
102
113
|
problems.push(`${at} (${model}): notify must be a boolean or one of on/off, got ${JSON.stringify(raw)}; keeping the provider default`);
|
|
103
114
|
return notifyDefault(model);
|
|
104
115
|
};
|
|
116
|
+
var FALLBACK_MARKER = /^(?:#\s*-*\s*fallback(?:\s+slots)?\s*-*|\[fallback\])$/i;
|
|
117
|
+
var isFallbackMarker = (line) => FALLBACK_MARKER.test(String(line).trim());
|
|
118
|
+
function parseSlotEntry(entry, at, problems) {
|
|
119
|
+
const model = typeof entry?.model === "string" ? entry.model.trim() : "";
|
|
120
|
+
if (!model.includes("/")) {
|
|
121
|
+
problems.push(`${at}: model must be written provider/model-id, got "${model}"`);
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
const weight = readWeight(entry, at, model, problems);
|
|
125
|
+
const ceiling = readCeiling(entry, weight, at, model, problems);
|
|
126
|
+
const rawBucket = entry?.bucket;
|
|
127
|
+
const bucket = typeof rawBucket === "string" ? rawBucket.trim() : void 0;
|
|
128
|
+
if (rawBucket !== void 0 && !bucket) {
|
|
129
|
+
problems.push(`${at} (${model}): bucket must be a non-empty name; using the model id`);
|
|
130
|
+
}
|
|
131
|
+
const notify = coerceNotify(entry.notify, at, model, problems);
|
|
132
|
+
const rawContext = entry?.context;
|
|
133
|
+
let context;
|
|
134
|
+
if (rawContext !== void 0) {
|
|
135
|
+
const parsedContext = typeof rawContext === "number" ? rawContext : Number(rawContext);
|
|
136
|
+
if (Number.isFinite(parsedContext) && parsedContext > 0) context = parsedContext;
|
|
137
|
+
else problems.push(`${at} (${model}): context must be a positive number, got ${JSON.stringify(rawContext)}; ignoring it`);
|
|
138
|
+
}
|
|
139
|
+
if (context !== void 0 && context < MIN_SLOT_CONTEXT) {
|
|
140
|
+
problems.push(`${at} (${model}): context ${context} is below the ${MIN_SLOT_CONTEXT}-token minimum; dropping the slot`);
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
const rawTpm = entry?.tpm;
|
|
144
|
+
let tpm;
|
|
145
|
+
if (rawTpm !== void 0) {
|
|
146
|
+
const parsedTpm = typeof rawTpm === "number" ? rawTpm : Number(rawTpm);
|
|
147
|
+
if (Number.isFinite(parsedTpm) && parsedTpm > 0) tpm = parsedTpm;
|
|
148
|
+
else problems.push(`${at} (${model}): tpm must be a positive number, got ${JSON.stringify(rawTpm)}; ignoring it`);
|
|
149
|
+
}
|
|
150
|
+
if (tpm !== void 0 && tpm < MIN_SLOT_TPM) {
|
|
151
|
+
problems.push(`${at} (${model}): tpm ${tpm} is below the ${MIN_SLOT_TPM}-token/minute minimum; dropping the slot`);
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
return { index: 0, model, weight, ...ceiling === void 0 ? {} : { ceiling }, ...bucket ? { bucket } : {}, notify, ...context === void 0 ? {} : { context }, ...tpm === void 0 ? {} : { tpm } };
|
|
155
|
+
}
|
|
105
156
|
function parseSlots(parsed, origin) {
|
|
106
157
|
const problems = [];
|
|
107
|
-
const
|
|
108
|
-
|
|
158
|
+
const primaryEntries = parsed?.slots;
|
|
159
|
+
const fallbackEntries = parsed?.fallbackSlots;
|
|
160
|
+
if ((!Array.isArray(primaryEntries) || primaryEntries.length === 0) && (!Array.isArray(fallbackEntries) || fallbackEntries.length === 0)) {
|
|
109
161
|
return { slots: FALLBACK_SLOTS, problems: [`${origin} has no non-empty "slots" array; using built-in defaults`] };
|
|
110
162
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
problems.push(`${at} (${model}): context ${context} is below the ${MIN_SLOT_CONTEXT}-token minimum; dropping the slot`);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
const rawTpm = entry?.tpm;
|
|
135
|
-
let tpm;
|
|
136
|
-
if (rawTpm !== void 0) {
|
|
137
|
-
const parsedTpm = typeof rawTpm === "number" ? rawTpm : Number(rawTpm);
|
|
138
|
-
if (Number.isFinite(parsedTpm) && parsedTpm > 0) tpm = parsedTpm;
|
|
139
|
-
else problems.push(`${at} (${model}): tpm must be a positive number, got ${JSON.stringify(rawTpm)}; ignoring it`);
|
|
140
|
-
}
|
|
141
|
-
if (tpm !== void 0 && tpm < MIN_SLOT_TPM) {
|
|
142
|
-
problems.push(`${at} (${model}): tpm ${tpm} is below the ${MIN_SLOT_TPM}-token/minute minimum; dropping the slot`);
|
|
143
|
-
return;
|
|
163
|
+
const parseList = (list, fallback2) => {
|
|
164
|
+
if (!Array.isArray(list)) return [];
|
|
165
|
+
const out = [];
|
|
166
|
+
list.forEach((entry, i) => {
|
|
167
|
+
const slot = parseSlotEntry(entry, `${fallback2 ? "fallback slot" : "slot"} ${i + 1}`, problems);
|
|
168
|
+
if (slot) out.push({ ...slot, ...fallback2 ? { fallback: true } : {} });
|
|
169
|
+
});
|
|
170
|
+
return out;
|
|
171
|
+
};
|
|
172
|
+
const primary = parseList(primaryEntries, false);
|
|
173
|
+
let fallback = parseList(fallbackEntries, true);
|
|
174
|
+
const primaryBuckets = new Set(primary.map((slot) => slotBucket(slot)));
|
|
175
|
+
const primaryModels = new Set(primary.map((slot) => slot.model));
|
|
176
|
+
fallback = fallback.filter((slot) => {
|
|
177
|
+
const shared = primaryModels.has(slot.model) || primaryBuckets.has(slotBucket(slot));
|
|
178
|
+
if (shared) {
|
|
179
|
+
problems.push(
|
|
180
|
+
`fallback slot (${slot.model}): shares a limit window with the primary pool (${slotBucket(slot)}); dropping it`
|
|
181
|
+
);
|
|
182
|
+
return false;
|
|
144
183
|
}
|
|
145
|
-
|
|
184
|
+
return true;
|
|
146
185
|
});
|
|
186
|
+
const slots = [...primary, ...fallback].map((slot, i) => ({ ...slot, index: i + 1 }));
|
|
147
187
|
if (slots.length > SLOTS_WARN_AT) {
|
|
148
188
|
problems.push(`${origin}: ${slots.length} slots is unusually large, but every entry is kept (the pool has no slot cap)`);
|
|
149
189
|
}
|
|
150
|
-
if (
|
|
151
|
-
problems.push(`${origin} yielded no usable slot; using built-in defaults`);
|
|
190
|
+
if (primary.length === 0) {
|
|
191
|
+
problems.push(`${origin} yielded no usable primary slot; using built-in defaults`);
|
|
152
192
|
return { slots: FALLBACK_SLOTS, problems };
|
|
153
193
|
}
|
|
154
194
|
return { slots, problems };
|
|
155
195
|
}
|
|
156
196
|
function parseTextSlots(text, origin) {
|
|
157
197
|
const entries = [];
|
|
198
|
+
const fallbackEntries = [];
|
|
199
|
+
let section = "primary";
|
|
158
200
|
for (const raw of text.split(/\r?\n/)) {
|
|
201
|
+
if (isFallbackMarker(raw)) {
|
|
202
|
+
section = "fallback";
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
159
205
|
const line = raw.split("#")[0].trim();
|
|
160
206
|
if (!line) continue;
|
|
161
207
|
const [model, ...rest] = line.split(/\s+/);
|
|
162
208
|
let weight;
|
|
209
|
+
let ceiling;
|
|
163
210
|
let bucket;
|
|
164
211
|
let notify;
|
|
165
212
|
let context;
|
|
@@ -169,6 +216,13 @@ function parseTextSlots(text, origin) {
|
|
|
169
216
|
if (notify === void 0) notify = token.slice("notify:".length);
|
|
170
217
|
continue;
|
|
171
218
|
}
|
|
219
|
+
if (token.startsWith("ceil:")) {
|
|
220
|
+
if (ceiling === void 0) {
|
|
221
|
+
const parsedCeiling = Number(token.slice("ceil:".length));
|
|
222
|
+
if (Number.isFinite(parsedCeiling) && parsedCeiling > 0) ceiling = parsedCeiling;
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
172
226
|
if (token.startsWith("c:")) {
|
|
173
227
|
if (context === void 0) {
|
|
174
228
|
const parsedContext = Number(token.slice(2));
|
|
@@ -189,9 +243,11 @@ function parseTextSlots(text, origin) {
|
|
|
189
243
|
weight = token;
|
|
190
244
|
}
|
|
191
245
|
}
|
|
192
|
-
|
|
246
|
+
const entry = { model, ...weight === void 0 ? {} : { weight }, ...ceiling === void 0 ? {} : { ceiling }, ...bucket === void 0 ? {} : { bucket }, ...notify === void 0 ? {} : { notify }, ...context === void 0 ? {} : { context }, ...tpm === void 0 ? {} : { tpm } };
|
|
247
|
+
if (section === "fallback") fallbackEntries.push(entry);
|
|
248
|
+
else entries.push(entry);
|
|
193
249
|
}
|
|
194
|
-
return parseSlots({ slots: entries }, origin);
|
|
250
|
+
return parseSlots({ slots: entries, ...fallbackEntries.length > 0 ? { fallbackSlots: fallbackEntries } : {} }, origin);
|
|
195
251
|
}
|
|
196
252
|
function parseSlotText(text, origin) {
|
|
197
253
|
if (text.trimStart().startsWith("{")) {
|
|
@@ -296,6 +352,20 @@ function readWeight(entry, at, model, problems) {
|
|
|
296
352
|
}
|
|
297
353
|
return weight;
|
|
298
354
|
}
|
|
355
|
+
function readCeiling(entry, weight, at, model, problems) {
|
|
356
|
+
const raw = entry?.ceiling;
|
|
357
|
+
if (raw === void 0) return void 0;
|
|
358
|
+
const ceiling = typeof raw === "number" ? raw : Number(raw);
|
|
359
|
+
if (!Number.isFinite(ceiling) || ceiling <= 0 || ceiling > MAX_CEILING) {
|
|
360
|
+
problems.push(`${at} (${model}): ceiling must be a number in (0, ${MAX_CEILING}], got ${JSON.stringify(raw)}; using the weight`);
|
|
361
|
+
return void 0;
|
|
362
|
+
}
|
|
363
|
+
if (ceiling < weight) {
|
|
364
|
+
problems.push(`${at} (${model}): ceiling ${ceiling} is below weight ${weight}; using the weight as the ceiling`);
|
|
365
|
+
return void 0;
|
|
366
|
+
}
|
|
367
|
+
return Math.floor(ceiling);
|
|
368
|
+
}
|
|
299
369
|
var UNAVAILABLE_PATTERN = /endpoint\s+is\s+unavailable/i;
|
|
300
370
|
var LIMIT_PATTERNS = [
|
|
301
371
|
/rate[\s_-]?limit/i,
|
|
@@ -303,6 +373,7 @@ var LIMIT_PATTERNS = [
|
|
|
303
373
|
/too many requests/i,
|
|
304
374
|
/\b429\b/,
|
|
305
375
|
/quota/i,
|
|
376
|
+
/budget\s+exhausted|api\s+budget/i,
|
|
306
377
|
/freelimiterror/i,
|
|
307
378
|
/overloaded/i,
|
|
308
379
|
UNAVAILABLE_PATTERN
|
|
@@ -346,7 +417,7 @@ function parseRateLimitHeaders(headers) {
|
|
|
346
417
|
}
|
|
347
418
|
return void 0;
|
|
348
419
|
}
|
|
349
|
-
var LIMIT_REASON_PATTERN = /rate[\s_-]?limit|usage limit|too many requests|quota|freelimiterror|overloaded|limits?[\s_-]?(?:reached|exceeded)/i;
|
|
420
|
+
var LIMIT_REASON_PATTERN = /rate[\s_-]?limit|usage limit|too many requests|quota|budget\s+exhausted|freelimiterror|overloaded|limits?[\s_-]?(?:reached|exceeded)/i;
|
|
350
421
|
var isLimitText = (text) => LIMIT_PATTERNS.some((re) => re.test(text)) || LIMIT_REACHED_PATTERN.test(text) && !CONTEXT_LIMIT_PATTERN.test(text);
|
|
351
422
|
var limitReason = (text) => {
|
|
352
423
|
if (!isLimitText(text)) return void 0;
|
|
@@ -436,7 +507,7 @@ function ensureSlots(s, onWarn) {
|
|
|
436
507
|
for (const problem of problems) onWarn(problem);
|
|
437
508
|
return slots;
|
|
438
509
|
}
|
|
439
|
-
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.bucket ?? ""}:${slotNotify(slot) ? "1" : "0"}:${slot.tpm ?? ""}`).join("|")}`;
|
|
510
|
+
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.ceiling ?? ""}:${slot.bucket ?? ""}:${slotNotify(slot) ? "1" : "0"}:${slot.tpm ?? ""}:${slot.fallback ? "f" : "p"}`).join("|")}`;
|
|
440
511
|
function reloadSlots(s, onWarn) {
|
|
441
512
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
442
513
|
const effective = applyLearnedTpm(s, slots, onWarn);
|
|
@@ -850,52 +921,54 @@ function snapshot(s) {
|
|
|
850
921
|
}
|
|
851
922
|
var loadOf = (counts, slot) => counts.get(slot) ?? 0;
|
|
852
923
|
var underCeiling = (slot, counts) => loadOf(counts, slot.index) < slotCeiling(slot);
|
|
924
|
+
var fillPassIndex = (slot, counts) => Math.floor(loadOf(counts, slot.index) / Math.max(1, slot.weight));
|
|
925
|
+
var pickByPass = (group, counts, prefer) => {
|
|
926
|
+
const open = group.filter((slot) => underCeiling(slot, counts));
|
|
927
|
+
if (open.length === 0) return void 0;
|
|
928
|
+
const lowest = Math.min(...open.map((slot) => fillPassIndex(slot, counts)));
|
|
929
|
+
const atPass = open.filter((slot) => fillPassIndex(slot, counts) === lowest);
|
|
930
|
+
return prefer ? atPass.reduce(prefer, atPass[0]) : atPass[0];
|
|
931
|
+
};
|
|
853
932
|
function pickSlot(s, snap, neededContext) {
|
|
854
933
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
855
|
-
const
|
|
934
|
+
const primary = slots.filter((slot) => !slot.fallback);
|
|
935
|
+
const fallback = slots.filter((slot) => slot.fallback);
|
|
936
|
+
const usablePrimary = primary.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
937
|
+
const usable = usablePrimary.length > 0 ? usablePrimary : fallback.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
856
938
|
if (usable.length === 0) return void 0;
|
|
857
939
|
const effective = (slot) => Math.min(slot.context ?? Number.POSITIVE_INFINITY, slot.tpm ?? Number.POSITIVE_INFINITY);
|
|
940
|
+
const leastLoaded = (best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best;
|
|
858
941
|
if (neededContext === void 0) {
|
|
859
|
-
|
|
860
|
-
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
861
|
-
}
|
|
862
|
-
return usable.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, usable[0]);
|
|
942
|
+
return pickByPass(usable, snap.counts) ?? usable.reduce(leastLoaded, usable[0]);
|
|
863
943
|
}
|
|
864
944
|
const adequate = usable.filter((slot) => effective(slot) >= neededContext);
|
|
865
945
|
const adequateKnown = adequate.filter((slot) => Number.isFinite(effective(slot)));
|
|
866
946
|
const candidates = adequate.length > 0 ? adequate : usable;
|
|
947
|
+
const smallest = (best, slot) => Number.isFinite(effective(slot)) && effective(slot) < effective(best) ? slot : best;
|
|
867
948
|
if (adequateKnown.length === 0) {
|
|
868
949
|
const known = usable.filter((slot) => Number.isFinite(effective(slot)));
|
|
869
950
|
if (known.length > 0) {
|
|
870
951
|
const largest = Math.max(...known.map((slot) => effective(slot)));
|
|
871
952
|
const largestSlots = known.filter((slot) => effective(slot) === largest);
|
|
872
|
-
|
|
873
|
-
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
874
|
-
}
|
|
875
|
-
return largestSlots.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, largestSlots[0]);
|
|
876
|
-
}
|
|
877
|
-
for (const slot of candidates) {
|
|
878
|
-
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
953
|
+
return pickByPass(largestSlots, snap.counts) ?? largestSlots.reduce(leastLoaded, largestSlots[0]);
|
|
879
954
|
}
|
|
880
|
-
return candidates
|
|
955
|
+
return pickByPass(candidates, snap.counts) ?? candidates.reduce(leastLoaded, candidates[0]);
|
|
881
956
|
}
|
|
882
|
-
|
|
883
|
-
if (underWeight.length > 0) {
|
|
884
|
-
const known = underWeight.filter((slot) => Number.isFinite(effective(slot)));
|
|
885
|
-
if (known.length === 0) return underWeight[0];
|
|
886
|
-
return known.reduce((best, slot) => effective(slot) < effective(best) ? slot : best, known[0]);
|
|
887
|
-
}
|
|
888
|
-
return candidates.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, candidates[0]);
|
|
957
|
+
return pickByPass(candidates, snap.counts, smallest) ?? candidates.reduce(leastLoaded, candidates[0]);
|
|
889
958
|
}
|
|
890
959
|
function poolPass(s, snap, neededContext) {
|
|
891
960
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
892
|
-
const
|
|
961
|
+
const primary = slots.filter((slot) => !slot.fallback);
|
|
962
|
+
const fallback = slots.filter((slot) => slot.fallback);
|
|
963
|
+
const usablePrimary = primary.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
964
|
+
const usable = usablePrimary.length > 0 ? usablePrimary : fallback.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
893
965
|
if (usable.length === 0) return "none";
|
|
894
966
|
const effective = (slot) => Math.min(slot.context ?? Number.POSITIVE_INFINITY, slot.tpm ?? Number.POSITIVE_INFINITY);
|
|
895
967
|
const candidates = neededContext === void 0 ? usable : usable.filter((slot) => effective(slot) >= neededContext);
|
|
896
968
|
const pool = candidates.length > 0 ? candidates : usable;
|
|
897
|
-
|
|
898
|
-
return "overflow";
|
|
969
|
+
const open = pool.filter((slot) => underCeiling(slot, snap.counts));
|
|
970
|
+
if (open.length === 0) return "overflow";
|
|
971
|
+
return Math.min(...open.map((slot) => fillPassIndex(slot, snap.counts))) + 1;
|
|
899
972
|
}
|
|
900
973
|
function agentFor(base, slot) {
|
|
901
974
|
return `${base}-${slot}`;
|
|
@@ -2167,7 +2240,7 @@ Follow the rules for the file in your system prompt, then report what you change
|
|
|
2167
2240
|
Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
|
|
2168
2241
|
|
|
2169
2242
|
1. Pool status \u2014 call the \`pool_status\` tool. Report the slot list, each
|
|
2170
|
-
slot's load against its
|
|
2243
|
+
slot's load against its ceiling, and which models
|
|
2171
2244
|
are cooling.
|
|
2172
2245
|
|
|
2173
2246
|
2. OmniRoute configured \u2014 inspect the \`@lawaty/lacode\` plugin options in the
|
|
@@ -2203,9 +2276,11 @@ The pool is ONE global slot table shared by every pooled base (explore, implemen
|
|
|
2203
2276
|
1. Establish the baseline. Read \`${USER_MODELS_TXT}\`. If it is absent, the pool is resolving from the bundled preset \`presets/free-tier.json\`; read that file to learn what is in effect. Call the \`pool_status\` tool only if you need live load or cooldown state.
|
|
2204
2277
|
|
|
2205
2278
|
2. Resolve the instruction above into a target slot table. These are the rules the loader enforces \u2014 never write a file that breaks one:
|
|
2206
|
-
- one slot per line: \`<provider>/<model-id> [weight]\`
|
|
2279
|
+
- one slot per line: \`<provider>/<model-id> [weight] [ceil:<max-parallel>] [c:<context>] [b:<bucket>]\`
|
|
2207
2280
|
- any number of slots; a model may appear on more than one line, and each line is its own slot with its own budget
|
|
2208
|
-
- weight is optional (default 1) and must be a number in (0, ${MAX_WEIGHT}]. It is the slot's
|
|
2281
|
+
- weight is optional (default 1) and must be a number in (0, ${MAX_WEIGHT}]. It is the slot's per-pass budget \u2014 how many concurrent sessions one pass may add. The pool's total capacity is the sum of the ceilings (which equals the sum of the weights when no ceiling is set). NOT a priority; pick order follows line order.
|
|
2282
|
+
- \`ceil:<max-parallel>\` is optional and is the slot's hard concurrency cap (its max parallel sessions). It must be at least the weight; later passes deepen a slot toward it after every slot has had its weight. Absent means the ceiling equals the weight (one pass).
|
|
2283
|
+
- a line that is exactly \`# --- fallback ---\` starts the fallback section. Keep it and the slots below it when rewriting the file: those slots are used only when no primary slot is usable, and each must not share a bucket or model with the primary.
|
|
2209
2284
|
- \`#\` starts a comment (whole-line or trailing); blank lines are ignored.
|
|
2210
2285
|
If the instruction names a model ambiguously, stop and ask rather than guessing.
|
|
2211
2286
|
|
|
@@ -3902,6 +3977,8 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3902
3977
|
const auth2 = isAuthFailureText(haystack2) || status2 === 401 || status2 === 403;
|
|
3903
3978
|
const limited2 = isLimitText(haystack2) || status2 === 429;
|
|
3904
3979
|
const outage = isUnavailableText(haystack2);
|
|
3980
|
+
const transientUpstream2 = status2 === 400 && isTransientUpstreamText(haystack2) && !auth2 && !limited2 && !outage;
|
|
3981
|
+
const unsupported2 = status2 === 400 && error?.data?.isRetryable === false && isUnsupportedParameterText(haystack2) && !transientUpstream2 && !auth2 && !limited2 && !outage;
|
|
3905
3982
|
if (missing2) {
|
|
3906
3983
|
const unbound = !own && sessionID ? [...s.own.entries()].find(([, claim]) => !claim.g && claim.s === sessionID && haystack2.includes(claim.m)) : void 0;
|
|
3907
3984
|
const target = own ?? (unbound ? { key: unbound[0], claim: unbound[1] } : void 0);
|
|
@@ -3927,6 +4004,30 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3927
4004
|
}
|
|
3928
4005
|
return;
|
|
3929
4006
|
}
|
|
4007
|
+
if (transientUpstream2) {
|
|
4008
|
+
if (own) {
|
|
4009
|
+
const transient = coolUnavailable(s, own.claim.m, TRANSIENT_UPSTREAM_COOLDOWN_MS, TRANSIENT_UPSTREAM_REASON);
|
|
4010
|
+
recordFailure(s, {
|
|
4011
|
+
ts: Date.now(),
|
|
4012
|
+
source: "observed",
|
|
4013
|
+
base: own.claim.v,
|
|
4014
|
+
slot: own.claim.k,
|
|
4015
|
+
model: own.claim.m,
|
|
4016
|
+
session: sessionID,
|
|
4017
|
+
reason: TRANSIENT_UPSTREAM_REASON,
|
|
4018
|
+
elapsedMs: Date.now() - own.claim.t,
|
|
4019
|
+
released: true,
|
|
4020
|
+
detail: haystack2.slice(0, 400)
|
|
4021
|
+
});
|
|
4022
|
+
release(s, own.key);
|
|
4023
|
+
await log(
|
|
4024
|
+
"warn",
|
|
4025
|
+
transient ? `cooling ${own.claim.m} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${TRANSIENT_UPSTREAM_REASON}) after a flaky upstream error` : `released ${own.claim.v} claim on ${own.claim.m} after a flaky upstream error`,
|
|
4026
|
+
{ base: own.claim.v, model: own.claim.m, slot: own.claim.k, session: sessionID }
|
|
4027
|
+
);
|
|
4028
|
+
}
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
3930
4031
|
const isTpm2 = isRequestTooLargeText(haystack2);
|
|
3931
4032
|
const statedLimit2 = isTpm2 ? parseStatedTokenLimit(haystack2) : void 0;
|
|
3932
4033
|
const headers2 = error?.data?.responseHeaders;
|
|
@@ -3953,23 +4054,40 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3953
4054
|
if (!limited2 && !outage && !auth2) {
|
|
3954
4055
|
if (own) {
|
|
3955
4056
|
const detail = haystack2.slice(0, 400);
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
4057
|
+
const aborted = isAbortText(haystack2);
|
|
4058
|
+
const reason2 = unsupported2 ? INVALID_REQUEST_REASON : "provider error";
|
|
4059
|
+
if (!aborted) {
|
|
4060
|
+
recordFailure(s, {
|
|
4061
|
+
ts: Date.now(),
|
|
4062
|
+
source: "observed",
|
|
4063
|
+
base: own.claim.v,
|
|
4064
|
+
slot: own.claim.k,
|
|
4065
|
+
model: own.claim.m,
|
|
4066
|
+
session: sessionID,
|
|
4067
|
+
reason: reason2,
|
|
4068
|
+
elapsedMs: Date.now() - own.claim.t,
|
|
4069
|
+
released: true,
|
|
4070
|
+
detail
|
|
4071
|
+
});
|
|
4072
|
+
if (!observedErrors.has(own.key)) {
|
|
4073
|
+
observedErrors.set(own.key, { reason: reason2, detail });
|
|
4074
|
+
if (observedErrors.size > 32) {
|
|
4075
|
+
const oldest = observedErrors.keys().next().value;
|
|
4076
|
+
if (oldest !== void 0) observedErrors.delete(oldest);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
const strike2 = recordStrike(
|
|
4080
|
+
s,
|
|
4081
|
+
own.claim.m,
|
|
4082
|
+
reason2,
|
|
4083
|
+
unsupported2 ? INVALID_REQUEST_COOLDOWN_MS : void 0
|
|
4084
|
+
);
|
|
4085
|
+
if (strike2) {
|
|
4086
|
+
await log(
|
|
4087
|
+
"warn",
|
|
4088
|
+
`bench ${own.claim.m} (${reason2}, strike ${strike2.strikes}, ${Math.round((strike2.until - Date.now()) / 1e3)}s) after a terminal session error`,
|
|
4089
|
+
{ model: own.claim.m, slot: own.claim.k, session: sessionID, reason: detail }
|
|
4090
|
+
);
|
|
3973
4091
|
}
|
|
3974
4092
|
}
|
|
3975
4093
|
release(s, own.key);
|
|
@@ -4094,6 +4212,8 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
4094
4212
|
const gateway5xx = status !== void 0 && status >= 500 && status <= 599;
|
|
4095
4213
|
const transientGateway = gateway5xx || isTransientGatewayText(haystack);
|
|
4096
4214
|
const missing = isModelMissingText(haystack);
|
|
4215
|
+
const transientUpstream = status === 400 && isTransientUpstreamText(haystack) && !auth && !limited && !transientGateway;
|
|
4216
|
+
const unsupported = status === 400 && info.error.data?.isRetryable === false && isUnsupportedParameterText(haystack) && !transientUpstream && !auth && !limited && !transientGateway;
|
|
4097
4217
|
const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
|
|
4098
4218
|
const ownMsg = claimFor(info.sessionID);
|
|
4099
4219
|
const isTpm = isRequestTooLargeText(haystack);
|
|
@@ -4120,24 +4240,36 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
4120
4240
|
if (!limited && !transientGateway && !auth && !missing) {
|
|
4121
4241
|
const ownMsgUnclassified = ownMsg;
|
|
4122
4242
|
if (ownMsgUnclassified) {
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
if (observedErrors.
|
|
4138
|
-
|
|
4139
|
-
if (
|
|
4243
|
+
const aborted = isAbortText(haystack);
|
|
4244
|
+
if (!aborted) {
|
|
4245
|
+
recordFailure(s, {
|
|
4246
|
+
ts: Date.now(),
|
|
4247
|
+
source: "observed",
|
|
4248
|
+
base: ownMsgUnclassified.claim.v,
|
|
4249
|
+
slot: ownMsgUnclassified.claim.k,
|
|
4250
|
+
model: ownMsgUnclassified.claim.m,
|
|
4251
|
+
session: info.sessionID,
|
|
4252
|
+
reason: "provider error",
|
|
4253
|
+
elapsedMs: Date.now() - ownMsgUnclassified.claim.t,
|
|
4254
|
+
released: true,
|
|
4255
|
+
detail: haystack.slice(0, 400)
|
|
4256
|
+
});
|
|
4257
|
+
if (!observedErrors.has(ownMsgUnclassified.key)) {
|
|
4258
|
+
observedErrors.set(ownMsgUnclassified.key, { reason: "provider error", detail: haystack.slice(0, 400) });
|
|
4259
|
+
if (observedErrors.size > 32) {
|
|
4260
|
+
const oldest = observedErrors.keys().next().value;
|
|
4261
|
+
if (oldest !== void 0) observedErrors.delete(oldest);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
const strike2 = recordStrike(s, ownMsgUnclassified.claim.m, "provider error");
|
|
4265
|
+
if (strike2) {
|
|
4266
|
+
await log(
|
|
4267
|
+
"warn",
|
|
4268
|
+
`bench ${ownMsgUnclassified.claim.m} (provider error, strike ${strike2.strikes}) after an unclassified terminal error`,
|
|
4269
|
+
{ model: ownMsgUnclassified.claim.m, slot: ownMsgUnclassified.claim.k, session: info.sessionID, reason: haystack.slice(0, 400) }
|
|
4270
|
+
);
|
|
4140
4271
|
}
|
|
4272
|
+
release(s, ownMsgUnclassified.key);
|
|
4141
4273
|
}
|
|
4142
4274
|
}
|
|
4143
4275
|
return;
|
|
@@ -4170,6 +4302,39 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
4170
4302
|
}
|
|
4171
4303
|
return;
|
|
4172
4304
|
}
|
|
4305
|
+
if (unsupported) {
|
|
4306
|
+
const ownUnsupported = ownMsg;
|
|
4307
|
+
if (ownUnsupported) {
|
|
4308
|
+
const strike2 = recordStrike(s, ownUnsupported.claim.m, INVALID_REQUEST_REASON, INVALID_REQUEST_COOLDOWN_MS);
|
|
4309
|
+
if (strike2) {
|
|
4310
|
+
await log(
|
|
4311
|
+
"warn",
|
|
4312
|
+
`bench ${ownUnsupported.claim.m} (${INVALID_REQUEST_REASON}, ${Math.round((strike2.until - Date.now()) / 1e3)}s) after a non-retryable unsupported-parameter error`,
|
|
4313
|
+
{ model: ownUnsupported.claim.m, slot: ownUnsupported.claim.k, session: info.sessionID, reason: haystack.slice(0, 400) }
|
|
4314
|
+
);
|
|
4315
|
+
}
|
|
4316
|
+
recordFailure(s, {
|
|
4317
|
+
ts: Date.now(),
|
|
4318
|
+
source: "observed",
|
|
4319
|
+
base: ownUnsupported.claim.v,
|
|
4320
|
+
slot: ownUnsupported.claim.k,
|
|
4321
|
+
model: ownUnsupported.claim.m,
|
|
4322
|
+
session: info.sessionID,
|
|
4323
|
+
reason: INVALID_REQUEST_REASON,
|
|
4324
|
+
elapsedMs: Date.now() - ownUnsupported.claim.t,
|
|
4325
|
+
released: true,
|
|
4326
|
+
detail: haystack.slice(0, 400)
|
|
4327
|
+
});
|
|
4328
|
+
release(s, ownUnsupported.key);
|
|
4329
|
+
await log("warn", `released ${ownUnsupported.claim.v} claim on ${ownUnsupported.claim.m} after an unsupported-parameter error`, {
|
|
4330
|
+
base: ownUnsupported.claim.v,
|
|
4331
|
+
model: ownUnsupported.claim.m,
|
|
4332
|
+
slot: ownUnsupported.claim.k,
|
|
4333
|
+
session: info.sessionID
|
|
4334
|
+
});
|
|
4335
|
+
}
|
|
4336
|
+
return;
|
|
4337
|
+
}
|
|
4173
4338
|
if (isUnavailableText(haystack) || !limited && !auth && transientGateway) {
|
|
4174
4339
|
const transient = coolUnavailable(s, model);
|
|
4175
4340
|
if (transient) await log("warn", `cooling ${model} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
|
|
@@ -4326,8 +4491,11 @@ ${detail}` : detail;
|
|
|
4326
4491
|
].filter(Boolean);
|
|
4327
4492
|
const marker = next && slot.index === next.index ? " <- next" : "";
|
|
4328
4493
|
const load = entry?.total ?? 0;
|
|
4494
|
+
const ceiling = slotCeiling(slot);
|
|
4329
4495
|
const atCeiling = !underCeiling(slot, snap.counts);
|
|
4330
|
-
|
|
4496
|
+
const cap = ceiling === slot.weight ? `w${slot.weight}` : `w${slot.weight} c${ceiling}`;
|
|
4497
|
+
const tier = slot.fallback ? " [fallback]" : "";
|
|
4498
|
+
return ` slot ${slot.index} ${cap} ${slot.model}${tier} load=${load}/${ceiling}${atCeiling ? " FULL" : ""}${marker} [${users}${notes.length ? "; " + notes.join("; ") : ""}]`;
|
|
4331
4499
|
};
|
|
4332
4500
|
const total = [...snap.counts.values()].reduce((sum, n) => sum + n, 0);
|
|
4333
4501
|
const coolingNow = [...snap.cooling.entries()].filter(([, st]) => st.until > now);
|
|
@@ -4336,7 +4504,7 @@ ${detail}` : detail;
|
|
|
4336
4504
|
for (const slot of slots) {
|
|
4337
4505
|
const name = slotBucket(slot);
|
|
4338
4506
|
const entry = buckets.get(name) ?? { seats: 0, slots: 0, cooling: false };
|
|
4339
|
-
entry.seats += slot
|
|
4507
|
+
entry.seats += slotCeiling(slot);
|
|
4340
4508
|
entry.slots++;
|
|
4341
4509
|
if (snap.cooling.has(slot.model)) entry.cooling = true;
|
|
4342
4510
|
buckets.set(name, entry);
|
|
@@ -4348,13 +4516,13 @@ ${detail}` : detail;
|
|
|
4348
4516
|
return {
|
|
4349
4517
|
title: `agent-pool: ${total} task(s) in flight, ${coolingNow.length} model(s) cooling`,
|
|
4350
4518
|
output: [
|
|
4351
|
-
`shared pool over ${slots.length} slot(s) from ${s.origin}; state ${pass}; bases: ${BASES.join(", ")}
|
|
4352
|
-
|
|
4519
|
+
`shared pool over ${slots.filter((slot) => !slot.fallback).length} primary slot(s) + ${slots.filter((slot) => slot.fallback).length} fallback slot(s) from ${s.origin}; state ${pass}; bases: ${BASES.join(", ")}
|
|
4520
|
+
ordered fill in declaration order: each pass fills every usable slot to its weight, then the next pass deepens a slot toward its ceiling (max parallel sessions). The fallback section is used only when no primary slot is usable. Capacity is the sum of the ceilings; weight alone is the per-pass budget (FULL = at ceiling).`,
|
|
4353
4521
|
...next ? [] : ["next: none - no routable slot; every slot is cooling, catalog-retired, or provider-unconfigured. A spawn will fail explicitly instead of routing."],
|
|
4354
4522
|
...sharedBuckets.length ? [`buckets: ${sharedBuckets.map(([name, entry]) => `${name} ${entry.seats} seat(s)${entry.cooling ? " COOLING" : ""}`).join("; ")}`] : [],
|
|
4355
4523
|
...degradedProviders.length ? [`provider degraded: ${degradedProviders.join(", ")} (flat window; a probe re-admits a model when it answers)`] : [],
|
|
4356
4524
|
...slots.map(line),
|
|
4357
|
-
...poolHealthLines(slots, now),
|
|
4525
|
+
...poolHealthLines(slots.filter((slot) => !slot.fallback), now),
|
|
4358
4526
|
"",
|
|
4359
4527
|
"by process:",
|
|
4360
4528
|
` ${s.id} (this process): ${snap.own.size} claim(s)`,
|
|
@@ -7355,6 +7523,43 @@ function normalizeChunk(value) {
|
|
|
7355
7523
|
}
|
|
7356
7524
|
return changed;
|
|
7357
7525
|
}
|
|
7526
|
+
var UPSTREAM_ATTEMPTS = 3;
|
|
7527
|
+
var UPSTREAM_RETRY_DELAY_MS = 250;
|
|
7528
|
+
var TRANSIENT_UPSTREAM_PATTERN2 = /reasoning_display/i;
|
|
7529
|
+
function requestUrl(input) {
|
|
7530
|
+
if (typeof input === "string") return input;
|
|
7531
|
+
if (input instanceof URL) return input.toString();
|
|
7532
|
+
if (input && typeof input === "object") {
|
|
7533
|
+
const url = input.url;
|
|
7534
|
+
if (typeof url === "string") return url;
|
|
7535
|
+
}
|
|
7536
|
+
return "";
|
|
7537
|
+
}
|
|
7538
|
+
function isOpencodeInferenceChat(raw) {
|
|
7539
|
+
if (!raw) return false;
|
|
7540
|
+
let url;
|
|
7541
|
+
try {
|
|
7542
|
+
url = new URL(raw);
|
|
7543
|
+
} catch {
|
|
7544
|
+
return false;
|
|
7545
|
+
}
|
|
7546
|
+
if (url.hostname !== "opencode.ai" && !url.hostname.endsWith(".opencode.ai")) return false;
|
|
7547
|
+
return url.pathname.includes("/inference/") && url.pathname.endsWith("/chat/completions");
|
|
7548
|
+
}
|
|
7549
|
+
function isReplayableInit(init2) {
|
|
7550
|
+
if (!init2 || typeof init2 !== "object") return false;
|
|
7551
|
+
const body = init2.body;
|
|
7552
|
+
return typeof body === "string" || body instanceof Uint8Array || body instanceof ArrayBuffer;
|
|
7553
|
+
}
|
|
7554
|
+
async function isTransientUpstreamResponse(response) {
|
|
7555
|
+
if (response.status !== 400) return false;
|
|
7556
|
+
try {
|
|
7557
|
+
const text = await response.clone().text();
|
|
7558
|
+
return TRANSIENT_UPSTREAM_PATTERN2.test(text) && /not\s+a\s+supported\s+value|unsupported/i.test(text);
|
|
7559
|
+
} catch {
|
|
7560
|
+
return false;
|
|
7561
|
+
}
|
|
7562
|
+
}
|
|
7358
7563
|
function normalizeSseLine(line) {
|
|
7359
7564
|
const trimmed = line.trim();
|
|
7360
7565
|
if (!trimmed.startsWith("data:")) return line;
|
|
@@ -7391,9 +7596,22 @@ function createChunkNormalizer() {
|
|
|
7391
7596
|
}
|
|
7392
7597
|
});
|
|
7393
7598
|
}
|
|
7394
|
-
|
|
7599
|
+
var sleep = (ms) => ms > 0 ? new Promise((resolve5) => setTimeout(resolve5, ms)) : Promise.resolve();
|
|
7600
|
+
function wrapFetch(original, options = {}) {
|
|
7601
|
+
const attempts = Math.max(1, Math.trunc(options.attempts ?? UPSTREAM_ATTEMPTS));
|
|
7602
|
+
const delayMs = Math.max(0, Math.trunc(options.delayMs ?? UPSTREAM_RETRY_DELAY_MS));
|
|
7395
7603
|
const wrapped = async (input, init2) => {
|
|
7396
|
-
const
|
|
7604
|
+
const retryable = attempts > 1 && isReplayableInit(init2) && isOpencodeInferenceChat(requestUrl(input));
|
|
7605
|
+
let response = await original(input, init2);
|
|
7606
|
+
for (let attempt = 1; retryable && attempt < attempts; attempt++) {
|
|
7607
|
+
if (!await isTransientUpstreamResponse(response)) break;
|
|
7608
|
+
try {
|
|
7609
|
+
await response.body?.cancel();
|
|
7610
|
+
} catch {
|
|
7611
|
+
}
|
|
7612
|
+
await sleep(delayMs);
|
|
7613
|
+
response = await original(input, init2);
|
|
7614
|
+
}
|
|
7397
7615
|
try {
|
|
7398
7616
|
const contentType = response.headers.get("content-type") ?? "";
|
|
7399
7617
|
if (!contentType.includes("text/event-stream") || !response.body) return response;
|