@eigenpal/sdk 0.10.56 → 0.11.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.
package/src/lib/files.ts CHANGED
@@ -132,7 +132,7 @@ function basename(path: string): string {
132
132
  * so the body can be replayed if the SDK retries the request. (A consumed
133
133
  * stream cannot be re-read; a Blob can.)
134
134
  */
135
- async function resolveFileBlob(file: FileInput): Promise<{ blob: Blob; filename: string }> {
135
+ export async function resolveFileBlob(file: FileInput): Promise<{ blob: Blob; filename: string }> {
136
136
  // `File` extends `Blob`, so this branch covers both.
137
137
  if (typeof Blob !== 'undefined' && file instanceof Blob) {
138
138
  return { blob: file, filename: (file as File).name || 'file' };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Default to Vercel's ~4.5 MiB function body limit. Operators with a different
3
+ * ingress limit can override it; `null` keeps every run file on multipart.
4
+ */
5
+ export const DEFAULT_MULTIPART_MAX_BYTES = Math.floor(4.5 * 1024 * 1024);
6
+ /** @deprecated Use {@link DEFAULT_MULTIPART_MAX_BYTES}. */
7
+ export const DIRECT_UPLOAD_BYTE_THRESHOLD = DEFAULT_MULTIPART_MAX_BYTES;
8
+
9
+ /**
10
+ * Conservative allowance for multipart boundaries, `target` / `input` /
11
+ * `overrides` / `metadata` parts, and Content-Disposition headers.
12
+ */
13
+ export const MULTIPART_ENVELOPE_HEADROOM_BYTES = 256 * 1024;
14
+
15
+ /** Max aggregate file-content bytes that may ride in one multipart run request. */
16
+ export function multipartFileByteBudget(
17
+ multipartMaxBytes: number | null = DEFAULT_MULTIPART_MAX_BYTES
18
+ ): number | null {
19
+ if (multipartMaxBytes === null) return null;
20
+ return Math.max(0, multipartMaxBytes - MULTIPART_ENVELOPE_HEADROOM_BYTES);
21
+ }
22
+
23
+ /**
24
+ * Choose which file keys must be pre-uploaded so remaining multipart file
25
+ * bytes plus envelope headroom stay under the configured multipart maximum.
26
+ *
27
+ * Prefers shedding the largest files first so smaller ones can stay on the
28
+ * single multipart round-trip when the aggregate still fits.
29
+ */
30
+ export function keysRequiringPreUpload(
31
+ files: ReadonlyArray<{ key: string; size: number }>,
32
+ multipartMaxBytes: number | null = DEFAULT_MULTIPART_MAX_BYTES
33
+ ): Set<string> {
34
+ const budget = multipartFileByteBudget(multipartMaxBytes);
35
+ const toPreUpload = new Set<string>();
36
+ if (budget === null) return toPreUpload;
37
+
38
+ for (const file of files) {
39
+ if (file.size > budget) {
40
+ toPreUpload.add(file.key);
41
+ }
42
+ }
43
+
44
+ let multipartTotal = 0;
45
+ for (const file of files) {
46
+ if (!toPreUpload.has(file.key)) multipartTotal += file.size;
47
+ }
48
+
49
+ const candidates = files
50
+ .filter((file) => !toPreUpload.has(file.key))
51
+ .slice()
52
+ .sort((a, b) => b.size - a.size || a.key.localeCompare(b.key));
53
+
54
+ for (const file of candidates) {
55
+ if (multipartTotal <= budget) break;
56
+ toPreUpload.add(file.key);
57
+ multipartTotal -= file.size;
58
+ }
59
+
60
+ return toPreUpload;
61
+ }
@@ -1,10 +1,52 @@
1
- import type { OperationResult } from '../client';
1
+ import type { OperationResult, RequestDispatchOptions } from '../client';
2
2
  import type { Client } from '../generated/client';
3
- import { filesContentGet, filesCreate, filesDelete, filesGet } from '../generated/sdk.gen';
3
+ import {
4
+ filesContentGet,
5
+ filesDelete,
6
+ filesGet,
7
+ filesUploadsAbort,
8
+ filesUploadsComplete,
9
+ filesUploadsCreate,
10
+ } from '../generated/sdk.gen';
4
11
 
5
- type Dispatch = <T>(call: () => Promise<OperationResult<T>>) => Promise<T>;
12
+ type Dispatch = <T>(
13
+ call: () => Promise<OperationResult<T>>,
14
+ options?: RequestDispatchOptions
15
+ ) => Promise<T>;
6
16
  type AnyResponse = any;
7
17
  type SignalOptions = { signal?: AbortSignal };
18
+ type UploadOptions = SignalOptions & {
19
+ /** Required only when `file` is a nameless Blob. */
20
+ filename?: string;
21
+ /**
22
+ * Tenant-scoped idempotency key for upload-session creation. Generated when
23
+ * omitted so SDK retries of a lost create response reuse the same reservation.
24
+ */
25
+ idempotencyKey?: string;
26
+ /**
27
+ * Optional lifecycle marker. Pass `run-input` for automatic `client.run`
28
+ * pre-uploads so the server can retain them for retries and reap them after 24 hours.
29
+ * Explicit `files.upload` callers should omit this (durable reusable file).
30
+ */
31
+ purpose?: 'run-input';
32
+ onProgress?: (uploadedBytes: number, totalBytes: number) => void;
33
+ };
34
+
35
+ export type CreateUploadInput = {
36
+ filename: string;
37
+ contentType: string;
38
+ size: number;
39
+ /** Compatible with server `CreateFileUploadSessionRequest.idempotencyKey`. */
40
+ idempotencyKey?: string;
41
+ purpose?: 'run-input';
42
+ };
43
+
44
+ function newIdempotencyKey(): string {
45
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
46
+ return crypto.randomUUID();
47
+ }
48
+ return `upload_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
49
+ }
8
50
 
9
51
  export class FilesResource {
10
52
  constructor(
@@ -12,15 +54,92 @@ export class FilesResource {
12
54
  private readonly dispatch: Dispatch
13
55
  ) {}
14
56
 
15
- async upload(file: Blob | File, options: SignalOptions = {}): Promise<AnyResponse> {
57
+ async upload(file: Blob | File, options: UploadOptions = {}): Promise<AnyResponse> {
58
+ const filename =
59
+ options.filename ??
60
+ (typeof File !== 'undefined' && file instanceof File ? file.name : undefined);
61
+ if (!filename) throw new Error('filename is required when uploading a Blob');
62
+ const idempotencyKey = options.idempotencyKey ?? newIdempotencyKey();
63
+ const negotiation = await this.createUpload(
64
+ {
65
+ filename,
66
+ contentType: file.type || 'application/octet-stream',
67
+ size: file.size,
68
+ idempotencyKey,
69
+ ...(options.purpose ? { purpose: options.purpose } : {}),
70
+ },
71
+ options
72
+ );
73
+
74
+ if (negotiation.transport === 'presigned-put') {
75
+ const response = await fetch(negotiation.url, {
76
+ method: 'PUT',
77
+ headers: Object.fromEntries(
78
+ Object.entries((negotiation.headers ?? {}) as Record<string, string>).filter(
79
+ ([name]) => name.toLowerCase() !== 'content-length'
80
+ )
81
+ ),
82
+ body: file,
83
+ signal: options.signal,
84
+ });
85
+ if (!response.ok) {
86
+ await this.abortUpload(negotiation.uploadId).catch(() => undefined);
87
+ throw new Error(`Storage upload failed (${response.status}); retry the upload`);
88
+ }
89
+ options.onProgress?.(file.size, file.size);
90
+ return this.completeUpload(negotiation.uploadId, options);
91
+ }
92
+
16
93
  const form = new FormData();
17
- form.append('file', file);
94
+ form.append('file', file, filename);
95
+ if (options.purpose) form.append('purpose', options.purpose);
96
+ const uploaded = await this.dispatch(
97
+ () =>
98
+ this.client.post({
99
+ url: negotiation.url,
100
+ body: form as never,
101
+ bodySerializer: null,
102
+ headers: { 'Content-Type': null },
103
+ signal: options.signal,
104
+ }) as Promise<OperationResult<AnyResponse>>
105
+ );
106
+ options.onProgress?.(file.size, file.size);
107
+ return uploaded;
108
+ }
109
+
110
+ async createUpload(input: CreateUploadInput, options: SignalOptions = {}): Promise<AnyResponse> {
111
+ const body = {
112
+ filename: input.filename,
113
+ contentType: input.contentType,
114
+ size: input.size,
115
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
116
+ ...(input.purpose ? { purpose: input.purpose } : {}),
117
+ };
118
+ return this.dispatch(() =>
119
+ filesUploadsCreate({
120
+ client: this.client,
121
+ // Server accepts optional idempotencyKey; generated types may lag the schema.
122
+ body: body as never,
123
+ signal: options.signal,
124
+ })
125
+ );
126
+ }
127
+
128
+ async completeUpload(uploadId: string, options: SignalOptions = {}): Promise<AnyResponse> {
18
129
  return this.dispatch(() =>
19
- filesCreate({
130
+ filesUploadsComplete({
20
131
  client: this.client,
21
- body: form as never,
22
- bodySerializer: null,
23
- headers: { 'Content-Type': null },
132
+ path: { uploadId },
133
+ signal: options.signal,
134
+ })
135
+ );
136
+ }
137
+
138
+ async abortUpload(uploadId: string, options: SignalOptions = {}): Promise<AnyResponse> {
139
+ return this.dispatch(() =>
140
+ filesUploadsAbort({
141
+ client: this.client,
142
+ path: { uploadId },
24
143
  signal: options.signal,
25
144
  })
26
145
  );
@@ -33,14 +152,18 @@ export class FilesResource {
33
152
  }
34
153
 
35
154
  async download(fileId: string, options: SignalOptions = {}): Promise<Blob> {
36
- return this.dispatch(async () => {
37
- const response = await filesContentGet({
38
- client: this.client,
39
- path: { id: fileId },
40
- signal: options.signal,
41
- });
42
- return response as OperationResult<Blob>;
43
- });
155
+ return this.dispatch(
156
+ async () => {
157
+ const response = await filesContentGet({
158
+ client: this.client,
159
+ path: { id: fileId },
160
+ parseAs: 'blob',
161
+ signal: options.signal,
162
+ });
163
+ return response as OperationResult<Blob>;
164
+ },
165
+ { responseKind: 'binary' }
166
+ );
44
167
  }
45
168
 
46
169
  async delete(fileId: string, options: SignalOptions = {}): Promise<AnyResponse> {
@@ -250,7 +250,7 @@ export class RunsReviewsResource {
250
250
  return this.dispatch(
251
251
  () =>
252
252
  this.client.post({
253
- url: '/api/v1/runs/{id}/reviews/expected',
253
+ url: '/v1/runs/{id}/reviews/expected',
254
254
  path: { id: runId },
255
255
  body: formData,
256
256
  bodySerializer: null,
@@ -11,5 +11,5 @@ import type { CreateClientConfig } from './generated/client.gen';
11
11
  */
12
12
  export const createClientConfig: CreateClientConfig = (config) => ({
13
13
  ...config,
14
- baseUrl: config?.baseUrl ?? 'https://studio.eigenpal.com',
14
+ baseUrl: config?.baseUrl ?? 'https://api.eigenpal.com',
15
15
  });
package/src/telemetry.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  export const SDK_LANGUAGE = 'typescript';
20
20
  // Rewritten at publish time by scripts/release/platform/render-sdk-versions.sh.
21
21
  // Keep this string literal exactly stable — sed matches on it.
22
- export const SDK_VERSION = '0.10.56';
22
+ export const SDK_VERSION = '0.11.1';
23
23
 
24
24
  function detectRuntime(): string {
25
25
  const g = globalThis as unknown as {