batchwork 1.0.0 → 1.1.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.
Files changed (58) hide show
  1. package/README.md +25 -0
  2. package/dist/batch.d.ts +18 -1
  3. package/dist/batch.d.ts.map +1 -1
  4. package/dist/body.d.ts +10 -2
  5. package/dist/body.d.ts.map +1 -1
  6. package/dist/{chunk-zp2cxkyb.js → chunk-d0g9wsxq.js} +162 -13
  7. package/dist/chunk-d0g9wsxq.js.map +13 -0
  8. package/dist/{chunk-kv3847wy.js → chunk-xtk2ycy6.js} +349 -78
  9. package/dist/chunk-xtk2ycy6.js.map +27 -0
  10. package/dist/{chunk-ab2d71gk.js → chunk-yzjd83s9.js} +120 -21
  11. package/dist/chunk-yzjd83s9.js.map +12 -0
  12. package/dist/errors.d.ts +1 -1
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/http.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +5 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/job.d.ts.map +1 -1
  20. package/dist/jsonl.d.ts +10 -16
  21. package/dist/jsonl.d.ts.map +1 -1
  22. package/dist/limits.d.ts +13 -0
  23. package/dist/limits.d.ts.map +1 -0
  24. package/dist/model.d.ts +16 -2
  25. package/dist/model.d.ts.map +1 -1
  26. package/dist/next/index.d.ts +3 -1
  27. package/dist/next/index.d.ts.map +1 -1
  28. package/dist/next/index.js +7 -4
  29. package/dist/next/index.js.map +3 -3
  30. package/dist/payload.d.ts +10 -0
  31. package/dist/payload.d.ts.map +1 -0
  32. package/dist/providers/adapter.d.ts +2 -1
  33. package/dist/providers/adapter.d.ts.map +1 -1
  34. package/dist/providers/anthropic.d.ts.map +1 -1
  35. package/dist/providers/google.d.ts.map +1 -1
  36. package/dist/providers/ids.d.ts +3 -0
  37. package/dist/providers/ids.d.ts.map +1 -0
  38. package/dist/providers/mistral.d.ts.map +1 -1
  39. package/dist/providers/openai-compatible.d.ts.map +1 -1
  40. package/dist/providers/shared.d.ts +2 -0
  41. package/dist/providers/shared.d.ts.map +1 -1
  42. package/dist/providers/together.d.ts.map +1 -1
  43. package/dist/providers/xai.d.ts.map +1 -1
  44. package/dist/server/index.d.ts +2 -2
  45. package/dist/server/index.d.ts.map +1 -1
  46. package/dist/server/index.js +2 -2
  47. package/dist/server/poller.d.ts +3 -0
  48. package/dist/server/poller.d.ts.map +1 -1
  49. package/dist/server/signing.d.ts +10 -2
  50. package/dist/server/signing.d.ts.map +1 -1
  51. package/dist/types.d.ts +31 -1
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/util.d.ts +2 -0
  54. package/dist/util.d.ts.map +1 -1
  55. package/package.json +1 -1
  56. package/dist/chunk-ab2d71gk.js.map +0 -12
  57. package/dist/chunk-kv3847wy.js.map +0 -24
  58. package/dist/chunk-zp2cxkyb.js.map +0 -13
