batchwork 1.0.0 → 1.0.1

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 (44) hide show
  1. package/dist/batch.d.ts.map +1 -1
  2. package/dist/body.d.ts +3 -2
  3. package/dist/body.d.ts.map +1 -1
  4. package/dist/{chunk-zp2cxkyb.js → chunk-e6qn48qa.js} +88 -13
  5. package/dist/chunk-e6qn48qa.js.map +13 -0
  6. package/dist/{chunk-kv3847wy.js → chunk-g481f961.js} +220 -56
  7. package/dist/chunk-g481f961.js.map +26 -0
  8. package/dist/{chunk-ab2d71gk.js → chunk-m4n610nm.js} +20 -7
  9. package/dist/chunk-m4n610nm.js.map +12 -0
  10. package/dist/http.d.ts.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/jsonl.d.ts +5 -15
  15. package/dist/jsonl.d.ts.map +1 -1
  16. package/dist/limits.d.ts +12 -0
  17. package/dist/limits.d.ts.map +1 -0
  18. package/dist/next/index.d.ts +3 -1
  19. package/dist/next/index.d.ts.map +1 -1
  20. package/dist/next/index.js +7 -4
  21. package/dist/next/index.js.map +3 -3
  22. package/dist/providers/adapter.d.ts +2 -1
  23. package/dist/providers/adapter.d.ts.map +1 -1
  24. package/dist/providers/anthropic.d.ts.map +1 -1
  25. package/dist/providers/google.d.ts.map +1 -1
  26. package/dist/providers/ids.d.ts +3 -0
  27. package/dist/providers/ids.d.ts.map +1 -0
  28. package/dist/providers/mistral.d.ts.map +1 -1
  29. package/dist/providers/openai-compatible.d.ts.map +1 -1
  30. package/dist/providers/together.d.ts.map +1 -1
  31. package/dist/providers/xai.d.ts.map +1 -1
  32. package/dist/server/index.d.ts +2 -2
  33. package/dist/server/index.d.ts.map +1 -1
  34. package/dist/server/index.js +2 -2
  35. package/dist/server/poller.d.ts +3 -0
  36. package/dist/server/poller.d.ts.map +1 -1
  37. package/dist/server/signing.d.ts +9 -2
  38. package/dist/server/signing.d.ts.map +1 -1
  39. package/dist/types.d.ts +7 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/chunk-ab2d71gk.js.map +0 -12
  43. package/dist/chunk-kv3847wy.js.map +0 -24
  44. package/dist/chunk-zp2cxkyb.js.map +0 -13
@@ -103,28 +103,65 @@ class BatchJob {
103
103
  }
104
104
  }
105
105
 
