@opengeni/sdk 1.3.0 → 1.4.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.
Files changed (59) hide show
  1. package/README.md +13 -3
  2. package/dist/adapters/next.js +2 -2
  3. package/dist/artifacts.js +4 -4
  4. package/dist/azure-live-transport.d.ts +32 -0
  5. package/dist/browser.js +3 -3
  6. package/dist/chat/index.js +5 -5
  7. package/dist/{chunk-RSKO3CMZ.js → chunk-BVIIRSJY.js} +2 -2
  8. package/dist/{chunk-G5NZGPTB.js → chunk-CA2T7QT4.js} +2 -2
  9. package/dist/{chunk-ZBICMU4Z.js → chunk-D534PIXF.js} +2 -2
  10. package/dist/{chunk-CNSPJYF6.js → chunk-FYQAMXDW.js} +104 -5
  11. package/dist/chunk-FYQAMXDW.js.map +1 -0
  12. package/dist/{chunk-IB6SUE3R.js → chunk-GYM54GKM.js} +1 -1
  13. package/dist/chunk-GYM54GKM.js.map +1 -0
  14. package/dist/{chunk-VYCTL62C.js → chunk-GZQSWAKD.js} +1 -1
  15. package/dist/chunk-GZQSWAKD.js.map +1 -0
  16. package/dist/{chunk-CREM7YRK.js → chunk-LEBDXYMA.js} +2 -2
  17. package/dist/{chunk-YVH2PH4X.js → chunk-MXV5O25O.js} +2 -2
  18. package/dist/{chunk-2UXZUCEZ.js → chunk-ZNC44QEX.js} +77 -7
  19. package/dist/chunk-ZNC44QEX.js.map +1 -0
  20. package/dist/codex-realtime-controller.d.ts +13 -0
  21. package/dist/codex-realtime-controller.js +3 -1
  22. package/dist/codex-realtime-v3-wire.d.ts +3 -2
  23. package/dist/codex-realtime.d.ts +4 -0
  24. package/dist/core.js +4 -4
  25. package/dist/document-authority.js +3 -3
  26. package/dist/editable-artifacts.js +1 -1
  27. package/dist/index.d.ts +3 -2
  28. package/dist/index.js +55 -18
  29. package/dist/index.js.map +1 -1
  30. package/dist/realtime.d.ts +1 -1
  31. package/dist/realtime.js +248 -2
  32. package/dist/realtime.js.map +1 -1
  33. package/dist/session-proxy-client.d.ts +7 -3
  34. package/dist/session-proxy.d.ts +9 -1
  35. package/dist/session-proxy.js +2 -2
  36. package/dist/site.js +3 -3
  37. package/dist/transcription.d.ts +1 -1
  38. package/dist/types.d.ts +30 -8
  39. package/package.json +3 -3
  40. package/src/azure-live-transport.ts +286 -0
  41. package/src/codex-realtime-controller.ts +95 -2
  42. package/src/codex-realtime-v3-wire.ts +4 -3
  43. package/src/codex-realtime-v3.ts +11 -2
  44. package/src/codex-realtime.ts +9 -1
  45. package/src/index.ts +3 -0
  46. package/src/realtime.ts +15 -6
  47. package/src/session-proxy-client.ts +7 -3
  48. package/src/session-proxy.ts +137 -4
  49. package/src/transcription.ts +3 -0
  50. package/src/types.ts +34 -5
  51. package/dist/chunk-2UXZUCEZ.js.map +0 -1
  52. package/dist/chunk-CNSPJYF6.js.map +0 -1
  53. package/dist/chunk-IB6SUE3R.js.map +0 -1
  54. package/dist/chunk-VYCTL62C.js.map +0 -1
  55. /package/dist/{chunk-RSKO3CMZ.js.map → chunk-BVIIRSJY.js.map} +0 -0
  56. /package/dist/{chunk-G5NZGPTB.js.map → chunk-CA2T7QT4.js.map} +0 -0
  57. /package/dist/{chunk-ZBICMU4Z.js.map → chunk-D534PIXF.js.map} +0 -0
  58. /package/dist/{chunk-CREM7YRK.js.map → chunk-LEBDXYMA.js.map} +0 -0
  59. /package/dist/{chunk-YVH2PH4X.js.map → chunk-MXV5O25O.js.map} +0 -0
@@ -120,8 +120,47 @@ export type CodexRealtimeControllerSnapshot = {
120
120
  reconnectAttempt: number;
121
121
  diagnostic: CodexRealtimeDiagnostic | null;
122
122
  error: string | null;
123
+ /**
124
+ * Set when Opengeni refused or ended deployment-funded voice for credits or
125
+ * availability. Codes match voice input: `insufficient_credits`,
126
+ * `allowance_exhausted`, `monthly_model_cost_limit`, plus
127
+ * `realtime_voice_unavailable`. Cleared on the next start.
128
+ */
129
+ refusal?: CodexRealtimeRefusal | null;
123
130
  };
