@beam-network/sdk 0.5.15 → 0.5.16
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 +57 -2
- package/dist/client.d.ts +18 -0
- package/dist/client.js +248 -6
- package/dist/huggingface.d.ts +101 -0
- package/dist/huggingface.js +298 -0
- package/dist/models.d.ts +53 -1
- package/dist/models.js +19 -0
- package/dist/nats-control.d.ts +2 -2
- package/dist/nats-control.js +2 -2
- package/dist/provider-signing.d.ts +4 -1
- package/dist/provider-signing.js +66 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ console.log(status.status);
|
|
|
43
43
|
await beam.close();
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
The main `createTransfer` API is provider-aware and strictly typed for S3, R2, S3-compatible, and
|
|
46
|
+
The main `createTransfer` API is provider-aware and strictly typed for S3, R2, S3-compatible, Hippius, and Hugging Face configs.
|
|
47
47
|
Use `testMode: true` to create a BeamCore test-mode transfer.
|
|
48
48
|
|
|
49
49
|
## S3-Compatible Providers
|
|
@@ -92,14 +92,69 @@ Use `S3ProviderConfig` for ordinary AWS S3 if preferred, and keep using
|
|
|
92
92
|
`S3CompatibleProviderConfig` when a provider needs a custom S3-compatible
|
|
93
93
|
endpoint.
|
|
94
94
|
|
|
95
|
+
## Hugging Face Hub
|
|
96
|
+
|
|
97
|
+
Use `HuggingFaceProviderConfig` to pull a file out of a Hub repo or push one into
|
|
98
|
+
one. The token stays in this process: a source resolves to the presigned CDN URL
|
|
99
|
+
the Hub redirects to, and a destination uses the presigned part URLs the Hub's
|
|
100
|
+
LFS batch endpoint issues.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { BeamClient, HuggingFaceProviderConfig, S3ProviderConfig } from "@beam-network/sdk";
|
|
104
|
+
|
|
105
|
+
const beam = new BeamClient({ apiKey: process.env.BEAM_API_KEY! });
|
|
106
|
+
|
|
107
|
+
const transfer = await beam.prepareProviderTransfer({
|
|
108
|
+
sources: [
|
|
109
|
+
HuggingFaceProviderConfig.create({
|
|
110
|
+
repo_id: "org/dataset",
|
|
111
|
+
path: "data/train.parquet",
|
|
112
|
+
repo_type: "dataset",
|
|
113
|
+
token: process.env.HF_TOKEN!
|
|
114
|
+
})
|
|
115
|
+
],
|
|
116
|
+
destinations: [
|
|
117
|
+
S3ProviderConfig.create({
|
|
118
|
+
bucket: "archive",
|
|
119
|
+
key: "datasets/train.parquet",
|
|
120
|
+
access_key_id: process.env.AWS_ACCESS_KEY_ID!,
|
|
121
|
+
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY!
|
|
122
|
+
})
|
|
123
|
+
],
|
|
124
|
+
name: "Hugging Face pull"
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// waitForTransfer commits any Hugging Face destination once the parts land.
|
|
128
|
+
await beam.waitForTransfer(transfer.transfer_id);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Constraints, all of them the Hub's rather than Beam's — see
|
|
132
|
+
[`HUGGINGFACE_PROVIDER.md`](../../HUGGINGFACE_PROVIDER.md) for the full protocol:
|
|
133
|
+
|
|
134
|
+
- **Sources are large-file (LFS or Xet) content.** Only those get the
|
|
135
|
+
credential-free CDN redirect; a small regular file is served inline and is
|
|
136
|
+
rejected with that reason. Hub **buckets** work as sources too, but cannot be
|
|
137
|
+
written to — they expose no LFS endpoint.
|
|
138
|
+
- **A destination needs the object's sha256 up front**, because the LFS batch
|
|
139
|
+
endpoint will not issue upload URLs without it. When the source is not itself a
|
|
140
|
+
Hub file, set `allow_source_rehash: true` to let the SDK read the source once to
|
|
141
|
+
compute it.
|
|
142
|
+
- **One source per Hugging Face destination**, because the Hub dictates the part
|
|
143
|
+
size and a plan carries a single chunk size.
|
|
144
|
+
- If you do not call `waitForTransfer`, call `finalizeHuggingFaceUploads(transferId)`
|
|
145
|
+
yourself once the transfer completes — without it the parts are uploaded but no
|
|
146
|
+
commit is made and the file does not appear in the repo.
|
|
147
|
+
|
|
95
148
|
For low-level raw transfer configs, use `createRawTransfer`; lifecycle transport still goes through NATS.
|
|
96
149
|
|
|
97
150
|
## Route Streaming And Payload Size
|
|
98
151
|
|
|
99
|
-
Provider transfers use `transfer-client-control/v6`. Every reply carries Runtime and transport epochs; prepare and every route-stream message carry a UUID route generation. S3, R2, and S3-compatible destinations retain direct multipart UploadPart/ListParts/HEAD handling.
|
|
152
|
+
Provider transfers use `transfer-client-control/v6`. Every reply carries Runtime and transport epochs; prepare and every route-stream message carry a UUID route generation. S3, R2, and S3-compatible destinations retain direct multipart UploadPart/ListParts/HEAD handling. Hippius and Hugging Face destinations take a plain PUT per chunk instead, so they carry no multipart group manifest.
|
|
100
153
|
|
|
101
154
|
The client signs up to 64 routes concurrently by default and emits 2,048-route logical batches. Encoded MessagePack requests target 4 MiB physical batches under `maxPayloadBytes` (8 MiB by default; explicit positive overrides remain supported). The splitter reserves control-envelope headroom for the live auth token and stable request identity so the final encoded request remains below the configured guard. Lifecycle control refreshes cached SDK auth before the final 30 seconds of token lifetime and retries `auth_token_expired` replies with the same request identity and payload but a fresh token, preserving Core request-conflict protection during large route streams. A single route above the 4 MiB target but within the configured guard publishes alone; a single route larger than the configured limit fails before publication with both sizes in the error. Multipart create and abort requests use a separate `multipartControlConcurrency` limit of 2 by default, with explicit positive overrides supported, and S3-compatible control requests make up to five attempts for transient provider failures. This keeps URL signing throughput independent from provider control-plane pressure.
|
|
102
155
|
|
|
156
|
+
Callers that need durable recovery accounting can provide `onMultipartGroupReady`. The SDK invokes it after creating a multipart upload and before streaming that group’s signed routes. Its payload contains transfer, source, destination, object-key, upload, size, part-count, and expiry identities only; it never includes credentials, request headers, or signed URLs. A rejected callback aborts the newly created upload and fails preparation closed.
|
|
157
|
+
|
|
103
158
|
The stream ID derives from the transfer, selected flow, and immutable plan identity, and each ordered batch ID includes its route-coordinate checksum. Manual multi-destination attachment requires `delivery_index` on every route. Lifecycle mutations retry transient failures up to three times with the same request identity. A non-recoverable provider setup or route failure raises `BeamProviderTransferError`, whose `errors`, `transferCancelled`, and `multipartCleanupComplete` fields retain the original provider, cancellation, and cleanup outcomes. `waitForTransfer` subscribes to the API-key-owned, at-most-once terminal signal before its first status read and reconciles every signal through authoritative status; subscription failure degrades to the same jittered 15-to-30-second status fallback. Call `close()` when the client is no longer needed; it also closes outstanding terminal waiters.
|
|
104
159
|
|
|
105
160
|
On completed provider transfers, Runtime may include an `integrity_audit_challenge` in status. Before releasing provider signing state, the TypeScript client signs exact read-only source and final-destination GET ranges and submits `transfer.integrity_audit_grants`; audit submission failures are best-effort and do not change transfer completion.
|
package/dist/client.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ type TransferPrepareInput = {
|
|
|
34
34
|
signedUrlFlow?: SignedUrlFlow;
|
|
35
35
|
idempotencyKey?: string;
|
|
36
36
|
routeGenerationId?: string;
|
|
37
|
+
chunkSize?: number;
|
|
37
38
|
};
|
|
38
39
|
export declare class BeamClient {
|
|
39
40
|
readonly apiKey: string;
|
|
@@ -46,6 +47,7 @@ export declare class BeamClient {
|
|
|
46
47
|
private readonly recoverySigners;
|
|
47
48
|
private readonly integrityAuditSigners;
|
|
48
49
|
private readonly integrityAuditSubmissions;
|
|
50
|
+
private readonly huggingFaceUploads;
|
|
49
51
|
constructor(options?: BeamClientOptions);
|
|
50
52
|
close(): Promise<void>;
|
|
51
53
|
openTransferTerminalWaiter(transferId: string): Promise<TransferTerminalSignalWaiter>;
|
|
@@ -62,6 +64,7 @@ export declare class BeamClient {
|
|
|
62
64
|
testMode?: boolean;
|
|
63
65
|
urlsExpiresAt?: string;
|
|
64
66
|
signedUrlFlow?: SignedUrlFlow;
|
|
67
|
+
chunkSize?: number;
|
|
65
68
|
}): Promise<TransferPlanResponse>;
|
|
66
69
|
prepareTransfer(input: TransferPrepareInput): Promise<TransferPrepareResponse>;
|
|
67
70
|
private prepareTransferWithRequestKey;
|
|
@@ -81,6 +84,21 @@ export declare class BeamClient {
|
|
|
81
84
|
autoDistribute?: boolean;
|
|
82
85
|
}): Promise<AttachSignedUrlsResponse>;
|
|
83
86
|
prepareProviderTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
|
|
87
|
+
/**
|
|
88
|
+
* Negotiate every Hugging Face destination before the plan exists.
|
|
89
|
+
*
|
|
90
|
+
* The Hub will not issue upload URLs without the object's sha256, and it chooses the part
|
|
91
|
+
* size itself, so this runs first and the plan is then requested at the Hub's chunk size.
|
|
92
|
+
*/
|
|
93
|
+
private planHuggingFaceUploads;
|
|
94
|
+
/** Fail before any byte moves if BeamCore did not adopt the Hub's part layout. */
|
|
95
|
+
private assertHuggingFacePlan;
|
|
96
|
+
/**
|
|
97
|
+
* Close every Hugging Face upload for a transfer: complete the LFS multipart, verify it, and
|
|
98
|
+
* commit the blob so the file appears in the repo. Runs only after the transfer is complete,
|
|
99
|
+
* so it never sits in the transfer's progression path.
|
|
100
|
+
*/
|
|
101
|
+
finalizeHuggingFaceUploads(transferId: string): Promise<void>;
|
|
84
102
|
private signProviderRoute;
|
|
85
103
|
private startProviderRouteRecoverySigner;
|
|
86
104
|
private recoveryMultipartUploadState;
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationReadRange, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signSourceReadRange } from "./provider-signing.js";
|
|
1
|
+
import { abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationReadRange, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signSourceReadRange, isHuggingFaceProvider } from "./provider-signing.js";
|
|
2
|
+
import { describe as describeHuggingFace, hashSourceStream, huggingFaceCommit, huggingFaceCompleteLfsUpload, huggingFaceLfsBatch, huggingFacePreupload, huggingFaceVerifyLfsUpload, readSourceSample } from "./huggingface.js";
|
|
2
3
|
import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes, isRecoverableRouteStreamError } from "./nats-control.js";
|
|
3
4
|
export class BeamRouteRecoveryPendingError extends Error {
|
|
4
5
|
transferId;
|
|
@@ -55,6 +56,7 @@ export class BeamClient {
|
|
|
55
56
|
recoverySigners = new Map();
|
|
56
57
|
integrityAuditSigners = new Map();
|
|
57
58
|
integrityAuditSubmissions = new Map();
|
|
59
|
+
huggingFaceUploads = new Map();
|
|
58
60
|
constructor(options = {}) {
|
|
59
61
|
if (!options.apiKey?.trim()) {
|
|
60
62
|
throw new Error("apiKey is required.");
|
|
@@ -203,6 +205,7 @@ export class BeamClient {
|
|
|
203
205
|
destinations: input.destinations,
|
|
204
206
|
name: input.name,
|
|
205
207
|
test_mode: input.testMode || undefined,
|
|
208
|
+
chunk_size: input.chunkSize,
|
|
206
209
|
urls_expires_at: input.urlsExpiresAt,
|
|
207
210
|
signed_url_flow: input.signedUrlFlow ?? "signed_url"
|
|
208
211
|
}));
|
|
@@ -225,6 +228,7 @@ export class BeamClient {
|
|
|
225
228
|
destinations: input.destinations,
|
|
226
229
|
name: input.name,
|
|
227
230
|
test_mode: input.testMode || undefined,
|
|
231
|
+
chunk_size: input.chunkSize,
|
|
228
232
|
urls_expires_at: input.urlsExpiresAt,
|
|
229
233
|
signed_url_flow: input.signedUrlFlow ?? "signed_url"
|
|
230
234
|
}), {
|
|
@@ -318,6 +322,7 @@ export class BeamClient {
|
|
|
318
322
|
destinations: input.destinations.map((destination) => ({ ...destination })),
|
|
319
323
|
onBeforeTransferPrepare: undefined,
|
|
320
324
|
onPrepared: undefined,
|
|
325
|
+
onMultipartGroupReady: undefined,
|
|
321
326
|
throwIfCancelled: undefined
|
|
322
327
|
};
|
|
323
328
|
const preparedDestinations = retainedRecoveryInput.destinations.map((destination, index) => prepareProviderDestination(destination, { index }));
|
|
@@ -332,12 +337,20 @@ export class BeamClient {
|
|
|
332
337
|
await input.throwIfCancelled?.();
|
|
333
338
|
const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl })));
|
|
334
339
|
await input.throwIfCancelled?.();
|
|
340
|
+
const huggingFace = await this.planHuggingFaceUploads({
|
|
341
|
+
sources: retainedRecoveryInput.sources,
|
|
342
|
+
preparedSources,
|
|
343
|
+
destinations: retainedRecoveryInput.destinations,
|
|
344
|
+
preparedDestinations
|
|
345
|
+
});
|
|
346
|
+
await input.throwIfCancelled?.();
|
|
335
347
|
await input.onBeforeTransferPrepare?.();
|
|
336
348
|
const prepared = await this.prepareTransfer({
|
|
337
349
|
sources: preparedSources,
|
|
338
350
|
destinations: preparedDestinations,
|
|
339
351
|
name: input.name,
|
|
340
352
|
testMode: input.testMode,
|
|
353
|
+
chunkSize: huggingFace.chunkSize ?? input.chunkSize,
|
|
341
354
|
signedUrlFlow: requestedSignedUrlFlow,
|
|
342
355
|
idempotencyKey: input.idempotencyKey,
|
|
343
356
|
routeGenerationId: input.routeGenerationId
|
|
@@ -345,6 +358,11 @@ export class BeamClient {
|
|
|
345
358
|
if (!prepared.success) {
|
|
346
359
|
return prepared;
|
|
347
360
|
}
|
|
361
|
+
if (huggingFace.states.length) {
|
|
362
|
+
this.assertHuggingFacePlan(prepared, huggingFace.states);
|
|
363
|
+
this.huggingFaceUploads.set(prepared.transfer_id, huggingFace.states);
|
|
364
|
+
}
|
|
365
|
+
const huggingFaceByDestination = new Map(huggingFace.states.map((state) => [state.destinationId, state]));
|
|
348
366
|
const sourcesById = new Map();
|
|
349
367
|
preparedSources.forEach((preparedSource, index) => {
|
|
350
368
|
const source = retainedRecoveryInput.sources[index];
|
|
@@ -409,6 +427,7 @@ export class BeamClient {
|
|
|
409
427
|
onGroupReady: async (state) => {
|
|
410
428
|
recoveryMultipartUploads.set(state.manifest.multipart_group_id, state);
|
|
411
429
|
validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
|
|
430
|
+
await input.onMultipartGroupReady?.(multipartGroupIdentity(prepared.transfer_id, state.manifest));
|
|
412
431
|
await routeStream.addManifestGroups([state.manifest]);
|
|
413
432
|
multipartGroupWaiters.get(state.manifest.multipart_group_id)?.resolve(state);
|
|
414
433
|
},
|
|
@@ -444,10 +463,10 @@ export class BeamClient {
|
|
|
444
463
|
if (!finalObjectKey)
|
|
445
464
|
throw new Error("destination signing target is missing object_key");
|
|
446
465
|
const multipartGroupId = multipartGroupStateKey(prepared.transfer_id, target.destination_id, chunk.source_id, finalObjectKey);
|
|
447
|
-
const upload =
|
|
466
|
+
const upload = isDirectPutDestination(destination)
|
|
448
467
|
? undefined
|
|
449
468
|
: await multipartGroupWaiters.get(multipartGroupId)?.promise;
|
|
450
|
-
if (!
|
|
469
|
+
if (!isDirectPutDestination(destination) && !upload)
|
|
451
470
|
throw new Error(`multipart group manifest is missing for ${chunk.source_id}:${target.destination_id}`);
|
|
452
471
|
return this.signProviderRoute({
|
|
453
472
|
chunk,
|
|
@@ -456,6 +475,7 @@ export class BeamClient {
|
|
|
456
475
|
destination,
|
|
457
476
|
expiresIn,
|
|
458
477
|
...(upload ? { upload } : {}),
|
|
478
|
+
huggingFaceUpload: huggingFaceByDestination.get(target.destination_id),
|
|
459
479
|
transferId: prepared.transfer_id,
|
|
460
480
|
finalObjectKey,
|
|
461
481
|
signedUrlFlow: requestedSignedUrlFlow,
|
|
@@ -546,7 +566,186 @@ export class BeamClient {
|
|
|
546
566
|
}
|
|
547
567
|
return prepared;
|
|
548
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* Negotiate every Hugging Face destination before the plan exists.
|
|
571
|
+
*
|
|
572
|
+
* The Hub will not issue upload URLs without the object's sha256, and it chooses the part
|
|
573
|
+
* size itself, so this runs first and the plan is then requested at the Hub's chunk size.
|
|
574
|
+
*/
|
|
575
|
+
async planHuggingFaceUploads(input) {
|
|
576
|
+
const targets = input.destinations
|
|
577
|
+
.map((destination, index) => ({ destination, index }))
|
|
578
|
+
.filter((entry) => isHuggingFaceProvider(entry.destination));
|
|
579
|
+
if (!targets.length)
|
|
580
|
+
return { states: [] };
|
|
581
|
+
if (input.preparedSources.length !== 1) {
|
|
582
|
+
throw new Error("a huggingface destination requires exactly one source: the Hub dictates the part size "
|
|
583
|
+
+ `and the plan carries a single chunk size, but ${input.preparedSources.length} sources were given`);
|
|
584
|
+
}
|
|
585
|
+
const preparedSource = input.preparedSources[0];
|
|
586
|
+
const sourceConfig = input.sources[0];
|
|
587
|
+
// For an LFS source the Hub already published the sha256 as the linked ETag.
|
|
588
|
+
const publishedSha256 = isHuggingFaceProvider(sourceConfig)
|
|
589
|
+
? typeof preparedSource.metadata?.sha256 === "string" ? preparedSource.metadata.sha256 : undefined
|
|
590
|
+
: undefined;
|
|
591
|
+
const states = [];
|
|
592
|
+
let chunkSize;
|
|
593
|
+
for (const { destination, index } of targets) {
|
|
594
|
+
const preparedDestination = input.preparedDestinations[index];
|
|
595
|
+
if ((destination.repo_type ?? "model") === "bucket") {
|
|
596
|
+
throw new Error(`${destination.repo_id} is a Hugging Face bucket, which Beam cannot write to. Buckets `
|
|
597
|
+
+ "expose no LFS batch endpoint; their only upload path is the Hub's Xet CAS client, "
|
|
598
|
+
+ "which cannot be expressed as presigned URLs for Beam's workers. Buckets do work as "
|
|
599
|
+
+ "a transfer source. Use `hf sync` to write to a bucket.");
|
|
600
|
+
}
|
|
601
|
+
const path = huggingFaceTargetPath(destination, preparedSource.filename);
|
|
602
|
+
const config = { ...destination, path };
|
|
603
|
+
let oid = publishedSha256;
|
|
604
|
+
if (!oid) {
|
|
605
|
+
if (!destination.allow_source_rehash) {
|
|
606
|
+
throw new Error(`uploading to ${describeHuggingFace(config)} needs the source sha256, which the Hub requires `
|
|
607
|
+
+ "before it issues upload URLs. The SDK must read the source once to compute it; set "
|
|
608
|
+
+ "allow_source_rehash: true to opt in.");
|
|
609
|
+
}
|
|
610
|
+
const hashed = await hashSourceStream(this.fetchImpl, preparedSource.url);
|
|
611
|
+
oid = hashed.sha256;
|
|
612
|
+
}
|
|
613
|
+
const sample = await readSourceSample(this.fetchImpl, preparedSource.url);
|
|
614
|
+
const preupload = await huggingFacePreupload(this.fetchImpl, config, {
|
|
615
|
+
size: preparedSource.size,
|
|
616
|
+
sample
|
|
617
|
+
});
|
|
618
|
+
if (preupload.shouldIgnore) {
|
|
619
|
+
throw new Error(`${describeHuggingFace(config)} is excluded by the repo's .gitignore`);
|
|
620
|
+
}
|
|
621
|
+
if (preupload.uploadMode !== "lfs") {
|
|
622
|
+
throw new Error(`${describeHuggingFace(config)} would be committed as a regular git blob, not an LFS blob. `
|
|
623
|
+
+ "Beam uploads through the LFS protocol only; add the path to .gitattributes as LFS.");
|
|
624
|
+
}
|
|
625
|
+
const plan = await huggingFaceLfsBatch(this.fetchImpl, config, {
|
|
626
|
+
oid,
|
|
627
|
+
size: preparedSource.size
|
|
628
|
+
});
|
|
629
|
+
if (plan.upload?.chunkSize !== undefined) {
|
|
630
|
+
if (chunkSize !== undefined && chunkSize !== plan.upload.chunkSize) {
|
|
631
|
+
throw new Error(`huggingface destinations disagree on part size (${chunkSize} vs ${plan.upload.chunkSize}); `
|
|
632
|
+
+ "the plan carries a single chunk size");
|
|
633
|
+
}
|
|
634
|
+
chunkSize = plan.upload.chunkSize;
|
|
635
|
+
}
|
|
636
|
+
states.push({
|
|
637
|
+
destination: config,
|
|
638
|
+
destinationId: preparedDestination.destination_id,
|
|
639
|
+
sourceId: preparedSource.source_id,
|
|
640
|
+
oid,
|
|
641
|
+
size: preparedSource.size,
|
|
642
|
+
chunkSize: plan.upload?.chunkSize,
|
|
643
|
+
partUrls: plan.upload?.partUrls ?? [],
|
|
644
|
+
uploadHref: plan.upload?.href,
|
|
645
|
+
verifyHref: plan.verifyHref,
|
|
646
|
+
// Runs alongside the transfer; the ETags are only needed at completion time.
|
|
647
|
+
partEtags: plan.upload?.chunkSize === undefined
|
|
648
|
+
? Promise.resolve([])
|
|
649
|
+
: hashSourceStream(this.fetchImpl, preparedSource.url, {
|
|
650
|
+
partSize: plan.upload.chunkSize,
|
|
651
|
+
sha256: false
|
|
652
|
+
}).then((hashed) => hashed.partEtags)
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return { states, chunkSize };
|
|
656
|
+
}
|
|
657
|
+
/** Fail before any byte moves if BeamCore did not adopt the Hub's part layout. */
|
|
658
|
+
assertHuggingFacePlan(prepared, states) {
|
|
659
|
+
for (const state of states) {
|
|
660
|
+
const planDestination = prepared.plan_descriptor.destinations.find((candidate) => candidate.destination_id === state.destinationId);
|
|
661
|
+
const planSource = prepared.plan_descriptor.sources.find((candidate) => candidate.source_id === state.sourceId);
|
|
662
|
+
if (!planDestination || !planSource) {
|
|
663
|
+
throw new Error(`BeamCore plan is missing the huggingface coordinate ${state.sourceId}:${state.destinationId}`);
|
|
664
|
+
}
|
|
665
|
+
const finalObjectKey = planDestination.final_object_keys[state.sourceId];
|
|
666
|
+
if (finalObjectKey !== state.destination.path) {
|
|
667
|
+
throw new Error(`BeamCore planned ${finalObjectKey} but the Hub upload was negotiated for ${state.destination.path}`);
|
|
668
|
+
}
|
|
669
|
+
if (state.chunkSize === undefined) {
|
|
670
|
+
if (planSource.chunk_count !== 1) {
|
|
671
|
+
throw new Error(`${describeHuggingFace(state.destination)} was issued a single-part upload, but the plan has `
|
|
672
|
+
+ `${planSource.chunk_count} chunks`);
|
|
673
|
+
}
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (prepared.plan_descriptor.chunk_size !== state.chunkSize) {
|
|
677
|
+
throw new Error(`the Hub requires ${state.chunkSize}-byte parts for ${describeHuggingFace(state.destination)}, `
|
|
678
|
+
+ `but BeamCore planned ${prepared.plan_descriptor.chunk_size}-byte chunks`);
|
|
679
|
+
}
|
|
680
|
+
if (planSource.chunk_count !== state.partUrls.length) {
|
|
681
|
+
throw new Error(`the Hub issued ${state.partUrls.length} part URLs for ${describeHuggingFace(state.destination)}, `
|
|
682
|
+
+ `but the plan has ${planSource.chunk_count} chunks`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Close every Hugging Face upload for a transfer: complete the LFS multipart, verify it, and
|
|
688
|
+
* commit the blob so the file appears in the repo. Runs only after the transfer is complete,
|
|
689
|
+
* so it never sits in the transfer's progression path.
|
|
690
|
+
*/
|
|
691
|
+
async finalizeHuggingFaceUploads(transferId) {
|
|
692
|
+
const states = this.huggingFaceUploads.get(transferId);
|
|
693
|
+
if (!states?.length)
|
|
694
|
+
return;
|
|
695
|
+
this.huggingFaceUploads.delete(transferId);
|
|
696
|
+
for (const state of states) {
|
|
697
|
+
if (state.uploadHref && state.chunkSize !== undefined) {
|
|
698
|
+
const etags = await state.partEtags;
|
|
699
|
+
if (etags.length !== state.partUrls.length) {
|
|
700
|
+
throw new Error(`computed ${etags.length} part ETags for ${describeHuggingFace(state.destination)}, `
|
|
701
|
+
+ `expected ${state.partUrls.length}`);
|
|
702
|
+
}
|
|
703
|
+
await huggingFaceCompleteLfsUpload(this.fetchImpl, state.destination, {
|
|
704
|
+
href: state.uploadHref,
|
|
705
|
+
oid: state.oid,
|
|
706
|
+
etags
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
if (state.verifyHref) {
|
|
710
|
+
await huggingFaceVerifyLfsUpload(this.fetchImpl, state.destination, {
|
|
711
|
+
href: state.verifyHref,
|
|
712
|
+
oid: state.oid,
|
|
713
|
+
size: state.size
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
await huggingFaceCommit(this.fetchImpl, state.destination, {
|
|
717
|
+
oid: state.oid,
|
|
718
|
+
size: state.size
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
549
722
|
async signProviderRoute(input) {
|
|
723
|
+
if (isHuggingFaceProvider(input.destination)) {
|
|
724
|
+
const state = input.huggingFaceUpload;
|
|
725
|
+
if (!state)
|
|
726
|
+
throw new Error(`huggingface upload state is missing for ${input.target.destination_id}`);
|
|
727
|
+
// The Hub presigns its own part targets; chunk N carries the URL for part N + 1.
|
|
728
|
+
const destUrl = state.chunkSize === undefined
|
|
729
|
+
? state.uploadHref
|
|
730
|
+
: state.partUrls[input.chunk.source_chunk_index];
|
|
731
|
+
if (!destUrl) {
|
|
732
|
+
throw new Error(`the Hub issued no upload URL for chunk ${input.chunk.source_chunk_index} of `
|
|
733
|
+
+ describeHuggingFace(state.destination));
|
|
734
|
+
}
|
|
735
|
+
return signDestinationRoute({
|
|
736
|
+
chunk: input.chunk,
|
|
737
|
+
target: {
|
|
738
|
+
...input.target,
|
|
739
|
+
object_key: input.finalObjectKey,
|
|
740
|
+
metadata: directPutRouteMetadata(input.target.metadata)
|
|
741
|
+
},
|
|
742
|
+
source: input.source,
|
|
743
|
+
destination: input.destination,
|
|
744
|
+
expiresIn: input.expiresIn,
|
|
745
|
+
destUrl,
|
|
746
|
+
fetchImpl: this.fetchImpl
|
|
747
|
+
});
|
|
748
|
+
}
|
|
550
749
|
if (isHippiusDestination(input.destination)) {
|
|
551
750
|
return signDestinationRoute({
|
|
552
751
|
chunk: input.chunk,
|
|
@@ -611,7 +810,7 @@ export class BeamClient {
|
|
|
611
810
|
const destination = input.destinationsById.get(requested.destination_id);
|
|
612
811
|
if (!source || !destination)
|
|
613
812
|
throw new Error("route recovery provider configuration is unavailable");
|
|
614
|
-
const upload =
|
|
813
|
+
const upload = isDirectPutDestination(destination)
|
|
615
814
|
? undefined
|
|
616
815
|
: await this.recoveryMultipartUploadState({
|
|
617
816
|
prepared: input.prepared,
|
|
@@ -643,6 +842,9 @@ export class BeamClient {
|
|
|
643
842
|
destination,
|
|
644
843
|
expiresIn: input.expiresIn,
|
|
645
844
|
...(upload ? { upload } : {}),
|
|
845
|
+
huggingFaceUpload: this.huggingFaceUploads
|
|
846
|
+
.get(input.prepared.transfer_id)
|
|
847
|
+
?.find((state) => state.destinationId === requested.destination_id),
|
|
646
848
|
transferId: input.prepared.transfer_id,
|
|
647
849
|
finalObjectKey: requested.final_object_key,
|
|
648
850
|
signedUrlFlow: "signed_url",
|
|
@@ -845,6 +1047,8 @@ export class BeamClient {
|
|
|
845
1047
|
while (true) {
|
|
846
1048
|
const status = await this.transferStatus(transferId);
|
|
847
1049
|
if (status.status === "completed") {
|
|
1050
|
+
// The parts have landed; publish them as a Hub commit before reporting success.
|
|
1051
|
+
await this.finalizeHuggingFaceUploads(transferId);
|
|
848
1052
|
return status;
|
|
849
1053
|
}
|
|
850
1054
|
if (status.status === "failed") {
|
|
@@ -1263,6 +1467,19 @@ function safeErrorCode(error) {
|
|
|
1263
1467
|
.find((value) => Number.isInteger(value) && Number(value) >= 100 && Number(value) <= 599);
|
|
1264
1468
|
return status === undefined ? code : `${code}:status=${status}`;
|
|
1265
1469
|
}
|
|
1470
|
+
function multipartGroupIdentity(transferId, manifest) {
|
|
1471
|
+
return {
|
|
1472
|
+
transferId,
|
|
1473
|
+
multipartGroupId: manifest.multipart_group_id,
|
|
1474
|
+
sourceId: manifest.source_id,
|
|
1475
|
+
destinationId: manifest.destination_id,
|
|
1476
|
+
objectKey: manifest.final_object_key,
|
|
1477
|
+
uploadId: manifest.upload_id,
|
|
1478
|
+
expectedObjectSize: manifest.expected_object_size,
|
|
1479
|
+
expectedPartCount: manifest.expected_part_count,
|
|
1480
|
+
expiresAt: manifest.urls_expires_at
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1266
1483
|
function createDeferred() {
|
|
1267
1484
|
let resolve;
|
|
1268
1485
|
let reject;
|
|
@@ -1277,7 +1494,7 @@ function createMultipartGroupWaiters(prepared, destinationsById) {
|
|
|
1277
1494
|
for (const source of prepared.plan_descriptor.sources) {
|
|
1278
1495
|
for (const destination of prepared.plan_descriptor.destinations) {
|
|
1279
1496
|
const destinationConfig = destinationsById.get(destination.destination_id);
|
|
1280
|
-
if (!destinationConfig ||
|
|
1497
|
+
if (!destinationConfig || isDirectPutDestination(destinationConfig))
|
|
1281
1498
|
continue;
|
|
1282
1499
|
const finalObjectKey = destination.final_object_keys[source.source_id];
|
|
1283
1500
|
if (!finalObjectKey)
|
|
@@ -1297,7 +1514,7 @@ async function createMultipartGroupManifest(input) {
|
|
|
1297
1514
|
if (!destinationConfig) {
|
|
1298
1515
|
throw new Error(`BeamCore returned unknown destination_id: ${destination.destination_id}`);
|
|
1299
1516
|
}
|
|
1300
|
-
if (
|
|
1517
|
+
if (isDirectPutDestination(destinationConfig)) {
|
|
1301
1518
|
return { ok: true, state: null };
|
|
1302
1519
|
}
|
|
1303
1520
|
const finalObjectKey = destination.final_object_keys[source.source_id];
|
|
@@ -1542,6 +1759,14 @@ const PART_ROUTE_METADATA_KEYS = new Set([
|
|
|
1542
1759
|
function partRouteMetadata(metadata) {
|
|
1543
1760
|
return Object.fromEntries(Object.entries(metadata ?? {}).filter(([key]) => PART_ROUTE_METADATA_KEYS.has(key)));
|
|
1544
1761
|
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Route metadata for a destination BeamCore does not treat as an S3 multipart target. It
|
|
1764
|
+
* rejects `part_number` there as an unexpected multipart signal, so drop it.
|
|
1765
|
+
*/
|
|
1766
|
+
function directPutRouteMetadata(metadata) {
|
|
1767
|
+
const { part_number: _partNumber, ...rest } = partRouteMetadata(metadata);
|
|
1768
|
+
return rest;
|
|
1769
|
+
}
|
|
1545
1770
|
function randomUuid() {
|
|
1546
1771
|
const cryptoApi = globalThis.crypto;
|
|
1547
1772
|
if (cryptoApi?.randomUUID)
|
|
@@ -1603,6 +1828,23 @@ async function abortCreatedUploads(uploads, concurrency) {
|
|
|
1603
1828
|
function isHippiusDestination(destination) {
|
|
1604
1829
|
return destination.provider === "hippius" && "api_token" in destination;
|
|
1605
1830
|
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Destinations that take a plain PUT per chunk instead of an S3 multipart upload. BeamCore
|
|
1833
|
+
* rejects multipart route metadata for these, so they never get a multipart group manifest.
|
|
1834
|
+
*/
|
|
1835
|
+
function isDirectPutDestination(destination) {
|
|
1836
|
+
return isHippiusDestination(destination) || isHuggingFaceProvider(destination);
|
|
1837
|
+
}
|
|
1838
|
+
/** Resolve the path a Hugging Face upload commits to, expanding a trailing-slash prefix. */
|
|
1839
|
+
function huggingFaceTargetPath(destination, sourceFilename) {
|
|
1840
|
+
const path = destination.path.replace(/^\/+/, "");
|
|
1841
|
+
if (!path.endsWith("/"))
|
|
1842
|
+
return path;
|
|
1843
|
+
if (!sourceFilename) {
|
|
1844
|
+
throw new Error(`huggingface path ${destination.path} is a folder and the source has no filename`);
|
|
1845
|
+
}
|
|
1846
|
+
return `${path}${sourceFilename}`;
|
|
1847
|
+
}
|
|
1606
1848
|
function sleep(ms) {
|
|
1607
1849
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1608
1850
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { HuggingFaceProviderConfig, HuggingFaceRepoType } from "./models.js";
|
|
2
|
+
export declare const HUGGINGFACE_DEFAULT_ENDPOINT = "https://huggingface.co";
|
|
3
|
+
export declare const HUGGINGFACE_DEFAULT_REVISION = "main";
|
|
4
|
+
export declare const HUGGINGFACE_DEFAULT_REPO_TYPE: HuggingFaceRepoType;
|
|
5
|
+
export interface HuggingFaceFileMetadata {
|
|
6
|
+
/** Credential-free presigned CDN URL the workers read from. */
|
|
7
|
+
url: string;
|
|
8
|
+
size: number;
|
|
9
|
+
/** sha256 for an LFS blob, git sha1 otherwise. */
|
|
10
|
+
etag?: string;
|
|
11
|
+
commitHash?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface HuggingFaceLfsUploadPlan {
|
|
14
|
+
oid: string;
|
|
15
|
+
size: number;
|
|
16
|
+
/** Absent when the Hub already stores this content and no upload is needed. */
|
|
17
|
+
upload?: {
|
|
18
|
+
/** Completion endpoint for multipart, or the single-part PUT target. */
|
|
19
|
+
href: string;
|
|
20
|
+
/** Part size the Hub requires. Absent for a single-part upload. */
|
|
21
|
+
chunkSize?: number;
|
|
22
|
+
/** Presigned part PUT URLs, ordered by part number. Empty for a single-part upload. */
|
|
23
|
+
partUrls: string[];
|
|
24
|
+
};
|
|
25
|
+
verifyHref?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function huggingFaceEndpoint(config: HuggingFaceProviderConfig): string;
|
|
28
|
+
export declare function huggingFaceRepoType(config: HuggingFaceProviderConfig): HuggingFaceRepoType;
|
|
29
|
+
export declare function huggingFaceRevision(config: HuggingFaceProviderConfig): string;
|
|
30
|
+
/**
|
|
31
|
+
* `{endpoint}/{prefix}{repo_id}/resolve/{revision}/{path}`, as built by `hf_hub_url`.
|
|
32
|
+
*
|
|
33
|
+
* Buckets are unversioned and take no revision segment, and the Hub escapes their whole key
|
|
34
|
+
* as one component — see `HfApi.get_bucket_file_metadata`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function huggingFaceResolveUrl(config: HuggingFaceProviderConfig): string;
|
|
37
|
+
/** `{endpoint}/api/{repo_type}s/{repo_id}`. */
|
|
38
|
+
export declare function huggingFaceApiBase(config: HuggingFaceProviderConfig): string;
|
|
39
|
+
/** `{endpoint}/{prefix}{repo_id}.git/info/lfs/objects/batch`. */
|
|
40
|
+
export declare function huggingFaceLfsBatchUrl(config: HuggingFaceProviderConfig): string;
|
|
41
|
+
/**
|
|
42
|
+
* HEAD the resolve URL and require the Hub to redirect to its CDN. The redirect target is
|
|
43
|
+
* presigned and carries no credential, so it is the only form of this URL that may be handed
|
|
44
|
+
* to BeamCore and the workers.
|
|
45
|
+
*/
|
|
46
|
+
export declare function huggingFaceFileMetadata(fetchImpl: typeof fetch, config: HuggingFaceProviderConfig): Promise<HuggingFaceFileMetadata>;
|
|
47
|
+
/**
|
|
48
|
+
* Ask the Hub whether a path is stored as an LFS blob or as a regular git blob.
|
|
49
|
+
* `sample` is the base64 of the first 512 bytes, exactly as `_fetch_upload_modes` sends it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function huggingFacePreupload(fetchImpl: typeof fetch, config: HuggingFaceProviderConfig, input: {
|
|
52
|
+
size: number;
|
|
53
|
+
sample: string;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
uploadMode: "lfs" | "regular";
|
|
56
|
+
shouldIgnore: boolean;
|
|
57
|
+
oid?: string;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Request upload instructions for one object. The Hub answers with either a single-part PUT
|
|
61
|
+
* target or a completion endpoint plus one presigned PUT URL per part.
|
|
62
|
+
*/
|
|
63
|
+
export declare function huggingFaceLfsBatch(fetchImpl: typeof fetch, config: HuggingFaceProviderConfig, input: {
|
|
64
|
+
oid: string;
|
|
65
|
+
size: number;
|
|
66
|
+
}): Promise<HuggingFaceLfsUploadPlan>;
|
|
67
|
+
/** Close a multipart LFS upload: `{oid, parts:[{partNumber, etag}]}` to the completion href. */
|
|
68
|
+
export declare function huggingFaceCompleteLfsUpload(fetchImpl: typeof fetch, config: HuggingFaceProviderConfig, input: {
|
|
69
|
+
href: string;
|
|
70
|
+
oid: string;
|
|
71
|
+
etags: string[];
|
|
72
|
+
}): Promise<void>;
|
|
73
|
+
/** Optional server-side check that the object landed intact. */
|
|
74
|
+
export declare function huggingFaceVerifyLfsUpload(fetchImpl: typeof fetch, config: HuggingFaceProviderConfig, input: {
|
|
75
|
+
href: string;
|
|
76
|
+
oid: string;
|
|
77
|
+
size: number;
|
|
78
|
+
}): Promise<void>;
|
|
79
|
+
/** Publish the uploaded blob as a commit. The body is NDJSON: a header line then one file line. */
|
|
80
|
+
export declare function huggingFaceCommit(fetchImpl: typeof fetch, config: HuggingFaceProviderConfig, input: {
|
|
81
|
+
oid: string;
|
|
82
|
+
size: number;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
commitOid?: string;
|
|
85
|
+
pullRequestUrl?: string;
|
|
86
|
+
}>;
|
|
87
|
+
/** Read the first `length` bytes of a URL and return them base64-encoded, for `preupload`. */
|
|
88
|
+
export declare function readSourceSample(fetchImpl: typeof fetch, url: string, length?: number): Promise<string>;
|
|
89
|
+
/**
|
|
90
|
+
* Stream a URL once and return the sha256 of the whole body plus, when `partSize` is given,
|
|
91
|
+
* the hex MD5 of every part. The Hub will not issue upload URLs without the sha256, and the
|
|
92
|
+
* per-part MD5 is the ETag the completion payload has to quote.
|
|
93
|
+
*/
|
|
94
|
+
export declare function hashSourceStream(fetchImpl: typeof fetch, url: string, options?: {
|
|
95
|
+
partSize?: number;
|
|
96
|
+
sha256?: boolean;
|
|
97
|
+
}): Promise<{
|
|
98
|
+
sha256?: string;
|
|
99
|
+
partEtags: string[];
|
|
100
|
+
}>;
|
|
101
|
+
export declare function describe(config: HuggingFaceProviderConfig): string;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
export const HUGGINGFACE_DEFAULT_ENDPOINT = "https://huggingface.co";
|
|
2
|
+
export const HUGGINGFACE_DEFAULT_REVISION = "main";
|
|
3
|
+
export const HUGGINGFACE_DEFAULT_REPO_TYPE = "model";
|
|
4
|
+
const LFS_CONTENT_TYPE = "application/vnd.git-lfs+json";
|
|
5
|
+
/** Mirrors `constants.REPO_TYPES_URL_PREFIXES`; models carry no prefix. */
|
|
6
|
+
const REPO_TYPE_URL_PREFIXES = {
|
|
7
|
+
model: "",
|
|
8
|
+
dataset: "datasets/",
|
|
9
|
+
space: "spaces/",
|
|
10
|
+
kernel: "kernels/",
|
|
11
|
+
bucket: "buckets/"
|
|
12
|
+
};
|
|
13
|
+
export function huggingFaceEndpoint(config) {
|
|
14
|
+
return (config.endpoint ?? HUGGINGFACE_DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
15
|
+
}
|
|
16
|
+
export function huggingFaceRepoType(config) {
|
|
17
|
+
return config.repo_type ?? HUGGINGFACE_DEFAULT_REPO_TYPE;
|
|
18
|
+
}
|
|
19
|
+
export function huggingFaceRevision(config) {
|
|
20
|
+
return config.revision ?? HUGGINGFACE_DEFAULT_REVISION;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `{endpoint}/{prefix}{repo_id}/resolve/{revision}/{path}`, as built by `hf_hub_url`.
|
|
24
|
+
*
|
|
25
|
+
* Buckets are unversioned and take no revision segment, and the Hub escapes their whole key
|
|
26
|
+
* as one component — see `HfApi.get_bucket_file_metadata`.
|
|
27
|
+
*/
|
|
28
|
+
export function huggingFaceResolveUrl(config) {
|
|
29
|
+
const repoType = huggingFaceRepoType(config);
|
|
30
|
+
const prefix = REPO_TYPE_URL_PREFIXES[repoType];
|
|
31
|
+
if (repoType === "bucket") {
|
|
32
|
+
return `${huggingFaceEndpoint(config)}/${prefix}${config.repo_id}/resolve/${encodeURIComponent(config.path)}`;
|
|
33
|
+
}
|
|
34
|
+
const revision = encodeURIComponent(huggingFaceRevision(config));
|
|
35
|
+
return `${huggingFaceEndpoint(config)}/${prefix}${config.repo_id}/resolve/${revision}/${encodePath(config.path)}`;
|
|
36
|
+
}
|
|
37
|
+
/** `{endpoint}/api/{repo_type}s/{repo_id}`. */
|
|
38
|
+
export function huggingFaceApiBase(config) {
|
|
39
|
+
return `${huggingFaceEndpoint(config)}/api/${huggingFaceRepoType(config)}s/${config.repo_id}`;
|
|
40
|
+
}
|
|
41
|
+
/** `{endpoint}/{prefix}{repo_id}.git/info/lfs/objects/batch`. */
|
|
42
|
+
export function huggingFaceLfsBatchUrl(config) {
|
|
43
|
+
const prefix = REPO_TYPE_URL_PREFIXES[huggingFaceRepoType(config)];
|
|
44
|
+
return `${huggingFaceEndpoint(config)}/${prefix}${config.repo_id}.git/info/lfs/objects/batch`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* HEAD the resolve URL and require the Hub to redirect to its CDN. The redirect target is
|
|
48
|
+
* presigned and carries no credential, so it is the only form of this URL that may be handed
|
|
49
|
+
* to BeamCore and the workers.
|
|
50
|
+
*/
|
|
51
|
+
export async function huggingFaceFileMetadata(fetchImpl, config) {
|
|
52
|
+
const resolveUrl = huggingFaceResolveUrl(config);
|
|
53
|
+
const response = await fetchImpl(resolveUrl, {
|
|
54
|
+
method: "HEAD",
|
|
55
|
+
redirect: "manual",
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${config.token}`,
|
|
58
|
+
// Compression would report a transformed length instead of the real object size.
|
|
59
|
+
"Accept-Encoding": "identity"
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (response.status >= 400) {
|
|
63
|
+
throw new Error(`Hugging Face file lookup failed status=${response.status} for ${describe(config)}`);
|
|
64
|
+
}
|
|
65
|
+
const location = response.headers.get("location");
|
|
66
|
+
if (!location || sameHost(resolveUrl, location)) {
|
|
67
|
+
throw new Error(`Hugging Face did not redirect ${describe(config)} to a presigned CDN URL. `
|
|
68
|
+
+ "Beam reads this file over plain HTTP without forwarding your token, and the Hub only "
|
|
69
|
+
+ "redirects for large-file (LFS or Xet) content. A small regular file is served inline "
|
|
70
|
+
+ "from the Hub instead and cannot be transferred.");
|
|
71
|
+
}
|
|
72
|
+
const linkedSize = response.headers.get("x-linked-size");
|
|
73
|
+
const size = Number(linkedSize ?? response.headers.get("content-length") ?? Number.NaN);
|
|
74
|
+
if (!Number.isFinite(size) || size <= 0) {
|
|
75
|
+
throw new Error(`Hugging Face did not report a size for ${describe(config)}`);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
url: location,
|
|
79
|
+
size,
|
|
80
|
+
etag: normalizeEtag(response.headers.get("x-linked-etag") ?? response.headers.get("etag")),
|
|
81
|
+
commitHash: response.headers.get("x-repo-commit") ?? undefined
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Ask the Hub whether a path is stored as an LFS blob or as a regular git blob.
|
|
86
|
+
* `sample` is the base64 of the first 512 bytes, exactly as `_fetch_upload_modes` sends it.
|
|
87
|
+
*/
|
|
88
|
+
export async function huggingFacePreupload(fetchImpl, config, input) {
|
|
89
|
+
const url = new URL(`${huggingFaceApiBase(config)}/preupload/${encodeURIComponent(huggingFaceRevision(config))}`);
|
|
90
|
+
if (config.create_pr)
|
|
91
|
+
url.searchParams.set("create_pr", "1");
|
|
92
|
+
const response = await fetchImpl(url, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: jsonHeaders(config),
|
|
95
|
+
body: JSON.stringify({ files: [{ path: config.path, sample: input.sample, size: input.size }] })
|
|
96
|
+
});
|
|
97
|
+
const payload = await readJson(response, "preupload", config);
|
|
98
|
+
const file = Array.isArray(payload.files) ? payload.files[0] : undefined;
|
|
99
|
+
if (!file || (file.uploadMode !== "lfs" && file.uploadMode !== "regular")) {
|
|
100
|
+
throw new Error(`Hugging Face preupload returned a malformed response for ${describe(config)}`);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
uploadMode: file.uploadMode,
|
|
104
|
+
shouldIgnore: Boolean(file.shouldIgnore),
|
|
105
|
+
oid: typeof file.oid === "string" ? file.oid : undefined
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Request upload instructions for one object. The Hub answers with either a single-part PUT
|
|
110
|
+
* target or a completion endpoint plus one presigned PUT URL per part.
|
|
111
|
+
*/
|
|
112
|
+
export async function huggingFaceLfsBatch(fetchImpl, config, input) {
|
|
113
|
+
const response = await fetchImpl(huggingFaceLfsBatchUrl(config), {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: `Bearer ${config.token}`,
|
|
117
|
+
Accept: LFS_CONTENT_TYPE,
|
|
118
|
+
"Content-Type": LFS_CONTENT_TYPE
|
|
119
|
+
},
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
operation: "upload",
|
|
122
|
+
transfers: ["basic", "multipart"],
|
|
123
|
+
hash_algo: "sha256",
|
|
124
|
+
ref: { name: huggingFaceRevision(config) },
|
|
125
|
+
objects: [{ oid: input.oid, size: input.size }]
|
|
126
|
+
})
|
|
127
|
+
});
|
|
128
|
+
const payload = await readJson(response, "LFS batch", config);
|
|
129
|
+
const object = Array.isArray(payload.objects) ? payload.objects[0] : undefined;
|
|
130
|
+
if (!object || typeof object.oid !== "string") {
|
|
131
|
+
throw new Error(`Hugging Face LFS batch returned a malformed response for ${describe(config)}`);
|
|
132
|
+
}
|
|
133
|
+
if (object.error) {
|
|
134
|
+
throw new Error(`Hugging Face LFS batch rejected ${describe(config)}: ${object.error.message ?? "unknown error"}`);
|
|
135
|
+
}
|
|
136
|
+
const uploadAction = object.actions?.upload;
|
|
137
|
+
if (!uploadAction) {
|
|
138
|
+
// No actions means the Hub already stores this content; only the commit is left to do.
|
|
139
|
+
return { oid: object.oid, size: input.size };
|
|
140
|
+
}
|
|
141
|
+
if (typeof uploadAction.href !== "string") {
|
|
142
|
+
throw new Error(`Hugging Face LFS batch returned no upload href for ${describe(config)}`);
|
|
143
|
+
}
|
|
144
|
+
const header = uploadAction.header ?? {};
|
|
145
|
+
const rawChunkSize = header.chunk_size;
|
|
146
|
+
const chunkSize = rawChunkSize === undefined ? undefined : Number(rawChunkSize);
|
|
147
|
+
if (chunkSize !== undefined && (!Number.isInteger(chunkSize) || chunkSize <= 0)) {
|
|
148
|
+
throw new Error(`Hugging Face LFS batch returned a malformed chunk_size '${rawChunkSize}' for ${describe(config)}`);
|
|
149
|
+
}
|
|
150
|
+
const partUrls = Object.entries(header)
|
|
151
|
+
.filter(([key]) => /^\d+$/.test(key))
|
|
152
|
+
.sort(([left], [right]) => Number(left) - Number(right))
|
|
153
|
+
.map(([, value]) => value);
|
|
154
|
+
if (chunkSize !== undefined && partUrls.length !== Math.ceil(input.size / chunkSize)) {
|
|
155
|
+
throw new Error(`Hugging Face returned ${partUrls.length} part URLs for ${describe(config)}, `
|
|
156
|
+
+ `expected ${Math.ceil(input.size / chunkSize)} at chunk_size ${chunkSize}`);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
oid: object.oid,
|
|
160
|
+
size: input.size,
|
|
161
|
+
upload: { href: uploadAction.href, chunkSize, partUrls },
|
|
162
|
+
verifyHref: typeof object.actions?.verify?.href === "string" ? object.actions.verify.href : undefined
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Close a multipart LFS upload: `{oid, parts:[{partNumber, etag}]}` to the completion href. */
|
|
166
|
+
export async function huggingFaceCompleteLfsUpload(fetchImpl, config, input) {
|
|
167
|
+
const response = await fetchImpl(input.href, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { Accept: LFS_CONTENT_TYPE, "Content-Type": LFS_CONTENT_TYPE },
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
oid: input.oid,
|
|
172
|
+
parts: input.etags.map((etag, index) => ({ partNumber: index + 1, etag }))
|
|
173
|
+
})
|
|
174
|
+
});
|
|
175
|
+
await assertOk(response, "LFS completion", config);
|
|
176
|
+
}
|
|
177
|
+
/** Optional server-side check that the object landed intact. */
|
|
178
|
+
export async function huggingFaceVerifyLfsUpload(fetchImpl, config, input) {
|
|
179
|
+
const response = await fetchImpl(input.href, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: jsonHeaders(config),
|
|
182
|
+
body: JSON.stringify({ oid: input.oid, size: input.size })
|
|
183
|
+
});
|
|
184
|
+
await assertOk(response, "LFS verify", config);
|
|
185
|
+
}
|
|
186
|
+
/** Publish the uploaded blob as a commit. The body is NDJSON: a header line then one file line. */
|
|
187
|
+
export async function huggingFaceCommit(fetchImpl, config, input) {
|
|
188
|
+
const url = new URL(`${huggingFaceApiBase(config)}/commit/${encodeURIComponent(huggingFaceRevision(config))}`);
|
|
189
|
+
if (config.create_pr)
|
|
190
|
+
url.searchParams.set("create_pr", "1");
|
|
191
|
+
const lines = [
|
|
192
|
+
{
|
|
193
|
+
key: "header",
|
|
194
|
+
value: {
|
|
195
|
+
summary: config.commit_message ?? `Upload ${config.path} with Beam`,
|
|
196
|
+
description: config.commit_description ?? ""
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
key: "lfsFile",
|
|
201
|
+
value: { path: config.path, algo: "sha256", oid: input.oid, size: input.size }
|
|
202
|
+
}
|
|
203
|
+
];
|
|
204
|
+
const response = await fetchImpl(url, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/x-ndjson" },
|
|
207
|
+
body: lines.map((line) => JSON.stringify(line)).join("\n")
|
|
208
|
+
});
|
|
209
|
+
const payload = await readJson(response, "commit", config);
|
|
210
|
+
return {
|
|
211
|
+
commitOid: typeof payload.commitOid === "string" ? payload.commitOid : undefined,
|
|
212
|
+
pullRequestUrl: typeof payload.pullRequestUrl === "string" ? payload.pullRequestUrl : undefined
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** Read the first `length` bytes of a URL and return them base64-encoded, for `preupload`. */
|
|
216
|
+
export async function readSourceSample(fetchImpl, url, length = 512) {
|
|
217
|
+
const response = await fetchImpl(url, { headers: { Range: `bytes=0-${length - 1}` } });
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw new Error(`source sample read failed status=${response.status}`);
|
|
220
|
+
}
|
|
221
|
+
return base64(new Uint8Array(await response.arrayBuffer()));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Stream a URL once and return the sha256 of the whole body plus, when `partSize` is given,
|
|
225
|
+
* the hex MD5 of every part. The Hub will not issue upload URLs without the sha256, and the
|
|
226
|
+
* per-part MD5 is the ETag the completion payload has to quote.
|
|
227
|
+
*/
|
|
228
|
+
export async function hashSourceStream(fetchImpl, url, options = {}) {
|
|
229
|
+
const { createHash } = await import("node:crypto");
|
|
230
|
+
const response = await fetchImpl(url);
|
|
231
|
+
if (!response.ok || !response.body) {
|
|
232
|
+
throw new Error(`source hash read failed status=${response.status}`);
|
|
233
|
+
}
|
|
234
|
+
const whole = options.sha256 === false ? null : createHash("sha256");
|
|
235
|
+
const partSize = options.partSize;
|
|
236
|
+
const partEtags = [];
|
|
237
|
+
let part = partSize ? createHash("md5") : null;
|
|
238
|
+
let partBytes = 0;
|
|
239
|
+
for await (const rawChunk of response.body) {
|
|
240
|
+
let chunk = rawChunk;
|
|
241
|
+
whole?.update(chunk);
|
|
242
|
+
while (partSize && part && chunk.length > 0) {
|
|
243
|
+
const room = partSize - partBytes;
|
|
244
|
+
if (chunk.length < room) {
|
|
245
|
+
part.update(chunk);
|
|
246
|
+
partBytes += chunk.length;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
part.update(chunk.subarray(0, room));
|
|
250
|
+
partEtags.push(part.digest("hex"));
|
|
251
|
+
part = createHash("md5");
|
|
252
|
+
partBytes = 0;
|
|
253
|
+
chunk = chunk.subarray(room);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (partSize && part && partBytes > 0) {
|
|
257
|
+
partEtags.push(part.digest("hex"));
|
|
258
|
+
}
|
|
259
|
+
return { sha256: whole?.digest("hex"), partEtags };
|
|
260
|
+
}
|
|
261
|
+
function encodePath(path) {
|
|
262
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
263
|
+
}
|
|
264
|
+
function sameHost(left, right) {
|
|
265
|
+
try {
|
|
266
|
+
return new URL(left).host === new URL(right, left).host;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function normalizeEtag(value) {
|
|
273
|
+
if (!value)
|
|
274
|
+
return undefined;
|
|
275
|
+
return value.replace(/^W\//, "").replace(/^"|"$/g, "");
|
|
276
|
+
}
|
|
277
|
+
function jsonHeaders(config) {
|
|
278
|
+
return { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json" };
|
|
279
|
+
}
|
|
280
|
+
async function readJson(response, step, config) {
|
|
281
|
+
await assertOk(response, step, config);
|
|
282
|
+
return (await response.json());
|
|
283
|
+
}
|
|
284
|
+
async function assertOk(response, step, config) {
|
|
285
|
+
if (response.ok)
|
|
286
|
+
return;
|
|
287
|
+
const detail = (await response.text().catch(() => "")).slice(0, 512);
|
|
288
|
+
throw new Error(`Hugging Face ${step} failed status=${response.status} for ${describe(config)}${detail ? `: ${detail}` : ""}`);
|
|
289
|
+
}
|
|
290
|
+
function base64(bytes) {
|
|
291
|
+
let binary = "";
|
|
292
|
+
for (const byte of bytes)
|
|
293
|
+
binary += String.fromCharCode(byte);
|
|
294
|
+
return typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
|
|
295
|
+
}
|
|
296
|
+
export function describe(config) {
|
|
297
|
+
return `${huggingFaceRepoType(config)} ${config.repo_id}@${huggingFaceRevision(config)}/${config.path}`;
|
|
298
|
+
}
|
package/dist/models.d.ts
CHANGED
|
@@ -182,6 +182,8 @@ export interface ProviderTransferResumeInput {
|
|
|
182
182
|
testMode?: boolean;
|
|
183
183
|
expiresIn?: number;
|
|
184
184
|
signedUrlFlow?: SignedUrlFlow;
|
|
185
|
+
/** Requested plan chunk size. BeamCore may raise it; the response carries the effective value. */
|
|
186
|
+
chunkSize?: number;
|
|
185
187
|
}
|
|
186
188
|
export interface TransferTerminalEvent {
|
|
187
189
|
schema_version: "transfer-client-control/v6";
|
|
@@ -237,13 +239,41 @@ export interface HippiusProviderConfig {
|
|
|
237
239
|
api_token: string;
|
|
238
240
|
base_url?: string;
|
|
239
241
|
}
|
|
240
|
-
export type
|
|
242
|
+
export type HuggingFaceRepoType = "model" | "dataset" | "space" | "kernel" | "bucket";
|
|
243
|
+
export interface HuggingFaceProviderConfig {
|
|
244
|
+
provider: "huggingface";
|
|
245
|
+
id?: string;
|
|
246
|
+
/** Namespace and repo or bucket name separated by a slash, for example `org/dataset`. */
|
|
247
|
+
repo_id: string;
|
|
248
|
+
/** Path of the file inside the repo. */
|
|
249
|
+
path: string;
|
|
250
|
+
/** Defaults to `model`. */
|
|
251
|
+
repo_type?: HuggingFaceRepoType;
|
|
252
|
+
/** Branch, tag or commit. Defaults to `main`. Buckets are unversioned and ignore it. */
|
|
253
|
+
revision?: string;
|
|
254
|
+
token: string;
|
|
255
|
+
/** Hub endpoint. Defaults to `https://huggingface.co`. */
|
|
256
|
+
endpoint?: string;
|
|
257
|
+
/** Destination only. Commit summary for the upload. */
|
|
258
|
+
commit_message?: string;
|
|
259
|
+
/** Destination only. Commit description for the upload. */
|
|
260
|
+
commit_description?: string;
|
|
261
|
+
/** Destination only. Open the upload as a pull request instead of committing to `revision`. */
|
|
262
|
+
create_pr?: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Destination only. The Hub issues LFS upload URLs only for a known sha256, so the SDK
|
|
265
|
+
* must read every source byte once to hash it. Opt in explicitly.
|
|
266
|
+
*/
|
|
267
|
+
allow_source_rehash?: boolean;
|
|
268
|
+
}
|
|
269
|
+
export type ProviderConfig = S3ProviderConfig | R2ProviderConfig | S3CompatibleProviderConfig | HippiusProviderConfig | HuggingFaceProviderConfig;
|
|
241
270
|
export type ProviderSourceConfig = ProviderConfig;
|
|
242
271
|
export type ProviderDestinationConfig = ProviderConfig;
|
|
243
272
|
export type S3ProviderConfigInput = Omit<S3ProviderConfig, "provider">;
|
|
244
273
|
export type R2ProviderConfigInput = Omit<R2ProviderConfig, "provider">;
|
|
245
274
|
export type S3CompatibleProviderConfigInput = Omit<S3CompatibleProviderConfig, "driver">;
|
|
246
275
|
export type HippiusProviderConfigInput = Omit<HippiusProviderConfig, "provider">;
|
|
276
|
+
export type HuggingFaceProviderConfigInput = Omit<HuggingFaceProviderConfig, "provider">;
|
|
247
277
|
export declare const S3ProviderConfig: Readonly<{
|
|
248
278
|
create(input: S3ProviderConfigInput): S3ProviderConfig;
|
|
249
279
|
}>;
|
|
@@ -256,6 +286,9 @@ export declare const S3CompatibleProviderConfig: Readonly<{
|
|
|
256
286
|
export declare const HippiusProviderConfig: Readonly<{
|
|
257
287
|
create(input: HippiusProviderConfigInput): HippiusProviderConfig;
|
|
258
288
|
}>;
|
|
289
|
+
export declare const HuggingFaceProviderConfig: Readonly<{
|
|
290
|
+
create(input: HuggingFaceProviderConfigInput): HuggingFaceProviderConfig;
|
|
291
|
+
}>;
|
|
259
292
|
export interface ProviderTransferCreateInput {
|
|
260
293
|
sources: ProviderSourceConfig[];
|
|
261
294
|
destinations: ProviderDestinationConfig[];
|
|
@@ -271,12 +304,31 @@ export interface ProviderTransferCreateInput {
|
|
|
271
304
|
distribute?: boolean;
|
|
272
305
|
onBeforeTransferPrepare?: () => void | Promise<void>;
|
|
273
306
|
onPrepared?: (prepared: TransferPrepareResponse) => void | Promise<void>;
|
|
307
|
+
/**
|
|
308
|
+
* Called after the SDK creates a multipart upload and before its signed
|
|
309
|
+
* routes are streamed. The payload contains durable identity only;
|
|
310
|
+
* credentials, headers, and signed URLs are never exposed.
|
|
311
|
+
*/
|
|
312
|
+
onMultipartGroupReady?: (group: ProviderMultipartGroupIdentity) => void | Promise<void>;
|
|
274
313
|
throwIfCancelled?: (transferId?: string) => void | Promise<void>;
|
|
275
314
|
signedUrlFlow?: SignedUrlFlow;
|
|
276
315
|
/** Stable caller identity used to derive the transfer and lifecycle request ids. */
|
|
277
316
|
idempotencyKey?: string;
|
|
278
317
|
/** Internal recovery generation; callers normally omit this. */
|
|
279
318
|
routeGenerationId?: string;
|
|
319
|
+
/** Requested plan chunk size. BeamCore may raise it; the response carries the effective value. */
|
|
320
|
+
chunkSize?: number;
|
|
321
|
+
}
|
|
322
|
+
export interface ProviderMultipartGroupIdentity {
|
|
323
|
+
transferId: string;
|
|
324
|
+
multipartGroupId: string;
|
|
325
|
+
sourceId: string;
|
|
326
|
+
destinationId: string;
|
|
327
|
+
objectKey: string;
|
|
328
|
+
uploadId: string;
|
|
329
|
+
expectedObjectSize: number;
|
|
330
|
+
expectedPartCount: number;
|
|
331
|
+
expiresAt: string;
|
|
280
332
|
}
|
|
281
333
|
export interface PlanningHttpSource {
|
|
282
334
|
source_id: string;
|
package/dist/models.js
CHANGED
|
@@ -38,6 +38,25 @@ export const HippiusProviderConfig = Object.freeze({
|
|
|
38
38
|
return { provider: "hippius", ...input };
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
|
+
const HUGGINGFACE_REPO_TYPES = [
|
|
42
|
+
"model",
|
|
43
|
+
"dataset",
|
|
44
|
+
"space",
|
|
45
|
+
"kernel",
|
|
46
|
+
"bucket"
|
|
47
|
+
];
|
|
48
|
+
export const HuggingFaceProviderConfig = Object.freeze({
|
|
49
|
+
create(input) {
|
|
50
|
+
requireFields("huggingface", input, ["repo_id", "path", "token"]);
|
|
51
|
+
if (input.repo_type !== undefined && !HUGGINGFACE_REPO_TYPES.includes(input.repo_type)) {
|
|
52
|
+
throw new Error(`huggingface config repo_type must be one of ${HUGGINGFACE_REPO_TYPES.join(", ")}.`);
|
|
53
|
+
}
|
|
54
|
+
if (!input.repo_id.includes("/")) {
|
|
55
|
+
throw new Error("huggingface config repo_id must be `namespace/name`.");
|
|
56
|
+
}
|
|
57
|
+
return { provider: "huggingface", ...input };
|
|
58
|
+
}
|
|
59
|
+
});
|
|
41
60
|
function requireFields(provider, input, fields) {
|
|
42
61
|
for (const field of fields) {
|
|
43
62
|
if (!hasText(input[field])) {
|
package/dist/nats-control.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type ConnectionOptions } from "nats";
|
|
2
2
|
import type { MultipartGroupManifest, SignedChunkRoute, TransferTerminalSignalWaiter } from "./models.js";
|
|
3
3
|
export declare const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v6";
|
|
4
|
-
export declare const BEAM_DEFAULT_NATS_URL = "tls://
|
|
5
|
-
export declare const BEAM_DEFAULT_NATS_WS_URL = "wss://
|
|
4
|
+
export declare const BEAM_DEFAULT_NATS_URL = "tls://orch-gateway.b1m.ai:4222";
|
|
5
|
+
export declare const BEAM_DEFAULT_NATS_WS_URL = "wss://orch-gateway.b1m.ai:443";
|
|
6
6
|
export declare const BEAM_DEFAULT_MAX_PAYLOAD_BYTES: number;
|
|
7
7
|
export declare const BEAM_ROUTE_TARGET_PAYLOAD_BYTES: number;
|
|
8
8
|
export declare const ROUTE_RECOVERY_SIGN_MESSAGE_TYPE: "transfer.route_recovery.sign";
|
package/dist/nats-control.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { encode, decode } from "@msgpack/msgpack";
|
|
2
2
|
import { connect } from "nats";
|
|
3
3
|
export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v6";
|
|
4
|
-
export const BEAM_DEFAULT_NATS_URL = "tls://
|
|
5
|
-
export const BEAM_DEFAULT_NATS_WS_URL = "wss://
|
|
4
|
+
export const BEAM_DEFAULT_NATS_URL = "tls://orch-gateway.b1m.ai:4222";
|
|
5
|
+
export const BEAM_DEFAULT_NATS_WS_URL = "wss://orch-gateway.b1m.ai:443";
|
|
6
6
|
// NATS enforces max_payload per message. This guard splits signed-route control
|
|
7
7
|
// messages before the broker rejects them; transfer bytes never flow through NATS.
|
|
8
8
|
export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChunkDestinationSigningTarget, ChunkSigningPlanItem, PlanningHttpSource, PreparedDestination, PreparedHttpSource, ProviderDestinationConfig, ProviderSourceConfig, R2ProviderConfig, S3CompatibleProviderConfig, S3ProviderConfig, SignedChunkRoute } from "./models.js";
|
|
1
|
+
import type { ChunkDestinationSigningTarget, ChunkSigningPlanItem, HuggingFaceProviderConfig, PlanningHttpSource, PreparedDestination, PreparedHttpSource, ProviderDestinationConfig, ProviderSourceConfig, R2ProviderConfig, S3CompatibleProviderConfig, S3ProviderConfig, SignedChunkRoute } from "./models.js";
|
|
2
2
|
export type AnyS3CompatibleProviderConfig = S3ProviderConfig | R2ProviderConfig | S3CompatibleProviderConfig;
|
|
3
3
|
export declare function nowIso(): string;
|
|
4
4
|
export declare function expiresAtIso(expiresIn: number): string;
|
|
@@ -45,6 +45,8 @@ export declare function signDestinationRoute(input: {
|
|
|
45
45
|
maxPartNumber?: number;
|
|
46
46
|
finalObjectMetadata?: Record<string, string>;
|
|
47
47
|
multipartGroupId?: string;
|
|
48
|
+
/** Pre-issued destination URL, used by providers that sign their own upload targets. */
|
|
49
|
+
destUrl?: string;
|
|
48
50
|
fetchImpl?: typeof fetch;
|
|
49
51
|
}): Promise<SignedChunkRoute>;
|
|
50
52
|
export declare function signSourceReadRange(input: {
|
|
@@ -72,3 +74,4 @@ export declare function isS3CompatibleProvider(config: ProviderSourceConfig | Pr
|
|
|
72
74
|
export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
|
|
73
75
|
export declare function s3CompatibleRegion(source: AnyS3CompatibleProviderConfig): string;
|
|
74
76
|
export declare function s3CompatibleForcePathStyle(source: AnyS3CompatibleProviderConfig, endpoint?: string | undefined): boolean | undefined;
|
|
77
|
+
export declare function isHuggingFaceProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is HuggingFaceProviderConfig;
|
package/dist/provider-signing.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, GetObjectCommand, HeadObjectCommand, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
|
|
2
2
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
3
|
+
import { huggingFaceEndpoint, huggingFaceFileMetadata, huggingFaceRepoType, huggingFaceRevision } from "./huggingface.js";
|
|
3
4
|
export function nowIso() {
|
|
4
5
|
return new Date().toISOString();
|
|
5
6
|
}
|
|
@@ -47,6 +48,18 @@ export async function prepareProviderSource(source, options = {}) {
|
|
|
47
48
|
metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
|
|
48
49
|
};
|
|
49
50
|
}
|
|
51
|
+
if (isHuggingFaceProvider(source)) {
|
|
52
|
+
const metadata = await huggingFaceFileMetadata(options.fetchImpl ?? globalThis.fetch, source);
|
|
53
|
+
return {
|
|
54
|
+
source_id: source.id ?? `src_${index}`,
|
|
55
|
+
type: "http",
|
|
56
|
+
provider: "huggingface",
|
|
57
|
+
url: metadata.url,
|
|
58
|
+
size: metadata.size,
|
|
59
|
+
filename: filename(source.path),
|
|
60
|
+
metadata: huggingFaceMetadata(source, metadata.etag, metadata.commitHash)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
50
63
|
return unsupportedProviderConfig(source);
|
|
51
64
|
}
|
|
52
65
|
export async function prepareProviderSourceForPlan(source, options = {}) {
|
|
@@ -83,6 +96,17 @@ export async function prepareProviderSourceForPlan(source, options = {}) {
|
|
|
83
96
|
metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
|
|
84
97
|
};
|
|
85
98
|
}
|
|
99
|
+
if (isHuggingFaceProvider(source)) {
|
|
100
|
+
const metadata = await huggingFaceFileMetadata(options.fetchImpl ?? globalThis.fetch, source);
|
|
101
|
+
return {
|
|
102
|
+
source_id: source.id ?? `src_${index}`,
|
|
103
|
+
type: "http",
|
|
104
|
+
provider: "huggingface",
|
|
105
|
+
size: metadata.size,
|
|
106
|
+
filename: filename(source.path),
|
|
107
|
+
metadata: huggingFaceMetadata(source, metadata.etag, metadata.commitHash)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
86
110
|
return unsupportedProviderConfig(source);
|
|
87
111
|
}
|
|
88
112
|
export function prepareProviderDestination(destination, options = {}) {
|
|
@@ -111,6 +135,14 @@ export function prepareProviderDestination(destination, options = {}) {
|
|
|
111
135
|
}
|
|
112
136
|
};
|
|
113
137
|
}
|
|
138
|
+
if (isHuggingFaceProvider(destination)) {
|
|
139
|
+
return {
|
|
140
|
+
destination_id: destinationId,
|
|
141
|
+
provider: "huggingface",
|
|
142
|
+
logical_prefix: destination.path,
|
|
143
|
+
metadata: huggingFaceMetadata(destination)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
114
146
|
return unsupportedProviderConfig(destination);
|
|
115
147
|
}
|
|
116
148
|
export async function createMultipartUpload(input) {
|
|
@@ -175,7 +207,7 @@ export async function signDestinationRoute(input) {
|
|
|
175
207
|
throw new Error("destination signing target is missing object_key");
|
|
176
208
|
}
|
|
177
209
|
const [destUrl, sourceRoute] = await Promise.all([
|
|
178
|
-
signDestinationUrl({
|
|
210
|
+
input.destUrl ?? signDestinationUrl({
|
|
179
211
|
destination: input.destination,
|
|
180
212
|
objectKey: targetObjectKey,
|
|
181
213
|
uploadId: input.uploadId,
|
|
@@ -245,6 +277,10 @@ export async function signSourceReadRange(input) {
|
|
|
245
277
|
headers: { Range: range }
|
|
246
278
|
};
|
|
247
279
|
}
|
|
280
|
+
if (isHuggingFaceProvider(input.source)) {
|
|
281
|
+
const metadata = await huggingFaceFileMetadata(input.fetchImpl ?? globalThis.fetch, input.source);
|
|
282
|
+
return { url: metadata.url, headers: { Range: range } };
|
|
283
|
+
}
|
|
248
284
|
return unsupportedProviderConfig(input.source);
|
|
249
285
|
}
|
|
250
286
|
export async function signDestinationReadRange(input) {
|
|
@@ -266,6 +302,15 @@ export async function signDestinationReadRange(input) {
|
|
|
266
302
|
headers: { Range: range }
|
|
267
303
|
};
|
|
268
304
|
}
|
|
305
|
+
if (isHuggingFaceProvider(input.destination)) {
|
|
306
|
+
// Read-back resolves the committed file. Before the commit lands the bytes exist only as
|
|
307
|
+
// uncommitted LFS parts, which the Hub does not expose.
|
|
308
|
+
const metadata = await huggingFaceFileMetadata(input.fetchImpl ?? globalThis.fetch, {
|
|
309
|
+
...input.destination,
|
|
310
|
+
path: input.objectKey
|
|
311
|
+
});
|
|
312
|
+
return { url: metadata.url, headers: { Range: range } };
|
|
313
|
+
}
|
|
269
314
|
return unsupportedProviderConfig(input.destination);
|
|
270
315
|
}
|
|
271
316
|
async function signSourceRoute(input) {
|
|
@@ -290,6 +335,10 @@ async function signSourceRoute(input) {
|
|
|
290
335
|
headers: { Range: range }
|
|
291
336
|
};
|
|
292
337
|
}
|
|
338
|
+
if (isHuggingFaceProvider(input.source)) {
|
|
339
|
+
const metadata = await huggingFaceFileMetadata(input.fetchImpl ?? globalThis.fetch, input.source);
|
|
340
|
+
return { url: metadata.url, headers: { Range: range } };
|
|
341
|
+
}
|
|
293
342
|
return unsupportedProviderConfig(input.source);
|
|
294
343
|
}
|
|
295
344
|
async function signDestinationUrl(input) {
|
|
@@ -396,6 +445,22 @@ function s3CompatibleMetadata(source, endpoint = s3CompatibleEndpoint(source)) {
|
|
|
396
445
|
function isHippiusProvider(config) {
|
|
397
446
|
return config.provider === "hippius" && "api_token" in config;
|
|
398
447
|
}
|
|
448
|
+
export function isHuggingFaceProvider(config) {
|
|
449
|
+
return config.provider === "huggingface" && "repo_id" in config;
|
|
450
|
+
}
|
|
451
|
+
function huggingFaceMetadata(config, etag, commitHash) {
|
|
452
|
+
return compactMetadata({
|
|
453
|
+
driver: "huggingface",
|
|
454
|
+
repo_id: config.repo_id,
|
|
455
|
+
repo_type: huggingFaceRepoType(config),
|
|
456
|
+
revision: huggingFaceRevision(config),
|
|
457
|
+
path: config.path,
|
|
458
|
+
endpoint: huggingFaceEndpoint(config),
|
|
459
|
+
// For an LFS blob the Hub's linked ETag is the object's sha256.
|
|
460
|
+
sha256: etag,
|
|
461
|
+
commit_hash: commitHash
|
|
462
|
+
});
|
|
463
|
+
}
|
|
399
464
|
async function hippiusPresign(fetchImpl, baseUrl, token, bucket, key, action, expiresIn) {
|
|
400
465
|
const url = new URL(`/api/objectstore/buckets/${bucket}/presigned-url/`, baseUrl);
|
|
401
466
|
url.searchParams.set("key", key);
|