106
- // src/http.ts
107
- var safeText = async (response) => {
108
- try {
109
- return await response.text();
110
- } catch {
111
- return "<no body>";
106
+ // src/limits.ts
107
+ var DEFAULT_LIMITS = {
108
+ captureConcurrency: 16,
109
+ maxRequestBytes: 20 * 1024 * 1024,
110
+ maxRequests: 50000,
111
+ maxUploadBytes: 200 * 1024 * 1024
112
+ };
113
+ var encoder = new TextEncoder;
114
+ var positiveInteger = (name, value) => {
115
+ if (!(Number.isInteger(value) && value > 0)) {
116
+ throw new BatchworkError(`batchwork: limits.${name} must be a positive integer.`);
117
+ }
118
+ return value;
119
+ };
120
+ var resolveBatchLimits = (limits) => ({
121
+ captureConcurrency: positiveInteger("captureConcurrency", limits?.captureConcurrency ?? DEFAULT_LIMITS.captureConcurrency),
122
+ maxRequestBytes: positiveInteger("maxRequestBytes", limits?.maxRequestBytes ?? DEFAULT_LIMITS.maxRequestBytes),
123
+ maxRequests: positiveInteger("maxRequests", limits?.maxRequests ?? DEFAULT_LIMITS.maxRequests),
124
+ maxUploadBytes: positiveInteger("maxUploadBytes", limits?.maxUploadBytes ?? DEFAULT_LIMITS.maxUploadBytes)
125
+ });
126
+ var byteLength = (value) => encoder.encode(value).length;
127
+ var assertByteLength = (label, value, maxBytes) => {
128
+ const bytes = byteLength(value);
129
+ if (bytes > maxBytes) {
130
+ throw new BatchworkError(`batchwork: ${label} is ${bytes} bytes, exceeding the ${maxBytes} byte limit.`);
112
131
  }
113
132
  };
114
- var assertOk = async (url, init, response) => {
133
+ var mapWithConcurrency = async (items, concurrency, mapper) => {
134
+ const results = [];
135
+ results.length = items.length;
136
+ let nextIndex = 0;
137
+ const workerCount = Math.min(concurrency, items.length);
138
+ const runNext = async () => {
139
+ const index = nextIndex;
140
+ nextIndex += 1;
141
+ if (index >= items.length) {
142
+ return;
143
+ }
144
+ results[index] = await mapper(items[index]);
145
+ await runNext();
146
+ };
147
+ await Promise.all(Array.from({ length: workerCount }, () => runNext()));
148
+ return results;
149
+ };
150
+
151
+ // src/http.ts
152
+ var assertOk = (url, init, response) => {
115
153
  if (!response.ok) {
116
- const detail = await safeText(response);
117
- throw new BatchworkError(`batchwork: ${init.method ?? "GET"} ${url} failed with ${response.status}: ${detail}`);
154
+ throw new BatchworkError(`batchwork: ${init.method ?? "GET"} ${url} failed with ${response.status}.`);
118
155
  }
119
156
  };
120
157
  var requestJson = async (url, init) => {
121
158
  const response = await fetch(url, init);
122
- await assertOk(url, init, response);
159
+ assertOk(url, init, response);
123
160
  return await response.json();
124
161
  };
125
162
  var requestStream = async (url, init) => {
126
163
  const response = await fetch(url, init);
127
- await assertOk(url, init, response);
164
+ assertOk(url, init, response);
128
165
  if (!response.body) {
129
166
  throw new BatchworkError(`batchwork: ${url} returned an empty body.`);
130
167
  }
@@ -134,6 +171,32 @@ var requestStream = async (url, init) => {
134
171
  // src/jsonl.ts
135
172
  var NEWLINE = `
136
173
  `;
174
+ var DEFAULT_MAX_JSONL_LINE_BYTES = 20 * 1024 * 1024;
175
+ var resolveMaxLineBytes = (options) => {
176
+ const maxLineBytes = options?.maxLineBytes ?? DEFAULT_MAX_JSONL_LINE_BYTES;
177
+ if (!(Number.isInteger(maxLineBytes) && maxLineBytes > 0)) {
178
+ throw new BatchworkError("batchwork: JSONL maxLineBytes must be a positive integer.");
179
+ }
180
+ return maxLineBytes;
181
+ };
182
+ var assertLineSize = (line, lineNumber, maxLineBytes) => {
183
+ const bytes = byteLength(line);
184
+ if (bytes > maxLineBytes) {
185
+ throw new BatchworkError(`batchwork: JSONL line ${lineNumber} is ${bytes} bytes, exceeding the ${maxLineBytes} byte limit.`);
186
+ }
187
+ };
188
+ var parseLine = (line, lineNumber, maxLineBytes) => {
189
+ assertLineSize(line, lineNumber, maxLineBytes);
190
+ const trimmed = line.trim();
191
+ if (trimmed.length === 0) {
192
+ return;
193
+ }
194
+ try {
195
+ return JSON.parse(trimmed);
196
+ } catch (error) {
197
+ throw new BatchworkError(`batchwork: invalid JSONL at line ${lineNumber}.`, { cause: error });
198
+ }
199
+ };
137
200
  var encodeJsonl = (items) => {
138
201
  if (items.length === 0) {
139
202
  return "";
@@ -160,25 +223,30 @@ async function* toByteIterable(source) {
160
223
  }
161
224
  yield* source;
162
225
  }
163
- async function* streamJsonl(source) {
226
+ async function* streamJsonl(source, options) {
164
227
  const decoder = new TextDecoder;
228
+ const maxLineBytes = resolveMaxLineBytes(options);
165
229
  let buffer = "";
230
+ let lineNumber = 1;
166
231
  for await (const chunk of toByteIterable(source)) {
167
232
  buffer += decoder.decode(chunk, { stream: true });
168
233
  let newlineIndex = buffer.indexOf(NEWLINE);
169
234
  while (newlineIndex !== -1) {
170
- const line = buffer.slice(0, newlineIndex).trim();
235
+ const line = buffer.slice(0, newlineIndex);
171
236
  buffer = buffer.slice(newlineIndex + 1);
172
- if (line.length > 0) {
173
- yield JSON.parse(line);
237
+ const parsed2 = parseLine(line, lineNumber, maxLineBytes);
238
+ if (parsed2 !== undefined) {
239
+ yield parsed2;
174
240
  }
241
+ lineNumber += 1;
175
242
  newlineIndex = buffer.indexOf(NEWLINE);
176
243
  }
244
+ assertLineSize(buffer, lineNumber, maxLineBytes);
177
245
  }
178
246
  buffer += decoder.decode();
179
- const tail = buffer.trim();
180
- if (tail.length > 0) {
181
- yield JSON.parse(tail);
247
+ const parsed = parseLine(buffer, lineNumber, maxLineBytes);
248
+ if (parsed !== undefined) {
249
+ yield parsed;
182
250
  }
183
251
  }
184
252
 
@@ -210,6 +278,22 @@ var toDate = (value) => {
210
278
  }
211
279
  };
212
280
 
281
+ // src/providers/ids.ts
282
+ var SIMPLE_PROVIDER_ID = /^[A-Za-z0-9_-]+$/u;
283
+ var assertSimpleProviderId = (label, id) => {
284
+ if (!SIMPLE_PROVIDER_ID.test(id)) {
285
+ throw new BatchworkError(`batchwork: invalid ${label}.`);
286
+ }
287
+ return id;
288
+ };
289
+ var assertPrefixedProviderId = (label, id, prefix) => {
290
+ const [actualPrefix, value, ...rest] = id.split("/");
291
+ if (rest.length > 0 || actualPrefix !== prefix || !value || !SIMPLE_PROVIDER_ID.test(value)) {
292
+ throw new BatchworkError(`batchwork: invalid ${label}.`);
293
+ }
294
+ return id;
295
+ };
296
+
213
297
  // src/providers/anthropic.ts
214
298
  var ANTHROPIC_BASE = "https://api.anthropic.com";
215
299
  var ANTHROPIC_VERSION = "2023-06-01";
@@ -221,6 +305,25 @@ var apiKey = (credentials) => {
221
305
  return key;
222
306
  };
223
307
  var baseUrl = (credentials) => credentials.baseURL ?? ANTHROPIC_BASE;
308
+ var validateResultsUrl = (rawUrl, credentials) => {
309
+ let resultsUrl;
310
+ let expectedBase;
311
+ try {
312
+ resultsUrl = new URL(rawUrl);
313
+ expectedBase = new URL(baseUrl(credentials));
314
+ } catch (error) {
315
+ throw new BatchworkError("batchwork: invalid Anthropic results_url.", {
316
+ cause: error
317
+ });
318
+ }
319
+ if (resultsUrl.origin !== expectedBase.origin) {
320
+ throw new BatchworkError("batchwork: Anthropic results_url must match the configured API origin.");
321
+ }
322
+ if (resultsUrl.username || resultsUrl.password) {
323
+ throw new BatchworkError("batchwork: Anthropic results_url must not include credentials.");
324
+ }
325
+ return resultsUrl.toString();
326
+ };
224
327
  var headers = (credentials) => ({
225
328
  "anthropic-version": ANTHROPIC_VERSION,
226
329
  "content-type": "application/json",
@@ -313,19 +416,23 @@ var normalizeResult = (line) => {
313
416
  return { customId, status: "canceled" };
314
417
  };
315
418
  var submit = async (input) => {
419
+ const limits = resolveBatchLimits(input.limits);
316
420
  const requests = input.built.map((item) => ({
317
421
  custom_id: item.customId,
318
422
  params: omit(item.body, "stream")
319
423
  }));
424
+ const body = JSON.stringify({ requests });
425
+ assertByteLength("batch upload payload", body, limits.maxUploadBytes);
320
426
  const raw = await requestJson(`${baseUrl(input.credentials)}/v1/messages/batches`, {
321
- body: JSON.stringify({ requests }),
427
+ body,
322
428
  headers: headers(input.credentials),
323
429
  method: "POST"
324
430
  });
325
431
  return normalizeSnapshot(raw);
326
432
  };
327
433
  var retrieve = async (id, credentials) => {
328
- const raw = await requestJson(`${baseUrl(credentials)}/v1/messages/batches/${id}`, { headers: headers(credentials) });
434
+ const batchId = assertSimpleProviderId("Anthropic batch id", id);
435
+ const raw = await requestJson(`${baseUrl(credentials)}/v1/messages/batches/${batchId}`, { headers: headers(credentials) });
329
436
  return normalizeSnapshot(raw);
330
437
  };
331
438
  async function* results(id, credentials) {
@@ -334,7 +441,7 @@ async function* results(id, credentials) {
334
441
  if (!resultsUrl) {
335
442
  throw new BatchworkError(`batchwork: results are not ready for batch "${id}" (status: ${snapshot.status}).`);
336
443
  }
337
- const stream = await requestStream(resultsUrl, {
444
+ const stream = await requestStream(validateResultsUrl(resultsUrl, credentials), {
338
445
  headers: headers(credentials)
339
446
  });
340
447
  for await (const line of streamJsonl(stream)) {
@@ -342,7 +449,8 @@ async function* results(id, credentials) {
342
449
  }
343
450
  }
344
451
  var cancel = async (id, credentials) => {
345
- await requestJson(`${baseUrl(credentials)}/v1/messages/batches/${id}/cancel`, {
452
+ const batchId = assertSimpleProviderId("Anthropic batch id", id);
453
+ await requestJson(`${baseUrl(credentials)}/v1/messages/batches/${batchId}/cancel`, {
346
454
  headers: headers(credentials),
347
455
  method: "POST"
348
456
  });
@@ -357,6 +465,7 @@ var anthropicAdapter = {
357
465
 
358
466
  // src/providers/google.ts
359
467
  var GOOGLE_BASE = "https://generativelanguage.googleapis.com/v1beta";
468
+ var GOOGLE_BATCH_PREFIX = "batches";
360
469
  var apiKey2 = (credentials) => {
361
470
  const key = credentials.apiKey ?? process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? process.env.GEMINI_API_KEY;
362
471
  if (!key) {
@@ -414,8 +523,9 @@ var normalizeSnapshot2 = (raw) => {
414
523
  const obj = asRecord(raw);
415
524
  const items = inlinedResponses(raw);
416
525
  const failed = items.filter((item) => asRecord(item).error).length;
526
+ const id = asString(obj.name) ?? "";
417
527
  return {
418
- id: asString(obj.name) ?? "",
528
+ id: id ? assertPrefixedProviderId("Google operation id", id, GOOGLE_BATCH_PREFIX) : "",
419
529
  provider: "google",
420
530
  raw,
421
531
  requestCounts: {
@@ -470,24 +580,28 @@ var normalizeResult2 = (item) => {
470
580
  };
471
581
  };
472
582
  var submit2 = async (input) => {
583
+ const limits = resolveBatchLimits(input.limits);
473
584
  const requests = input.built.map((item) => ({
474
585
  metadata: { key: item.customId },
475
586
  request: omit(item.body, "stream")
476
587
  }));
588
+ const body = JSON.stringify({
589
+ batch: {
590
+ display_name: "batchwork",
591
+ input_config: { requests: { requests } }
592
+ }
593
+ });
594
+ assertByteLength("batch upload payload", body, limits.maxUploadBytes);
477
595
  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
- }),
596
+ body,
484
597
  headers: headers2(input.credentials),
485
598
  method: "POST"
486
599
  });
487
600
  return normalizeSnapshot2(raw);
488
601
  };
489
602
  var retrieve2 = async (id, credentials) => {
490
- const raw = await requestJson(`${baseUrl2(credentials)}/${id}`, {
603
+ const operationId = assertPrefixedProviderId("Google operation id", id, GOOGLE_BATCH_PREFIX);
604
+ const raw = await requestJson(`${baseUrl2(credentials)}/${operationId}`, {
491
605
  headers: headers2(credentials)
492
606
  });
493
607
  return normalizeSnapshot2(raw);
@@ -510,7 +624,8 @@ async function* results2(id, credentials) {
510
624
  }
511
625
  }
512
626
  var cancel2 = async (id, credentials) => {
513
- await requestJson(`${baseUrl2(credentials)}/${id}:cancel`, {
627
+ const operationId = assertPrefixedProviderId("Google operation id", id, GOOGLE_BATCH_PREFIX);
628
+ await requestJson(`${baseUrl2(credentials)}/${operationId}:cancel`, {
514
629
  headers: headers2(credentials),
515
630
  method: "POST"
516
631
  });
@@ -669,6 +784,7 @@ var createOpenAICompatibleAdapter = (config) => {
669
784
  ...credentials.headers
670
785
  });
671
786
  const submit3 = async (input) => {
787
+ const limits = resolveBatchLimits(input.limits);
672
788
  const endpoint = config.normalizeEndpoint ? config.normalizeEndpoint(input.endpoint) : input.endpoint;
673
789
  const jsonl = encodeJsonl(input.built.map((item) => {
674
790
  const body = omit(item.body, "stream");
@@ -682,6 +798,7 @@ var createOpenAICompatibleAdapter = (config) => {
682
798
  url: endpoint
683
799
  };
684
800
  }));
801
+ assertByteLength("batch upload JSONL", jsonl, limits.maxUploadBytes);
685
802
  const headers3 = authHeaders(input.credentials);
686
803
  const url = baseUrl3(input.credentials);
687
804
  const purpose = config.filePurpose ?? "batch";
@@ -699,7 +816,8 @@ var createOpenAICompatibleAdapter = (config) => {
699
816
  return normalizeSnapshot3(raw, config.id);
700
817
  };
701
818
  const retrieve3 = async (id, credentials) => {
702
- const raw = await requestJson(`${baseUrl3(credentials)}/batches/${id}`, {
819
+ const batchId = assertSimpleProviderId(`${config.id} batch id`, id);
820
+ const raw = await requestJson(`${baseUrl3(credentials)}/batches/${batchId}`, {
703
821
  headers: authHeaders(credentials)
704
822
  });
705
823
  return normalizeSnapshot3(raw, config.id);
@@ -714,14 +832,15 @@ var createOpenAICompatibleAdapter = (config) => {
714
832
  }
715
833
  const headers3 = authHeaders(credentials);
716
834
  if (outputFileId) {
717
- yield* streamResultFile(outputFileId, baseUrl3(credentials), headers3);
835
+ yield* streamResultFile(assertSimpleProviderId(`${config.id} output file id`, outputFileId), baseUrl3(credentials), headers3);
718
836
  }
719
837
  if (errorFileId) {
720
- yield* streamResultFile(errorFileId, baseUrl3(credentials), headers3);
838
+ yield* streamResultFile(assertSimpleProviderId(`${config.id} error file id`, errorFileId), baseUrl3(credentials), headers3);
721
839
  }
722
840
  }
723
841
  const cancel3 = async (id, credentials) => {
724
- await requestJson(`${baseUrl3(credentials)}/batches/${id}/cancel`, {
842
+ const batchId = assertSimpleProviderId(`${config.id} batch id`, id);
843
+ await requestJson(`${baseUrl3(credentials)}/batches/${batchId}/cancel`, {
725
844
  headers: authHeaders(credentials),
726
845
  method: "POST"
727
846
  });
@@ -776,10 +895,11 @@ var normalizeSnapshot4 = (raw) => {
776
895
  const obj = asRecord(raw);
777
896
  const succeeded = asNumber(obj.succeeded_requests) ?? 0;
778
897
  const failed = asNumber(obj.failed_requests) ?? 0;
898
+ const id = asString(obj.id) ?? "";
779
899
  return {
780
900
  completedAt: toDate(obj.completed_at),
781
901
  createdAt: toDate(obj.created_at),
782
- id: asString(obj.id) ?? "",
902
+ id: id ? assertSimpleProviderId("Mistral job id", id) : "",
783
903
  provider: "mistral",
784
904
  raw,
785
905
  requestCounts: {
@@ -791,10 +911,12 @@ var normalizeSnapshot4 = (raw) => {
791
911
  };
792
912
  };
793
913
  var submit3 = async (input) => {
914
+ const limits = resolveBatchLimits(input.limits);
794
915
  const jsonl = encodeJsonl(input.built.map((item) => ({
795
916
  body: omit(omit(item.body, "stream"), "model"),
796
917
  custom_id: item.customId
797
918
  })));
919
+ assertByteLength("batch upload JSONL", jsonl, limits.maxUploadBytes);
798
920
  const inputFileId = await uploadInputFile(jsonl, baseUrl3(input.credentials), authHeaders(input.credentials));
799
921
  const raw = await requestJson(`${baseUrl3(input.credentials)}/batch/jobs`, {
800
922
  body: JSON.stringify({
@@ -812,7 +934,8 @@ var submit3 = async (input) => {
812
934
  return normalizeSnapshot4(raw);
813
935
  };
814
936
  var retrieve3 = async (id, credentials) => {
815
- const raw = await requestJson(`${baseUrl3(credentials)}/batch/jobs/${id}`, {
937
+ const jobId = assertSimpleProviderId("Mistral job id", id);
938
+ const raw = await requestJson(`${baseUrl3(credentials)}/batch/jobs/${jobId}`, {
816
939
  headers: authHeaders(credentials)
817
940
  });
818
941
  return normalizeSnapshot4(raw);
@@ -824,17 +947,18 @@ async function* results3(id, credentials) {
824
947
  const errorFileId = asString(raw.error_file);
825
948
  const headers3 = authHeaders(credentials);
826
949
  if (outputFileId) {
827
- yield* streamResultFile(outputFileId, baseUrl3(credentials), headers3);
950
+ yield* streamResultFile(assertSimpleProviderId("Mistral output file id", outputFileId), baseUrl3(credentials), headers3);
828
951
  }
829
952
  if (errorFileId) {
830
- yield* streamResultFile(errorFileId, baseUrl3(credentials), headers3);
953
+ yield* streamResultFile(assertSimpleProviderId("Mistral error file id", errorFileId), baseUrl3(credentials), headers3);
831
954
  }
832
955
  if (!(outputFileId || errorFileId)) {
833
956
  throw new BatchworkError(`batchwork: results are not ready for batch "${id}" (status: ${snapshot.status}).`);
834
957
  }
835
958
  }
836
959
  var cancel3 = async (id, credentials) => {
837
- await requestJson(`${baseUrl3(credentials)}/batch/jobs/${id}/cancel`, {
960
+ const jobId = assertSimpleProviderId("Mistral job id", id);
961
+ await requestJson(`${baseUrl3(credentials)}/batch/jobs/${jobId}/cancel`, {
838
962
  headers: authHeaders(credentials),
839
963
  method: "POST"
840
964
  });
@@ -859,12 +983,41 @@ var openaiAdapter = createOpenAICompatibleAdapter({
859
983
  // src/providers/together.ts
860
984
  var INPUT_FILE_NAME = "batchwork.jsonl";
861
985
  var HTTP_FOUND = 302;
862
- var safeText2 = async (response) => {
986
+ var parseIpv4 = (host) => {
987
+ if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(host)) {
988
+ return;
989
+ }
990
+ const parts = host.split(".").map(Number);
991
+ const valid = parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255);
992
+ return valid ? parts : undefined;
993
+ };
994
+ var isPrivateIpv4 = (parts) => {
995
+ const [a = 0, b = 0] = parts;
996
+ 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;
997
+ };
998
+ var isPrivateIpv6 = (host) => {
999
+ const normalized = host.replace(/^\[/u, "").replace(/\]$/u, "").toLowerCase();
1000
+ return normalized === "::" || normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:");
1001
+ };
1002
+ var validateUploadLocation = (location) => {
1003
+ let url;
863
1004
  try {
864
- return await response.text();
865
- } catch {
866
- return "<no body>";
1005
+ url = new URL(location);
1006
+ } catch (error) {
1007
+ throw new BatchworkError("batchwork: Together upload Location must be a valid URL.", { cause: error });
1008
+ }
1009
+ if (url.protocol !== "https:") {
1010
+ throw new BatchworkError("batchwork: Together upload Location must use https.");
1011
+ }
1012
+ if (url.username || url.password) {
1013
+ throw new BatchworkError("batchwork: Together upload Location must not include credentials.");
867
1014
  }
1015
+ const host = url.hostname.toLowerCase();
1016
+ const ipv4 = parseIpv4(host);
1017
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || ipv4 && isPrivateIpv4(ipv4) || isPrivateIpv6(host)) {
1018
+ throw new BatchworkError("batchwork: Together upload Location must not target localhost or private networks.");
1019
+ }
1020
+ return url.toString();
868
1021
  };
869
1022
  var uploadTogetherFile = async (args) => {
870
1023
  const metadata = new FormData;
@@ -880,17 +1033,22 @@ var uploadTogetherFile = async (args) => {
880
1033
  const location = init.headers.get("location");
881
1034
  const fileId = init.headers.get("x-together-file-id");
882
1035
  if (init.status !== HTTP_FOUND || !(location && fileId)) {
883
- throw new BatchworkError(`batchwork: Together upload could not be initiated (${init.status}): ${await safeText2(init)}`);
1036
+ throw new BatchworkError(`batchwork: Together upload could not be initiated (${init.status}).`);
884
1037
  }
885
- const upload = await fetch(location, { body: args.jsonl, method: "PUT" });
1038
+ const uploadLocation = validateUploadLocation(location);
1039
+ const upload = await fetch(uploadLocation, {
1040
+ body: args.jsonl,
1041
+ method: "PUT"
1042
+ });
886
1043
  if (!upload.ok) {
887
- throw new BatchworkError(`batchwork: Together file upload failed (${upload.status}): ${await safeText2(upload)}`);
1044
+ throw new BatchworkError(`batchwork: Together file upload failed (${upload.status}).`);
888
1045
  }
889
- await requestJson(`${args.baseUrl}/files/${fileId}/preprocess`, {
1046
+ const safeFileId = assertSimpleProviderId("Together file id", fileId);
1047
+ await requestJson(`${args.baseUrl}/files/${safeFileId}/preprocess`, {
890
1048
  headers: args.headers,
891
1049
  method: "POST"
892
1050
  });
893
- return fileId;
1051
+ return safeFileId;
894
1052
  };
895
1053
  var togetherAdapter = createOpenAICompatibleAdapter({
896
1054
  apiKeyEnv: "TOGETHER_API_KEY",
@@ -932,11 +1090,12 @@ var deriveStatus = (state) => {
932
1090
  var normalizeSnapshot5 = (raw) => {
933
1091
  const obj = asRecord(raw);
934
1092
  const state = asRecord(obj.state);
1093
+ const id = asString(obj.batch_id) ?? asString(obj.id) ?? "";
935
1094
  return {
936
1095
  completedAt: toDate(obj.cancel_time),
937
1096
  createdAt: toDate(obj.create_time),
938
1097
  expiresAt: toDate(obj.expire_time ?? obj.expires_at),
939
- id: asString(obj.batch_id) ?? asString(obj.id) ?? "",
1098
+ id: id ? assertSimpleProviderId("xAI batch id", id) : "",
940
1099
  provider: "xai",
941
1100
  raw,
942
1101
  requestCounts: {
@@ -977,12 +1136,14 @@ var normalizeResult3 = (item) => {
977
1136
  };
978
1137
  };
979
1138
  var submit4 = async (input) => {
1139
+ const limits = resolveBatchLimits(input.limits);
980
1140
  const jsonl = encodeJsonl(input.built.map((item) => ({
981
1141
  body: omit(item.body, "stream"),
982
1142
  custom_id: item.customId,
983
1143
  method: "POST",
984
1144
  url: input.endpoint
985
1145
  })));
1146
+ assertByteLength("batch upload JSONL", jsonl, limits.maxUploadBytes);
986
1147
  const inputFileId = await uploadInputFile(jsonl, baseUrl4(input.credentials), authHeaders2(input.credentials), { purpose: null });
987
1148
  const raw = await requestJson(`${baseUrl4(input.credentials)}/batches`, {
988
1149
  body: JSON.stringify({ input_file_id: inputFileId, name: "batchwork" }),
@@ -995,12 +1156,14 @@ var submit4 = async (input) => {
995
1156
  return normalizeSnapshot5(raw);
996
1157
  };
997
1158
  var retrieve4 = async (id, credentials) => {
998
- const raw = await requestJson(`${baseUrl4(credentials)}/batches/${id}`, {
1159
+ const batchId = assertSimpleProviderId("xAI batch id", id);
1160
+ const raw = await requestJson(`${baseUrl4(credentials)}/batches/${batchId}`, {
999
1161
  headers: authHeaders2(credentials)
1000
1162
  });
1001
1163
  return normalizeSnapshot5(raw);
1002
1164
  };
1003
1165
  async function* results4(id, credentials) {
1166
+ const batchId = assertSimpleProviderId("xAI batch id", id);
1004
1167
  const headers3 = authHeaders2(credentials);
1005
1168
  let token;
1006
1169
  do {
@@ -1008,7 +1171,7 @@ async function* results4(id, credentials) {
1008
1171
  if (token) {
1009
1172
  query.set("pagination_token", token);
1010
1173
  }
1011
- const raw = await requestJson(`${baseUrl4(credentials)}/batches/${id}/results?${query.toString()}`, { headers: headers3 });
1174
+ const raw = await requestJson(`${baseUrl4(credentials)}/batches/${batchId}/results?${query.toString()}`, { headers: headers3 });
1012
1175
  const page = asRecord(raw);
1013
1176
  for (const item of Array.isArray(page.results) ? page.results : []) {
1014
1177
  yield normalizeResult3(item);
@@ -1017,7 +1180,8 @@ async function* results4(id, credentials) {
1017
1180
  } while (token);
1018
1181
  }
1019
1182
  var cancel4 = async (id, credentials) => {
1020
- await requestJson(`${baseUrl4(credentials)}/batches/${id}:cancel`, {
1183
+ const batchId = assertSimpleProviderId("xAI batch id", id);
1184
+ await requestJson(`${baseUrl4(credentials)}/batches/${batchId}:cancel`, {
1021
1185
  headers: authHeaders2(credentials),
1022
1186
  method: "POST"
1023
1187
  });
@@ -1042,7 +1206,7 @@ var adapters = {
1042
1206
  };
1043
1207
  var getAdapter = (provider) => adapters[provider];
1044
1208
 
1045
- export { BatchworkError, UnsupportedProviderError, MissingDependencyError, isTerminalStatus, BatchJob, getAdapter };
1209
+ export { BatchworkError, UnsupportedProviderError, MissingDependencyError, resolveBatchLimits, assertByteLength, mapWithConcurrency, isTerminalStatus, BatchJob, getAdapter };
1046
1210
 
1047
- //# debugId=DA60AE45A8F12B3C64756E2164756E21
1048
- //# sourceMappingURL=chunk-kv3847wy.js.map
1211
+ //# debugId=A38719FEA6433ECA64756E2164756E21
1212
+ //# sourceMappingURL=chunk-g481f961.js.map