@opengeni/sdk 1.4.0 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +12 -0
  2. package/dist/adapters/next.js +2 -2
  3. package/dist/artifacts.js +5 -5
  4. package/dist/browser.js +3 -3
  5. package/dist/chat/index.js +15 -9
  6. package/dist/chat/index.js.map +1 -1
  7. package/dist/{chunk-BVIIRSJY.js → chunk-6SWFOZUP.js} +3 -3
  8. package/dist/{chunk-LEBDXYMA.js → chunk-7A5ZDWQH.js} +2 -2
  9. package/dist/{chunk-GYM54GKM.js → chunk-DTIMLEPB.js} +1 -1
  10. package/dist/chunk-DTIMLEPB.js.map +1 -0
  11. package/dist/{chunk-CA2T7QT4.js → chunk-HORQJBLP.js} +2 -2
  12. package/dist/{chunk-NC57YMUO.js → chunk-IJWKWFSF.js} +1 -1
  13. package/dist/chunk-IJWKWFSF.js.map +1 -0
  14. package/dist/{chunk-ZNC44QEX.js → chunk-JOI7XCG6.js} +45 -14
  15. package/dist/chunk-JOI7XCG6.js.map +1 -0
  16. package/dist/{chunk-D534PIXF.js → chunk-MAFL52JD.js} +2 -2
  17. package/dist/{chunk-FYQAMXDW.js → chunk-OLLQBZAE.js} +111 -5
  18. package/dist/chunk-OLLQBZAE.js.map +1 -0
  19. package/dist/{chunk-MXV5O25O.js → chunk-W72BOLRJ.js} +2 -2
  20. package/dist/codex-realtime-controller.js +1 -1
  21. package/dist/core.js +4 -4
  22. package/dist/document-authority.js +4 -4
  23. package/dist/editable-artifacts.js +1 -1
  24. package/dist/index.js +9 -9
  25. package/dist/interaction.d.ts +5 -0
  26. package/dist/interaction.js +1 -1
  27. package/dist/realtime.js +1 -1
  28. package/dist/session-proxy.d.ts +29 -6
  29. package/dist/session-proxy.js +2 -2
  30. package/dist/site.js +4 -4
  31. package/dist/types.d.ts +17 -0
  32. package/package.json +3 -3
  33. package/src/chat/opengeni.ts +14 -3
  34. package/src/codex-realtime-controller.ts +65 -14
  35. package/src/interaction.ts +5 -0
  36. package/src/session-proxy.ts +189 -9
  37. package/src/types.ts +15 -0
  38. package/dist/chunk-FYQAMXDW.js.map +0 -1
  39. package/dist/chunk-GYM54GKM.js.map +0 -1
  40. package/dist/chunk-NC57YMUO.js.map +0 -1
  41. package/dist/chunk-ZNC44QEX.js.map +0 -1
  42. /package/dist/{chunk-BVIIRSJY.js.map → chunk-6SWFOZUP.js.map} +0 -0
  43. /package/dist/{chunk-LEBDXYMA.js.map → chunk-7A5ZDWQH.js.map} +0 -0
  44. /package/dist/{chunk-CA2T7QT4.js.map → chunk-HORQJBLP.js.map} +0 -0
  45. /package/dist/{chunk-D534PIXF.js.map → chunk-MAFL52JD.js.map} +0 -0
  46. /package/dist/{chunk-MXV5O25O.js.map → chunk-W72BOLRJ.js.map} +0 -0
