@robzilla1738/agentswarm 0.3.0 → 0.6.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/README.md +51 -11
- package/dist/agent.js +18 -2
- package/dist/cli.js +39 -8
- package/dist/config.js +62 -6
- package/dist/crawltools.js +247 -0
- package/dist/deepseek.js +125 -10
- package/dist/executor.js +993 -144
- package/dist/hub.js +85 -6
- package/dist/journal.js +61 -11
- package/dist/memory.js +84 -0
- package/dist/pdftext.js +211 -0
- package/dist/prompts.js +124 -23
- package/dist/report.js +289 -0
- package/dist/run.js +15 -2
- package/dist/sandbox.js +11 -0
- package/dist/searchcore.js +244 -0
- package/dist/state.js +85 -3
- package/dist/tools.js +392 -25
- package/dist/util.js +85 -0
- package/dist/webtools.js +327 -66
- package/package.json +3 -2
- package/ui/out/404/index.html +1 -1
- package/ui/out/404.html +1 -1
- package/ui/out/_next/static/chunks/532-35122e93f37719b9.js +1 -0
- package/ui/out/_next/static/chunks/677-721ce1c8b7a6a317.js +1 -0
- package/ui/out/_next/static/chunks/app/page-dc9f6744d203e76c.js +1 -0
- package/ui/out/_next/static/chunks/app/run/page-3674e103981703a2.js +1 -0
- package/ui/out/_next/static/chunks/app/settings/page-41a5d8ba43ecfd4a.js +1 -0
- package/ui/out/_next/static/css/d95c2ba395730031.css +3 -0
- package/ui/out/fonts/PlanetKosmos.ttf +0 -0
- package/ui/out/index.html +1 -1
- package/ui/out/index.txt +3 -3
- package/ui/out/run/index.html +1 -1
- package/ui/out/run/index.txt +3 -3
- package/ui/out/settings/index.html +1 -1
- package/ui/out/settings/index.txt +3 -3
- package/ui/out/_next/static/chunks/383-289a866b246b41cc.js +0 -1
- package/ui/out/_next/static/chunks/619-ba102abea3e3d0e4.js +0 -1
- package/ui/out/_next/static/chunks/677-7ab85a6f38c3a235.js +0 -1
- package/ui/out/_next/static/chunks/app/page-0fda5b8e77d90b84.js +0 -1
- package/ui/out/_next/static/chunks/app/run/page-07aab6b1224c3c8c.js +0 -1
- package/ui/out/_next/static/chunks/app/settings/page-528482d468d84cfa.js +0 -1
- package/ui/out/_next/static/css/e2c82b53bf4519e8.css +0 -3
- /package/ui/out/_next/static/{Rm5Fhkds2-wIOnVlME55J → 7_pihFubDGD40BCy2ynlr}/_buildManifest.js +0 -0
- /package/ui/out/_next/static/{Rm5Fhkds2-wIOnVlME55J → 7_pihFubDGD40BCy2ynlr}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveCrawlBackend = resolveCrawlBackend;
|
|
4
|
+
exports.hasScrapeBackend = hasScrapeBackend;
|
|
5
|
+
exports.crawlSite = crawlSite;
|
|
6
|
+
exports.scrapeUrl = scrapeUrl;
|
|
7
|
+
exports.slugForUrl = slugForUrl;
|
|
8
|
+
const util_1 = require("./util");
|
|
9
|
+
const PER_PAGE_CHAR_CAP = 200_000;
|
|
10
|
+
const TOTAL_CHAR_BUDGET = 8_000_000;
|
|
11
|
+
const CRAWL_DEADLINE_MS = 120_000;
|
|
12
|
+
/** auto = first configured: Firecrawl → context.dev → deepcrawl. "off" or nothing configured → null. */
|
|
13
|
+
function resolveCrawlBackend(cfg) {
|
|
14
|
+
if (cfg.crawlBackend === "off")
|
|
15
|
+
return null;
|
|
16
|
+
const configured = {
|
|
17
|
+
firecrawl: Boolean(cfg.firecrawlApiKey),
|
|
18
|
+
contextdev: Boolean(cfg.contextdevApiKey),
|
|
19
|
+
deepcrawl: Boolean(cfg.deepcrawlApiKey && cfg.deepcrawlBaseUrl),
|
|
20
|
+
};
|
|
21
|
+
if (cfg.crawlBackend !== "auto")
|
|
22
|
+
return configured[cfg.crawlBackend] ? cfg.crawlBackend : null;
|
|
23
|
+
for (const id of ["firecrawl", "contextdev", "deepcrawl"]) {
|
|
24
|
+
if (configured[id])
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/** Backends usable for single-page scrape in fetch_url (the custom deepcrawl contract has no scrape endpoint). */
|
|
30
|
+
function hasScrapeBackend(cfg) {
|
|
31
|
+
const b = resolveCrawlBackend(cfg);
|
|
32
|
+
return b === "firecrawl" || b === "contextdev";
|
|
33
|
+
}
|
|
34
|
+
async function crawlSite(cfg, opts) {
|
|
35
|
+
const backend = resolveCrawlBackend(cfg);
|
|
36
|
+
if (!backend)
|
|
37
|
+
throw new Error("no crawl backend configured — add a Firecrawl/context.dev/deepcrawl key in Settings");
|
|
38
|
+
const warnings = [];
|
|
39
|
+
let pages;
|
|
40
|
+
if (backend === "firecrawl")
|
|
41
|
+
pages = await firecrawlCrawl(cfg, opts, warnings);
|
|
42
|
+
else if (backend === "contextdev")
|
|
43
|
+
pages = await contextdevCrawl(cfg, opts);
|
|
44
|
+
else
|
|
45
|
+
pages = await deepcrawlCrawl(cfg, opts);
|
|
46
|
+
// Normalize: drop empty/binary pages, cap per-page and total size.
|
|
47
|
+
const clean = [];
|
|
48
|
+
let skipped = 0;
|
|
49
|
+
let total = 0;
|
|
50
|
+
for (const p of pages) {
|
|
51
|
+
if (clean.length >= opts.maxPages)
|
|
52
|
+
break;
|
|
53
|
+
const md = (p.markdown || "").trim();
|
|
54
|
+
if (!md || md.includes("\u0000")) {
|
|
55
|
+
skipped++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const body = (0, util_1.truncateMiddle)(md, PER_PAGE_CHAR_CAP, "chars");
|
|
59
|
+
if (total + body.length > TOTAL_CHAR_BUDGET) {
|
|
60
|
+
warnings.push(`stopped at ${clean.length} pages: total content budget reached`);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
total += body.length;
|
|
64
|
+
clean.push({ url: p.url, title: p.title, markdown: body });
|
|
65
|
+
}
|
|
66
|
+
if (skipped)
|
|
67
|
+
warnings.push(`${skipped} empty page${skipped > 1 ? "s" : ""} skipped`);
|
|
68
|
+
return { backend, pages: clean, warnings };
|
|
69
|
+
}
|
|
70
|
+
/** Single-page scrape via the configured backend. Throws on failure — callers fall through to their own fetch path. */
|
|
71
|
+
async function scrapeUrl(cfg, url, signal) {
|
|
72
|
+
const backend = resolveCrawlBackend(cfg);
|
|
73
|
+
if (backend === "firecrawl") {
|
|
74
|
+
const data = await callJson("firecrawl", "https://api.firecrawl.dev/v1/scrape", cfg.firecrawlApiKey, { url, formats: ["markdown"] }, 30_000, signal);
|
|
75
|
+
const md = String(data?.data?.markdown ?? "");
|
|
76
|
+
if (!md.trim())
|
|
77
|
+
throw new Error("firecrawl: empty scrape result");
|
|
78
|
+
const title = data?.data?.metadata?.title;
|
|
79
|
+
return title ? `# ${title}\n\n${md}` : md;
|
|
80
|
+
}
|
|
81
|
+
if (backend === "contextdev") {
|
|
82
|
+
const data = await callJson("context.dev", "https://api.context.dev/v1/web/scrape", cfg.contextdevApiKey, { url }, 30_000, signal);
|
|
83
|
+
const md = String(data?.markdown ?? data?.results?.[0]?.markdown ?? "");
|
|
84
|
+
if (!md.trim())
|
|
85
|
+
throw new Error("context.dev: empty scrape result");
|
|
86
|
+
const title = data?.metadata?.title ?? data?.results?.[0]?.metadata?.title;
|
|
87
|
+
return title ? `# ${title}\n\n${md}` : md;
|
|
88
|
+
}
|
|
89
|
+
throw new Error("no scrape-capable crawl backend configured");
|
|
90
|
+
}
|
|
91
|
+
/** "https://docs.foo.com/a/b?x=1" → filesystem-safe { host, slug } with no separators or traversal. */
|
|
92
|
+
function slugForUrl(url) {
|
|
93
|
+
let u;
|
|
94
|
+
try {
|
|
95
|
+
u = new URL(url);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { host: "site", slug: sanitize(url) || "page" };
|
|
99
|
+
}
|
|
100
|
+
const host = sanitize(u.hostname) || "site";
|
|
101
|
+
const slug = sanitize(u.pathname.replace(/\/+$/, "")) || "index";
|
|
102
|
+
return { host, slug };
|
|
103
|
+
}
|
|
104
|
+
function sanitize(s) {
|
|
105
|
+
return s
|
|
106
|
+
.toLowerCase()
|
|
107
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
108
|
+
.replace(/\.{2,}/g, ".")
|
|
109
|
+
.replace(/-{2,}/g, "-")
|
|
110
|
+
.replace(/^[-.]+|[-.]+$/g, "")
|
|
111
|
+
.slice(0, 120);
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------- backends
|
|
114
|
+
async function firecrawlCrawl(cfg, opts, warnings) {
|
|
115
|
+
const start = await callJson("firecrawl", "https://api.firecrawl.dev/v1/crawl", cfg.firecrawlApiKey, {
|
|
116
|
+
url: opts.url,
|
|
117
|
+
limit: opts.maxPages,
|
|
118
|
+
...(opts.includePaths?.length ? { includePaths: opts.includePaths } : {}),
|
|
119
|
+
scrapeOptions: { formats: ["markdown"] },
|
|
120
|
+
}, 30_000, opts.signal);
|
|
121
|
+
const jobId = start?.id;
|
|
122
|
+
if (!jobId)
|
|
123
|
+
throw new Error(`firecrawl: crawl did not start (${start?.error || "no job id"})`);
|
|
124
|
+
const pollMs = opts.pollMs ?? 3000;
|
|
125
|
+
const deadline = Date.now() + CRAWL_DEADLINE_MS;
|
|
126
|
+
let last = null;
|
|
127
|
+
for (;;) {
|
|
128
|
+
opts.signal?.throwIfAborted();
|
|
129
|
+
last = await getJson("firecrawl", `https://api.firecrawl.dev/v1/crawl/${jobId}`, cfg.firecrawlApiKey, opts.signal);
|
|
130
|
+
if (last?.status === "completed")
|
|
131
|
+
break;
|
|
132
|
+
if (last?.status === "failed")
|
|
133
|
+
throw new Error(`firecrawl: crawl failed (${last?.error || "unknown error"})`);
|
|
134
|
+
if (Date.now() > deadline) {
|
|
135
|
+
const partial = mapFirecrawlPages(last);
|
|
136
|
+
if (!partial.length)
|
|
137
|
+
throw new Error("firecrawl: crawl still running after 120s with no pages yet — try fewer pages");
|
|
138
|
+
warnings.push(`crawl still running after 120s; returning ${partial.length} partial pages`);
|
|
139
|
+
return partial;
|
|
140
|
+
}
|
|
141
|
+
await sleep(pollMs, opts.signal);
|
|
142
|
+
}
|
|
143
|
+
// Completed: collect pages, following `next` pagination until maxPages.
|
|
144
|
+
const pages = mapFirecrawlPages(last);
|
|
145
|
+
let next = last?.next;
|
|
146
|
+
while (next && pages.length < opts.maxPages) {
|
|
147
|
+
const more = await getJson("firecrawl", String(next), cfg.firecrawlApiKey, opts.signal);
|
|
148
|
+
pages.push(...mapFirecrawlPages(more));
|
|
149
|
+
next = more?.next;
|
|
150
|
+
}
|
|
151
|
+
return pages;
|
|
152
|
+
}
|
|
153
|
+
function mapFirecrawlPages(res) {
|
|
154
|
+
const data = Array.isArray(res?.data) ? res.data : [];
|
|
155
|
+
return data.map((d) => ({
|
|
156
|
+
url: String(d?.metadata?.sourceURL ?? d?.metadata?.url ?? ""),
|
|
157
|
+
title: String(d?.metadata?.title ?? ""),
|
|
158
|
+
markdown: String(d?.markdown ?? ""),
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
async function contextdevCrawl(cfg, opts) {
|
|
162
|
+
const data = await callJson("context.dev", "https://api.context.dev/v1/web/crawl", cfg.contextdevApiKey, {
|
|
163
|
+
url: opts.url,
|
|
164
|
+
max_pages: opts.maxPages,
|
|
165
|
+
...(opts.includePaths?.length ? { include_paths: opts.includePaths } : {}),
|
|
166
|
+
}, CRAWL_DEADLINE_MS, opts.signal);
|
|
167
|
+
const results = Array.isArray(data?.results) ? data.results : [];
|
|
168
|
+
return results.map((r) => ({
|
|
169
|
+
url: String(r?.metadata?.url ?? r?.url ?? ""),
|
|
170
|
+
title: String(r?.metadata?.title ?? r?.title ?? ""),
|
|
171
|
+
markdown: String(r?.markdown ?? ""),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
async function deepcrawlCrawl(cfg, opts) {
|
|
175
|
+
const base = cfg.deepcrawlBaseUrl.replace(/\/+$/, "");
|
|
176
|
+
const data = await callJson("deepcrawl", `${base}/crawl`, cfg.deepcrawlApiKey, {
|
|
177
|
+
url: opts.url,
|
|
178
|
+
max_pages: opts.maxPages,
|
|
179
|
+
...(opts.includePaths?.length ? { include_paths: opts.includePaths } : {}),
|
|
180
|
+
}, CRAWL_DEADLINE_MS, opts.signal);
|
|
181
|
+
// Accept either the context.dev-compatible shape or a flat pages[] list.
|
|
182
|
+
if (Array.isArray(data?.results)) {
|
|
183
|
+
return data.results.map((r) => ({
|
|
184
|
+
url: String(r?.metadata?.url ?? r?.url ?? ""),
|
|
185
|
+
title: String(r?.metadata?.title ?? r?.title ?? ""),
|
|
186
|
+
markdown: String(r?.markdown ?? ""),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(data?.pages)) {
|
|
190
|
+
return data.pages.map((p) => ({
|
|
191
|
+
url: String(p?.url ?? ""),
|
|
192
|
+
title: String(p?.title ?? ""),
|
|
193
|
+
markdown: String(p?.markdown ?? p?.content ?? ""),
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
throw new Error("deepcrawl: unrecognized response shape (expected results[] or pages[])");
|
|
197
|
+
}
|
|
198
|
+
// ---------------------------------------------------------------- plumbing
|
|
199
|
+
function friendlyHttpError(service, status, body) {
|
|
200
|
+
if (status === 401 || status === 403) {
|
|
201
|
+
return new Error(`${service} API key invalid or unauthorized (HTTP ${status}) — check Settings → Crawl integrations`);
|
|
202
|
+
}
|
|
203
|
+
if (status === 402)
|
|
204
|
+
return new Error(`${service}: quota or credits exhausted (HTTP 402)`);
|
|
205
|
+
if (status === 429)
|
|
206
|
+
return new Error(`${service}: rate limited (HTTP 429) — retry later`);
|
|
207
|
+
return new Error(`${service}: HTTP ${status} ${(0, util_1.truncateMiddle)(body, 300, "chars")}`);
|
|
208
|
+
}
|
|
209
|
+
function mergeSignal(timeoutMs, signal) {
|
|
210
|
+
const t = AbortSignal.timeout(timeoutMs);
|
|
211
|
+
if (!signal)
|
|
212
|
+
return t;
|
|
213
|
+
return typeof AbortSignal.any === "function" ? AbortSignal.any([t, signal]) : signal;
|
|
214
|
+
}
|
|
215
|
+
async function callJson(service, url, key, body, timeoutMs, signal) {
|
|
216
|
+
const res = await fetch(url, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
219
|
+
body: JSON.stringify(body),
|
|
220
|
+
signal: mergeSignal(timeoutMs, signal),
|
|
221
|
+
});
|
|
222
|
+
if (!res.ok)
|
|
223
|
+
throw friendlyHttpError(service, res.status, await res.text().catch(() => ""));
|
|
224
|
+
return res.json();
|
|
225
|
+
}
|
|
226
|
+
async function getJson(service, url, key, signal) {
|
|
227
|
+
const res = await fetch(url, {
|
|
228
|
+
headers: { authorization: `Bearer ${key}` },
|
|
229
|
+
signal: mergeSignal(30_000, signal),
|
|
230
|
+
});
|
|
231
|
+
if (!res.ok)
|
|
232
|
+
throw friendlyHttpError(service, res.status, await res.text().catch(() => ""));
|
|
233
|
+
return res.json();
|
|
234
|
+
}
|
|
235
|
+
function sleep(ms, signal) {
|
|
236
|
+
return new Promise((resolve, reject) => {
|
|
237
|
+
const t = setTimeout(() => {
|
|
238
|
+
signal?.removeEventListener("abort", onAbort);
|
|
239
|
+
resolve();
|
|
240
|
+
}, ms);
|
|
241
|
+
const onAbort = () => {
|
|
242
|
+
clearTimeout(t);
|
|
243
|
+
reject(new Error("aborted"));
|
|
244
|
+
};
|
|
245
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
246
|
+
});
|
|
247
|
+
}
|
package/dist/deepseek.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CancelledError = exports.ApiError = void 0;
|
|
3
|
+
exports.CallGate = exports.CancelledError = exports.ApiError = void 0;
|
|
4
|
+
exports.gateFor = gateFor;
|
|
4
5
|
exports.chat = chat;
|
|
5
6
|
exports.isFatalAuthError = isFatalAuthError;
|
|
6
7
|
exports.validateAuth = validateAuth;
|
|
@@ -13,10 +14,13 @@ function providerOf(cfg) {
|
|
|
13
14
|
class ApiError extends Error {
|
|
14
15
|
status;
|
|
15
16
|
body;
|
|
16
|
-
|
|
17
|
+
/** Parsed Retry-After (ms) when the server sent one with a 429. */
|
|
18
|
+
retryAfterMs;
|
|
19
|
+
constructor(status, body, retryAfterMs) {
|
|
17
20
|
super(`API ${status}: ${body.slice(0, 600)}`);
|
|
18
21
|
this.status = status;
|
|
19
22
|
this.body = body;
|
|
23
|
+
this.retryAfterMs = retryAfterMs;
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
exports.ApiError = ApiError;
|
|
@@ -66,25 +70,135 @@ function sanitizeMessages(messages, thinking) {
|
|
|
66
70
|
});
|
|
67
71
|
}
|
|
68
72
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
73
|
+
* Bounds concurrent streaming calls per provider endpoint so a 100-agent swarm
|
|
74
|
+
* doesn't turn into a 429 storm. AIMD: a 429 halves the ceiling (min 2) and
|
|
75
|
+
* imposes the server's Retry-After as a cool-down; sustained successes recover
|
|
76
|
+
* it additively back toward the configured max. Two-tier FIFO: "high" priority
|
|
77
|
+
* (conductor/orchestration) jumps ahead so queued workers can't starve the
|
|
78
|
+
* brain of the swarm.
|
|
79
|
+
*/
|
|
80
|
+
class CallGate {
|
|
81
|
+
max;
|
|
82
|
+
ceiling;
|
|
83
|
+
active = 0;
|
|
84
|
+
high = [];
|
|
85
|
+
low = [];
|
|
86
|
+
successes = 0;
|
|
87
|
+
cooldownUntil = 0;
|
|
88
|
+
onState;
|
|
89
|
+
constructor(max) {
|
|
90
|
+
this.max = Math.max(1, max);
|
|
91
|
+
this.ceiling = this.max;
|
|
92
|
+
}
|
|
93
|
+
state() {
|
|
94
|
+
return { ceiling: this.ceiling, active: this.active, queued: this.high.length + this.low.length };
|
|
95
|
+
}
|
|
96
|
+
configure(max) {
|
|
97
|
+
this.max = Math.max(1, max);
|
|
98
|
+
if (this.ceiling > this.max)
|
|
99
|
+
this.ceiling = this.max;
|
|
100
|
+
this.pump();
|
|
101
|
+
}
|
|
102
|
+
async acquire(priority, signal) {
|
|
103
|
+
const wait = this.cooldownUntil - Date.now();
|
|
104
|
+
if (wait > 0)
|
|
105
|
+
await (0, util_1.sleep)(wait);
|
|
106
|
+
if (signal?.aborted)
|
|
107
|
+
throw new CancelledError();
|
|
108
|
+
if (this.active < this.ceiling) {
|
|
109
|
+
this.active++;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await new Promise((resolve, reject) => {
|
|
113
|
+
const queue = priority === "high" ? this.high : this.low;
|
|
114
|
+
const entry = () => {
|
|
115
|
+
signal?.removeEventListener("abort", onAbort);
|
|
116
|
+
resolve();
|
|
117
|
+
};
|
|
118
|
+
const onAbort = () => {
|
|
119
|
+
const i = queue.indexOf(entry);
|
|
120
|
+
if (i >= 0)
|
|
121
|
+
queue.splice(i, 1);
|
|
122
|
+
reject(new CancelledError());
|
|
123
|
+
};
|
|
124
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
125
|
+
queue.push(entry);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
release() {
|
|
129
|
+
this.active = Math.max(0, this.active - 1);
|
|
130
|
+
this.pump();
|
|
131
|
+
}
|
|
132
|
+
reportRateLimit(retryAfterMs) {
|
|
133
|
+
this.ceiling = Math.max(2, Math.floor(this.ceiling / 2));
|
|
134
|
+
this.successes = 0;
|
|
135
|
+
if (retryAfterMs && retryAfterMs > 0) {
|
|
136
|
+
this.cooldownUntil = Math.max(this.cooldownUntil, Date.now() + Math.min(retryAfterMs, 60_000));
|
|
137
|
+
}
|
|
138
|
+
this.onState?.(this.state());
|
|
139
|
+
}
|
|
140
|
+
reportSuccess() {
|
|
141
|
+
if (this.ceiling >= this.max)
|
|
142
|
+
return;
|
|
143
|
+
if (++this.successes >= 10) {
|
|
144
|
+
this.successes = 0;
|
|
145
|
+
this.ceiling++;
|
|
146
|
+
this.onState?.(this.state());
|
|
147
|
+
this.pump();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
pump() {
|
|
151
|
+
while (this.active < this.ceiling) {
|
|
152
|
+
const next = this.high.shift() ?? this.low.shift();
|
|
153
|
+
if (!next)
|
|
154
|
+
break;
|
|
155
|
+
this.active++;
|
|
156
|
+
next();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.CallGate = CallGate;
|
|
161
|
+
const gates = new Map();
|
|
162
|
+
function gateFor(cfg) {
|
|
163
|
+
const key = cfg.baseUrl;
|
|
164
|
+
let g = gates.get(key);
|
|
165
|
+
if (!g) {
|
|
166
|
+
g = new CallGate(cfg.maxConcurrentCalls);
|
|
167
|
+
gates.set(key, g);
|
|
168
|
+
}
|
|
169
|
+
g.configure(cfg.maxConcurrentCalls);
|
|
170
|
+
return g;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* One streaming chat-completions call with retries, behind the global gate.
|
|
174
|
+
* The retry backoff sleeps OUTSIDE the gate so a waiting call never holds a
|
|
175
|
+
* concurrency slot.
|
|
71
176
|
*/
|
|
72
177
|
async function chat(cfg, o) {
|
|
178
|
+
const gate = gateFor(cfg);
|
|
73
179
|
let lastErr;
|
|
74
180
|
const attempts = 4;
|
|
75
181
|
for (let i = 0; i < attempts; i++) {
|
|
182
|
+
await gate.acquire(o.priority ?? "normal", o.signal);
|
|
76
183
|
try {
|
|
77
|
-
|
|
184
|
+
const res = await chatOnce(cfg, o);
|
|
185
|
+
gate.reportSuccess();
|
|
186
|
+
return res;
|
|
78
187
|
}
|
|
79
188
|
catch (e) {
|
|
80
189
|
lastErr = e;
|
|
190
|
+
if (e instanceof ApiError && e.status === 429)
|
|
191
|
+
gate.reportRateLimit(e.retryAfterMs);
|
|
81
192
|
if (!retryable(e) || i === attempts - 1)
|
|
82
193
|
throw e;
|
|
83
|
-
const backoff = [1500, 5000, 15000][i] ?? 15000;
|
|
84
|
-
await (0, util_1.sleep)(backoff + Math.random() * 1000);
|
|
85
|
-
if (o.signal?.aborted)
|
|
86
|
-
throw new CancelledError();
|
|
87
194
|
}
|
|
195
|
+
finally {
|
|
196
|
+
gate.release();
|
|
197
|
+
}
|
|
198
|
+
const backoff = [1500, 5000, 15000][i] ?? 15000;
|
|
199
|
+
await (0, util_1.sleep)(backoff + Math.random() * 1000);
|
|
200
|
+
if (o.signal?.aborted)
|
|
201
|
+
throw new CancelledError();
|
|
88
202
|
}
|
|
89
203
|
throw lastErr;
|
|
90
204
|
}
|
|
@@ -149,7 +263,8 @@ async function chatOnce(cfg, o) {
|
|
|
149
263
|
});
|
|
150
264
|
if (!res.ok) {
|
|
151
265
|
const text = await res.text().catch(() => "");
|
|
152
|
-
|
|
266
|
+
const ra = Number(res.headers.get("retry-after"));
|
|
267
|
+
throw new ApiError(res.status, text, Number.isFinite(ra) && ra >= 0 ? ra * 1000 : undefined);
|
|
153
268
|
}
|
|
154
269
|
if (!res.body)
|
|
155
270
|
throw new ApiError(0, "empty response body");
|