@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.
- package/README.md +13 -3
- package/dist/adapters/next.js +2 -2
- package/dist/artifacts.js +4 -4
- package/dist/azure-live-transport.d.ts +32 -0
- package/dist/browser.js +3 -3
- package/dist/chat/index.js +5 -5
- package/dist/{chunk-RSKO3CMZ.js → chunk-BVIIRSJY.js} +2 -2
- package/dist/{chunk-G5NZGPTB.js → chunk-CA2T7QT4.js} +2 -2
- package/dist/{chunk-ZBICMU4Z.js → chunk-D534PIXF.js} +2 -2
- package/dist/{chunk-CNSPJYF6.js → chunk-FYQAMXDW.js} +104 -5
- package/dist/chunk-FYQAMXDW.js.map +1 -0
- package/dist/{chunk-IB6SUE3R.js → chunk-GYM54GKM.js} +1 -1
- package/dist/chunk-GYM54GKM.js.map +1 -0
- package/dist/{chunk-VYCTL62C.js → chunk-GZQSWAKD.js} +1 -1
- package/dist/chunk-GZQSWAKD.js.map +1 -0
- package/dist/{chunk-CREM7YRK.js → chunk-LEBDXYMA.js} +2 -2
- package/dist/{chunk-YVH2PH4X.js → chunk-MXV5O25O.js} +2 -2
- package/dist/{chunk-2UXZUCEZ.js → chunk-ZNC44QEX.js} +77 -7
- package/dist/chunk-ZNC44QEX.js.map +1 -0
- package/dist/codex-realtime-controller.d.ts +13 -0
- package/dist/codex-realtime-controller.js +3 -1
- package/dist/codex-realtime-v3-wire.d.ts +3 -2
- package/dist/codex-realtime.d.ts +4 -0
- package/dist/core.js +4 -4
- package/dist/document-authority.js +3 -3
- package/dist/editable-artifacts.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +55 -18
- package/dist/index.js.map +1 -1
- package/dist/realtime.d.ts +1 -1
- package/dist/realtime.js +248 -2
- package/dist/realtime.js.map +1 -1
- package/dist/session-proxy-client.d.ts +7 -3
- package/dist/session-proxy.d.ts +9 -1
- package/dist/session-proxy.js +2 -2
- package/dist/site.js +3 -3
- package/dist/transcription.d.ts +1 -1
- package/dist/types.d.ts +30 -8
- package/package.json +3 -3
- package/src/azure-live-transport.ts +286 -0
- package/src/codex-realtime-controller.ts +95 -2
- package/src/codex-realtime-v3-wire.ts +4 -3
- package/src/codex-realtime-v3.ts +11 -2
- package/src/codex-realtime.ts +9 -1
- package/src/index.ts +3 -0
- package/src/realtime.ts +15 -6
- package/src/session-proxy-client.ts +7 -3
- package/src/session-proxy.ts +137 -4
- package/src/transcription.ts +3 -0
- package/src/types.ts +34 -5
- package/dist/chunk-2UXZUCEZ.js.map +0 -1
- package/dist/chunk-CNSPJYF6.js.map +0 -1
- package/dist/chunk-IB6SUE3R.js.map +0 -1
- package/dist/chunk-VYCTL62C.js.map +0 -1
- /package/dist/{chunk-RSKO3CMZ.js.map → chunk-BVIIRSJY.js.map} +0 -0
- /package/dist/{chunk-G5NZGPTB.js.map → chunk-CA2T7QT4.js.map} +0 -0
- /package/dist/{chunk-ZBICMU4Z.js.map → chunk-D534PIXF.js.map} +0 -0
- /package/dist/{chunk-CREM7YRK.js.map → chunk-LEBDXYMA.js.map} +0 -0
- /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(
|
package/src/codex-realtime-v3.ts
CHANGED
|
@@ -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 } : {}),
|
package/src/codex-realtime.ts
CHANGED
|
@@ -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 (
|
|
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 === "
|
|
78
|
-
? {
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
/**
|
|
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)}`,
|
package/src/session-proxy.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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 (!
|
|
390
|
+
if (!PROXY_METHODS.includes(method)) {
|
|
382
391
|
return new Response(null, {
|
|
383
392
|
status: 405,
|
|
384
|
-
headers: { Allow: "
|
|
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
|
-
|
|
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;
|
package/src/transcription.ts
CHANGED
|
@@ -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
|
|
4301
|
-
* native deployments. The live socket is
|
|
4302
|
-
* directly; the cache partition identifies
|
|
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"
|