@@ -110,7 +110,12 @@ export type SessionProxyCreateInput = {
110
110
  export type SessionProxyMessageInput = {
111
111
  /** Absent for `create`. */
112
112
  sessionId?: string | undefined;
113
- delivery: "create" | "send" | "steer" | "submit";
113
+ /**
114
+ * `realtime` is live voice: once when a call starts (refuse it, or rotate
115
+ * MCP credentials), and before each batch of finalized transcripts and spoken
116
+ * requests is saved (refuse it, or add `modelContext`).
117
+ */
118
+ delivery: "create" | "send" | "steer" | "submit" | "realtime";
114
119
  };
115
120
 
116
121
  /** Server-side additions the host attaches to one forwarded user message or response. */
@@ -118,7 +123,9 @@ export type SessionProxyMessageExtras = {
118
123
  /**
119
124
  * Model-visible context for this message (current page, time zone, today's
120
125
  * date). Placed before any context the browser sent. Not secret. Ignored for
121
- * approval decisions and human-input responses, which are not new messages.
126
+ * approval decisions and human-input responses, which are not new messages,
127
+ * and when a live voice call starts. Keep it stable for the same message:
128
+ * a retried voice entry must carry the same context to be accepted.
122
129
  */
123
130
  modelContext?: string | undefined;
124
131
  /**
@@ -193,9 +200,10 @@ export type SessionProxyHandlerOptions = {
193
200
  | undefined;
194
201
  /**
195
202
  * Called before every forwarded user message (send, steer, composer submit,
196
- * and browser-started create), approval decision, and human-input response.
197
- * Return server-owned `modelContext` (messages only) and MCP credential
198
- * rotations, or a `Response` to reject the action.
203
+ * and browser-started create), approval decision, human-input response, and
204
+ * live voice start and transcript save (`delivery: "realtime"`).
205
+ * Return server-owned `modelContext` (messages and voice transcripts only)
206
+ * and MCP credential rotations, or a `Response` to reject the action.
199
207
  */
200
208
  beforeForwardMessage?:
201
209
  | ((
@@ -237,6 +245,18 @@ export type SessionProxyHandlerOptions = {
237
245
  * hide the microphone. Defaults to true.
238
246
  */
239
247
  voiceInput?: boolean | undefined;
248
+ /**
249
+ * Forward live speech-to-speech voice for existing chats as the resolved
250
+ * user: the voice model catalog and the call lifecycle routes under
251
+ * `.../sessions/{id}/realtime`. Opengeni still requires that user's
252
+ * `sessions:control` permission, binds the call to that user and browser,
253
+ * runs spoken requests as ordinary steers of the chat, and meters
254
+ * deployment-funded voice against the organization's credits. The stock
255
+ * composer shows the voice button only when a voice model is available.
256
+ * `false` reports `realtimeVoice: false` in the client config so stock UIs
257
+ * hide it, and refuses the routes. Defaults to true.
258
+ */
259
+ realtimeVoice?: boolean | undefined;
240
260
  /**
241
261
  * Let the browser read a file from the session's sandbox (`POST .../fs/read`)
242
262
  * so `sandbox:` links in agent replies can be downloaded. Only `path`,
@@ -291,10 +311,20 @@ export type SessionProxyHandlerOptions = {
291
311
  * settings decide which models end users see.
292
312
  */
293
313
  modelSelection?: boolean | undefined;
294
- /** SSE heartbeat interval. Defaults to 15 seconds. */
314
+ /**
315
+ * SSE heartbeat interval. Defaults to 5 seconds, under Bun.serve's default
316
+ * 10-second `idleTimeout`, which otherwise closes a quiet event stream.
317
+ */
295
318
  heartbeatMs?: number | undefined;
296
319
  };
297
320
 
321
+ /**
322
+ * Default SSE heartbeat. Bun.serve closes a connection that sends nothing for
323
+ * 10 seconds by default, so a quiet stream (a long tool call, an idle chat)
324
+ * would drop and reconnect every 10 seconds through a Bun or Hono-on-Bun host.
325
+ */
326
+ const SESSION_PROXY_DEFAULT_HEARTBEAT_MS = 5_000;
327
+
298
328
  const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
299
329
  const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
300
330
  const QUEUE_OPERATIONS: ReadonlySet<string> = new Set(["move", "edit", "steer", "delete"]);
@@ -351,9 +381,12 @@ export function createSessionProxyHandler(
351
381
  ): (request: Request) => Promise<Response> {
352
382
  const service = isFacade(target) ? target.client : target;
353
383
  const defaultSource = isFacade(target) ? target.source : "default";
384
+ // One rejected-key notice per handler (each handler holds one server key).
385
+ const rejectedKeyNotice = { sent: false };
354
386
  const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
355
387
  const filesEnabled = options.files ?? true;
356
388
  const voiceInputEnabled = options.voiceInput ?? true;
389
+ const realtimeVoiceEnabled = options.realtimeVoice ?? true;
357
390
  const sandboxFilesEnabled = options.sandboxFiles === true;
358
391
  const artifactsEnabled = options.artifacts !== undefined && options.artifacts !== false;
359
392
  const editableLiveUrl = artifactsEnabled
@@ -363,7 +396,7 @@ export function createSessionProxyHandler(
363
396
  )
364
397
  : null;
365
398
  const modelSelection = options.modelSelection ?? true;
366
- const heartbeatMs = options.heartbeatMs ?? 15_000;
399
+ const heartbeatMs = options.heartbeatMs ?? SESSION_PROXY_DEFAULT_HEARTBEAT_MS;
367
400
  const sessionList = options.sessionList ?? "mine";
368
401
  const archiveEnabled = options.archive ?? true;
369
402
  const chats = options.chats ?? "private";
@@ -520,6 +553,39 @@ export function createSessionProxyHandler(
520
553
  ],
521
554
  };
522
555
  };
556
+ /**
557
+ * Best-effort standalone rotation before a voice call. Opengeni refuses
558
+ * it while a turn is running; that turn's message already refreshed them.
559
+ */
560
+ const refreshRealtimeCredentials = async (
561
+ sessionId: string,
562
+ updates: SessionMcpCredentialUpdateInput[],
563
+ ): Promise<void> => {
564
+ if (updates.length === 0) return;
565
+ try {
566
+ const servers = (await client.getSession(workspaceId, sessionId, call)).mcpServers ?? [];
567
+ const rotations = updates.flatMap((update) => {
568
+ const server = servers.find((candidate) => candidate.id === update.id);
569
+ return server
570
+ ? [
571
+ {
572
+ id: update.id,
573
+ expectedCredentialVersion: server.credentialVersion,
574
+ expectedServerUrl: server.url,
575
+ headers: update.headers,
576
+ },
577
+ ]
578
+ : [];
579
+ });
580
+ if (rotations.length === 0) return;
581
+ await client.rotateSessionMcpCredentials(workspaceId, sessionId, {
582
+ operationKey: crypto.randomUUID(),
583
+ updates: rotations,
584
+ });
585
+ } catch (error) {
586
+ if (!(error instanceof OpenGeniApiError)) throw error;
587
+ }
588
+ };
523
589
  /** Browser input sanitized, then server-owned extras merged in. */
524
590
  const forwardMessage = async (
525
591
  value: unknown,
@@ -617,6 +683,7 @@ export function createSessionProxyHandler(
617
683
  // Whether the stock "New chat" and "Archive" actions can succeed.
618
684
  sessionCreation: options.createSession !== undefined,
619
685
  archive: archiveEnabled,
686
+ ...(realtimeVoiceEnabled ? {} : { realtimeVoice: false }),
620
687
  // Explicit true also tells stock UIs to offer end users the model picker.
621
688
  ...(modelSelection
622
689
  ? options.modelSelection === true
@@ -650,6 +717,10 @@ export function createSessionProxyHandler(
650
717
  if (area === "model-catalog" && tail.length === 0 && method === "GET") {
651
718
  return await read(`${base}/model-catalog`);
652
719
  }
720
+ if (area === "realtime-model-catalog" && tail.length === 0 && method === "GET") {
721
+ if (!realtimeVoiceEnabled) return errorJson(404, "route_not_allowed", "Not found.");
722
+ return await read(`${base}/realtime-model-catalog`);
723
+ }
653
724
  if (area === "live-events" && tail.length === 1 && tail[0] === "stream" && method === "GET") {
654
725
  // Surface authorization failures as HTTP status before streaming.
655
726
  await client.requestJson("GET", base, undefined, {}, call);
@@ -879,6 +950,34 @@ export function createSessionProxyHandler(
879
950
  ? payload
880
951
  : json(await client.requestJson("POST", path, payload));
881
952
 
953
+ if (op[0] === "realtime") {
954
+ if (!realtimeVoiceEnabled) return errorJson(404, "route_not_allowed", "Not found.");
955
+ const realtime = realtimeRoute(method, op.slice(1));
956
+ if (!realtime) return errorJson(404, "route_not_allowed", "Not found.");
957
+ if (!body) reject(400, "invalid_body", "A JSON object body is required.");
958
+ const path = `${session}/${op.join("/")}`;
959
+ if (realtime === "begin") {
960
+ // The host may refuse a call; MCP credentials (including the tool
961
+ // server's per-user token) are refreshed before voice can delegate.
962
+ const extras = await messageExtras({
963
+ sessionId,
964
+ delivery: "realtime",
965
+ });
966
+ if (extras instanceof Response) return extras;
967
+ await refreshRealtimeCredentials(sessionId, extras?.mcpCredentialUpdates ?? []);
968
+ }
969
+ let payload: Record<string, unknown> = body;
970
+ if (realtime === "sync" && options.beforeForwardMessage && hasRealtimeMessages(body)) {
971
+ const extras = await options.beforeForwardMessage(
972
+ { sessionId, delivery: "realtime" },
973
+ context,
974
+ );
975
+ if (extras instanceof Response) return extras;
976
+ payload = withRealtimeContext(body, extras?.modelContext);
977
+ }
978
+ return json(await client.requestJson(method, path, payload));
979
+ }
980
+
882
981
  switch (route) {
883
982
  case "GET ":
884
983
  return await read(session);
@@ -1020,7 +1119,7 @@ export function createSessionProxyHandler(
1020
1119
  }
1021
1120
  return errorJson(404, "route_not_allowed", "Not found.");
1022
1121
  } catch (error) {
1023
- return errorResponse(error);
1122
+ return errorResponse(error, rejectedKeyNotice);
1024
1123
  }
1025
1124
  };
1026
1125
  }
@@ -1033,6 +1132,67 @@ const PROXY_METHODS: readonly string[] = ["GET", "POST", "PUT", "PATCH", "DELETE
1033
1132
 
1034
1133
  const UPLOAD_FIELDS = ["scope", "filename", "contentType", "sizeBytes", "sha256"] as const;
1035
1134
 
1135
+ type RealtimeRoute = "begin" | "connect" | "heartbeat" | "end" | "activate" | "sync";
1136
+ const REALTIME_CONNECT_ROUTES: ReadonlySet<string> = new Set(["webrtc", "gateway", "supergrok"]);
1137
+ /** Ledger entries that become (or join) messages and so accept `modelContext`. */
1138
+ const REALTIME_MESSAGE_KINDS: ReadonlySet<string> = new Set([
1139
+ "delegation_call",
1140
+ "user_transcript",
1141
+ "assistant_transcript",
1142
+ ]);
1143
+
1144
+ /**
1145
+ * The live voice routes a browser call uses, after `realtime/`: begin, provider
1146
+ * connect, heartbeat, end, connection activation, and transcript sync. Bodies
1147
+ * pass to Opengeni, which validates them and binds the call to the acting user.
1148
+ */
1149
+ function realtimeRoute(method: string, rest: string[]): RealtimeRoute | null {
1150
+ if (rest.length === 0) return method === "POST" ? "begin" : null;
1151
+ if (rest.length === 1) {
1152
+ if (REALTIME_CONNECT_ROUTES.has(rest[0]!)) return method === "POST" ? "connect" : null;
1153
+ return method === "DELETE" ? "end" : null;
1154
+ }
1155
+ if (rest.length === 2 && rest[1] === "heartbeat") return method === "PATCH" ? "heartbeat" : null;
1156
+ if (rest.length === 2 && rest[1] === "sync") return method === "POST" ? "sync" : null;
1157
+ if (rest.length === 4 && rest[1] === "connections" && rest[3] === "activate") {
1158
+ return method === "POST" ? "activate" : null;
1159
+ }
1160
+ return null;
1161
+ }
1162
+
1163
+ function hasRealtimeMessages(body: Record<string, unknown>): boolean {
1164
+ return (
1165
+ Array.isArray(body.entries) &&
1166
+ body.entries.some(
1167
+ (entry) =>
1168
+ entry !== null &&
1169
+ typeof entry === "object" &&
1170
+ REALTIME_MESSAGE_KINDS.has((entry as { kind?: unknown }).kind as string),
1171
+ )
1172
+ );
1173
+ }
1174
+
1175
+ /** Server context goes before the browser's on each message-bearing entry. */
1176
+ function withRealtimeContext(
1177
+ body: Record<string, unknown>,
1178
+ serverContext: string | undefined,
1179
+ ): Record<string, unknown> {
1180
+ if (!serverContext?.trim() || !Array.isArray(body.entries)) return body;
1181
+ return {
1182
+ ...body,
1183
+ entries: body.entries.map((entry: unknown) => {
1184
+ if (entry === null || typeof entry !== "object") return entry;
1185
+ const record = entry as Record<string, unknown>;
1186
+ if (!REALTIME_MESSAGE_KINDS.has(record.kind as string)) return entry;
1187
+ const modelContext = joinContext(
1188
+ serverContext,
1189
+ typeof record.modelContext === "string" ? record.modelContext : undefined,
1190
+ );
1191
+ return modelContext ? { ...record, modelContext } : entry;
1192
+ }),
1193
+ };
1194
+ }
1195
+
1036
1196
  type NormalizedToolServer = SessionProxyToolServer & {
1037
1197
  url: string;
1038
1198
  id: string;
@@ -1724,8 +1884,24 @@ function workspaceLiveStream(
1724
1884
  });
1725
1885
  }
1726
1886
 
1887
+ /**
1888
+ * The proxy always calls Opengeni with the server's own key, so an upstream
1889
+ * 401 means that key is wrong, expired or revoked, never that the end user is
1890
+ * signed out. Tell the developer once, in the server log, what to fix.
1891
+ */
1892
+ function warnRejectedApiKey(error: OpenGeniApiError, notice: { sent: boolean }): void {
1893
+ if (notice.sent) return;
1894
+ notice.sent = true;
1895
+ console.warn(
1896
+ `[@opengeni/sdk] Opengeni rejected the session proxy's API key (401${
1897
+ error.correlationId ? `, reference ${error.correlationId}` : ""
1898
+ }). It may be expired, revoked or mistyped: create a new key in Opengeni under ` +
1899
+ "Organization settings > Developer and update OPENGENI_API_KEY on the server.",
1900
+ );
1901
+ }
1902
+
1727
1903
  /** Preserve OpenGeni's error envelope so the browser SDK keeps codes, retryability, and outcome facts. */
1728
- function errorResponse(error: unknown): Response {
1904
+ function errorResponse(error: unknown, rejectedKeyNotice: { sent: boolean }): Response {
1729
1905
  if (error instanceof ProxyRejection) return errorJson(error.status, error.code, error.message);
1730
1906
  if (error instanceof OpenGeniSetupError) {
1731
1907
  return json(
@@ -1741,6 +1917,7 @@ function errorResponse(error: unknown): Response {
1741
1917
  );
1742
1918
  }
1743
1919
  if (error instanceof OpenGeniApiError) {
1920
+ if (error.status === 401) warnRejectedApiKey(error, rejectedKeyNotice);
1744
1921
  const status = error.status >= 400 && error.status <= 599 ? error.status : 502;
1745
1922
  // A decoded upstream envelope is forwarded verbatim; the SDK only retains decodable bodies.
1746
1923
  if (error.body) {
@@ -1769,5 +1946,8 @@ function errorResponse(error: unknown): Response {
1769
1946
  if (error instanceof Error && error.name === "AbortError") {
1770
1947
  return errorJson(499, "aborted", "The request was aborted.");
1771
1948
  }
1949
+ // The browser gets no details; the host's server log needs the cause
1950
+ // (for example a missing OPENGENI_API_KEY).
1951
+ console.error("[@opengeni/sdk] Session proxy request failed:", error);
1772
1952
  return errorJson(500, "proxy_error", "Request could not be completed.");
1773
1953
  }
package/src/types.ts CHANGED
@@ -4301,6 +4301,13 @@ export type ClientConfig = {
4301
4301
  modelSelection?: boolean | undefined;
4302
4302
  /** Session proxy sandbox-path download opt-in; absent on native deployments. */
4303
4303
  sandboxFiles?: boolean | undefined;
4304
+ /**
4305
+ * `false` when a host's session proxy turns live voice off
4306
+ * (`createSessionProxyHandler({ realtimeVoice: false })`), so UIs hide the
4307
+ * voice button. Otherwise absent: availability comes from the workspace's
4308
+ * realtime model catalog.
4309
+ */
4310
+ realtimeVoice?: boolean | undefined;
4304
4311
  /**
4305
4312
  * Session proxy only: the workspace the proxy resolved for this user, so a
4306
4313
  * browser pointed at the proxy (`<OpenGeniChat baseUrl=... />`) needs no
@@ -7435,6 +7442,12 @@ export type CapabilityRuntime = {
7435
7442
  | "missing_verification";
7436
7443
  }
7437
7444
  | undefined;
7445
+ /**
7446
+ * Present when connecting needs an operator-registered OAuth client because
7447
+ * the provider refuses self-registration; `configured` is whether this
7448
+ * deployment has one. Connector surfaces offer the row only when true.
7449
+ */
7450
+ operatorOAuthClient?: { configured: boolean } | undefined;
7438
7451
  };
7439
7452
 
7440
7453
  export type CapabilityCatalogItem = {
@@ -8239,6 +8252,8 @@ export type PromotionalCreditScope = {
8239
8252
  export type PromotionalCreditBalance = PromotionalCreditScope & {
8240
8253
  grantId: string;
8241
8254
  remainingMicros: number;
8255
+ /** Also pays for dictation and live voice, like general credits (signup credits). */
8256
+ coversVoice?: boolean | undefined;
8242
8257
  };
8243
8258
 
8244
8259
  export type BillingBalance = {