@opengeni/sdk 0.20.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/index.d.ts +466 -7
- package/dist/index.js +281 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +368 -49
- package/src/errors.ts +22 -1
- package/src/index.ts +43 -0
- package/src/types.ts +545 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Framework-agnostic TypeScript SDK for the OpenGeni API: typed client, session lifecycle, SSE event streaming with reconnect + replay-by-sequence, and proxy re-streaming helpers.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
package/src/client.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
OpenGeniApiContractMismatchError,
|
|
3
|
+
OpenGeniApiError,
|
|
4
|
+
OpenGeniSessionListCursorError,
|
|
5
|
+
} from "./errors";
|
|
2
6
|
import {
|
|
3
7
|
streamSessionEvents,
|
|
4
8
|
type SessionEventStreamTransport,
|
|
@@ -16,6 +20,8 @@ import type {
|
|
|
16
20
|
CodexAccount,
|
|
17
21
|
CodexAccountsResponse,
|
|
18
22
|
CodexRotationSettings,
|
|
23
|
+
CodexOverviewResponse,
|
|
24
|
+
CodexAllocatorUpdate,
|
|
19
25
|
CodexConnectionStatus,
|
|
20
26
|
CodexConnectPoll,
|
|
21
27
|
CodexConnectStart,
|
|
@@ -28,6 +34,7 @@ import type {
|
|
|
28
34
|
CapabilityInstallation,
|
|
29
35
|
AddDocumentRequest,
|
|
30
36
|
ClientConfig,
|
|
37
|
+
WorkspaceModelCatalogResponse,
|
|
31
38
|
ClientSessionEventInput,
|
|
32
39
|
CompactSessionContextResult,
|
|
33
40
|
CompleteFileUploadResponse,
|
|
@@ -82,6 +89,9 @@ import type {
|
|
|
82
89
|
ListWorkspaceMembersResponse,
|
|
83
90
|
PackInstallation,
|
|
84
91
|
ReasoningEffort,
|
|
92
|
+
RetainedArtifactContent,
|
|
93
|
+
RetainedArtifactContentOptions,
|
|
94
|
+
RetainedArtifactMetadata,
|
|
85
95
|
RegisterCapabilityPackRequest,
|
|
86
96
|
ResourceRef,
|
|
87
97
|
ScheduledTask,
|
|
@@ -90,19 +100,25 @@ import type {
|
|
|
90
100
|
SessionListResponse,
|
|
91
101
|
UpdateSessionPinRequest,
|
|
92
102
|
SessionEvent,
|
|
103
|
+
SessionEventCompactResult,
|
|
104
|
+
SessionEventCompactResultOptions,
|
|
93
105
|
SessionEventListOptions,
|
|
94
106
|
SessionEventPage,
|
|
95
107
|
SessionGoal,
|
|
96
108
|
SessionHumanInputRequest,
|
|
97
109
|
SessionLineageResponse,
|
|
98
110
|
SessionMcpCredentialUpdateInput,
|
|
111
|
+
UpdateSessionMcpApprovalPolicyRequest,
|
|
112
|
+
UpdateSessionMcpApprovalPolicyResponse,
|
|
99
113
|
SessionQueueSnapshot,
|
|
100
114
|
SessionQueueMutationResponse,
|
|
101
115
|
ComposerDraft,
|
|
102
116
|
DeleteSessionQueueItemRequest,
|
|
103
117
|
EditSessionQueueItemRequest,
|
|
104
118
|
MoveSessionQueueItemRequest,
|
|
119
|
+
NewSessionDraft,
|
|
105
120
|
SaveComposerDraftRequest,
|
|
121
|
+
SaveNewSessionDraftRequest,
|
|
106
122
|
SteerSessionQueueItemRequest,
|
|
107
123
|
SessionControlResponse,
|
|
108
124
|
WorkspaceInferenceControlResponse,
|
|
@@ -177,7 +193,22 @@ import type {
|
|
|
177
193
|
OAuthStartRequest,
|
|
178
194
|
OAuthStartResponse,
|
|
179
195
|
} from "./types";
|
|
180
|
-
import {
|
|
196
|
+
import {
|
|
197
|
+
OPENGENI_API_CONTRACT_HEADER,
|
|
198
|
+
OPENGENI_API_CONTRACT_REVISION,
|
|
199
|
+
RETAINED_OUTPUT_MAX_PAGE_BYTES,
|
|
200
|
+
} from "./types";
|
|
201
|
+
|
|
202
|
+
function sessionListQuery(options: {
|
|
203
|
+
limit?: number;
|
|
204
|
+
parentSessionId?: string | null;
|
|
205
|
+
}): Record<string, string> {
|
|
206
|
+
const { limit, parentSessionId } = options;
|
|
207
|
+
return {
|
|
208
|
+
...(limit === undefined ? {} : { limit: String(limit) }),
|
|
209
|
+
...(parentSessionId === undefined ? {} : { parentSessionId: parentSessionId ?? "null" }),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
181
212
|
|
|
182
213
|
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
183
214
|
|
|
@@ -255,6 +286,24 @@ export class OpenGeniClient {
|
|
|
255
286
|
);
|
|
256
287
|
}
|
|
257
288
|
|
|
289
|
+
async getNewSessionDraft(workspaceId: string): Promise<NewSessionDraft> {
|
|
290
|
+
return await this.requestJson<NewSessionDraft>(
|
|
291
|
+
"GET",
|
|
292
|
+
`/v1/workspaces/${workspaceId}/new-session-draft`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async saveNewSessionDraft(
|
|
297
|
+
workspaceId: string,
|
|
298
|
+
request: SaveNewSessionDraftRequest,
|
|
299
|
+
): Promise<NewSessionDraft> {
|
|
300
|
+
return await this.requestJson<NewSessionDraft>(
|
|
301
|
+
"PUT",
|
|
302
|
+
`/v1/workspaces/${workspaceId}/new-session-draft`,
|
|
303
|
+
request,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
258
307
|
async getSession(workspaceId: string, sessionId: string): Promise<Session> {
|
|
259
308
|
return await this.requestJson<Session>(
|
|
260
309
|
"GET",
|
|
@@ -274,6 +323,24 @@ export class OpenGeniClient {
|
|
|
274
323
|
);
|
|
275
324
|
}
|
|
276
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Replace one attached MCP server's approval policy. The change is captured
|
|
328
|
+
* by the next claimed attempt; already-claimed work keeps its immutable
|
|
329
|
+
* policy snapshot.
|
|
330
|
+
*/
|
|
331
|
+
async updateSessionMcpApprovalPolicy(
|
|
332
|
+
workspaceId: string,
|
|
333
|
+
sessionId: string,
|
|
334
|
+
serverId: string,
|
|
335
|
+
request: UpdateSessionMcpApprovalPolicyRequest,
|
|
336
|
+
): Promise<UpdateSessionMcpApprovalPolicyResponse> {
|
|
337
|
+
return await this.requestJson<UpdateSessionMcpApprovalPolicyResponse>(
|
|
338
|
+
"PATCH",
|
|
339
|
+
`/v1/workspaces/${workspaceId}/sessions/${sessionId}/mcp-servers/${encodeURIComponent(serverId)}/approval-policy`,
|
|
340
|
+
request,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
277
344
|
async listSessions(
|
|
278
345
|
workspaceId: string,
|
|
279
346
|
options: {
|
|
@@ -282,21 +349,21 @@ export class OpenGeniClient {
|
|
|
282
349
|
search?: string;
|
|
283
350
|
} = {},
|
|
284
351
|
): Promise<Session[]> {
|
|
352
|
+
// Search was added with the pin-aware page endpoint. An older API silently
|
|
353
|
+
// ignores unknown query parameters on the historical array endpoint, which
|
|
354
|
+
// would turn a search into a plausible-looking unfiltered result. Route
|
|
355
|
+
// searches through listSessionPage so its rolling-version shape check can
|
|
356
|
+
// fail explicitly on an older server; retain the array endpoint for every
|
|
357
|
+
// pre-existing call shape.
|
|
358
|
+
if (options.search?.trim()) {
|
|
359
|
+
const page = await this.listSessionPage(workspaceId, options);
|
|
360
|
+
return [...page.pinned, ...page.sessions];
|
|
361
|
+
}
|
|
285
362
|
return await this.requestJson<Session[]>(
|
|
286
363
|
"GET",
|
|
287
364
|
`/v1/workspaces/${workspaceId}/sessions`,
|
|
288
365
|
undefined,
|
|
289
|
-
|
|
290
|
-
...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
|
|
291
|
-
...(options.search?.trim() ? { search: options.search.trim() } : {}),
|
|
292
|
-
...(Object.prototype.hasOwnProperty.call(options, "parentSessionId") &&
|
|
293
|
-
options.parentSessionId !== undefined
|
|
294
|
-
? {
|
|
295
|
-
parentSessionId:
|
|
296
|
-
options.parentSessionId === null ? "null" : String(options.parentSessionId),
|
|
297
|
-
}
|
|
298
|
-
: {}),
|
|
299
|
-
},
|
|
366
|
+
sessionListQuery(options),
|
|
300
367
|
);
|
|
301
368
|
}
|
|
302
369
|
|
|
@@ -308,26 +375,51 @@ export class OpenGeniClient {
|
|
|
308
375
|
parentSessionId?: string | null;
|
|
309
376
|
cursor?: string;
|
|
310
377
|
search?: string;
|
|
378
|
+
/** Return only the complete personal pinned projection. */
|
|
379
|
+
pinsOnly?: boolean;
|
|
311
380
|
} = {},
|
|
312
381
|
): Promise<SessionListResponse> {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
? {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
382
|
+
const search = options.search?.trim();
|
|
383
|
+
let response: SessionListResponse | Session[];
|
|
384
|
+
try {
|
|
385
|
+
response = await this.requestJson<SessionListResponse | Session[]>(
|
|
386
|
+
"GET",
|
|
387
|
+
`/v1/workspaces/${workspaceId}/sessions`,
|
|
388
|
+
undefined,
|
|
389
|
+
{
|
|
390
|
+
view: "page",
|
|
391
|
+
...sessionListQuery(options),
|
|
392
|
+
...(options.cursor !== undefined ? { cursor: options.cursor } : {}),
|
|
393
|
+
...(search ? { search } : {}),
|
|
394
|
+
...(options.pinsOnly ? { pinsOnly: "true" } : {}),
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (error instanceof OpenGeniApiError && error.status === 410) {
|
|
399
|
+
throw new OpenGeniSessionListCursorError(error.status, error.body);
|
|
400
|
+
}
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
if (Array.isArray(response)) {
|
|
404
|
+
// Rolling/same-major compatibility: an older API ignores `view=page` and
|
|
405
|
+
// returns the historical array. That is an honest one-page projection;
|
|
406
|
+
// never pretend it honored a cursor supplied directly by a caller.
|
|
407
|
+
if (options.cursor) {
|
|
408
|
+
throw new Error("The connected OpenGeni API does not support stable session-page cursors");
|
|
409
|
+
}
|
|
410
|
+
// Older APIs ignore unknown query parameters. Treating their unfiltered
|
|
411
|
+
// array as a successful search would be worse than an explicit rolling-
|
|
412
|
+
// upgrade error (and client-side filtering cannot recover matches beyond
|
|
413
|
+
// the old endpoint's bounded first page).
|
|
414
|
+
if (search) {
|
|
415
|
+
throw new Error("The connected OpenGeni API does not support session search");
|
|
416
|
+
}
|
|
417
|
+
if (options.pinsOnly) {
|
|
418
|
+
throw new Error("The connected OpenGeni API does not support pins-only session lists");
|
|
419
|
+
}
|
|
420
|
+
return { pinned: [], sessions: response, nextCursor: null };
|
|
421
|
+
}
|
|
422
|
+
return response;
|
|
331
423
|
}
|
|
332
424
|
|
|
333
425
|
/** Set this authenticated member's personal workspace pin for a session. */
|
|
@@ -533,8 +625,18 @@ export class OpenGeniClient {
|
|
|
533
625
|
async listEventPage(
|
|
534
626
|
workspaceId: string,
|
|
535
627
|
sessionId: string,
|
|
536
|
-
options:
|
|
537
|
-
): Promise<
|
|
628
|
+
options: SessionEventCompactResultOptions,
|
|
629
|
+
): Promise<SessionEventCompactResult | null>;
|
|
630
|
+
async listEventPage(
|
|
631
|
+
workspaceId: string,
|
|
632
|
+
sessionId: string,
|
|
633
|
+
options?: SessionEventListOptions,
|
|
634
|
+
): Promise<SessionEventPage>;
|
|
635
|
+
async listEventPage(
|
|
636
|
+
workspaceId: string,
|
|
637
|
+
sessionId: string,
|
|
638
|
+
options: SessionEventListOptions | SessionEventCompactResultOptions = {},
|
|
639
|
+
): Promise<SessionEventPage | SessionEventCompactResult | null> {
|
|
538
640
|
if (
|
|
539
641
|
options.latest &&
|
|
540
642
|
["includeTypes", "excludeTypes", "includeClasses", "excludeClasses"].some((name) =>
|
|
@@ -543,22 +645,32 @@ export class OpenGeniClient {
|
|
|
543
645
|
) {
|
|
544
646
|
throw new TypeError("latest cannot be combined with event filters");
|
|
545
647
|
}
|
|
648
|
+
if (options.resultMode === "compact" && !options.latest) {
|
|
649
|
+
throw new TypeError("resultMode=compact requires latest");
|
|
650
|
+
}
|
|
651
|
+
const listOptions: SessionEventListOptions | null =
|
|
652
|
+
options.resultMode === "compact" ? null : options;
|
|
546
653
|
const response = await this.fetchImpl(
|
|
547
654
|
this.url(`/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`, {
|
|
548
|
-
...(
|
|
549
|
-
...(
|
|
550
|
-
...(
|
|
551
|
-
...(
|
|
655
|
+
...(listOptions?.after !== undefined ? { after: String(listOptions.after) } : {}),
|
|
656
|
+
...(listOptions?.before !== undefined ? { before: String(listOptions.before) } : {}),
|
|
657
|
+
...(listOptions?.limit !== undefined ? { limit: String(listOptions.limit) } : {}),
|
|
658
|
+
...(listOptions?.compact ? { compact: "1" } : {}),
|
|
552
659
|
...(options.mode ? { mode: options.mode } : {}),
|
|
553
|
-
...(
|
|
660
|
+
...(listOptions?.direction ? { direction: listOptions.direction } : {}),
|
|
554
661
|
...(options.payloadMode ? { payloadMode: options.payloadMode } : {}),
|
|
555
|
-
...(options.
|
|
556
|
-
...(
|
|
557
|
-
|
|
558
|
-
|
|
662
|
+
...(options.resultMode ? { resultMode: options.resultMode } : {}),
|
|
663
|
+
...(listOptions?.includeTypes?.length
|
|
664
|
+
? { includeTypes: listOptions.includeTypes.join(",") }
|
|
665
|
+
: {}),
|
|
666
|
+
...(listOptions?.excludeTypes?.length
|
|
667
|
+
? { excludeTypes: listOptions.excludeTypes.join(",") }
|
|
668
|
+
: {}),
|
|
669
|
+
...(listOptions?.includeClasses?.length
|
|
670
|
+
? { includeClasses: listOptions.includeClasses.join(",") }
|
|
559
671
|
: {}),
|
|
560
|
-
...(
|
|
561
|
-
? { excludeClasses:
|
|
672
|
+
...(listOptions?.excludeClasses?.length
|
|
673
|
+
? { excludeClasses: listOptions.excludeClasses.join(",") }
|
|
562
674
|
: {}),
|
|
563
675
|
...(options.latest ? { latest: options.latest } : {}),
|
|
564
676
|
}),
|
|
@@ -569,7 +681,11 @@ export class OpenGeniClient {
|
|
|
569
681
|
);
|
|
570
682
|
assertApiContractResponse(response);
|
|
571
683
|
if (!response.ok) throw new OpenGeniApiError(response.status, await safeText(response));
|
|
572
|
-
const
|
|
684
|
+
const body = await response.json();
|
|
685
|
+
if (options.resultMode === "compact") {
|
|
686
|
+
return body as SessionEventCompactResult;
|
|
687
|
+
}
|
|
688
|
+
const events = body as SessionEvent[];
|
|
573
689
|
const integerHeader = (name: string): number | null => {
|
|
574
690
|
const raw = response.headers.get(name);
|
|
575
691
|
if (raw === null) return null;
|
|
@@ -613,6 +729,23 @@ export class OpenGeniClient {
|
|
|
613
729
|
};
|
|
614
730
|
}
|
|
615
731
|
|
|
732
|
+
/**
|
|
733
|
+
* Fetch the authoritative newest-sequence semantic result directly. This is
|
|
734
|
+
* the callback-loss recovery path: it reads one compact durable result and
|
|
735
|
+
* never creates a model turn. `latest: "receipt"` aliases `tool_receipt`;
|
|
736
|
+
* turn generation remains scoped retry metadata.
|
|
737
|
+
*/
|
|
738
|
+
async getLatestEventResult(
|
|
739
|
+
workspaceId: string,
|
|
740
|
+
sessionId: string,
|
|
741
|
+
options: Omit<SessionEventCompactResultOptions, "resultMode"> = { latest: "terminal" },
|
|
742
|
+
): Promise<SessionEventCompactResult | null> {
|
|
743
|
+
return await this.listEventPage(workspaceId, sessionId, {
|
|
744
|
+
...options,
|
|
745
|
+
resultMode: "compact",
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
616
749
|
/** POST a user/control event to the session. Returns the accepted event. */
|
|
617
750
|
async sendEvent(
|
|
618
751
|
workspaceId: string,
|
|
@@ -1418,6 +1551,14 @@ export class OpenGeniClient {
|
|
|
1418
1551
|
return config;
|
|
1419
1552
|
}
|
|
1420
1553
|
|
|
1554
|
+
/** Authenticated model definitions plus workspace-specific selectability. */
|
|
1555
|
+
async getWorkspaceModelCatalog(workspaceId: string): Promise<WorkspaceModelCatalogResponse> {
|
|
1556
|
+
return await this.requestJson<WorkspaceModelCatalogResponse>(
|
|
1557
|
+
"GET",
|
|
1558
|
+
`/v1/workspaces/${workspaceId}/model-catalog`,
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1421
1562
|
/** The caller's access context: subject, account + workspace grants, defaults. */
|
|
1422
1563
|
async getAccessContext(): Promise<AccessContext> {
|
|
1423
1564
|
return await this.requestJson<AccessContext>("GET", "/v1/access/me");
|
|
@@ -1838,21 +1979,27 @@ export class OpenGeniClient {
|
|
|
1838
1979
|
* -> complete. Returns the ready `FileAsset`.
|
|
1839
1980
|
*/
|
|
1840
1981
|
async uploadFile(workspaceId: string, input: UploadFileInput): Promise<FileAsset> {
|
|
1841
|
-
//
|
|
1842
|
-
//
|
|
1982
|
+
// Snapshot mutable inputs before hashing so the digest always describes the
|
|
1983
|
+
// exact bytes later sent to object storage. Copy Uint8Array views into a
|
|
1984
|
+
// Blob so byte offsets/shared buffers can't leak surrounding bytes.
|
|
1843
1985
|
const body: Blob | ArrayBuffer | string =
|
|
1844
|
-
input.data instanceof Uint8Array
|
|
1986
|
+
input.data instanceof Uint8Array
|
|
1987
|
+
? new Blob([input.data.slice()])
|
|
1988
|
+
: input.data instanceof ArrayBuffer
|
|
1989
|
+
? input.data.slice(0)
|
|
1990
|
+
: input.data;
|
|
1845
1991
|
const sizeBytes =
|
|
1846
1992
|
typeof body === "string"
|
|
1847
1993
|
? new TextEncoder().encode(body).byteLength
|
|
1848
1994
|
: body instanceof Blob
|
|
1849
1995
|
? body.size
|
|
1850
1996
|
: body.byteLength;
|
|
1997
|
+
const sha256 = input.sha256 ?? (await sha256ForUpload(body));
|
|
1851
1998
|
const upload = await this.beginFileUpload(workspaceId, {
|
|
1852
1999
|
filename: input.filename,
|
|
1853
2000
|
contentType: input.contentType,
|
|
1854
2001
|
sizeBytes,
|
|
1855
|
-
|
|
2002
|
+
sha256,
|
|
1856
2003
|
});
|
|
1857
2004
|
const putResponse = await this.fetchImpl(upload.putUrl, {
|
|
1858
2005
|
method: "PUT",
|
|
@@ -1878,6 +2025,80 @@ export class OpenGeniClient {
|
|
|
1878
2025
|
);
|
|
1879
2026
|
}
|
|
1880
2027
|
|
|
2028
|
+
/** Read provider-neutral retained evidence metadata; never returns a storage location. */
|
|
2029
|
+
async getRetainedArtifact(
|
|
2030
|
+
workspaceId: string,
|
|
2031
|
+
artifactId: string,
|
|
2032
|
+
): Promise<RetainedArtifactMetadata> {
|
|
2033
|
+
return await this.requestJson<RetainedArtifactMetadata>(
|
|
2034
|
+
"GET",
|
|
2035
|
+
`/v1/workspaces/${workspaceId}/artifacts/${artifactId}`,
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Read at most one authenticated retained-evidence range from the API. This
|
|
2041
|
+
* deliberately does not use the ordinary signed file-download URL.
|
|
2042
|
+
*/
|
|
2043
|
+
async getRetainedArtifactContent(
|
|
2044
|
+
workspaceId: string,
|
|
2045
|
+
artifactId: string,
|
|
2046
|
+
options: RetainedArtifactContentOptions = {},
|
|
2047
|
+
): Promise<RetainedArtifactContent> {
|
|
2048
|
+
if (options.range && (options.range.length > 128 || /[^\x20-\x7e]/.test(options.range))) {
|
|
2049
|
+
throw new RangeError("retained artifact range must be at most 128 printable ASCII bytes");
|
|
2050
|
+
}
|
|
2051
|
+
const response = await this.fetchImpl(
|
|
2052
|
+
this.url(`/v1/workspaces/${workspaceId}/artifacts/${artifactId}/content`),
|
|
2053
|
+
{
|
|
2054
|
+
method: "GET",
|
|
2055
|
+
headers: {
|
|
2056
|
+
...this.headers(),
|
|
2057
|
+
Accept: "application/octet-stream",
|
|
2058
|
+
...(options.range ? { Range: options.range } : {}),
|
|
2059
|
+
},
|
|
2060
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
2061
|
+
},
|
|
2062
|
+
);
|
|
2063
|
+
try {
|
|
2064
|
+
assertApiContractResponse(response);
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
await cancelResponseBody(response, "retained artifact API contract mismatch");
|
|
2067
|
+
throw error;
|
|
2068
|
+
}
|
|
2069
|
+
if (!response.ok) {
|
|
2070
|
+
throw new OpenGeniApiError(response.status, await safeBoundedText(response));
|
|
2071
|
+
}
|
|
2072
|
+
if (response.status !== 200 && response.status !== 206) {
|
|
2073
|
+
await cancelResponseBody(response, "unexpected retained artifact response status");
|
|
2074
|
+
throw new OpenGeniApiError(response.status, "unexpected retained artifact response status");
|
|
2075
|
+
}
|
|
2076
|
+
if (response.headers.get("accept-ranges") !== "bytes") {
|
|
2077
|
+
await cancelResponseBody(response, "retained artifact response omitted byte-range support");
|
|
2078
|
+
throw new OpenGeniApiError(502, "retained artifact response omitted byte-range support");
|
|
2079
|
+
}
|
|
2080
|
+
let declaredLength: number | null;
|
|
2081
|
+
try {
|
|
2082
|
+
declaredLength = parseBoundedContentLength(response.headers.get("content-length"));
|
|
2083
|
+
} catch (error) {
|
|
2084
|
+
await cancelResponseBody(response, "invalid retained artifact content-length");
|
|
2085
|
+
throw error;
|
|
2086
|
+
}
|
|
2087
|
+
const bytes = await readBoundedResponseBytes(
|
|
2088
|
+
response,
|
|
2089
|
+
RETAINED_OUTPUT_MAX_PAGE_BYTES,
|
|
2090
|
+
declaredLength,
|
|
2091
|
+
);
|
|
2092
|
+
return {
|
|
2093
|
+
bytes,
|
|
2094
|
+
status: response.status,
|
|
2095
|
+
contentType: response.headers.get("content-type") ?? "application/octet-stream",
|
|
2096
|
+
contentLength: bytes.byteLength,
|
|
2097
|
+
contentRange: response.headers.get("content-range"),
|
|
2098
|
+
acceptRanges: "bytes",
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
|
|
1881
2102
|
/** Mint a short-lived signed download URL for a ready file. */
|
|
1882
2103
|
async createFileDownloadUrl(
|
|
1883
2104
|
workspaceId: string,
|
|
@@ -2416,6 +2637,14 @@ export class OpenGeniClient {
|
|
|
2416
2637
|
);
|
|
2417
2638
|
}
|
|
2418
2639
|
|
|
2640
|
+
/** Live independently-settled quota + reset-credit overview for every account. */
|
|
2641
|
+
async codexOverview(workspaceId: string): Promise<CodexOverviewResponse> {
|
|
2642
|
+
return await this.requestJson<CodexOverviewResponse>(
|
|
2643
|
+
"GET",
|
|
2644
|
+
`/v1/workspaces/${workspaceId}/codex/overview`,
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2419
2648
|
/** Disconnect ALL accounts (legacy workspace-wide). Prefer `disconnectCodexAccount`. */
|
|
2420
2649
|
async codexDisconnect(workspaceId: string): Promise<{ disconnected: boolean }> {
|
|
2421
2650
|
return await this.requestJson<{ disconnected: boolean }>(
|
|
@@ -2458,6 +2687,19 @@ export class OpenGeniClient {
|
|
|
2458
2687
|
);
|
|
2459
2688
|
}
|
|
2460
2689
|
|
|
2690
|
+
/** Toggle only NEW automatic allocations under independent allocator OCC. */
|
|
2691
|
+
async setCodexAccountAllocator(
|
|
2692
|
+
workspaceId: string,
|
|
2693
|
+
accountId: string,
|
|
2694
|
+
input: { enabled: boolean; expectedVersion: number },
|
|
2695
|
+
): Promise<CodexAllocatorUpdate> {
|
|
2696
|
+
return await this.requestJson<CodexAllocatorUpdate>(
|
|
2697
|
+
"PATCH",
|
|
2698
|
+
`/v1/workspaces/${workspaceId}/codex/accounts/${accountId}/allocator`,
|
|
2699
|
+
input,
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2461
2703
|
/** Disconnect ONE Codex account by id (re-picks active when the removed one was active). */
|
|
2462
2704
|
async disconnectCodexAccount(
|
|
2463
2705
|
workspaceId: string,
|
|
@@ -2544,6 +2786,17 @@ function assertApiContractResponse(response: Response): void {
|
|
|
2544
2786
|
}
|
|
2545
2787
|
}
|
|
2546
2788
|
|
|
2789
|
+
async function sha256ForUpload(body: Blob | ArrayBuffer | string): Promise<string> {
|
|
2790
|
+
const bytes =
|
|
2791
|
+
typeof body === "string"
|
|
2792
|
+
? new TextEncoder().encode(body)
|
|
2793
|
+
: body instanceof Blob
|
|
2794
|
+
? new Uint8Array(await body.arrayBuffer())
|
|
2795
|
+
: new Uint8Array(body);
|
|
2796
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
2797
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2547
2800
|
async function safeText(response: Response): Promise<string> {
|
|
2548
2801
|
try {
|
|
2549
2802
|
return await response.text();
|
|
@@ -2551,3 +2804,69 @@ async function safeText(response: Response): Promise<string> {
|
|
|
2551
2804
|
return "";
|
|
2552
2805
|
}
|
|
2553
2806
|
}
|
|
2807
|
+
|
|
2808
|
+
async function safeBoundedText(response: Response): Promise<string> {
|
|
2809
|
+
try {
|
|
2810
|
+
return new TextDecoder().decode(await readBoundedResponseBytes(response, 64 * 1024, null));
|
|
2811
|
+
} catch {
|
|
2812
|
+
return "";
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
async function cancelResponseBody(response: Response, reason: string): Promise<void> {
|
|
2817
|
+
await response.body?.cancel(reason).catch(() => undefined);
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
function parseBoundedContentLength(value: string | null): number | null {
|
|
2821
|
+
if (value === null) return null;
|
|
2822
|
+
if (!/^\d+$/.test(value)) {
|
|
2823
|
+
throw new OpenGeniApiError(502, "invalid retained artifact content-length");
|
|
2824
|
+
}
|
|
2825
|
+
const length = Number(value);
|
|
2826
|
+
if (!Number.isSafeInteger(length) || length > RETAINED_OUTPUT_MAX_PAGE_BYTES) {
|
|
2827
|
+
throw new OpenGeniApiError(502, "retained artifact response exceeds the SDK byte limit");
|
|
2828
|
+
}
|
|
2829
|
+
return length;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
async function readBoundedResponseBytes(
|
|
2833
|
+
response: Response,
|
|
2834
|
+
maxBytes: number,
|
|
2835
|
+
expectedBytes: number | null,
|
|
2836
|
+
): Promise<Uint8Array> {
|
|
2837
|
+
if (!response.body) {
|
|
2838
|
+
if (expectedBytes !== null && expectedBytes !== 0) {
|
|
2839
|
+
throw new OpenGeniApiError(502, "retained artifact response length mismatch");
|
|
2840
|
+
}
|
|
2841
|
+
return new Uint8Array();
|
|
2842
|
+
}
|
|
2843
|
+
const reader = response.body.getReader();
|
|
2844
|
+
const chunks: Uint8Array[] = [];
|
|
2845
|
+
let totalBytes = 0;
|
|
2846
|
+
try {
|
|
2847
|
+
while (true) {
|
|
2848
|
+
const { done, value } = await reader.read();
|
|
2849
|
+
if (done) break;
|
|
2850
|
+
totalBytes += value.byteLength;
|
|
2851
|
+
if (totalBytes > maxBytes) {
|
|
2852
|
+
await reader
|
|
2853
|
+
.cancel("retained artifact response exceeded the SDK byte limit")
|
|
2854
|
+
.catch(() => undefined);
|
|
2855
|
+
throw new OpenGeniApiError(502, "retained artifact response exceeds the SDK byte limit");
|
|
2856
|
+
}
|
|
2857
|
+
chunks.push(value);
|
|
2858
|
+
}
|
|
2859
|
+
} finally {
|
|
2860
|
+
reader.releaseLock();
|
|
2861
|
+
}
|
|
2862
|
+
if (expectedBytes !== null && totalBytes !== expectedBytes) {
|
|
2863
|
+
throw new OpenGeniApiError(502, "retained artifact response length mismatch");
|
|
2864
|
+
}
|
|
2865
|
+
const bytes = new Uint8Array(totalBytes);
|
|
2866
|
+
let offset = 0;
|
|
2867
|
+
for (const chunk of chunks) {
|
|
2868
|
+
bytes.set(chunk, offset);
|
|
2869
|
+
offset += chunk.byteLength;
|
|
2870
|
+
}
|
|
2871
|
+
return bytes;
|
|
2872
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
1
|
/** Error for a non-2xx OpenGeni API response. */
|
|
2
2
|
export class OpenGeniApiError extends Error {
|
|
3
3
|
readonly status: number;
|
|
4
|
+
readonly code: string | undefined;
|
|
4
5
|
readonly body: string;
|
|
5
6
|
|
|
6
7
|
constructor(status: number, body: string) {
|
|
7
|
-
|
|
8
|
+
const decoded = decodeApiErrorBody(body);
|
|
9
|
+
super(`OpenGeni API ${status}: ${decoded.message ?? (body || "(empty body)")}`);
|
|
8
10
|
this.name = "OpenGeniApiError";
|
|
9
11
|
this.status = status;
|
|
12
|
+
this.code = decoded.code;
|
|
10
13
|
this.body = body;
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
function decodeApiErrorBody(body: string): { code?: string; message?: string } {
|
|
18
|
+
if (!body) return {};
|
|
19
|
+
try {
|
|
20
|
+
const decoded: unknown = JSON.parse(body);
|
|
21
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) return {};
|
|
22
|
+
const record = decoded as Record<string, unknown>;
|
|
23
|
+
return {
|
|
24
|
+
...(typeof record.code === "string" && record.code ? { code: record.code } : {}),
|
|
25
|
+
...(typeof record.message === "string" && record.message ? { message: record.message } : {}),
|
|
26
|
+
};
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A short-lived session-list snapshot cursor can no longer be continued. */
|
|
33
|
+
export class OpenGeniSessionListCursorError extends OpenGeniApiError {}
|
|
34
|
+
|
|
14
35
|
/** The browser bundle and API disagree about their state-changing wire contract. */
|
|
15
36
|
export class OpenGeniApiContractMismatchError extends Error {
|
|
16
37
|
readonly expected: string;
|