124
131
 
132
+ export type CodexRealtimeRefusal = { code: string; message: string };
133
+
134
+ const REALTIME_REFUSAL_CODES = new Set([
135
+ "insufficient_credits",
136
+ "allowance_exhausted",
137
+ "monthly_model_cost_limit",
138
+ "realtime_voice_unavailable",
139
+ ]);
140
+
141
+ /** A definitive credit/availability refusal from Opengeni, with its plain message. */
142
+ export function codexRealtimeRefusal(error: unknown): CodexRealtimeRefusal | null {
143
+ if (!(error instanceof OpenGeniApiError) || !error.code) return null;
144
+ if (!REALTIME_REFUSAL_CODES.has(error.code)) return null;
145
+ let message: string | null = null;
146
+ try {
147
+ const body = JSON.parse(error.body) as Record<string, unknown>;
148
+ const nested =
149
+ body.error && typeof body.error === "object" ? (body.error as Record<string, unknown>) : body;
150
+ message = typeof nested.message === "string" && nested.message ? nested.message : null;
151
+ } catch {
152
+ message = null;
153
+ }
154
+ return {
155
+ code: error.code,
156
+ message:
157
+ message ??
158
+ (error.code === "insufficient_credits"
159
+ ? "Live voice needs Opengeni credits. Add credits to continue."
160
+ : "Live voice is unavailable right now."),
161
+ };
162
+ }
163
+
125
164
  export type CodexRealtimeControllerClient = {
126
165
  beginSessionRealtime(
127
166
  workspaceId: string,
@@ -178,6 +217,7 @@ export type CodexRealtimeOwnerStorage = Pick<Storage, "getItem" | "setItem" | "r
178
217
 
179
218
  /** Canonical browser-owner storage namespace for a public realtime model. */
180
219
  export function sessionRealtimeOwnerStorageNamespace(model: SessionRealtimeModel): string {
220
+ if (model === "opengeni-azure/gpt-live-1") return "azure-live-owner";
181
221
  if (model === "gpt-live-1-boulder-alpha") return "codex-realtime-owner";
182
222
  if (model === "supergrok/grok-voice-think-fast-2.0") return "xai-realtime-owner";
183
223
  return "gateway-realtime-owner";
@@ -355,6 +395,7 @@ export function createCodexRealtimeController(
355
395
  reconnectAttempt: 0,
356
396
  diagnostic: null,
357
397
  error: null,
398
+ refusal: null,
358
399
  };
359
400
  let active: ConnectionRuntime | null = null;
360
401
  let pendingAbort: AbortController | null = null;
@@ -720,6 +761,11 @@ export function createCodexRealtimeController(
720
761
  transitionEnded("Realtime owner no longer exists");
721
762
  return;
722
763
  }
764
+ const refusal = codexRealtimeRefusal(error);
765
+ if (refusal) {
766
+ await endForRefusal(refusal);
767
+ return;
768
+ }
723
769
  if (error instanceof OpenGeniApiError && error.status === 409 && error.retryable && owner) {
724
770
  try {
725
771
  const reconciled = await begin(owner, true, false);
@@ -883,6 +929,15 @@ export function createCodexRealtimeController(
883
929
  });
884
930
  let connected: CodexRealtimeWebrtcSession;
885
931
  try {
932
+ // Persist a fragment provider's tail before the replacement broker reads
933
+ // its startup history, while the old connection still owns the ledger.
934
+ if (active?.transport.drain) {
935
+ await active.transport.drain();
936
+ await active.bridge.flush();
937
+ }
938
+ if (closed || stopping || abort.signal.aborted || pendingGeneration !== targetGeneration) {
939
+ throw abort.signal.reason ?? new DOMException("Aborted", "AbortError");
940
+ }
886
941
  const media = await ensureMicrophone(replaceMicrophone, abort.signal);
887
942
  const operationId = randomUUID();
888
943
  const commonTransportInput = {
@@ -1098,6 +1153,7 @@ export function createCodexRealtimeController(
1098
1153
  };
1099
1154
 
1100
1155
  const heartbeat = async (): Promise<void> => {
1156
+ let stopInstruction: CodexRealtimeRefusal | null = null;
1101
1157
  await exclusive(async () => {
1102
1158
  const current = state.mode;
1103
1159
  if (!owner || !current || current.state !== "active") {
@@ -1121,6 +1177,37 @@ export function createCodexRealtimeController(
1121
1177
  return;
1122
1178
  }
1123
1179
  publish({ mode: result.mode, realtimeId: result.mode.id, error: null });
1180
+ stopInstruction = result.stop ?? null;
1181
+ });
1182
+ // The server stopped extending the lease (for example, out of credits):
1183
+ // drain and end gracefully inside the remaining lease.
1184
+ const instruction = stopInstruction as CodexRealtimeRefusal | null;
1185
+ stopInstruction = null;
1186
+ if (instruction && !stopping && !closed) await endForRefusal(instruction);
1187
+ };
1188
+
1189
+ /**
1190
+ * End the call because Opengeni refused it, keep final speech, and leave a
1191
+ * terminal, non-retrying state that explains why.
1192
+ */
1193
+ const endForRefusal = async (refusal: CodexRealtimeRefusal): Promise<void> => {
1194
+ try {
1195
+ await controller.stop();
1196
+ } catch {
1197
+ // The lease is no longer extended; a failed graceful end lapses on its own.
1198
+ stopping = false;
1199
+ closeBrowserResources();
1200
+ clearOwner();
1201
+ }
1202
+ recoveryTerminal = false;
1203
+ publish({
1204
+ status: "error",
1205
+ realtimeId: null,
1206
+ mode: null,
1207
+ bridge: null,
1208
+ diagnostic: diagnostic("terminal_stop", refusal.message, false),
1209
+ error: refusal.message,
1210
+ refusal,
1124
1211
  });
1125
1212
  };
1126
1213
 
@@ -1186,11 +1273,12 @@ export function createCodexRealtimeController(
1186
1273
  };
1187
1274
  owner = record;
1188
1275
  storage?.setItem(storageKey, JSON.stringify(record));
1189
- publish({ status: "starting", error: null, diagnostic: null });
1276
+ publish({ status: "starting", error: null, diagnostic: null, refusal: null });
1190
1277
  try {
1191
1278
  await begin(record, false);
1192
1279
  } catch (error) {
1193
1280
  closeBrowserResources(false);
1281
+ const refusal = codexRealtimeRefusal(error);
1194
1282
  const failedMode = state.mode as SessionRealtimeMode | null;
1195
1283
  if (failedMode?.state === "active") {
1196
1284
  await handleConnectionFailure(error, "reconnect", false);
@@ -1200,9 +1288,13 @@ export function createCodexRealtimeController(
1200
1288
  status: "error",
1201
1289
  realtimeId: null,
1202
1290
  mode: null,
1203
- error: safeError(error),
1291
+ error: refusal?.message ?? safeError(error),
1292
+ ...(refusal
1293
+ ? { refusal, diagnostic: diagnostic("terminal_stop", refusal.message, false) }
1294
+ : {}),
1204
1295
  });
1205
1296
  }
1297
+ if (refusal) return;
1206
1298
  throw error;
1207
1299
  }
1208
1300
  },
@@ -1337,6 +1429,7 @@ export function createCodexRealtimeController(
1337
1429
  connectionTask = null;
1338
1430
  void retiredTask?.catch(() => undefined);
1339
1431
  try {
1432
+ await active?.transport.drain?.();
1340
1433
  await active?.bridge.sealAndFlush();
1341
1434
  closeBrowserResources();
1342
1435
  let current = state.mode;
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * These shapes are pinned to openai/codex@fa1d4c40. V3 delegates work with
5
5
  * `delegation.created`; it does not expose the ordinary Realtime V2
6
- * `function_call` protocol.
6
+ * `function_call` protocol. `transcript.segment` is an application adapter
7
+ * extension for providers that emit fragments without semantic turn boundaries.
7
8
  */
8
9
 
9
10
  export const CODEX_REALTIME_INITIAL_ITEMS_MAX_COUNT = 128;
@@ -40,7 +41,7 @@ export type CodexRealtimeV3Event =
40
41
  endMs: number | null;
41
42
  })
42
43
  | (ProviderEventIdentity & {
43
- type: "turn.done";
44
+ type: "turn.done" | "transcript.segment";
44
45
  turnId: string;
45
46
  role: "user" | "assistant";
46
47
  transcript: string;
@@ -173,7 +174,7 @@ export function parseCodexRealtimeV3Event(payload: string): CodexRealtimeV3Parse
173
174
  };
174
175
  }
175
176
 
176
- if (type === "turn.done") {
177
+ if (type === "turn.done" || type === "transcript.segment") {
177
178
  const turn = record(event.turn);
178
179
  const turnIdField = boundedStringField(turn, "id", CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES);
179
180
  const transcriptField = boundedStringField(
@@ -324,6 +324,12 @@ export function createCodexRealtimeV3Bridge(
324
324
  clientAckThroughSequence = Math.max(clientAckThroughSequence ?? 0, entry.sequence);
325
325
  }
326
326
  }
327
+ // Persist final inbound fragments even after the provider starts closing.
328
+ // Pending outbound context remains replayable on the next connection.
329
+ if (options.events.readyState !== "open") {
330
+ publish();
331
+ continue;
332
+ }
327
333
  for (let index = 0; index < result.outbound.length;) {
328
334
  const entry = result.outbound[index]!;
329
335
  if (sentSequences.has(entry.sequence)) {
@@ -497,7 +503,7 @@ export function createCodexRealtimeV3Bridge(
497
503
  }
498
504
  } else if (event.type === "output_audio.delta") {
499
505
  speaking = true;
500
- } else if (event.type === "turn.done") {
506
+ } else if (event.type === "turn.done" || event.type === "transcript.segment") {
501
507
  speaking = false;
502
508
  if (event.transcript.length > 0 && !finalizedTurnIds.has(event.turnId)) {
503
509
  const coveredByDelegationItemId =
@@ -570,7 +576,7 @@ export function createCodexRealtimeV3Bridge(
570
576
 
571
577
  function finalTranscript(
572
578
  randomUUID: () => string,
573
- event: Extract<CodexRealtimeV3Event, { type: "turn.done" }>,
579
+ event: Extract<CodexRealtimeV3Event, { type: "turn.done" | "transcript.segment" }>,
574
580
  coveredByDelegationItemId: string | null,
575
581
  modelContext: string | undefined,
576
582
  ): SessionRealtimeInboundEntry {
@@ -581,6 +587,9 @@ function finalTranscript(
581
587
  text: event.transcript,
582
588
  payload: {
583
589
  turnId: event.turnId,
590
+ ...(event.type === "transcript.segment"
591
+ ? { transcriptSource: "provider_fragments", grouping: "application_segment" }
592
+ : {}),
584
593
  ...(coveredByDelegationItemId ? { coveredByDelegationItemId } : {}),
585
594
  },
586
595
  ...(modelContext ? { modelContext } : {}),
@@ -37,6 +37,8 @@ export type AcquireCodexRealtimeMicrophoneOptions = {
37
37
 
38
38
  export type StartCodexRealtimeWebrtcOptions = {
39
39
  negotiate: CodexRealtimeNegotiator;
40
+ /** Exact configured provider model expected from the server broker. */
41
+ expectedModel?: CodexRealtimeWebrtcResponse["model"];
40
42
  realtimeId: string;
41
43
  operationId: string;
42
44
  browserInstanceId: string;
@@ -77,6 +79,8 @@ export type CodexRealtimeWebrtcSession = {
77
79
  setOutputMuted(muted: boolean): void;
78
80
  activateRemoteAudio(): void;
79
81
  retryAudibleOutput(): Promise<boolean>;
82
+ /** Gracefully finish provider output before sealing the durable ledger. */
83
+ drain?(): Promise<void>;
80
84
  /** Idempotently close media, data channel, and peer transport. */
81
85
  stop(): void;
82
86
  };
@@ -260,7 +264,11 @@ export async function startCodexRealtimeWebrtc(
260
264
  { signal: options.signal },
261
265
  );
262
266
  throwIfAborted(options.signal);
263
- if (answer.version !== "v3" || answer.model !== "gpt-live-1-boulder-alpha" || !answer.sdp) {
267
+ if (
268
+ answer.version !== "v3" ||
269
+ answer.model !== (options.expectedModel ?? "gpt-live-1-boulder-alpha") ||
270
+ !answer.sdp
271
+ ) {
264
272
  throw new Error("Opengeni returned an incompatible Codex realtime answer");
265
273
  }
266
274
  await peerConnection.setRemoteDescription({
package/src/index.ts CHANGED
@@ -356,9 +356,11 @@ export type {
356
356
  CodexRealtimeControllerStatus,
357
357
  CodexRealtimeMicrophoneState,
358
358
  CodexRealtimeOwnerStorage,
359
+ CodexRealtimeRefusal,
359
360
  CreateCodexRealtimeControllerOptions,
360
361
  RealtimeControllerTransportStarter,
361
362
  } from "./codex-realtime-controller";
363
+ export { codexRealtimeRefusal } from "./codex-realtime-controller";
362
364
  export {
363
365
  createGatewayRealtimeTransportStarter,
364
366
  createXaiSubscriptionRealtimeTransportStarter,
@@ -749,6 +751,7 @@ export type {
749
751
  SessionRealtimeMode,
750
752
  SessionRealtimeModel,
751
753
  SessionRealtimeMutationResponse,
754
+ SessionRealtimeStopInstruction,
752
755
  SessionRealtimeState,
753
756
  WorkspaceModelCatalogModel,
754
757
  WorkspaceModelCatalogResponse,
package/src/realtime.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { createAzureLiveTransportStarter } from "./azure-live-transport";
1
2
  /**
2
3
  * Public, provider-neutral realtime browser SDK.
3
4
  *
@@ -34,7 +35,7 @@ export type SessionRealtimeClientLike = CodexRealtimeControllerClient & {
34
35
  ): Promise<WorkspaceRealtimeModelCatalogResponse>;
35
36
  };
36
37
 
37
- export type SessionRealtimeTransportKind = "codex" | "gateway" | "xai-subscription";
38
+ export type SessionRealtimeTransportKind = "codex" | "gateway" | "xai-subscription" | "azure-live";
38
39
 
39
40
  /** Provider-neutral names for the existing, battle-tested controller projection. */
40
41
  export type SessionRealtimeController = CodexRealtimeController;
@@ -57,6 +58,7 @@ export type CreateSessionRealtimeControllerOptions = Omit<
57
58
  export function sessionRealtimeTransportKind(
58
59
  model: SessionRealtimeModel,
59
60
  ): SessionRealtimeTransportKind {
61
+ if (model === "opengeni-azure/gpt-live-1") return "azure-live";
60
62
  if (model === "gpt-live-1-boulder-alpha") return "codex";
61
63
  if (model === "supergrok/grok-voice-think-fast-2.0") return "xai-subscription";
62
64
  return "gateway";
@@ -74,11 +76,18 @@ export function createSessionRealtimeController(
74
76
  return createCodexRealtimeController({
75
77
  ...options,
76
78
  ownerStorageNamespace: sessionRealtimeOwnerStorageNamespace(options.model),
77
- ...(transport === "gateway"
78
- ? { startTransport: createGatewayRealtimeTransportStarter() }
79
- : transport === "xai-subscription"
80
- ? { startTransport: createXaiSubscriptionRealtimeTransportStarter() }
81
- : {}),
79
+ ...(transport === "azure-live"
80
+ ? {
81
+ startTransport: createAzureLiveTransportStarter({
82
+ remoteAudio: options.remoteAudio,
83
+ createPeerConnection: options.createPeerConnection,
84
+ }),
85
+ }
86
+ : transport === "gateway"
87
+ ? { startTransport: createGatewayRealtimeTransportStarter() }
88
+ : transport === "xai-subscription"
89
+ ? { startTransport: createXaiSubscriptionRealtimeTransportStarter() }
90
+ : {}),
82
91
  });
83
92
  }
84
93
 
@@ -31,15 +31,19 @@ export function getSessionProxyWorkspaceGrant(
31
31
  );
32
32
  }
33
33
 
34
- /** Exact association read. The API reauthorizes the source session on every call. */
34
+ /**
35
+ * Exact association read. The API reauthorizes the source session on every
36
+ * call. `retained` proves a generated image or video, or a published sandbox
37
+ * file, originated in that session.
38
+ */
35
39
  export function getSessionProxyArtifactAssociation(
36
40
  client: Pick<OpenGeniEmbeddingClient, "requestJson">,
37
41
  workspaceId: string,
38
42
  sessionId: string,
39
- kind: "editable" | "site",
43
+ kind: "editable" | "site" | "retained",
40
44
  artifactId: string,
41
45
  options: OpenGeniRequestOptions = {},
42
- ): Promise<{ sessionId: string; artifactId: string; kind: "editable" | "site" }> {
46
+ ): Promise<{ sessionId: string; artifactId: string; kind: "editable" | "site" | "retained" }> {
43
47
  return client.requestJson(
44
48
  "GET",
45
49
  `/v1/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/artifact-associations/${encodeURIComponent(artifactId)}`,
@@ -10,6 +10,7 @@ import type {
10
10
  FileResourceRef,
11
11
  LatencyMode,
12
12
  ReasoningEffort,
13
+ RetainedArtifactContent,
13
14
  SessionMcpCredentialUpdateInput,
14
15
  } from "./types";
15
16
  import { mintToolToken, resolveToolTokenSecret } from "./tool-auth";
@@ -218,7 +219,15 @@ export type SessionProxyHandlerOptions = {
218
219
  basePath?: string | undefined;
219
220
  /** Maximum JSON request body. Defaults to 1 MiB. */
220
221
  maxBodyBytes?: number | undefined;
221
- /** Expose composer file attachments (upload begin/complete, download URL). Defaults to true. */
222
+ /**
223
+ * Expose composer file attachments (upload begin/complete, download URL) and
224
+ * the media agents produce in a session: generated images, browser and
225
+ * computer screenshots, published sandbox files, and generated video
226
+ * playback. Workspace-level artifact reads must name their session in the
227
+ * `x-opengeni-session-id` header (`SessionConversation` does this); the proxy
228
+ * runs `authorizeSession` and only forwards an artifact OpenGeni proves that
229
+ * session produced. Defaults to true.
230
+ */
222
231
  files?: boolean | undefined;
223
232
  /**
224
233
  * Forward composer voice input (`POST .../transcriptions`, one recording per
@@ -378,10 +387,10 @@ export function createSessionProxyHandler(
378
387
  return async (request) => {
379
388
  try {
380
389
  const method = request.method;
381
- if (!["GET", "POST", "PUT", "PATCH"].includes(method)) {
390
+ if (!PROXY_METHODS.includes(method)) {
382
391
  return new Response(null, {
383
392
  status: 405,
384
- headers: { Allow: "GET, POST, PUT, PATCH" },
393
+ headers: { Allow: PROXY_METHODS.join(", ") },
385
394
  });
386
395
  }
387
396
  const resolved = await options.resolve(request);
@@ -602,7 +611,12 @@ export function createSessionProxyHandler(
602
611
  // reads it here instead of knowing any Opengeni id.
603
612
  workspaceId,
604
613
  sandboxFiles: sandboxFilesEnabled,
605
- ...(artifacts ? { artifacts } : {}),
614
+ // Stock UIs show Site previews and the artifact viewer only when
615
+ // this proxy serves them; `false` keeps them from failing on click.
616
+ artifacts: artifacts ?? false,
617
+ // Whether the stock "New chat" and "Archive" actions can succeed.
618
+ sessionCreation: options.createSession !== undefined,
619
+ archive: archiveEnabled,
606
620
  // Explicit true also tells stock UIs to offer end users the model picker.
607
621
  ...(modelSelection
608
622
  ? options.modelSelection === true
@@ -676,6 +690,53 @@ export function createSessionProxyHandler(
676
690
  return errorJson(404, "route_not_allowed", "Not found.");
677
691
  }
678
692
 
693
+ if (area === "artifacts") {
694
+ // Retained media a session produced: only an artifact OpenGeni proves
695
+ // that session produced, never an arbitrary workspace artifact.
696
+ const [artifactId, ...artifactOp] = tail as [string | undefined, ...string[]];
697
+ const route = `${method} ${artifactOp.join("/")}`;
698
+ if (
699
+ !filesEnabled ||
700
+ !artifactId ||
701
+ !SEGMENT.test(artifactId) ||
702
+ (route !== "GET content" && route !== "POST playback-source")
703
+ ) {
704
+ return errorJson(404, "route_not_allowed", "Not found.");
705
+ }
706
+ const sessionId = request.headers.get(SESSION_SCOPE_HEADER)?.trim() ?? "";
707
+ if (!SEGMENT.test(sessionId)) {
708
+ reject(400, "session_scope_required", "Artifact reads must name their session.");
709
+ }
710
+ if (options.authorizeSession && !(await options.authorizeSession(sessionId, context))) {
711
+ return errorJson(404, "session_not_found", "Session not found.");
712
+ }
713
+ await getSessionProxyArtifactAssociation(
714
+ client,
715
+ workspaceId,
716
+ sessionId,
717
+ "retained",
718
+ artifactId,
719
+ call,
720
+ );
721
+ if (route === "POST playback-source") {
722
+ return json(
723
+ await client.requestJson(
724
+ "POST",
725
+ `${base}/artifacts/${artifactId}/playback-source`,
726
+ undefined,
727
+ {},
728
+ call,
729
+ ),
730
+ );
731
+ }
732
+ return retainedContent(
733
+ await client.getRetainedArtifactContent(workspaceId, artifactId, {
734
+ ...retainedRange(request),
735
+ signal: request.signal,
736
+ }),
737
+ );
738
+ }
739
+
679
740
  if (area === "transcriptions" && tail.length === 0 && method === "POST") {
680
741
  if (!voiceInputEnabled) return errorJson(404, "route_not_allowed", "Not found.");
681
742
  return json(await client.transcribeAudio(workspaceId, await transcriptionInput(request)));
@@ -899,6 +960,28 @@ export function createSessionProxyHandler(
899
960
  }
900
961
  case "GET human-input-requests":
901
962
  return await read(`${session}/human-input-requests`);
963
+ case "GET goal":
964
+ return await read(`${session}/goal`);
965
+ case "PATCH goal": {
966
+ // Pause and resume only: the objective, limits and completion stay
967
+ // with the agent and the product's own server. A browser rationale is
968
+ // dropped, so end-user text never reaches the goal record.
969
+ const status = body?.status;
970
+ if (
971
+ (status !== "paused" && status !== "active") ||
972
+ Object.keys(body ?? {}).some((key) => key !== "status" && key !== "rationale")
973
+ ) {
974
+ reject(
975
+ 403,
976
+ "goal_update_not_allowed",
977
+ "Only { status: paused | active } is available.",
978
+ );
979
+ }
980
+ return json(await client.requestJson("PATCH", `${session}/goal`, { status }));
981
+ }
982
+ case "DELETE goal":
983
+ await client.deleteGoal(workspaceId, sessionId);
984
+ return new Response(null, { status: 204 });
902
985
  case "POST fs/read":
903
986
  case "POST fs/read-workspace": {
904
987
  if (!sandboxFilesEnabled) return errorJson(404, "route_not_allowed", "Not found.");
@@ -909,6 +992,21 @@ export function createSessionProxyHandler(
909
992
  );
910
993
  }
911
994
  }
995
+ if (op[0] === "artifacts" && (op.length === 2 || op[2] === "content") && method === "GET") {
996
+ // Retained screenshots: the API matches the artifact to this session;
997
+ // the proxy first proves the user can read the session itself.
998
+ if (!filesEnabled || op.length > 3) {
999
+ return errorJson(404, "route_not_allowed", "Not found.");
1000
+ }
1001
+ await client.getSession(workspaceId, sessionId, call);
1002
+ if (op.length === 2) return await read(`${session}/artifacts/${op[1]}`);
1003
+ return retainedContent(
1004
+ await client.getSessionRetainedArtifactContent(workspaceId, sessionId, op[1]!, {
1005
+ ...retainedRange(request),
1006
+ signal: request.signal,
1007
+ }),
1008
+ );
1009
+ }
912
1010
  if (op.length === 2 && op[0] === "human-input-requests" && method === "GET") {
913
1011
  return await read(`${session}/human-input-requests/${op[1]}`);
914
1012
  }
@@ -927,6 +1025,12 @@ export function createSessionProxyHandler(
927
1025
  };
928
1026
  }
929
1027
 
1028
+ /**
1029
+ * Methods any forwarded route uses. Module-level so the public API inventory
1030
+ * attributes a verb only to the routes that actually forward it.
1031
+ */
1032
+ const PROXY_METHODS: readonly string[] = ["GET", "POST", "PUT", "PATCH", "DELETE"];
1033
+
930
1034
  const UPLOAD_FIELDS = ["scope", "filename", "contentType", "sizeBytes", "sha256"] as const;
931
1035
 
932
1036
  type NormalizedToolServer = SessionProxyToolServer & {
@@ -1188,6 +1292,35 @@ async function legacyViewerGrant(
1188
1292
  return grant;
1189
1293
  }
1190
1294
 
1295
+ /** The browser's byte range, passed through unchanged when well formed. */
1296
+ function retainedRange(request: Request): { range?: string } {
1297
+ const range = request.headers.get("range");
1298
+ if (range === null) return {};
1299
+ if (range.length > 128 || /[^\x20-\x7e]/.test(range)) {
1300
+ reject(400, "invalid_range", "Range must be at most 128 printable ASCII bytes.");
1301
+ }
1302
+ return { range };
1303
+ }
1304
+
1305
+ /** One bounded retained-artifact page, with the range facts the browser SDK verifies. */
1306
+ function retainedContent(content: RetainedArtifactContent): Response {
1307
+ return new Response(content.bytes as Uint8Array<ArrayBuffer>, {
1308
+ status: content.status,
1309
+ headers: {
1310
+ "Content-Type": content.contentType,
1311
+ "Content-Length": String(content.contentLength),
1312
+ ...(content.contentRange ? { "Content-Range": content.contentRange } : {}),
1313
+ "Accept-Ranges": "bytes",
1314
+ "Cache-Control": "private, no-store",
1315
+ // Bytes for the conversation's own reader; never rendered on this origin.
1316
+ "Content-Security-Policy": "sandbox",
1317
+ "Content-Disposition": "attachment",
1318
+ "Cross-Origin-Resource-Policy": "same-origin",
1319
+ "X-Content-Type-Options": "nosniff",
1320
+ },
1321
+ });
1322
+ }
1323
+
1191
1324
  /** Sandbox link download: one path, no route/target override. */
1192
1325
  function sandboxRead(body: Record<string, unknown> | undefined): Record<string, unknown> {
1193
1326
  const path = body?.path;
@@ -106,6 +106,9 @@ export type TranscriptionLifecycleStatus =
106
106
  | "error";
107
107
 
108
108
  export type TranscriptionErrorCode =
109
+ | "insufficient_credits"
110
+ | "allowance_exhausted"
111
+ | "monthly_model_cost_limit"
109
112
  | "permission_denied"
110
113
  | "not_supported"
111
114
  | "network"
package/src/types.ts CHANGED
@@ -83,7 +83,7 @@ export type CodexRealtimeWebrtcRequest = {
83
83
  export type CodexRealtimeWebrtcResponse = {
84
84
  sdp: string;
85
85
  version: CodexRealtimeWebrtcVersion;
86
- model: "gpt-live-1-boulder-alpha";
86
+ model: "gpt-live-1-boulder-alpha" | "opengeni-azure/gpt-live-1";
87
87
  connectionId: string;
88
88
  connectionEpoch: number;
89
89
  startupFenceSequence: number;
@@ -271,6 +271,7 @@ export type SyncSessionRealtimeLedgerResponse = {
271
271
  };
272
272
 
273
273
  export type SessionRealtimeModel =
274
+ | "opengeni-azure/gpt-live-1"
274
275
  | "gpt-live-1-boulder-alpha"
275
276
  | "supergrok/grok-voice-think-fast-2.0"
276
277
  | "opengeni-gateway/openai/gpt-realtime-2.1"
@@ -287,6 +288,8 @@ export type WorkspaceRealtimeModelCatalogItem = {
287
288
  description: string;
288
289
  available: boolean;
289
290
  unavailableReason: string | null;
291
+ /** Machine-readable reason when unavailable, e.g. `insufficient_credits`. */
292
+ unavailableCode?: string | null | undefined;
290
293
  recommended: boolean;
291
294
  };
292
295
 
@@ -333,9 +336,17 @@ export type EndSessionRealtimeRequest = RenewSessionRealtimeRequest & {
333
336
  reason: Extract<SessionRealtimeEndReason, "user_stop" | "browser_unload">;
334
337
  };
335
338
 
339
+ /** Server instruction to end a live call now (for example, out of credits). */
340
+ export type SessionRealtimeStopInstruction = {
341
+ code: string;
342
+ message: string;
343
+ };
344
+
336
345
  export type SessionRealtimeMutationResponse = {
337
346
  mode: SessionRealtimeMode;
338
347
  replay: boolean;
348
+ /** Present on a heartbeat when the server stopped extending the lease. */
349
+ stop?: SessionRealtimeStopInstruction | undefined;
339
350
  };
340
351
 
341
352
  export type SessionStatus =
@@ -4297,16 +4308,30 @@ export type ClientConfig = {
4297
4308
  */
4298
4309
  workspaceId?: string | undefined;
4299
4310
  /**
4300
- * Session proxy capability for the embedded artifact viewer; absent on
4301
- * native deployments. The live socket is ticket-authenticated and reached
4302
- * directly; the cache partition identifies the proxied user.
4311
+ * Session proxy capability for the embedded artifact viewer and inline Site
4312
+ * previews; absent on native deployments. The live socket is
4313
+ * ticket-authenticated and reached directly; the cache partition identifies
4314
+ * the proxied user. `false` when the proxy does not serve artifacts, so
4315
+ * stock UIs show Site previews as unavailable instead of failing on click.
4303
4316
  */
4304
4317
  artifacts?:
4305
4318
  | {
4306
4319
  editableLiveUrl: string;
4307
4320
  cachePartition: { accountId: string; principalId: string; authorizationEpoch: string };
4308
4321
  }
4322
+ | false
4309
4323
  | undefined;
4324
+ /**
4325
+ * Session proxy only: whether the browser may start a chat (the proxy has a
4326
+ * `createSession` hook). Stock UIs hide "New chat" when `false`; absent on
4327
+ * native deployments and older proxies.
4328
+ */
4329
+ sessionCreation?: boolean | undefined;
4330
+ /**
4331
+ * Session proxy only: whether users may archive or restore their chats.
4332
+ * Stock UIs hide "Archive" when `false`; absent on native deployments.
4333
+ */
4334
+ archive?: boolean | undefined;
4310
4335
  /** Native browser microphone capture + server-side transcription capability. */
4311
4336
  voiceInput?: ClientVoiceInputConfig | undefined;
4312
4337
  /**
@@ -4382,7 +4407,10 @@ export type TranscriptionRecordingErrorCode =
4382
4407
  | "unavailable"
4383
4408
  | "too_large"
4384
4409
  | "invalid_audio"
4385
- | "unknown";
4410
+ | "unknown"
4411
+ | "insufficient_credits"
4412
+ | "allowance_exhausted"
4413
+ | "monthly_model_cost_limit";
4386
4414
 
4387
4415
  export type TranscriptionRecordingState =
4388
4416
  | "uploading"
@@ -5075,6 +5103,7 @@ export type UpdateSlackChannelRoutesRequest = {
5075
5103
  };
5076
5104
 
5077
5105
  export type VoiceInputProviderId =
5106
+ | "azure-mai"
5078
5107
  | "supergrok-subscription"
5079
5108
  | "codex-subscription"
5080
5109
  | "openai"