@@ -8,8 +8,8 @@ class BatchworkError extends Error {
8
8
 
9
9
  class UnsupportedProviderError extends BatchworkError {
10
10
  provider;
11
- constructor(provider) {
12
- super(`batchwork: provider "${provider}" is not supported yet. Supported providers: openai, anthropic, google, groq, mistral, together, xai.`);
11
+ constructor(provider, detail) {
12
+ super(detail ?? `batchwork: provider "${provider}" is not supported yet. Supported providers: openai, anthropic, google, groq, mistral, together, xai.`);
13
13
  this.name = "UnsupportedProviderError";
14
14
  this.provider = provider;
15
15
  }
@@ -36,11 +36,16 @@ var delay = (ms, signal) => new Promise((resolve, reject) => {
36
36
  reject(new BatchworkError("batchwork: wait aborted."));
37
37
  return;
38
38
  }
39
- const timer = setTimeout(resolve, ms);
40
- signal?.addEventListener("abort", () => {
39
+ let timer;
40
+ const onAbort = () => {
41
41
  clearTimeout(timer);
42
42
  reject(new BatchworkError("batchwork: wait aborted."));
43
- }, { once: true });
43
+ };
44
+ timer = setTimeout(() => {
45
+ signal?.removeEventListener("abort", onAbort);
46
+ resolve();
47
+ }, ms);
48
+ signal?.addEventListener("abort", onAbort, { once: true });
44
49
  });
45
50
 
46
51
  class BatchJob {
@@ -103,28 +108,67 @@ class BatchJob {
103
108
  }
104
109
  }
105
110
 
106
- // src/http.ts
107
- var safeText = async (response) => {
108
- try {
109
- return await response.text();
110
- } catch {
111
- return "<no body>";
111
+ // src/limits.ts
112
+ var DEFAULT_LIMITS = {
113
+ captureConcurrency: 16,
114
+ maxRequestBytes: 20 * 1024 * 1024,
115
+ maxRequests: 50000,
116
+ maxUploadBytes: 200 * 1024 * 1024
117
+ };
118
+ var encoder = new TextEncoder;
119
+ var positiveInteger = (name, value) => {
120
+ if (!(Number.isInteger(value) && value > 0)) {
121
+ throw new BatchworkError(`batchwork: limits.${name} must be a positive integer.`);
112
122
  }
123
+ return value;
124
+ };
125
+ var resolveBatchLimits = (limits) => ({
126
+ captureConcurrency: positiveInteger("captureConcurrency", limits?.captureConcurrency ?? DEFAULT_LIMITS.captureConcurrency),
127
+ maxRequestBytes: positiveInteger("maxRequestBytes", limits?.maxRequestBytes ?? DEFAULT_LIMITS.maxRequestBytes),
128
+ maxRequests: positiveInteger("maxRequests", limits?.maxRequests ?? DEFAULT_LIMITS.maxRequests),
129
+ maxUploadBytes: positiveInteger("maxUploadBytes", limits?.maxUploadBytes ?? DEFAULT_LIMITS.maxUploadBytes)
130
+ });
131
+ var byteLength = (value) => encoder.encode(value).length;
132
+ var assertByteCount = (label, bytes, maxBytes) => {
133
+ if (bytes > maxBytes) {
134
+ throw new BatchworkError(`batchwork: ${label} is ${bytes} bytes, exceeding the ${maxBytes} byte limit.`);
135
+ }
136
+ };
137
+ var assertByteLength = (label, value, maxBytes) => {
138
+ assertByteCount(label, byteLength(value), maxBytes);
139
+ };
140
+ var mapWithConcurrency = async (items, concurrency, mapper) => {
141
+ const results = [];
142
+ results.length = items.length;
143
+ let nextIndex = 0;
144
+ const workerCount = Math.min(concurrency, items.length);
145
+ const runNext = async () => {
146
+ const index = nextIndex;
147
+ nextIndex += 1;
148
+ if (index >= items.length) {
149
+ return;
150
+ }
151
+ results[index] = await mapper(items[index]);
152
+ await runNext();
153
+ };
154
+ await Promise.all(Array.from({ length: workerCount }, () => runNext()));
155
+ return results;
113
156
  };
114
- var assertOk = async (url, init, response) => {
157
+
158
+ // src/http.ts
159
+ var assertOk = (url, init, response) => {
115
160
  if (!response.ok) {
116
- const detail = await safeText(response);
117
- throw new BatchworkError(`batchwork: ${init.method ?? "GET"} ${url} failed with ${response.status}: ${detail}`);
161
+ throw new BatchworkError(`batchwork: ${init.method ?? "GET"} ${url} failed with ${response.status}.`);
118
162
  }
119
163
  };
120
164
  var requestJson = async (url, init) => {
121
165
  const response = await fetch(url, init);
122
- await assertOk(url, init, response);
166
+ assertOk(url, init, response);
123
167
  return await response.json();
124
168
  };
125
169
  var requestStream = async (url, init) => {
126
170
  const response = await fetch(url, init);
127
- await assertOk(url, init, response);
171
+ assertOk(url, init, response);
128
172
  if (!response.body) {
129
173
  throw new BatchworkError(`batchwork: ${url} returned an empty body.`);
130
174
  }
@@ -134,12 +178,60 @@ var requestStream = async (url, init) => {
134
178
  // src/jsonl.ts
135
179
  var NEWLINE = `
136
180
  `;
137
- var encodeJsonl = (items) => {
181
+ var DEFAULT_MAX_JSONL_LINE_BYTES = 20 * 1024 * 1024;
182
+ var NEWLINE_BYTES = byteLength(NEWLINE);
183
+ var resolveMaxLineBytes = (options) => {
184
+ const maxLineBytes = options?.maxLineBytes ?? DEFAULT_MAX_JSONL_LINE_BYTES;
185
+ if (!(Number.isInteger(maxLineBytes) && maxLineBytes > 0)) {
186
+ throw new BatchworkError("batchwork: JSONL maxLineBytes must be a positive integer.");
187
+ }
188
+ return maxLineBytes;
189
+ };
190
+ var assertLineSize = (line, lineNumber, maxLineBytes) => {
191
+ const bytes = byteLength(line);
192
+ if (bytes > maxLineBytes) {
193
+ throw new BatchworkError(`batchwork: JSONL line ${lineNumber} is ${bytes} bytes, exceeding the ${maxLineBytes} byte limit.`);
194
+ }
195
+ };
196
+ var parseLine = (line, lineNumber, maxLineBytes) => {
197
+ assertLineSize(line, lineNumber, maxLineBytes);
198
+ const trimmed = line.trim();
199
+ if (trimmed.length === 0) {
200
+ return;
201
+ }
202
+ try {
203
+ return JSON.parse(trimmed);
204
+ } catch (error) {
205
+ throw new BatchworkError(`batchwork: invalid JSONL at line ${lineNumber}.`, { cause: error });
206
+ }
207
+ };
208
+ var resolveMaxBytes = (options) => {
209
+ const maxBytes = options?.maxBytes;
210
+ if (maxBytes !== undefined && !(Number.isInteger(maxBytes) && maxBytes > 0)) {
211
+ throw new BatchworkError("batchwork: JSONL maxBytes must be a positive integer.");
212
+ }
213
+ return maxBytes;
214
+ };
215
+ var encodeJsonl = (items, options) => {
138
216
  if (items.length === 0) {
139
217
  return "";
140
218
  }
141
- const body = items.map((item) => JSON.stringify(item)).join(NEWLINE);
142
- return `${body}${NEWLINE}`;
219
+ const lines = [];
220
+ const maxBytes = resolveMaxBytes(options);
221
+ const label = options?.label ?? "JSONL";
222
+ let bytes = 0;
223
+ for (const item of items) {
224
+ const line = JSON.stringify(item);
225
+ if (line === undefined) {
226
+ throw new BatchworkError(`batchwork: ${label} contains a value that cannot be JSON encoded.`);
227
+ }
228
+ bytes += byteLength(line) + NEWLINE_BYTES;
229
+ if (maxBytes !== undefined) {
230
+ assertByteCount(label, bytes, maxBytes);
231
+ }
232
+ lines.push(line);
233
+ }
234
+ return `${lines.join(NEWLINE)}${NEWLINE}`;
143
235
  };
144
236
  var isReadableStream = (source) => ("getReader" in source) && typeof source.getReader === "function";
145
237
  async function* toByteIterable(source) {
@@ -160,28 +252,59 @@ async function* toByteIterable(source) {
160
252
  }
161
253
  yield* source;
162
254
  }
163
- async function* streamJsonl(source) {
255
+ async function* streamJsonl(source, options) {
164
256
  const decoder = new TextDecoder;
257
+ const maxLineBytes = resolveMaxLineBytes(options);
165
258
  let buffer = "";
259
+ let lineNumber = 1;
166
260
  for await (const chunk of toByteIterable(source)) {
167
261
  buffer += decoder.decode(chunk, { stream: true });
168
262
  let newlineIndex = buffer.indexOf(NEWLINE);
169
263
  while (newlineIndex !== -1) {
170
- const line = buffer.slice(0, newlineIndex).trim();
264
+ const line = buffer.slice(0, newlineIndex);
171
265
  buffer = buffer.slice(newlineIndex + 1);
172
- if (line.length > 0) {
173
- yield JSON.parse(line);
266
+ const parsed2 = parseLine(line, lineNumber, maxLineBytes);
267
+ if (parsed2 !== undefined) {
268
+ yield parsed2;
174
269
  }
270
+ lineNumber += 1;
175
271
  newlineIndex = buffer.indexOf(NEWLINE);
176
272
  }
273
+ assertLineSize(buffer, lineNumber, maxLineBytes);
177
274
  }
178
275
  buffer += decoder.decode();
179
- const tail = buffer.trim();
180
- if (tail.length > 0) {
181
- yield JSON.parse(tail);
276
+ const parsed = parseLine(buffer, lineNumber, maxLineBytes);
277
+ if (parsed !== undefined) {
278
+ yield parsed;
182
279
  }
183
280
  }
184
281
 
282
+ // src/payload.ts
283
+ var encodeJsonArrayPayload = ({
284
+ items,
285
+ label,
286
+ maxBytes,
287
+ prefix,
288
+ suffix
289
+ }) => {
290
+ const encodedItems = [];
291
+ let bytes = byteLength(prefix) + byteLength(suffix);
292
+ assertByteCount(label, bytes, maxBytes);
293
+ for (const [index, item] of items.entries()) {
294
+ const encoded = JSON.stringify(item);
295
+ if (encoded === undefined) {
296
+ throw new BatchworkError(`batchwork: ${label} contains a value that cannot be JSON encoded.`);
297
+ }
298
+ bytes += byteLength(encoded);
299
+ if (index > 0) {
300
+ bytes += 1;
301
+ }
302
+ assertByteCount(label, bytes, maxBytes);
303
+ encodedItems.push(encoded);
304
+ }
305
+ return `${prefix}${encodedItems.join(",")}${suffix}`;
306
+ };
307
+
185
308
  // src/util.ts
186
309
  var asRecord = (value) => {
187
310
  if (typeof value === "object" && value !== null) {
@@ -192,6 +315,13 @@ var asRecord = (value) => {
192
315
  var asString = (value) => typeof value === "string" ? value : undefined;
193
316
  var asNumber = (value) => typeof value === "number" ? value : undefined;
194
317
  var asArray = (value) => Array.isArray(value) ? value : [];
318
+ var asNumberArray = (value) => {
319
+ if (!Array.isArray(value) || value.length === 0) {
320
+ return;
321
+ }
322
+ const numbers = value.filter((item) => typeof item === "number");
323
+ return numbers.length === value.length ? numbers : undefined;
324
+ };
195
325
  var omit = (obj, key) => {
196
326
  const result = {};
197
327
  for (const [k, v] of Object.entries(obj)) {
@@ -201,13 +331,30 @@ var omit = (obj, key) => {
201
331
  }
202
332
  return result;
203
333
  };
334
+ var validDate = (date) => Number.isNaN(date.getTime()) ? undefined : date;
204
335
  var toDate = (value) => {
205
336
  if (typeof value === "string") {
206
- return new Date(value);
337
+ return validDate(new Date(value));
207
338
  }
208
339
  if (typeof value === "number") {
209
- return new Date(value * 1000);
340
+ return validDate(new Date(value * 1000));
341
+ }
342
+ };
343
+
344
+ // src/providers/ids.ts
345
+ var SIMPLE_PROVIDER_ID = /^[A-Za-z0-9_-]+$/u;
346
+ var assertSimpleProviderId = (label, id) => {
347
+ if (!SIMPLE_PROVIDER_ID.test(id)) {
348
+ throw new BatchworkError(`batchwork: invalid ${label}.`);
210
349
  }
350
+ return id;
351
+ };
352
+ var assertPrefixedProviderId = (label, id, prefix) => {
353
+ const [actualPrefix, value, ...rest] = id.split("/");
354
+ if (rest.length > 0 || actualPrefix !== prefix || !value || !SIMPLE_PROVIDER_ID.test(value)) {
355
+ throw new BatchworkError(`batchwork: invalid ${label}.`);
356
+ }
357
+ return id;
211
358
  };
212
359
 
213
360
  // src/providers/anthropic.ts
@@ -221,6 +368,25 @@ var apiKey = (credentials) => {
221
368
  return key;
222
369
  };
223
370
  var baseUrl = (credentials) => credentials.baseURL ?? ANTHROPIC_BASE;
371
+ var validateResultsUrl = (rawUrl, credentials) => {
372
+ let resultsUrl;
373
+ let expectedBase;
374
+ try {
375
+ resultsUrl = new URL(rawUrl);
376
+ expectedBase = new URL(baseUrl(credentials));
377
+ } catch (error) {
378
+ throw new BatchworkError("batchwork: invalid Anthropic results_url.", {
379
+ cause: error
380
+ });
381
+ }
382
+ if (resultsUrl.origin !== expectedBase.origin) {
383
+ throw new BatchworkError("batchwork: Anthropic results_url must match the configured API origin.");
384
+ }
385
+ if (resultsUrl.username || resultsUrl.password) {
386
+ throw new BatchworkError("batchwork: Anthropic results_url must not include credentials.");
387
+ }
388
+ return resultsUrl.toString();
389
+ };
224
390
  var headers = (credentials) => ({
225
391
  "anthropic-version": ANTHROPIC_VERSION,
226
392
  "content-type": "application/json",
@@ -313,19 +479,28 @@ var normalizeResult = (line) => {
313
479
  return { customId, status: "canceled" };
314
480
  };
315
481
  var submit = async (input) => {
482
+ const limits = resolveBatchLimits(input.limits);
316
483
  const requests = input.built.map((item) => ({
317
484
  custom_id: item.customId,
318
485
  params: omit(item.body, "stream")
319
486
  }));
487
+ const body = encodeJsonArrayPayload({
488
+ items: requests,
489
+ label: "batch upload payload",
490
+ maxBytes: limits.maxUploadBytes,
491
+ prefix: '{"requests":[',
492
+ suffix: "]}"
493
+ });
320
494
  const raw = await requestJson(`${baseUrl(input.credentials)}/v1/messages/batches`, {
321
- body: JSON.stringify({ requests }),
495
+ body,
322
496
  headers: headers(input.credentials),
323
497
  method: "POST"
324
498
  });
325
499
  return normalizeSnapshot(raw);
326
500
  };
327
501
  var retrieve = async (id, credentials) => {
328
- const raw = await requestJson(`${baseUrl(credentials)}/v1/messages/batches/${id}`, { headers: headers(credentials) });
502
+ const batchId = assertSimpleProviderId("Anthropic batch id", id);
503
+ const raw = await requestJson(`${baseUrl(credentials)}/v1/messages/batches/${batchId}`, { headers: headers(credentials) });
329
504
  return normalizeSnapshot(raw);
330
505
  };
331
506
  async function* results(id, credentials) {
@@ -334,15 +509,17 @@ async function* results(id, credentials) {
334
509
  if (!resultsUrl) {
335
510
  throw new BatchworkError(`batchwork: results are not ready for batch "${id}" (status: ${snapshot.status}).`);
336
511
  }
337
- const stream = await requestStream(resultsUrl, {
338
- headers: headers(credentials)
512
+ const stream = await requestStream(validateResultsUrl(resultsUrl, credentials), {
513
+ headers: headers(credentials),
514
+ redirect: "manual"
339
515
  });
340
516
  for await (const line of streamJsonl(stream)) {
341
517
  yield normalizeResult(line);
342
518
  }
343
519
  }
344
520
  var cancel = async (id, credentials) => {
345
- await requestJson(`${baseUrl(credentials)}/v1/messages/batches/${id}/cancel`, {
521
+ const batchId = assertSimpleProviderId("Anthropic batch id", id);
522
+ await requestJson(`${baseUrl(credentials)}/v1/messages/batches/${batchId}/cancel`, {
346
523
  headers: headers(credentials),
347
524
  method: "POST"
348
525
  });
@@ -357,6 +534,7 @@ var anthropicAdapter = {
357
534
 
358
535
  // src/providers/google.ts
359
536
  var GOOGLE_BASE = "https://generativelanguage.googleapis.com/v1beta";
537
+ var GOOGLE_BATCH_PREFIX = "batches";
360
538
  var apiKey2 = (credentials) => {
361
539
  const key = credentials.apiKey ?? process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? process.env.GEMINI_API_KEY;
362
540
  if (!key) {
@@ -414,8 +592,9 @@ var normalizeSnapshot2 = (raw) => {
414
592
  const obj = asRecord(raw);
415
593
  const items = inlinedResponses(raw);
416
594
  const failed = items.filter((item) => asRecord(item).error).length;
595
+ const id = asString(obj.name) ?? "";
417
596
  return {
418
- id: asString(obj.name) ?? "",
597
+ id: id ? assertPrefixedProviderId("Google operation id", id, GOOGLE_BATCH_PREFIX) : "",
419
598
  provider: "google",
420
599
  raw,
421
600
  requestCounts: {
@@ -431,6 +610,7 @@ var textFromResponse = (response) => {
431
610
  const text = asArray(asRecord(candidate.content).parts).map((part) => asString(asRecord(part).text) ?? "").join("");
432
611
  return text.length > 0 ? text : undefined;
433
612
  };
613
+ var embeddingFromResponse = (response) => asNumberArray(asRecord(asRecord(response).embedding).values);
434
614
  var usageFromResponse = (response) => {
435
615
  const usage = asRecord(asRecord(response).usageMetadata);
436
616
  const inputTokens = asNumber(usage.promptTokenCount);
@@ -463,31 +643,61 @@ var normalizeResult2 = (item) => {
463
643
  }
464
644
  return {
465
645
  customId,
646
+ embedding: embeddingFromResponse(obj.response),
466
647
  response: obj.response,
467
648
  status: "succeeded",
468
649
  text: textFromResponse(obj.response),
469
650
  usage: usageFromResponse(obj.response)
470
651
  };
471
652
  };
653
+ var EMBED_CONFIG_KEYS = new Set([
654
+ "outputDimensionality",
655
+ "taskType",
656
+ "title"
657
+ ]);
658
+ var toEmbedRequest = (body) => {
659
+ const request = {};
660
+ const config = {};
661
+ for (const [key, value] of Object.entries(body)) {
662
+ if (EMBED_CONFIG_KEYS.has(key)) {
663
+ config[key] = value;
664
+ } else {
665
+ request[key] = value;
666
+ }
667
+ }
668
+ if (Object.keys(config).length > 0) {
669
+ request.embedContentConfig = config;
670
+ }
671
+ return request;
672
+ };
472
673
  var submit2 = async (input) => {
473
- const requests = input.built.map((item) => ({
474
- metadata: { key: item.customId },
475
- request: omit(item.body, "stream")
476
- }));
477
- const raw = await requestJson(`${baseUrl2(input.credentials)}/models/${input.modelId}:batchGenerateContent`, {
478
- body: JSON.stringify({
479
- batch: {
480
- display_name: "batchwork",
481
- input_config: { requests: { requests } }
482
- }
483
- }),
674
+ const limits = resolveBatchLimits(input.limits);
675
+ const isEmbedding = input.endpoint.toLowerCase().includes("embedcontent");
676
+ const method = isEmbedding ? "asyncBatchEmbedContent" : "batchGenerateContent";
677
+ const requests = input.built.map((item) => {
678
+ const payload = omit(item.body, "stream");
679
+ return {
680
+ metadata: { key: item.customId },
681
+ request: isEmbedding ? toEmbedRequest(payload) : payload
682
+ };
683
+ });
684
+ const body = encodeJsonArrayPayload({
685
+ items: requests,
686
+ label: "batch upload payload",
687
+ maxBytes: limits.maxUploadBytes,
688
+ prefix: '{"batch":{"display_name":"batchwork","input_config":{"requests":{"requests":[',
689
+ suffix: "]}}}}"
690
+ });
691
+ const raw = await requestJson(`${baseUrl2(input.credentials)}/models/${input.modelId}:${method}`, {
692
+ body,
484
693
  headers: headers2(input.credentials),
485
694
  method: "POST"
486
695
  });
487
696
  return normalizeSnapshot2(raw);
488
697
  };
489
698
  var retrieve2 = async (id, credentials) => {
490
- const raw = await requestJson(`${baseUrl2(credentials)}/${id}`, {
699
+ const operationId = assertPrefixedProviderId("Google operation id", id, GOOGLE_BATCH_PREFIX);
700
+ const raw = await requestJson(`${baseUrl2(credentials)}/${operationId}`, {
491
701
  headers: headers2(credentials)
492
702
  });
493
703
  return normalizeSnapshot2(raw);
@@ -510,7 +720,8 @@ async function* results2(id, credentials) {
510
720
  }
511
721
  }
512
722
  var cancel2 = async (id, credentials) => {
513
- await requestJson(`${baseUrl2(credentials)}/${id}:cancel`, {
723
+ const operationId = assertPrefixedProviderId("Google operation id", id, GOOGLE_BATCH_PREFIX);
724
+ await requestJson(`${baseUrl2(credentials)}/${operationId}:cancel`, {
514
725
  headers: headers2(credentials),
515
726
  method: "POST"
516
727
  });
@@ -544,6 +755,13 @@ var textFromBody = (body) => {
544
755
  }
545
756
  return asString(obj.output_text);
546
757
  };
758
+ var embeddingFromBody = (body) => {
759
+ const data = asArray(asRecord(body).data);
760
+ if (data.length === 0) {
761
+ return;
762
+ }
763
+ return asNumberArray(asRecord(data[0]).embedding);
764
+ };
547
765
  var usageFromBody = (body) => {
548
766
  const usage = asRecord(asRecord(body).usage);
549
767
  const inputTokens = asNumber(usage.prompt_tokens) ?? asNumber(usage.input_tokens);
@@ -584,6 +802,7 @@ var normalizeOpenAIResult = (line) => {
584
802
  if (statusCode >= HTTP_OK_MIN && statusCode < HTTP_OK_MAX) {
585
803
  return {
586
804
  customId,
805
+ embedding: embeddingFromBody(response.body),
587
806
  response: response.body,
588
807
  status: "succeeded",
589
808
  text: textFromBody(response.body),
@@ -607,13 +826,15 @@ var uploadInputFile = async (jsonl, baseUrl3, headers3, options = {}) => {
607
826
  const raw = await requestJson(`${baseUrl3}/files`, {
608
827
  body: form,
609
828
  headers: headers3,
610
- method: "POST"
829
+ method: "POST",
830
+ redirect: "manual"
611
831
  });
612
832
  return raw.id;
613
833
  };
614
834
  async function* streamResultFile(fileId, baseUrl3, headers3) {
615
835
  const stream = await requestStream(`${baseUrl3}/files/${fileId}/content`, {
616
- headers: headers3
836
+ headers: headers3,
837
+ redirect: "manual"
617
838
  });
618
839
  for await (const line of streamJsonl(stream)) {
619
840
  yield normalizeOpenAIResult(line);
@@ -669,6 +890,7 @@ var createOpenAICompatibleAdapter = (config) => {
669
890
  ...credentials.headers
670
891
  });
671
892
  const submit3 = async (input) => {
893
+ const limits = resolveBatchLimits(input.limits);
672
894
  const endpoint = config.normalizeEndpoint ? config.normalizeEndpoint(input.endpoint) : input.endpoint;
673
895
  const jsonl = encodeJsonl(input.built.map((item) => {
674
896
  const body = omit(item.body, "stream");
@@ -681,7 +903,7 @@ var createOpenAICompatibleAdapter = (config) => {
681
903
  method: "POST",
682
904
  url: endpoint
683
905
  };
684
- }));
906
+ }), { label: "batch upload JSONL", maxBytes: limits.maxUploadBytes });
685
907
  const headers3 = authHeaders(input.credentials);
686
908
  const url = baseUrl3(input.credentials);
687
909
  const purpose = config.filePurpose ?? "batch";
@@ -699,7 +921,8 @@ var createOpenAICompatibleAdapter = (config) => {
699
921
  return normalizeSnapshot3(raw, config.id);
700
922
  };
701
923
  const retrieve3 = async (id, credentials) => {
702
- const raw = await requestJson(`${baseUrl3(credentials)}/batches/${id}`, {
924
+ const batchId = assertSimpleProviderId(`${config.id} batch id`, id);
925
+ const raw = await requestJson(`${baseUrl3(credentials)}/batches/${batchId}`, {
703
926
  headers: authHeaders(credentials)
704
927
  });
705
928
  return normalizeSnapshot3(raw, config.id);
@@ -714,14 +937,15 @@ var createOpenAICompatibleAdapter = (config) => {
714
937
  }
715
938
  const headers3 = authHeaders(credentials);
716
939
  if (outputFileId) {
717
- yield* streamResultFile(outputFileId, baseUrl3(credentials), headers3);
940
+ yield* streamResultFile(assertSimpleProviderId(`${config.id} output file id`, outputFileId), baseUrl3(credentials), headers3);
718
941
  }
719
942
  if (errorFileId) {
720
- yield* streamResultFile(errorFileId, baseUrl3(credentials), headers3);
943
+ yield* streamResultFile(assertSimpleProviderId(`${config.id} error file id`, errorFileId), baseUrl3(credentials), headers3);
721
944
  }
722
945
  }
723
946
  const cancel3 = async (id, credentials) => {
724
- await requestJson(`${baseUrl3(credentials)}/batches/${id}/cancel`, {
947
+ const batchId = assertSimpleProviderId(`${config.id} batch id`, id);
948
+ await requestJson(`${baseUrl3(credentials)}/batches/${batchId}/cancel`, {
725
949
  headers: authHeaders(credentials),
726
950
  method: "POST"
727
951
  });
@@ -776,10 +1000,11 @@ var normalizeSnapshot4 = (raw) => {
776
1000
  const obj = asRecord(raw);
777
1001
  const succeeded = asNumber(obj.succeeded_requests) ?? 0;
778
1002
  const failed = asNumber(obj.failed_requests) ?? 0;
1003
+ const id = asString(obj.id) ?? "";
779
1004
  return {
780
1005
  completedAt: toDate(obj.completed_at),
781
1006
  createdAt: toDate(obj.created_at),
782
- id: asString(obj.id) ?? "",
1007
+ id: id ? assertSimpleProviderId("Mistral job id", id) : "",
783
1008
  provider: "mistral",
784
1009
  raw,
785
1010
  requestCounts: {
@@ -791,10 +1016,11 @@ var normalizeSnapshot4 = (raw) => {
791
1016
  };
792
1017
  };
793
1018
  var submit3 = async (input) => {
1019
+ const limits = resolveBatchLimits(input.limits);
794
1020
  const jsonl = encodeJsonl(input.built.map((item) => ({
795
1021
  body: omit(omit(item.body, "stream"), "model"),
796
1022
  custom_id: item.customId
797
- })));
1023
+ })), { label: "batch upload JSONL", maxBytes: limits.maxUploadBytes });
798
1024
  const inputFileId = await uploadInputFile(jsonl, baseUrl3(input.credentials), authHeaders(input.credentials));
799
1025
  const raw = await requestJson(`${baseUrl3(input.credentials)}/batch/jobs`, {
800
1026
  body: JSON.stringify({
@@ -812,7 +1038,8 @@ var submit3 = async (input) => {
812
1038
  return normalizeSnapshot4(raw);
813
1039
  };
814
1040
  var retrieve3 = async (id, credentials) => {
815
- const raw = await requestJson(`${baseUrl3(credentials)}/batch/jobs/${id}`, {
1041
+ const jobId = assertSimpleProviderId("Mistral job id", id);
1042
+ const raw = await requestJson(`${baseUrl3(credentials)}/batch/jobs/${jobId}`, {
816
1043
  headers: authHeaders(credentials)
817
1044
  });
818
1045
  return normalizeSnapshot4(raw);
@@ -824,17 +1051,18 @@ async function* results3(id, credentials) {
824
1051
  const errorFileId = asString(raw.error_file);
825
1052
  const headers3 = authHeaders(credentials);
826
1053
  if (outputFileId) {
827
- yield* streamResultFile(outputFileId, baseUrl3(credentials), headers3);
1054
+ yield* streamResultFile(assertSimpleProviderId("Mistral output file id", outputFileId), baseUrl3(credentials), headers3);
828
1055
  }
829
1056
  if (errorFileId) {
830
- yield* streamResultFile(errorFileId, baseUrl3(credentials), headers3);
1057
+ yield* streamResultFile(assertSimpleProviderId("Mistral error file id", errorFileId), baseUrl3(credentials), headers3);
831
1058
  }
832
1059
  if (!(outputFileId || errorFileId)) {
833
1060
  throw new BatchworkError(`batchwork: results are not ready for batch "${id}" (status: ${snapshot.status}).`);
834
1061
  }
835
1062
  }
836
1063
  var cancel3 = async (id, credentials) => {
837
- await requestJson(`${baseUrl3(credentials)}/batch/jobs/${id}/cancel`, {
1064
+ const jobId = assertSimpleProviderId("Mistral job id", id);
1065
+ await requestJson(`${baseUrl3(credentials)}/batch/jobs/${jobId}/cancel`, {
838
1066
  headers: authHeaders(credentials),
839
1067
  method: "POST"
840
1068
  });
@@ -859,12 +1087,44 @@ var openaiAdapter = createOpenAICompatibleAdapter({
859
1087
  // src/providers/together.ts
860
1088
  var INPUT_FILE_NAME = "batchwork.jsonl";
861
1089
  var HTTP_FOUND = 302;
862
- var safeText2 = async (response) => {
1090
+ var parseIpv4 = (host) => {
1091
+ if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(host)) {
1092
+ return;
1093
+ }
1094
+ const parts = host.split(".").map(Number);
1095
+ const valid = parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255);
1096
+ return valid ? parts : undefined;
1097
+ };
1098
+ var isPrivateIpv4 = (parts) => {
1099
+ const [a = 0, b = 0] = parts;
1100
+ return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a >= 224;
1101
+ };
1102
+ var isPrivateIpv6 = (host) => {
1103
+ const normalized = host.replace(/^\[/u, "").replace(/\]$/u, "").toLowerCase();
1104
+ if (!normalized.includes(":")) {
1105
+ return false;
1106
+ }
1107
+ return normalized === "::" || normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:");
1108
+ };
1109
+ var validateUploadLocation = (location) => {
1110
+ let url;
863
1111
  try {
864
- return await response.text();
865
- } catch {
866
- return "<no body>";
1112
+ url = new URL(location);
1113
+ } catch (error) {
1114
+ throw new BatchworkError("batchwork: Together upload Location must be a valid URL.", { cause: error });
867
1115
  }
1116
+ if (url.protocol !== "https:") {
1117
+ throw new BatchworkError("batchwork: Together upload Location must use https.");
1118
+ }
1119
+ if (url.username || url.password) {
1120
+ throw new BatchworkError("batchwork: Together upload Location must not include credentials.");
1121
+ }
1122
+ const host = url.hostname.toLowerCase();
1123
+ const ipv4 = parseIpv4(host);
1124
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || ipv4 && isPrivateIpv4(ipv4) || isPrivateIpv6(host)) {
1125
+ throw new BatchworkError("batchwork: Together upload Location must not target localhost or private networks.");
1126
+ }
1127
+ return url.toString();
868
1128
  };
869
1129
  var uploadTogetherFile = async (args) => {
870
1130
  const metadata = new FormData;
@@ -880,17 +1140,23 @@ var uploadTogetherFile = async (args) => {
880
1140
  const location = init.headers.get("location");
881
1141
  const fileId = init.headers.get("x-together-file-id");
882
1142
  if (init.status !== HTTP_FOUND || !(location && fileId)) {
883
- throw new BatchworkError(`batchwork: Together upload could not be initiated (${init.status}): ${await safeText2(init)}`);
1143
+ throw new BatchworkError(`batchwork: Together upload could not be initiated (${init.status}).`);
884
1144
  }
885
- const upload = await fetch(location, { body: args.jsonl, method: "PUT" });
1145
+ const uploadLocation = validateUploadLocation(location);
1146
+ const upload = await fetch(uploadLocation, {
1147
+ body: args.jsonl,
1148
+ method: "PUT",
1149
+ redirect: "manual"
1150
+ });
886
1151
  if (!upload.ok) {
887
- throw new BatchworkError(`batchwork: Together file upload failed (${upload.status}): ${await safeText2(upload)}`);
1152
+ throw new BatchworkError(`batchwork: Together file upload failed (${upload.status}).`);
888
1153
  }
889
- await requestJson(`${args.baseUrl}/files/${fileId}/preprocess`, {
1154
+ const safeFileId = assertSimpleProviderId("Together file id", fileId);
1155
+ await requestJson(`${args.baseUrl}/files/${safeFileId}/preprocess`, {
890
1156
  headers: args.headers,
891
1157
  method: "POST"
892
1158
  });
893
- return fileId;
1159
+ return safeFileId;
894
1160
  };
895
1161
  var togetherAdapter = createOpenAICompatibleAdapter({
896
1162
  apiKeyEnv: "TOGETHER_API_KEY",
@@ -932,11 +1198,12 @@ var deriveStatus = (state) => {
932
1198
  var normalizeSnapshot5 = (raw) => {
933
1199
  const obj = asRecord(raw);
934
1200
  const state = asRecord(obj.state);
1201
+ const id = asString(obj.batch_id) ?? asString(obj.id) ?? "";
935
1202
  return {
936
- completedAt: toDate(obj.cancel_time),
1203
+ completedAt: toDate(obj.finish_time ?? obj.completed_at),
937
1204
  createdAt: toDate(obj.create_time),
938
1205
  expiresAt: toDate(obj.expire_time ?? obj.expires_at),
939
- id: asString(obj.batch_id) ?? asString(obj.id) ?? "",
1206
+ id: id ? assertSimpleProviderId("xAI batch id", id) : "",
940
1207
  provider: "xai",
941
1208
  raw,
942
1209
  requestCounts: {
@@ -977,12 +1244,13 @@ var normalizeResult3 = (item) => {
977
1244
  };
978
1245
  };
979
1246
  var submit4 = async (input) => {
1247
+ const limits = resolveBatchLimits(input.limits);
980
1248
  const jsonl = encodeJsonl(input.built.map((item) => ({
981
1249
  body: omit(item.body, "stream"),
982
1250
  custom_id: item.customId,
983
1251
  method: "POST",
984
1252
  url: input.endpoint
985
- })));
1253
+ })), { label: "batch upload JSONL", maxBytes: limits.maxUploadBytes });
986
1254
  const inputFileId = await uploadInputFile(jsonl, baseUrl4(input.credentials), authHeaders2(input.credentials), { purpose: null });
987
1255
  const raw = await requestJson(`${baseUrl4(input.credentials)}/batches`, {
988
1256
  body: JSON.stringify({ input_file_id: inputFileId, name: "batchwork" }),
@@ -995,12 +1263,14 @@ var submit4 = async (input) => {
995
1263
  return normalizeSnapshot5(raw);
996
1264
  };
997
1265
  var retrieve4 = async (id, credentials) => {
998
- const raw = await requestJson(`${baseUrl4(credentials)}/batches/${id}`, {
1266
+ const batchId = assertSimpleProviderId("xAI batch id", id);
1267
+ const raw = await requestJson(`${baseUrl4(credentials)}/batches/${batchId}`, {
999
1268
  headers: authHeaders2(credentials)
1000
1269
  });
1001
1270
  return normalizeSnapshot5(raw);
1002
1271
  };
1003
1272
  async function* results4(id, credentials) {
1273
+ const batchId = assertSimpleProviderId("xAI batch id", id);
1004
1274
  const headers3 = authHeaders2(credentials);
1005
1275
  let token;
1006
1276
  do {
@@ -1008,7 +1278,7 @@ async function* results4(id, credentials) {
1008
1278
  if (token) {
1009
1279
  query.set("pagination_token", token);
1010
1280
  }
1011
- const raw = await requestJson(`${baseUrl4(credentials)}/batches/${id}/results?${query.toString()}`, { headers: headers3 });
1281
+ const raw = await requestJson(`${baseUrl4(credentials)}/batches/${batchId}/results?${query.toString()}`, { headers: headers3 });
1012
1282
  const page = asRecord(raw);
1013
1283
  for (const item of Array.isArray(page.results) ? page.results : []) {
1014
1284
  yield normalizeResult3(item);
@@ -1017,7 +1287,8 @@ async function* results4(id, credentials) {
1017
1287
  } while (token);
1018
1288
  }
1019
1289
  var cancel4 = async (id, credentials) => {
1020
- await requestJson(`${baseUrl4(credentials)}/batches/${id}:cancel`, {
1290
+ const batchId = assertSimpleProviderId("xAI batch id", id);
1291
+ await requestJson(`${baseUrl4(credentials)}/batches/${batchId}:cancel`, {
1021
1292
  headers: authHeaders2(credentials),
1022
1293
  method: "POST"
1023
1294
  });
@@ -1042,7 +1313,7 @@ var adapters = {
1042
1313
  };
1043
1314
  var getAdapter = (provider) => adapters[provider];
1044
1315
 
1045
- export { BatchworkError, UnsupportedProviderError, MissingDependencyError, isTerminalStatus, BatchJob, getAdapter };
1316
+ export { BatchworkError, UnsupportedProviderError, MissingDependencyError, resolveBatchLimits, assertByteLength, mapWithConcurrency, isTerminalStatus, BatchJob, getAdapter };
1046
1317
 
1047
- //# debugId=DA60AE45A8F12B3C64756E2164756E21
1048
- //# sourceMappingURL=chunk-kv3847wy.js.map
1318
+ //# debugId=A771CB719D6F2AEA64756E2164756E21
1319
+ //# sourceMappingURL=chunk-xtk2ycy6.js.map