@opengeni/sdk 4.4.1 → 4.5.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 +27 -0
- package/dist/adapters/next.js +2 -2
- package/dist/artifacts.js +4 -4
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +5 -3
- package/dist/chat/index.js +5 -5
- package/dist/{chunk-3BWTS2MJ.js → chunk-ARDVIZIJ.js} +2 -2
- package/dist/{chunk-PVWOBYAZ.js → chunk-BXMNDWAN.js} +256 -78
- package/dist/chunk-BXMNDWAN.js.map +1 -0
- package/dist/{chunk-IVHLADZ7.js → chunk-GRUMGFK6.js} +2 -2
- package/dist/{chunk-2RVPHF37.js → chunk-JBAGMSN2.js} +66 -10
- package/dist/chunk-JBAGMSN2.js.map +1 -0
- package/dist/{chunk-35S72HAE.js → chunk-U4JUWOZV.js} +5 -5
- package/dist/{chunk-BLID3DCD.js → chunk-V4AIZAFF.js} +1 -1
- package/dist/chunk-V4AIZAFF.js.map +1 -0
- package/dist/{chunk-ALXDFL6C.js → chunk-VSOXNL3T.js} +2 -2
- package/dist/{chunk-CT6P2W2C.js → chunk-WI3WUT5H.js} +2 -2
- package/dist/client.d.ts +24 -1
- package/dist/codex-realtime-controller.js +1 -1
- package/dist/core.d.ts +2 -0
- package/dist/core.js +5 -3
- package/dist/document-authority.js +3 -3
- package/dist/editable-artifacts.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +35 -33
- package/dist/index.js.map +1 -1
- package/dist/read-retry.d.ts +61 -0
- package/dist/realtime.js +5 -5
- package/dist/session-proxy.d.ts +6 -1
- package/dist/session-proxy.js +2 -2
- package/dist/site-bridge-host.js +3 -3
- package/dist/site-browser-runtime.gen.d.ts +1 -1
- package/dist/site-document.js +1 -1
- package/dist/site-document.js.map +1 -1
- package/dist/site.js +3 -3
- package/dist/types.d.ts +40 -0
- package/dist/workspace-artifacts.d.ts +5 -0
- package/dist/workspace-live-stream.d.ts +7 -0
- package/package.json +3 -3
- package/src/browser.ts +2 -0
- package/src/client.ts +191 -73
- package/src/core.ts +2 -0
- package/src/index.ts +4 -0
- package/src/read-retry.ts +207 -0
- package/src/session-proxy.ts +95 -4
- package/src/site-browser-runtime.gen.ts +1 -1
- package/src/types.ts +38 -0
- package/src/workspace-artifacts.ts +6 -0
- package/src/workspace-live-stream.ts +12 -1
- package/dist/chunk-2RVPHF37.js.map +0 -1
- package/dist/chunk-BLID3DCD.js.map +0 -1
- package/dist/chunk-PVWOBYAZ.js.map +0 -1
- /package/dist/{chunk-3BWTS2MJ.js.map → chunk-ARDVIZIJ.js.map} +0 -0
- /package/dist/{chunk-IVHLADZ7.js.map → chunk-GRUMGFK6.js.map} +0 -0
- /package/dist/{chunk-35S72HAE.js.map → chunk-U4JUWOZV.js.map} +0 -0
- /package/dist/{chunk-ALXDFL6C.js.map → chunk-VSOXNL3T.js.map} +0 -0
- /package/dist/{chunk-CT6P2W2C.js.map → chunk-WI3WUT5H.js.map} +0 -0
package/dist/site.js
CHANGED
|
@@ -24,13 +24,13 @@ import {
|
|
|
24
24
|
sanitizeOpenGeniSiteToolInvokeRequest,
|
|
25
25
|
serveSiteHttp,
|
|
26
26
|
siteSessionPath
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-VSOXNL3T.js";
|
|
28
28
|
import "./chunk-OV3POE62.js";
|
|
29
29
|
import "./chunk-ORYDCRFM.js";
|
|
30
|
-
import "./chunk-
|
|
30
|
+
import "./chunk-BXMNDWAN.js";
|
|
31
31
|
import "./chunk-X5ITQVXE.js";
|
|
32
32
|
import "./chunk-72YZCZWF.js";
|
|
33
|
-
import "./chunk-
|
|
33
|
+
import "./chunk-V4AIZAFF.js";
|
|
34
34
|
import "./chunk-POYRW6KT.js";
|
|
35
35
|
import "./chunk-ODKRQUWR.js";
|
|
36
36
|
export {
|
package/dist/types.d.ts
CHANGED
|
@@ -1649,6 +1649,32 @@ export type SessionListTotals = {
|
|
|
1649
1649
|
waiting?: number;
|
|
1650
1650
|
}>;
|
|
1651
1651
|
};
|
|
1652
|
+
/**
|
|
1653
|
+
* One Site row in one project (`siteConversations: "group"`): a Site with
|
|
1654
|
+
* the viewer's conversations, created by a chat they list, or opened by them.
|
|
1655
|
+
* Counts are complete for the viewer, independent of page size.
|
|
1656
|
+
*/
|
|
1657
|
+
export type SiteConversationGroup = {
|
|
1658
|
+
siteId: string;
|
|
1659
|
+
channelId: string | null;
|
|
1660
|
+
/** The Site's current title when the viewer can read it, else its creation label. */
|
|
1661
|
+
title: string;
|
|
1662
|
+
/** Root conversations in the group; 0 for a Site with none yet. */
|
|
1663
|
+
conversations: number;
|
|
1664
|
+
/** Latest activity: its conversations, its own changes, or the viewer opening it. */
|
|
1665
|
+
updatedAt: string;
|
|
1666
|
+
/** Creation time of the group's newest conversation, else of the Site. */
|
|
1667
|
+
createdAt: string;
|
|
1668
|
+
total: number;
|
|
1669
|
+
attention: number;
|
|
1670
|
+
attentionSince: string | null;
|
|
1671
|
+
failed: number;
|
|
1672
|
+
active: number;
|
|
1673
|
+
queued: number;
|
|
1674
|
+
waiting: number;
|
|
1675
|
+
unread: number;
|
|
1676
|
+
activeWork: number;
|
|
1677
|
+
};
|
|
1652
1678
|
export type SessionListResponse = {
|
|
1653
1679
|
totals?: SessionListTotals;
|
|
1654
1680
|
needsYouOnly?: true;
|
|
@@ -1669,6 +1695,12 @@ export type SessionListResponse = {
|
|
|
1669
1695
|
originSiteId?: string;
|
|
1670
1696
|
/** Server-resolved Site access filter, when requested. */
|
|
1671
1697
|
siteAccessId?: string;
|
|
1698
|
+
/** Receipt for the requested Site conversation mode. */
|
|
1699
|
+
siteConversations?: "include" | "group";
|
|
1700
|
+
/** Complete Site rows for a `group` root page, newest activity first. */
|
|
1701
|
+
siteGroups?: SiteConversationGroup[];
|
|
1702
|
+
/** True when Site rows beyond the server bound were omitted. */
|
|
1703
|
+
siteGroupsTruncated?: true;
|
|
1672
1704
|
sessions: Session[];
|
|
1673
1705
|
nextCursor: string | null;
|
|
1674
1706
|
};
|
|
@@ -4157,6 +4189,14 @@ export type ClientConfig = {
|
|
|
4157
4189
|
claudeSubscriptionEnabled?: boolean | undefined;
|
|
4158
4190
|
/** Whether people can connect their own computers; absent on older servers. */
|
|
4159
4191
|
connectedMachines?: boolean | undefined;
|
|
4192
|
+
/**
|
|
4193
|
+
* The Chrome extension people add to connect their own Chrome, and its
|
|
4194
|
+
* Chrome Web Store page; absent on older servers (Opengeni Browser).
|
|
4195
|
+
*/
|
|
4196
|
+
browserExtension?: {
|
|
4197
|
+
name: string;
|
|
4198
|
+
installUrl: string;
|
|
4199
|
+
} | undefined;
|
|
4160
4200
|
defaultModel: string;
|
|
4161
4201
|
allowedModels: string[];
|
|
4162
4202
|
models: ClientModel[];
|
|
@@ -26,6 +26,11 @@ export type WorkspaceArtifactVersion = {
|
|
|
26
26
|
createdBySubjectId: string;
|
|
27
27
|
createdAt: string;
|
|
28
28
|
};
|
|
29
|
+
/** This person's latest recorded open of a Site, which orders its sidebar row. */
|
|
30
|
+
export type SiteOpenedResponse = {
|
|
31
|
+
/** When the open was recorded (ISO 8601). */
|
|
32
|
+
openedAt: string;
|
|
33
|
+
};
|
|
29
34
|
/** Who can see a Site: everyone in the workspace, or only its owner ("Only me"). */
|
|
30
35
|
export type WorkspaceArtifactVisibility = "private" | "workspace";
|
|
31
36
|
export type WorkspaceArtifact = {
|
|
@@ -7,6 +7,13 @@ export type WorkspaceSessionListEvent = {
|
|
|
7
7
|
workspaceId: string;
|
|
8
8
|
available: boolean;
|
|
9
9
|
sessionIds: string[] | null;
|
|
10
|
+
/**
|
|
11
|
+
* Changed Site conversations visible to this viewer. Ordinary lists omit
|
|
12
|
+
* them; only a surface that shows Site conversations needs these. A hint
|
|
13
|
+
* whose `sessionIds` is empty and that carries only these changes nothing
|
|
14
|
+
* for ordinary lists.
|
|
15
|
+
*/
|
|
16
|
+
siteSessionIds?: string[];
|
|
10
17
|
};
|
|
11
18
|
export type WorkspaceLiveEvent = WorkspaceControlEvent | WorkspaceInteractionRevisionEvent | WorkspaceSessionListEvent;
|
|
12
19
|
export type WorkspaceLiveStreamOptions = Omit<StreamSessionEventsOptions, "after"> & {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/sdk",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.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": {
|
|
@@ -218,8 +218,8 @@
|
|
|
218
218
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
219
219
|
},
|
|
220
220
|
"dependencies": {
|
|
221
|
-
"@opengeni/connect": "4.
|
|
222
|
-
"@opengeni/contracts": "4.
|
|
221
|
+
"@opengeni/connect": "4.5.0",
|
|
222
|
+
"@opengeni/contracts": "4.5.0"
|
|
223
223
|
},
|
|
224
224
|
"engines": {
|
|
225
225
|
"node": ">=18"
|
package/src/browser.ts
CHANGED
|
@@ -45,6 +45,8 @@ export {
|
|
|
45
45
|
isRetryableStreamError,
|
|
46
46
|
} from "./errors";
|
|
47
47
|
export type { OpenGeniSecureContextRequiredReason } from "./errors";
|
|
48
|
+
export { isTransientRequestError } from "./read-retry";
|
|
49
|
+
export type { OpenGeniReadRetryOptions } from "./read-retry";
|
|
48
50
|
export { resolveWorkspaceVoiceInputEnabled } from "./transcription";
|
|
49
51
|
export { OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION } from "./types";
|
|
50
52
|
|
package/src/client.ts
CHANGED
|
@@ -104,6 +104,7 @@ import type {
|
|
|
104
104
|
} from "./knowledge";
|
|
105
105
|
import type {
|
|
106
106
|
DuplicateWorkspaceArtifactRequest,
|
|
107
|
+
SiteOpenedResponse,
|
|
107
108
|
RollbackWorkspaceArtifactRequest,
|
|
108
109
|
SetWorkspaceArtifactStatusRequest,
|
|
109
110
|
UpdateWorkspaceArtifactVisibilityRequest,
|
|
@@ -150,6 +151,19 @@ import {
|
|
|
150
151
|
} from "./stream";
|
|
151
152
|
import type { SessionModelContextResponse } from "./model-context";
|
|
152
153
|
import { withDeprecationNotices, type OpenGeniDeprecationHandler } from "./deprecation";
|
|
154
|
+
import {
|
|
155
|
+
abortableReadRetryDelay,
|
|
156
|
+
isSafeReadMethod,
|
|
157
|
+
isTransientGatewayStatus,
|
|
158
|
+
invalidApiResponseError,
|
|
159
|
+
isTransientRequestError,
|
|
160
|
+
markTransportFailure,
|
|
161
|
+
parseRetryAfterMs,
|
|
162
|
+
readRetryDelayMs,
|
|
163
|
+
resolveReadRetryPolicy,
|
|
164
|
+
type OpenGeniReadRetryOptions,
|
|
165
|
+
type ReadRetryPolicy,
|
|
166
|
+
} from "./read-retry";
|
|
153
167
|
import type {
|
|
154
168
|
SkillRecord,
|
|
155
169
|
SkillSummary,
|
|
@@ -825,6 +839,13 @@ export type SessionListPageOptions = {
|
|
|
825
839
|
mineOnly?: boolean;
|
|
826
840
|
/** Created through this Site. In a Site-bound client, "current" resolves to its own Site. */
|
|
827
841
|
originSiteId?: string;
|
|
842
|
+
/**
|
|
843
|
+
* Conversations that run inside a Site are left out of ordinary lists.
|
|
844
|
+
* `include` lists them as ordinary rows; `group` (a root page) keeps them
|
|
845
|
+
* out of the rows and returns one `siteGroups` entry per Site and project.
|
|
846
|
+
* Older servers omit the `siteConversations` receipt and the Site rows.
|
|
847
|
+
*/
|
|
848
|
+
siteConversations?: "include" | "group";
|
|
828
849
|
limit?: number;
|
|
829
850
|
parentSessionId?: string | null;
|
|
830
851
|
/** Only sessions carrying this exact opaque end-user label. */
|
|
@@ -915,6 +936,15 @@ export type WorkspaceControlEventPage = {
|
|
|
915
936
|
nextAfter: number | null;
|
|
916
937
|
};
|
|
917
938
|
|
|
939
|
+
/** What one safe-read attempt observed, for the retry decision. */
|
|
940
|
+
type ReadAttemptOutcome = {
|
|
941
|
+
/** `fetch` or the response body rejected (reset connection, HTTP/2 error, ...). */
|
|
942
|
+
transportFailure: boolean;
|
|
943
|
+
/** A real 502, 503 or 504 response. */
|
|
944
|
+
gatewayFailure: boolean;
|
|
945
|
+
retryAfterMs: number | null;
|
|
946
|
+
};
|
|
947
|
+
|
|
918
948
|
export type OpenGeniClientOptions = {
|
|
919
949
|
/** Explicit compatibility for an API predating targeted tool calls. No automatic downgrade. */
|
|
920
950
|
toolGatewayMode?: "target" | "catalog";
|
|
@@ -954,6 +984,14 @@ export type OpenGeniClientOptions = {
|
|
|
954
984
|
* `"compatible"` everywhere else.
|
|
955
985
|
*/
|
|
956
986
|
apiContract?: "strict" | "compatible" | undefined;
|
|
987
|
+
/**
|
|
988
|
+
* Bounded quiet retry for safe JSON reads (`GET`/`HEAD`) that fail
|
|
989
|
+
* transiently: a transport error or a 502, 503 or 504 (honoring
|
|
990
|
+
* `Retry-After`). This rides out API rolling deploys and short network
|
|
991
|
+
* blips. Mutations are never retried. On by default (4 attempts within
|
|
992
|
+
* 5 seconds); pass `false` to turn it off or options to tune it.
|
|
993
|
+
*/
|
|
994
|
+
readRetry?: OpenGeniReadRetryOptions | false | undefined;
|
|
957
995
|
};
|
|
958
996
|
|
|
959
997
|
function defaultApiContractMode(options: OpenGeniClientOptions): "strict" | "compatible" {
|
|
@@ -1190,6 +1228,7 @@ export class OpenGeniClient {
|
|
|
1190
1228
|
private readonly fetchImpl: FetchLike;
|
|
1191
1229
|
private readonly sessionCommandTimeoutMs: number;
|
|
1192
1230
|
private readonly apiContractStrict: boolean;
|
|
1231
|
+
private readonly readRetryPolicy: ReadRetryPolicy | null;
|
|
1193
1232
|
private readonly active = new Map<string, SingleFlightReadEntry>();
|
|
1194
1233
|
private readonly generations = new Map<string, number>();
|
|
1195
1234
|
private readonly queued = new Map<string, SingleFlightReadEntry>();
|
|
@@ -1212,6 +1251,7 @@ export class OpenGeniClient {
|
|
|
1212
1251
|
}
|
|
1213
1252
|
this.sessionCommandTimeoutMs = sessionCommandTimeoutMs;
|
|
1214
1253
|
this.apiContractStrict = defaultApiContractMode(options) === "strict";
|
|
1254
|
+
this.readRetryPolicy = resolveReadRetryPolicy(options.readRetry);
|
|
1215
1255
|
this.interaction = new OpenGeniInteractionClient(this);
|
|
1216
1256
|
this.tools = createLazyToolsFacade(this);
|
|
1217
1257
|
}
|
|
@@ -1326,7 +1366,7 @@ export class OpenGeniClient {
|
|
|
1326
1366
|
options.signal ? { signal: options.signal } : {},
|
|
1327
1367
|
);
|
|
1328
1368
|
if (!isTranscriptionRecordingListResponse(body)) {
|
|
1329
|
-
throw
|
|
1369
|
+
throw invalidApiResponseError("Invalid transcription recording list response.", {
|
|
1330
1370
|
code: "invalid_response",
|
|
1331
1371
|
});
|
|
1332
1372
|
}
|
|
@@ -1998,6 +2038,7 @@ export class OpenGeniClient {
|
|
|
1998
2038
|
if (options.mineOnly) query.mineOnly = "true";
|
|
1999
2039
|
if (options.includePinned === false) query.includePinned = "false";
|
|
2000
2040
|
if (options.archivedOnly) query.archivedOnly = "true";
|
|
2041
|
+
if (options.siteConversations) query.siteConversations = options.siteConversations;
|
|
2001
2042
|
result = await this.requestJson<SessionListResponse | SessionListEntryResponse | Session[]>(
|
|
2002
2043
|
"GET",
|
|
2003
2044
|
path,
|
|
@@ -2078,6 +2119,17 @@ export class OpenGeniClient {
|
|
|
2078
2119
|
return response;
|
|
2079
2120
|
}
|
|
2080
2121
|
|
|
2122
|
+
/**
|
|
2123
|
+
* Record that this person opened a Site, which orders its sidebar row like
|
|
2124
|
+
* session activity. Personal; needs `artifacts:read`.
|
|
2125
|
+
*/
|
|
2126
|
+
async recordSiteOpened(workspaceId: string, siteId: string): Promise<SiteOpenedResponse> {
|
|
2127
|
+
return this.requestJson(
|
|
2128
|
+
"POST",
|
|
2129
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/published-artifacts/${encodeURIComponent(siteId)}/opened`,
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2081
2133
|
/** Console/owner-authorized agent only. Never available through the Site frame bridge. */
|
|
2082
2134
|
async listSiteSessionAccess(
|
|
2083
2135
|
workspaceId: string,
|
|
@@ -4772,7 +4824,7 @@ export class OpenGeniClient {
|
|
|
4772
4824
|
header.length > 64 * 1024
|
|
4773
4825
|
) {
|
|
4774
4826
|
await cancelResponseBody(response, "browser frame metadata is invalid");
|
|
4775
|
-
throw
|
|
4827
|
+
throw invalidApiResponseError("browser frame metadata is invalid");
|
|
4776
4828
|
}
|
|
4777
4829
|
let metadata: BrowserFrameMetadata;
|
|
4778
4830
|
try {
|
|
@@ -4781,7 +4833,7 @@ export class OpenGeniClient {
|
|
|
4781
4833
|
);
|
|
4782
4834
|
} catch {
|
|
4783
4835
|
await cancelResponseBody(response, "browser frame metadata is invalid");
|
|
4784
|
-
throw
|
|
4836
|
+
throw invalidApiResponseError("browser frame metadata is invalid");
|
|
4785
4837
|
}
|
|
4786
4838
|
const data = await readBoundedResponseBytes(response, 24 * 1024 * 1024, null);
|
|
4787
4839
|
if (
|
|
@@ -4789,7 +4841,7 @@ export class OpenGeniClient {
|
|
|
4789
4841
|
metadata.targetId !== targetId ||
|
|
4790
4842
|
metadata.mediaType !== mediaType
|
|
4791
4843
|
) {
|
|
4792
|
-
throw
|
|
4844
|
+
throw invalidApiResponseError("browser frame evidence does not match its request");
|
|
4793
4845
|
}
|
|
4794
4846
|
return { ...metadata, data };
|
|
4795
4847
|
}
|
|
@@ -4838,7 +4890,7 @@ export class OpenGeniClient {
|
|
|
4838
4890
|
options,
|
|
4839
4891
|
);
|
|
4840
4892
|
if (state.browserSessionId !== browserSessionId || state.targetId !== targetId) {
|
|
4841
|
-
throw
|
|
4893
|
+
throw invalidApiResponseError("browser target state belongs to another binding");
|
|
4842
4894
|
}
|
|
4843
4895
|
return state;
|
|
4844
4896
|
}
|
|
@@ -4865,7 +4917,7 @@ export class OpenGeniClient {
|
|
|
4865
4917
|
result.documentGeneration !== request.expectedDocumentGeneration ||
|
|
4866
4918
|
result.frameId !== request.expectedFrameId
|
|
4867
4919
|
) {
|
|
4868
|
-
throw
|
|
4920
|
+
throw invalidApiResponseError("browser DOM read belongs to another binding");
|
|
4869
4921
|
}
|
|
4870
4922
|
return result;
|
|
4871
4923
|
}
|
|
@@ -5193,19 +5245,19 @@ export class OpenGeniClient {
|
|
|
5193
5245
|
const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
5194
5246
|
if (mediaType !== "image/jpeg" && mediaType !== "image/png") {
|
|
5195
5247
|
await cancelResponseBody(response, "computer frame media type is invalid");
|
|
5196
|
-
throw
|
|
5248
|
+
throw invalidApiResponseError("computer frame media type is invalid");
|
|
5197
5249
|
}
|
|
5198
5250
|
const metadataHeader = response.headers.get("x-opengeni-computer-frame");
|
|
5199
5251
|
if (!metadataHeader || metadataHeader.length > 32 * 1024) {
|
|
5200
5252
|
await cancelResponseBody(response, "computer frame metadata is invalid");
|
|
5201
|
-
throw
|
|
5253
|
+
throw invalidApiResponseError("computer frame metadata is invalid");
|
|
5202
5254
|
}
|
|
5203
5255
|
let metadata: ReturnType<typeof decodeComputerFrameMetadataHeader>;
|
|
5204
5256
|
try {
|
|
5205
5257
|
metadata = decodeComputerFrameMetadataHeader(metadataHeader);
|
|
5206
5258
|
} catch {
|
|
5207
5259
|
await cancelResponseBody(response, "computer frame metadata is invalid");
|
|
5208
|
-
throw
|
|
5260
|
+
throw invalidApiResponseError("computer frame metadata is invalid");
|
|
5209
5261
|
}
|
|
5210
5262
|
const bytes = await readBoundedResponseBytes(response, 256 * 1024, null);
|
|
5211
5263
|
const mismatchReason = computerFrameEvidenceMismatchReason(metadata, {
|
|
@@ -5215,7 +5267,7 @@ export class OpenGeniClient {
|
|
|
5215
5267
|
sha256: await sha256Hex(bytes),
|
|
5216
5268
|
});
|
|
5217
5269
|
if (mismatchReason) {
|
|
5218
|
-
throw
|
|
5270
|
+
throw invalidApiResponseError("computer frame evidence does not match its request");
|
|
5219
5271
|
}
|
|
5220
5272
|
return { ...metadata, data: bytes };
|
|
5221
5273
|
}
|
|
@@ -7801,7 +7853,7 @@ export class OpenGeniClient {
|
|
|
7801
7853
|
metadata.originalBytes <= 0 ||
|
|
7802
7854
|
metadata.originalBytes > COMPUTER_SCREENSHOT_MAX_BYTES
|
|
7803
7855
|
) {
|
|
7804
|
-
throw
|
|
7856
|
+
throw invalidApiResponseError("retained screenshot metadata is invalid");
|
|
7805
7857
|
}
|
|
7806
7858
|
const bytes = await this.downloadRetainedArtifactBytes(metadata, options);
|
|
7807
7859
|
return { metadata, bytes };
|
|
@@ -7818,7 +7870,7 @@ export class OpenGeniClient {
|
|
|
7818
7870
|
const bytes = new Uint8Array(artifact.originalBytes);
|
|
7819
7871
|
const pageBytes = Math.min(artifact.retrieval.maxRangeBytes, RETAINED_OUTPUT_MAX_PAGE_BYTES);
|
|
7820
7872
|
if (!Number.isSafeInteger(pageBytes) || pageBytes <= 0) {
|
|
7821
|
-
throw
|
|
7873
|
+
throw invalidApiResponseError("retained artifact range metadata is invalid");
|
|
7822
7874
|
}
|
|
7823
7875
|
for (let start = 0; start < bytes.byteLength; start += pageBytes) {
|
|
7824
7876
|
options.signal?.throwIfAborted();
|
|
@@ -7841,7 +7893,7 @@ export class OpenGeniClient {
|
|
|
7841
7893
|
}
|
|
7842
7894
|
}
|
|
7843
7895
|
}
|
|
7844
|
-
if (!page) throw
|
|
7896
|
+
if (!page) throw invalidApiResponseError("retained artifact range retry exhausted");
|
|
7845
7897
|
const expectedLength = end - start + 1;
|
|
7846
7898
|
if (
|
|
7847
7899
|
page.status !== 206 ||
|
|
@@ -7849,12 +7901,12 @@ export class OpenGeniClient {
|
|
|
7849
7901
|
page.contentLength !== expectedLength ||
|
|
7850
7902
|
page.contentRange !== `bytes ${start}-${end}/${artifact.originalBytes}`
|
|
7851
7903
|
) {
|
|
7852
|
-
throw
|
|
7904
|
+
throw invalidApiResponseError("retained artifact range response is invalid");
|
|
7853
7905
|
}
|
|
7854
7906
|
bytes.set(page.bytes, start);
|
|
7855
7907
|
}
|
|
7856
7908
|
if ((await sha256Hex(bytes)) !== artifact.sha256) {
|
|
7857
|
-
throw
|
|
7909
|
+
throw invalidApiResponseError("retained artifact checksum mismatch");
|
|
7858
7910
|
}
|
|
7859
7911
|
return bytes;
|
|
7860
7912
|
}
|
|
@@ -7894,7 +7946,7 @@ export class OpenGeniClient {
|
|
|
7894
7946
|
}
|
|
7895
7947
|
if (response.headers.get("accept-ranges") !== "bytes") {
|
|
7896
7948
|
await cancelResponseBody(response, "retained artifact response omitted byte-range support");
|
|
7897
|
-
throw
|
|
7949
|
+
throw invalidApiResponseError("retained artifact response omitted byte-range support");
|
|
7898
7950
|
}
|
|
7899
7951
|
let declaredLength: number | null;
|
|
7900
7952
|
try {
|
|
@@ -11363,64 +11415,130 @@ export class OpenGeniClient {
|
|
|
11363
11415
|
const correlationId = crypto.randomUUID();
|
|
11364
11416
|
const abort = requestAbortSignal(options);
|
|
11365
11417
|
try {
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
method,
|
|
11375
|
-
headers: {
|
|
11376
|
-
...headers,
|
|
11377
|
-
Accept: "application/json",
|
|
11378
|
-
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
11379
|
-
},
|
|
11380
|
-
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
11381
|
-
...(abort.signal ? { signal: abort.signal } : {}),
|
|
11382
|
-
}),
|
|
11418
|
+
const retryPolicy = isSafeReadMethod(method) ? this.readRetryPolicy : null;
|
|
11419
|
+
if (!retryPolicy) {
|
|
11420
|
+
return await this.requestJsonAttempt<T>(
|
|
11421
|
+
method,
|
|
11422
|
+
path,
|
|
11423
|
+
body,
|
|
11424
|
+
query,
|
|
11425
|
+
options,
|
|
11383
11426
|
abort.signal,
|
|
11427
|
+
correlationId,
|
|
11428
|
+
null,
|
|
11384
11429
|
);
|
|
11385
|
-
} catch (error) {
|
|
11386
|
-
if (options.signal?.aborted) throw error;
|
|
11387
|
-
if (isMutationMethod(method)) {
|
|
11388
|
-
throw mutationTransportError(correlationId);
|
|
11389
|
-
}
|
|
11390
|
-
throw error;
|
|
11391
11430
|
}
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11431
|
+
// Safe reads only: a transient failure is retried quietly with jittered
|
|
11432
|
+
// backoff (or the server's Retry-After) inside a small total budget.
|
|
11433
|
+
const startedAt = Date.now();
|
|
11434
|
+
for (let retry = 1; ; retry += 1) {
|
|
11435
|
+
const attempt: ReadAttemptOutcome = {
|
|
11436
|
+
transportFailure: false,
|
|
11437
|
+
gatewayFailure: false,
|
|
11438
|
+
retryAfterMs: null,
|
|
11439
|
+
};
|
|
11440
|
+
try {
|
|
11441
|
+
return await this.requestJsonAttempt<T>(
|
|
11442
|
+
method,
|
|
11443
|
+
path,
|
|
11444
|
+
body,
|
|
11445
|
+
query,
|
|
11446
|
+
options,
|
|
11397
11447
|
abort.signal,
|
|
11448
|
+
correlationId,
|
|
11449
|
+
attempt,
|
|
11398
11450
|
);
|
|
11399
|
-
}
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11451
|
+
} catch (error) {
|
|
11452
|
+
// Only the transport itself or a real 502/503/504 response is
|
|
11453
|
+
// transient; a malformed success response is not retried.
|
|
11454
|
+
const transient =
|
|
11455
|
+
attempt.transportFailure || (attempt.gatewayFailure && isTransientRequestError(error));
|
|
11456
|
+
if (abort.signal?.aborted || !transient) throw error;
|
|
11457
|
+
const delay = readRetryDelayMs(
|
|
11458
|
+
retryPolicy,
|
|
11459
|
+
retry,
|
|
11460
|
+
Date.now() - startedAt,
|
|
11461
|
+
attempt.retryAfterMs,
|
|
11404
11462
|
);
|
|
11405
|
-
|
|
11463
|
+
if (delay === null) throw error;
|
|
11464
|
+
await abortableReadRetryDelay(delay, abort.signal);
|
|
11406
11465
|
}
|
|
11407
|
-
await awaitWithAbort(assertJsonResponse(response, { method, correlationId }), abort.signal);
|
|
11408
|
-
const value = await awaitWithAbort(response.json(), abort.signal);
|
|
11409
|
-
recordObservedSessionSequence(value, response.headers.get(SESSION_SEQUENCE_HEADER));
|
|
11410
|
-
return value as T;
|
|
11411
|
-
} catch (error) {
|
|
11412
|
-
if (options.signal?.aborted) throw error;
|
|
11413
|
-
if (error instanceof OpenGeniApiError) throw error;
|
|
11414
|
-
if (isMutationMethod(method)) {
|
|
11415
|
-
throw mutationTransportError(correlationId);
|
|
11416
|
-
}
|
|
11417
|
-
throw error;
|
|
11418
11466
|
}
|
|
11419
11467
|
} finally {
|
|
11420
11468
|
abort.dispose();
|
|
11421
11469
|
}
|
|
11422
11470
|
}
|
|
11423
11471
|
|
|
11472
|
+
private async requestJsonAttempt<T>(
|
|
11473
|
+
method: string,
|
|
11474
|
+
path: string,
|
|
11475
|
+
body: unknown,
|
|
11476
|
+
query: Record<string, string>,
|
|
11477
|
+
options: OpenGeniRequestOptions,
|
|
11478
|
+
signal: AbortSignal | undefined,
|
|
11479
|
+
correlationId: string,
|
|
11480
|
+
attempt: ReadAttemptOutcome | null,
|
|
11481
|
+
): Promise<T> {
|
|
11482
|
+
if (signal?.aborted) {
|
|
11483
|
+
throw signal.reason ?? new DOMException("Request aborted", "AbortError");
|
|
11484
|
+
}
|
|
11485
|
+
const headers = this.headers(correlationId);
|
|
11486
|
+
let response: FetchResponse;
|
|
11487
|
+
try {
|
|
11488
|
+
response = await awaitWithAbort(
|
|
11489
|
+
this.fetchImpl(this.url(path, query), {
|
|
11490
|
+
method,
|
|
11491
|
+
headers: {
|
|
11492
|
+
...headers,
|
|
11493
|
+
Accept: "application/json",
|
|
11494
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
11495
|
+
},
|
|
11496
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
11497
|
+
...(signal ? { signal } : {}),
|
|
11498
|
+
}),
|
|
11499
|
+
signal,
|
|
11500
|
+
);
|
|
11501
|
+
} catch (error) {
|
|
11502
|
+
if (options.signal?.aborted) throw error;
|
|
11503
|
+
if (isMutationMethod(method)) {
|
|
11504
|
+
throw mutationTransportError(correlationId);
|
|
11505
|
+
}
|
|
11506
|
+
if (error instanceof TypeError && attempt) attempt.transportFailure = true;
|
|
11507
|
+
markTransportFailure(error);
|
|
11508
|
+
throw error;
|
|
11509
|
+
}
|
|
11510
|
+
assertApiContractResponse(response, this.apiContractStrict);
|
|
11511
|
+
try {
|
|
11512
|
+
if (!response.ok) {
|
|
11513
|
+
if (attempt && isTransientGatewayStatus(response.status)) {
|
|
11514
|
+
attempt.gatewayFailure = true;
|
|
11515
|
+
attempt.retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
11516
|
+
}
|
|
11517
|
+
throw await awaitWithAbort(
|
|
11518
|
+
apiErrorFromResponse(response, { method, correlationId }),
|
|
11519
|
+
signal,
|
|
11520
|
+
);
|
|
11521
|
+
}
|
|
11522
|
+
if (options.responseType === "void") {
|
|
11523
|
+
await awaitWithAbort(cancelResponseBody(response, "discarding void API response"), signal);
|
|
11524
|
+
return undefined as T;
|
|
11525
|
+
}
|
|
11526
|
+
await awaitWithAbort(assertJsonResponse(response, { method, correlationId }), signal);
|
|
11527
|
+
const value = await awaitWithAbort(response.json(), signal);
|
|
11528
|
+
recordObservedSessionSequence(value, response.headers.get(SESSION_SEQUENCE_HEADER));
|
|
11529
|
+
return value as T;
|
|
11530
|
+
} catch (error) {
|
|
11531
|
+
if (options.signal?.aborted) throw error;
|
|
11532
|
+
if (error instanceof OpenGeniApiError) throw error;
|
|
11533
|
+
if (isMutationMethod(method)) {
|
|
11534
|
+
throw mutationTransportError(correlationId);
|
|
11535
|
+
}
|
|
11536
|
+
if (error instanceof TypeError && attempt) attempt.transportFailure = true;
|
|
11537
|
+
markTransportFailure(error);
|
|
11538
|
+
throw error;
|
|
11539
|
+
}
|
|
11540
|
+
}
|
|
11541
|
+
|
|
11424
11542
|
/** Authenticated, contract-checked response for bounded streaming downloads. */
|
|
11425
11543
|
protected async requestResponse(
|
|
11426
11544
|
method: string,
|
|
@@ -11565,7 +11683,7 @@ function isTranscriptionRecordingResponse(value: unknown): value is Transcriptio
|
|
|
11565
11683
|
|
|
11566
11684
|
function transcriptionRecordingResponse(value: unknown): TranscriptionRecordingResponse {
|
|
11567
11685
|
if (isTranscriptionRecordingResponse(value)) return value;
|
|
11568
|
-
throw
|
|
11686
|
+
throw invalidApiResponseError("Invalid transcription recording response.", {
|
|
11569
11687
|
code: "invalid_response",
|
|
11570
11688
|
});
|
|
11571
11689
|
}
|
|
@@ -11705,7 +11823,7 @@ async function assertJsonResponse(
|
|
|
11705
11823
|
): Promise<void> {
|
|
11706
11824
|
if (isJsonContentType(response.headers.get("content-type"))) return;
|
|
11707
11825
|
await cancelResponseBody(response, "unexpected non-JSON API response");
|
|
11708
|
-
throw
|
|
11826
|
+
throw invalidApiResponseError("", {
|
|
11709
11827
|
code: "upstream_unavailable",
|
|
11710
11828
|
retryable: true,
|
|
11711
11829
|
correlationId: response.headers.get(OPENGENI_CORRELATION_HEADER) ?? context.correlationId,
|
|
@@ -11826,7 +11944,7 @@ function assertRetainedWorkspaceArtifactReceipt(
|
|
|
11826
11944
|
!parseRetainedGeneratedImageReference(artifact, workspaceId) &&
|
|
11827
11945
|
!parseRetainedWorkspaceFileReference(artifact, workspaceId)
|
|
11828
11946
|
) {
|
|
11829
|
-
throw
|
|
11947
|
+
throw invalidApiResponseError("retained workspace artifact receipt is invalid");
|
|
11830
11948
|
}
|
|
11831
11949
|
}
|
|
11832
11950
|
|
|
@@ -11837,16 +11955,16 @@ function assertSafeArtifactDownloadUrl(value: FileDownloadUrlResponse): void {
|
|
|
11837
11955
|
typeof value.expiresAt !== "string" ||
|
|
11838
11956
|
!Number.isFinite(Date.parse(value.expiresAt))
|
|
11839
11957
|
) {
|
|
11840
|
-
throw
|
|
11958
|
+
throw invalidApiResponseError("retained artifact download URL is invalid");
|
|
11841
11959
|
}
|
|
11842
11960
|
let parsed: URL;
|
|
11843
11961
|
try {
|
|
11844
11962
|
parsed = new URL(value.url);
|
|
11845
11963
|
} catch {
|
|
11846
|
-
throw
|
|
11964
|
+
throw invalidApiResponseError("retained artifact download URL is invalid");
|
|
11847
11965
|
}
|
|
11848
11966
|
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password) {
|
|
11849
|
-
throw
|
|
11967
|
+
throw invalidApiResponseError("retained artifact download URL is unsafe");
|
|
11850
11968
|
}
|
|
11851
11969
|
}
|
|
11852
11970
|
|
|
@@ -11865,7 +11983,7 @@ function assertSafeVideoArtifactPlaybackSource(
|
|
|
11865
11983
|
value.sizeBytes > GENERATED_VIDEO_MAX_BYTES ||
|
|
11866
11984
|
!/^[0-9a-f]{64}$/.test(value.sha256)
|
|
11867
11985
|
) {
|
|
11868
|
-
throw
|
|
11986
|
+
throw invalidApiResponseError("generated-video playback source is invalid");
|
|
11869
11987
|
}
|
|
11870
11988
|
assertSafeArtifactDownloadUrl({ url: value.url, expiresAt: value.expiresAt });
|
|
11871
11989
|
}
|
|
@@ -11873,11 +11991,11 @@ function assertSafeVideoArtifactPlaybackSource(
|
|
|
11873
11991
|
function parseBoundedContentLength(value: string | null): number | null {
|
|
11874
11992
|
if (value === null) return null;
|
|
11875
11993
|
if (!/^\d+$/.test(value)) {
|
|
11876
|
-
throw
|
|
11994
|
+
throw invalidApiResponseError("invalid retained artifact content-length");
|
|
11877
11995
|
}
|
|
11878
11996
|
const length = Number(value);
|
|
11879
11997
|
if (!Number.isSafeInteger(length) || length > RETAINED_OUTPUT_MAX_PAGE_BYTES) {
|
|
11880
|
-
throw
|
|
11998
|
+
throw invalidApiResponseError("retained artifact response exceeds the SDK byte limit");
|
|
11881
11999
|
}
|
|
11882
12000
|
return length;
|
|
11883
12001
|
}
|
|
@@ -11889,7 +12007,7 @@ async function readBoundedResponseBytes(
|
|
|
11889
12007
|
): Promise<Uint8Array> {
|
|
11890
12008
|
if (!response.body) {
|
|
11891
12009
|
if (expectedBytes !== null && expectedBytes !== 0) {
|
|
11892
|
-
throw
|
|
12010
|
+
throw invalidApiResponseError("retained artifact response length mismatch");
|
|
11893
12011
|
}
|
|
11894
12012
|
return new Uint8Array();
|
|
11895
12013
|
}
|
|
@@ -11905,7 +12023,7 @@ async function readBoundedResponseBytes(
|
|
|
11905
12023
|
await reader
|
|
11906
12024
|
.cancel("retained artifact response exceeded the SDK byte limit")
|
|
11907
12025
|
.catch(() => undefined);
|
|
11908
|
-
throw
|
|
12026
|
+
throw invalidApiResponseError("retained artifact response exceeds the SDK byte limit");
|
|
11909
12027
|
}
|
|
11910
12028
|
chunks.push(value);
|
|
11911
12029
|
}
|
|
@@ -11913,7 +12031,7 @@ async function readBoundedResponseBytes(
|
|
|
11913
12031
|
reader.releaseLock();
|
|
11914
12032
|
}
|
|
11915
12033
|
if (expectedBytes !== null && totalBytes !== expectedBytes) {
|
|
11916
|
-
throw
|
|
12034
|
+
throw invalidApiResponseError("retained artifact response length mismatch");
|
|
11917
12035
|
}
|
|
11918
12036
|
const bytes = new Uint8Array(totalBytes);
|
|
11919
12037
|
let offset = 0;
|
package/src/core.ts
CHANGED
|
@@ -35,6 +35,8 @@ export {
|
|
|
35
35
|
isRetryableStreamError,
|
|
36
36
|
} from "./errors";
|
|
37
37
|
export type { OpenGeniSecureContextRequiredReason } from "./errors";
|
|
38
|
+
export { isTransientRequestError } from "./read-retry";
|
|
39
|
+
export type { OpenGeniReadRetryOptions } from "./read-retry";
|
|
38
40
|
export { parseDeprecationNotice } from "./deprecation";
|
|
39
41
|
export type { OpenGeniDeprecationHandler, OpenGeniDeprecationNotice } from "./deprecation";
|
|
40
42
|
export { resolveWorkspaceVoiceInputEnabled } from "./transcription";
|