@opengeni/react 0.52.1 → 1.0.1
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 +51 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +43 -14
- package/dist/artifacts.js.map +1 -1
- package/dist/{chunk-ROIRTM37.js → chunk-5UQPNLIO.js} +6 -6
- package/dist/{chunk-JR3QFY6B.js → chunk-6KLX2RH5.js} +49 -2
- package/dist/chunk-6KLX2RH5.js.map +1 -0
- package/dist/{chunk-FWS3KC3Z.js → chunk-B6WPZSTT.js} +104 -104
- package/dist/{chunk-FWS3KC3Z.js.map → chunk-B6WPZSTT.js.map} +1 -1
- package/dist/{chunk-HDBG7XGO.js → chunk-C5ZPSSS5.js} +50 -1
- package/dist/chunk-C5ZPSSS5.js.map +1 -0
- package/dist/{chunk-X4S56Y2H.js → chunk-HA46CYQK.js} +135 -11
- package/dist/chunk-HA46CYQK.js.map +1 -0
- package/dist/{chunk-724BW4TW.js → chunk-IGC6A2ZI.js} +2 -2
- package/dist/chunk-LLOLUHVG.js +5937 -0
- package/dist/chunk-LLOLUHVG.js.map +1 -0
- package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
- package/dist/chunk-MKPIAGVL.js.map +1 -0
- package/dist/chunk-R3AZOAB5.js +933 -0
- package/dist/chunk-R3AZOAB5.js.map +1 -0
- package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
- package/dist/chunk-XBZWOM2K.js.map +1 -0
- package/dist/{chunk-JAGDUCZQ.js → chunk-Y7OEDUJ5.js} +17 -13
- package/dist/chunk-Y7OEDUJ5.js.map +1 -0
- package/dist/client.d.ts +6 -4
- package/dist/components/artifacts/index.d.ts +1 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
- package/dist/components/browser-input.d.ts +7 -0
- package/dist/components/browser-viewer.d.ts +5 -4
- package/dist/components/computer-viewer.d.ts +2 -1
- package/dist/components/desktop-viewer.d.ts +26 -2
- package/dist/components/interaction-intervention-banner.d.ts +11 -0
- package/dist/components/sandbox-terminal.d.ts +4 -1
- package/dist/components/sandbox-workspace.d.ts +3 -0
- package/dist/composer.js +6 -6
- package/dist/hooks/use-auth-runs.d.ts +32 -0
- package/dist/hooks/use-browser-downloads.d.ts +25 -0
- package/dist/hooks/use-browser-identities.d.ts +5 -1
- package/dist/hooks/use-browser-session.d.ts +2 -1
- package/dist/hooks/use-browser-sessions.d.ts +1 -1
- package/dist/hooks/use-channels.d.ts +26 -0
- package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
- package/dist/hooks/use-computer-session.d.ts +2 -1
- package/dist/hooks/use-computer-sessions.d.ts +1 -1
- package/dist/hooks/use-desktop-stream.d.ts +4 -0
- package/dist/hooks/use-interaction-interventions.d.ts +27 -0
- package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
- package/dist/hooks/use-network-routes.d.ts +25 -0
- package/dist/hooks/use-site-auth-connections.d.ts +25 -0
- package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
- package/dist/hooks/use-terminal-stream.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1220 -1545
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +13 -3
- package/dist/interaction.js +520 -6
- package/dist/interaction.js.map +1 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
- package/dist/lib/host-platform.d.ts +5 -0
- package/dist/lib/relay-wire.d.ts +20 -1
- package/dist/machines.js +3 -3
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/provider.d.ts +3 -2
- package/dist/realtime/realtime-control.d.ts +6 -2
- package/dist/realtime.js +138 -24
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +10 -2
- package/dist/session-ui.js +3 -3
- package/dist/session.js +11 -11
- package/dist/timeline/types.d.ts +5 -0
- package/package.json +2 -2
- package/src/artifacts.ts +3 -0
- package/src/client.ts +73 -3
- package/src/components/artifacts/index.ts +5 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
- package/src/components/browser-input.ts +65 -0
- package/src/components/browser-viewer.tsx +1287 -177
- package/src/components/computer-viewer.tsx +611 -114
- package/src/components/desktop-viewer.tsx +114 -1
- package/src/components/interaction-intervention-banner.tsx +99 -0
- package/src/components/machines/machine-detail.tsx +1 -1
- package/src/components/model-policy-picker.tsx +15 -0
- package/src/components/sandbox-terminal.tsx +24 -7
- package/src/components/sandbox-workspace.tsx +42 -4
- package/src/components/workspace-dock.tsx +59 -5
- package/src/hooks/use-attached-browsers.ts +13 -11
- package/src/hooks/use-auth-runs.ts +278 -0
- package/src/hooks/use-browser-downloads.ts +218 -0
- package/src/hooks/use-browser-frame-stream.ts +168 -59
- package/src/hooks/use-browser-identities.ts +47 -6
- package/src/hooks/use-browser-session.ts +94 -27
- package/src/hooks/use-browser-sessions.ts +19 -14
- package/src/hooks/use-channels.ts +99 -0
- package/src/hooks/use-computer-frame-stream.ts +184 -61
- package/src/hooks/use-computer-session.ts +22 -2
- package/src/hooks/use-computer-sessions.ts +19 -13
- package/src/hooks/use-desktop-stream.ts +39 -7
- package/src/hooks/use-interaction-interventions.ts +242 -0
- package/src/hooks/use-interaction-invalidation.ts +66 -0
- package/src/hooks/use-machine-chip.ts +13 -3
- package/src/hooks/use-network-routes.ts +214 -0
- package/src/hooks/use-site-auth-connections.ts +226 -0
- package/src/hooks/use-supergrok-accounts.ts +81 -0
- package/src/hooks/use-terminal-stream.ts +288 -55
- package/src/index.ts +8 -0
- package/src/interaction.ts +30 -0
- package/src/lib/host-platform.ts +10 -0
- package/src/lib/relay-wire.ts +43 -4
- package/src/lib/terminal-capability.ts +1 -1
- package/src/model-policy.ts +19 -4
- package/src/provider.tsx +133 -1
- package/src/realtime/realtime-control.tsx +181 -26
- package/src/session-context.ts +66 -1
- package/src/timeline/activity-rail.tsx +6 -0
- package/src/timeline/projection.ts +58 -0
- package/src/timeline/types.ts +2 -0
- package/styles/compiled.css +64 -6
- package/dist/chunk-23EJ676W.js.map +0 -1
- package/dist/chunk-6BBWWOOU.js +0 -82
- package/dist/chunk-6BBWWOOU.js.map +0 -1
- package/dist/chunk-7I2KB3OK.js +0 -3895
- package/dist/chunk-7I2KB3OK.js.map +0 -1
- package/dist/chunk-HDBG7XGO.js.map +0 -1
- package/dist/chunk-JAGDUCZQ.js.map +0 -1
- package/dist/chunk-JR3QFY6B.js.map +0 -1
- package/dist/chunk-WYA65Y3F.js.map +0 -1
- package/dist/chunk-X4S56Y2H.js.map +0 -1
- package/dist/interactive-workbench-viewers-KHNOHWQE.js +0 -13
- /package/dist/{chunk-ROIRTM37.js.map → chunk-5UQPNLIO.js.map} +0 -0
- /package/dist/{chunk-724BW4TW.js.map → chunk-IGC6A2ZI.js.map} +0 -0
- /package/dist/{interactive-workbench-viewers-KHNOHWQE.js.map → interactive-workbench-viewers-TOHBXJEE.js.map} +0 -0
package/src/model-policy.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ClientModel, ReasoningEffort, WorkspaceModelCatalogModel } from "@opengeni/sdk";
|
|
2
2
|
|
|
3
|
-
export type PickerBillingClass =
|
|
3
|
+
export type PickerBillingClass =
|
|
4
|
+
| "opengeni_credits"
|
|
5
|
+
| "codex_subscription"
|
|
6
|
+
| "supergrok_subscription"
|
|
7
|
+
| "byok";
|
|
4
8
|
|
|
5
9
|
export type PickerModelRow<TCatalog extends ClientModel = WorkspaceModelCatalogModel> = {
|
|
6
10
|
id: string;
|
|
@@ -21,12 +25,14 @@ export type LatencyModeId = "standard" | "priority" | "fast";
|
|
|
21
25
|
const BILLING_CLASS_ORDER: PickerBillingClass[] = [
|
|
22
26
|
"opengeni_credits",
|
|
23
27
|
"codex_subscription",
|
|
28
|
+
"supergrok_subscription",
|
|
24
29
|
"byok",
|
|
25
30
|
];
|
|
26
31
|
|
|
27
32
|
const BILLING_CLASS_LABELS: Record<PickerBillingClass, string> = {
|
|
28
33
|
opengeni_credits: "OpenGeni",
|
|
29
34
|
codex_subscription: "Codex",
|
|
35
|
+
supergrok_subscription: "SuperGrok",
|
|
30
36
|
byok: "Your Gateway",
|
|
31
37
|
};
|
|
32
38
|
|
|
@@ -41,6 +47,9 @@ const AVAILABILITY_REASON_LABELS: Record<string, string> = {
|
|
|
41
47
|
};
|
|
42
48
|
|
|
43
49
|
export function billingClassForModel(model: ClientModel): PickerBillingClass {
|
|
50
|
+
if (model.source === "supergrok") {
|
|
51
|
+
return "supergrok_subscription";
|
|
52
|
+
}
|
|
44
53
|
if (model.source === "codex") {
|
|
45
54
|
return "codex_subscription";
|
|
46
55
|
}
|
|
@@ -48,7 +57,9 @@ export function billingClassForModel(model: ClientModel): PickerBillingClass {
|
|
|
48
57
|
return "byok";
|
|
49
58
|
}
|
|
50
59
|
if (model.credentialSource?.kind === "connected_subscription") {
|
|
51
|
-
return "
|
|
60
|
+
return model.credentialSource.provider === "xai"
|
|
61
|
+
? "supergrok_subscription"
|
|
62
|
+
: "codex_subscription";
|
|
52
63
|
}
|
|
53
64
|
if (model.credentialSource?.kind === "workspace_connection") {
|
|
54
65
|
return "byok";
|
|
@@ -135,7 +146,9 @@ export function payerSummaryForModel(model: ClientModel): string {
|
|
|
135
146
|
return "OpenGeni credits · automatic managed route";
|
|
136
147
|
}
|
|
137
148
|
if (billing.upstreamPayer === "connected_subscription") {
|
|
138
|
-
return
|
|
149
|
+
return model.source === "supergrok"
|
|
150
|
+
? "SuperGrok subscription · external billing"
|
|
151
|
+
: "Codex subscription · external billing";
|
|
139
152
|
}
|
|
140
153
|
if (billing.upstreamPayer === "workspace") {
|
|
141
154
|
return "Billed to your AI Gateway";
|
|
@@ -149,7 +162,9 @@ export function advancedSourceSummary(model: ClientModel): string | null {
|
|
|
149
162
|
return null;
|
|
150
163
|
}
|
|
151
164
|
if (source.kind === "connected_subscription") {
|
|
152
|
-
return
|
|
165
|
+
return source.provider === "xai"
|
|
166
|
+
? "Connected SuperGrok subscription"
|
|
167
|
+
: "Connected Codex subscription";
|
|
153
168
|
}
|
|
154
169
|
if (source.kind === "workspace_connection") {
|
|
155
170
|
return "Workspace AI Gateway";
|
package/src/provider.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
OPENGENI_API_CONTRACT_REVISION,
|
|
4
4
|
type StreamConnectionState,
|
|
5
5
|
type WorkspaceControlEvent,
|
|
6
|
+
type WorkspaceInteractionRevisionEvent,
|
|
6
7
|
} from "@opengeni/sdk";
|
|
7
8
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
8
9
|
import type { SessionClientLike } from "./client";
|
|
@@ -17,6 +18,7 @@ export type OpenGeniProviderProps = {
|
|
|
17
18
|
client: SessionClientLike;
|
|
18
19
|
workspaceId: string;
|
|
19
20
|
onWorkspaceControlEvent?: ((event: WorkspaceControlEvent) => void) | undefined;
|
|
21
|
+
onWorkspaceInteractionEvent?: ((event: WorkspaceInteractionRevisionEvent) => void) | undefined;
|
|
20
22
|
children?: ReactNode;
|
|
21
23
|
};
|
|
22
24
|
|
|
@@ -28,6 +30,7 @@ export function OpenGeniProvider({
|
|
|
28
30
|
client,
|
|
29
31
|
workspaceId,
|
|
30
32
|
onWorkspaceControlEvent,
|
|
33
|
+
onWorkspaceInteractionEvent,
|
|
31
34
|
children,
|
|
32
35
|
}: OpenGeniProviderProps) {
|
|
33
36
|
const [workspaceControlEvent, setWorkspaceControlEvent] = useState<WorkspaceControlEvent | null>(
|
|
@@ -36,15 +39,25 @@ export function OpenGeniProvider({
|
|
|
36
39
|
const [workspaceControlConnectionState, setWorkspaceControlConnectionState] = useState<
|
|
37
40
|
StreamConnectionState | "idle" | "error"
|
|
38
41
|
>("idle");
|
|
42
|
+
const [workspaceInteractionEvent, setWorkspaceInteractionEvent] =
|
|
43
|
+
useState<WorkspaceInteractionRevisionEvent | null>(null);
|
|
44
|
+
const [workspaceInteractionConnectionState, setWorkspaceInteractionConnectionState] = useState<
|
|
45
|
+
StreamConnectionState | "idle" | "error"
|
|
46
|
+
>("idle");
|
|
39
47
|
const [contractMismatch, setContractMismatch] = useState<OpenGeniApiContractMismatchError | null>(
|
|
40
48
|
null,
|
|
41
49
|
);
|
|
42
50
|
const callbackRef = useRef(onWorkspaceControlEvent);
|
|
51
|
+
const interactionCallbackRef = useRef(onWorkspaceInteractionEvent);
|
|
43
52
|
const reconcilersRef = useRef(new Map<string, Map<string, () => Promise<void>>>());
|
|
44
53
|
const reconcileInFlightRef = useRef(new Map<string, Promise<void>>());
|
|
45
54
|
const workspaceControlSequencesRef = useRef(new Map<string, number>());
|
|
55
|
+
const workspaceInteractionSequencesRef = useRef(new Map<string, number>());
|
|
46
56
|
const pageLive = usePageLiveActivity();
|
|
57
|
+
const supportsWorkspaceLiveStream =
|
|
58
|
+
"streamWorkspaceLiveEvents" in client && typeof client.streamWorkspaceLiveEvents === "function";
|
|
47
59
|
callbackRef.current = onWorkspaceControlEvent;
|
|
60
|
+
interactionCallbackRef.current = onWorkspaceInteractionEvent;
|
|
48
61
|
|
|
49
62
|
const verifyApiContract = useCallback(async (): Promise<void> => {
|
|
50
63
|
try {
|
|
@@ -105,6 +118,78 @@ export function OpenGeniProvider({
|
|
|
105
118
|
);
|
|
106
119
|
|
|
107
120
|
useEffect(() => {
|
|
121
|
+
if (!supportsWorkspaceLiveStream) return;
|
|
122
|
+
const openLiveStream = client.streamWorkspaceLiveEvents!;
|
|
123
|
+
setWorkspaceControlEvent(null);
|
|
124
|
+
setWorkspaceInteractionEvent(null);
|
|
125
|
+
if (!pageLive) {
|
|
126
|
+
setWorkspaceControlConnectionState("idle");
|
|
127
|
+
setWorkspaceInteractionConnectionState("idle");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const setConnectionState = (state: StreamConnectionState) => {
|
|
132
|
+
setWorkspaceControlConnectionState(state);
|
|
133
|
+
setWorkspaceInteractionConnectionState(state);
|
|
134
|
+
};
|
|
135
|
+
setConnectionState("connecting");
|
|
136
|
+
void (async () => {
|
|
137
|
+
try {
|
|
138
|
+
await verifyApiContract();
|
|
139
|
+
const workspace = await client.getWorkspace(workspaceId);
|
|
140
|
+
const controlAfter = Math.max(
|
|
141
|
+
workspaceControlSequencesRef.current.get(workspaceId) ?? 0,
|
|
142
|
+
workspace.inferenceControl.revision,
|
|
143
|
+
);
|
|
144
|
+
const interactionAfter = workspaceInteractionSequencesRef.current.get(workspaceId) ?? 0;
|
|
145
|
+
workspaceControlSequencesRef.current.set(workspaceId, controlAfter);
|
|
146
|
+
const stream = openLiveStream.call(client, workspaceId, {
|
|
147
|
+
controlAfter,
|
|
148
|
+
interactionAfter,
|
|
149
|
+
signal: controller.signal,
|
|
150
|
+
onStateChange: setConnectionState,
|
|
151
|
+
});
|
|
152
|
+
for await (const event of stream) {
|
|
153
|
+
if (controller.signal.aborted) return;
|
|
154
|
+
if (event.type === "workspace.control.changed") {
|
|
155
|
+
workspaceControlSequencesRef.current.set(
|
|
156
|
+
workspaceId,
|
|
157
|
+
Math.max(workspaceControlSequencesRef.current.get(workspaceId) ?? 0, event.sequence),
|
|
158
|
+
);
|
|
159
|
+
setWorkspaceControlEvent((current) =>
|
|
160
|
+
!current || event.sequence > current.sequence ? event : current,
|
|
161
|
+
);
|
|
162
|
+
callbackRef.current?.(event);
|
|
163
|
+
} else {
|
|
164
|
+
workspaceInteractionSequencesRef.current.set(
|
|
165
|
+
workspaceId,
|
|
166
|
+
Math.max(
|
|
167
|
+
workspaceInteractionSequencesRef.current.get(workspaceId) ?? 0,
|
|
168
|
+
event.sequence,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
setWorkspaceInteractionEvent((current) =>
|
|
172
|
+
!current || event.sequence > current.sequence ? event : current,
|
|
173
|
+
);
|
|
174
|
+
interactionCallbackRef.current?.(event);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error instanceof OpenGeniApiContractMismatchError) {
|
|
179
|
+
setContractMismatch(error);
|
|
180
|
+
reloadForContractMismatchOnce(error);
|
|
181
|
+
}
|
|
182
|
+
if (!controller.signal.aborted) {
|
|
183
|
+
setWorkspaceControlConnectionState("error");
|
|
184
|
+
setWorkspaceInteractionConnectionState("error");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
})();
|
|
188
|
+
return () => controller.abort();
|
|
189
|
+
}, [client, pageLive, supportsWorkspaceLiveStream, verifyApiContract, workspaceId]);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (supportsWorkspaceLiveStream) return;
|
|
108
193
|
if (!pageLive) {
|
|
109
194
|
setWorkspaceControlConnectionState("idle");
|
|
110
195
|
return;
|
|
@@ -145,7 +230,50 @@ export function OpenGeniProvider({
|
|
|
145
230
|
}
|
|
146
231
|
})();
|
|
147
232
|
return () => controller.abort();
|
|
148
|
-
}, [client, pageLive, verifyApiContract, workspaceId]);
|
|
233
|
+
}, [client, pageLive, supportsWorkspaceLiveStream, verifyApiContract, workspaceId]);
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (supportsWorkspaceLiveStream) return;
|
|
237
|
+
setWorkspaceInteractionEvent(null);
|
|
238
|
+
if (!pageLive) {
|
|
239
|
+
setWorkspaceInteractionConnectionState("idle");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const controller = new AbortController();
|
|
243
|
+
setWorkspaceInteractionConnectionState("connecting");
|
|
244
|
+
void (async () => {
|
|
245
|
+
try {
|
|
246
|
+
await verifyApiContract();
|
|
247
|
+
const resumeAfter = workspaceInteractionSequencesRef.current.get(workspaceId) ?? 0;
|
|
248
|
+
const stream = client.streamWorkspaceInteractionRevisions(workspaceId, {
|
|
249
|
+
after: resumeAfter,
|
|
250
|
+
signal: controller.signal,
|
|
251
|
+
onStateChange: setWorkspaceInteractionConnectionState,
|
|
252
|
+
});
|
|
253
|
+
for await (const event of stream) {
|
|
254
|
+
if (controller.signal.aborted) return;
|
|
255
|
+
workspaceInteractionSequencesRef.current.set(
|
|
256
|
+
workspaceId,
|
|
257
|
+
Math.max(
|
|
258
|
+
workspaceInteractionSequencesRef.current.get(workspaceId) ?? 0,
|
|
259
|
+
event.sequence,
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
setWorkspaceInteractionEvent((current) =>
|
|
263
|
+
!current || event.sequence > current.sequence ? event : current,
|
|
264
|
+
);
|
|
265
|
+
interactionCallbackRef.current?.(event);
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof OpenGeniApiContractMismatchError) {
|
|
269
|
+
setContractMismatch(error);
|
|
270
|
+
reloadForContractMismatchOnce(error);
|
|
271
|
+
}
|
|
272
|
+
if (!controller.signal.aborted) setWorkspaceInteractionConnectionState("error");
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
return () => controller.abort();
|
|
276
|
+
}, [client, pageLive, supportsWorkspaceLiveStream, verifyApiContract, workspaceId]);
|
|
149
277
|
|
|
150
278
|
const value = useMemo(
|
|
151
279
|
() => ({
|
|
@@ -153,6 +281,8 @@ export function OpenGeniProvider({
|
|
|
153
281
|
workspaceId,
|
|
154
282
|
workspaceControlEvent,
|
|
155
283
|
workspaceControlConnectionState,
|
|
284
|
+
workspaceInteractionEvent,
|
|
285
|
+
workspaceInteractionConnectionState,
|
|
156
286
|
registerSessionReconciler,
|
|
157
287
|
reconcileSession,
|
|
158
288
|
}),
|
|
@@ -161,6 +291,8 @@ export function OpenGeniProvider({
|
|
|
161
291
|
workspaceId,
|
|
162
292
|
workspaceControlEvent,
|
|
163
293
|
workspaceControlConnectionState,
|
|
294
|
+
workspaceInteractionEvent,
|
|
295
|
+
workspaceInteractionConnectionState,
|
|
164
296
|
registerSessionReconciler,
|
|
165
297
|
reconcileSession,
|
|
166
298
|
],
|
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
export type RealtimeModelOption = {
|
|
58
58
|
id: SessionRealtimeModel;
|
|
59
59
|
label: string;
|
|
60
|
-
provider: "OpenGeni" | "Connected Codex" | "Your Gateway";
|
|
60
|
+
provider: "OpenGeni" | "Connected Codex" | "Connected SuperGrok" | "Your Gateway";
|
|
61
61
|
description: string;
|
|
62
62
|
available: boolean;
|
|
63
63
|
unavailableReason: string | null;
|
|
@@ -74,7 +74,12 @@ const CODEX_LIVE_MODEL: RealtimeModelOption = {
|
|
|
74
74
|
recommended: false,
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
const REALTIME_MODEL_PROVIDERS = [
|
|
77
|
+
const REALTIME_MODEL_PROVIDERS = [
|
|
78
|
+
"OpenGeni",
|
|
79
|
+
"Connected Codex",
|
|
80
|
+
"Connected SuperGrok",
|
|
81
|
+
"Your Gateway",
|
|
82
|
+
] as const;
|
|
78
83
|
type RealtimeModelProvider = (typeof REALTIME_MODEL_PROVIDERS)[number];
|
|
79
84
|
|
|
80
85
|
const REALTIME_PROVIDER_META: Record<
|
|
@@ -86,12 +91,115 @@ const REALTIME_PROVIDER_META: Record<
|
|
|
86
91
|
billingClass: "codex_subscription",
|
|
87
92
|
hint: "ChatGPT / Codex plan",
|
|
88
93
|
},
|
|
94
|
+
"Connected SuperGrok": {
|
|
95
|
+
billingClass: "supergrok_subscription",
|
|
96
|
+
hint: "SuperGrok / xAI plan",
|
|
97
|
+
},
|
|
89
98
|
"Your Gateway": { billingClass: "byok", hint: "Billed to your AI Gateway" },
|
|
90
99
|
};
|
|
91
100
|
const REALTIME_MODEL_STORAGE_PREFIX = "opengeni:realtime-model";
|
|
101
|
+
const REALTIME_MODEL_CATALOG_CACHE_TTL_MS = 60_000;
|
|
102
|
+
const REALTIME_MODEL_CATALOG_CACHE_MAX_WORKSPACES = 64;
|
|
92
103
|
|
|
93
104
|
type RealtimeControllerClient = EmbeddedRealtimeSessionClientLike & SessionRealtimeClientLike;
|
|
94
105
|
|
|
106
|
+
type RealtimeModelCatalogCacheEntry =
|
|
107
|
+
| { state: "loading"; promise: Promise<RealtimeModelOption[]> }
|
|
108
|
+
| { state: "ready"; models: RealtimeModelOption[]; expiresAt: number };
|
|
109
|
+
|
|
110
|
+
// Scope advisory availability to the exact client object and workspace. The
|
|
111
|
+
// server still authorizes every realtime operation; changing principals should
|
|
112
|
+
// replace the client just as it does for the rest of the SDK state.
|
|
113
|
+
const realtimeModelCatalogCache = new WeakMap<
|
|
114
|
+
RealtimeControllerClient,
|
|
115
|
+
Map<string, RealtimeModelCatalogCacheEntry>
|
|
116
|
+
>();
|
|
117
|
+
|
|
118
|
+
function realtimeModelFallback(codexConnected: boolean): RealtimeModelOption[] {
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
...CODEX_LIVE_MODEL,
|
|
122
|
+
available: codexConnected,
|
|
123
|
+
unavailableReason: codexConnected ? null : CODEX_LIVE_MODEL.unavailableReason,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function realtimeModelCatalogCacheFor(
|
|
129
|
+
client: RealtimeControllerClient,
|
|
130
|
+
): Map<string, RealtimeModelCatalogCacheEntry> {
|
|
131
|
+
const existing = realtimeModelCatalogCache.get(client);
|
|
132
|
+
if (existing) return existing;
|
|
133
|
+
const created = new Map<string, RealtimeModelCatalogCacheEntry>();
|
|
134
|
+
realtimeModelCatalogCache.set(client, created);
|
|
135
|
+
return created;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readCachedRealtimeModelCatalog(
|
|
139
|
+
client: RealtimeControllerClient,
|
|
140
|
+
workspaceId: string,
|
|
141
|
+
now = Date.now(),
|
|
142
|
+
): RealtimeModelOption[] | null {
|
|
143
|
+
const cache = realtimeModelCatalogCache.get(client);
|
|
144
|
+
const entry = cache?.get(workspaceId);
|
|
145
|
+
if (!entry || entry.state === "loading") return null;
|
|
146
|
+
if (entry.expiresAt > now) return entry.models;
|
|
147
|
+
cache?.delete(workspaceId);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function pruneRealtimeModelCatalogCache(
|
|
152
|
+
cache: Map<string, RealtimeModelCatalogCacheEntry>,
|
|
153
|
+
now: number,
|
|
154
|
+
): void {
|
|
155
|
+
for (const [workspaceId, entry] of cache) {
|
|
156
|
+
if (entry.state === "ready" && entry.expiresAt <= now) cache.delete(workspaceId);
|
|
157
|
+
}
|
|
158
|
+
while (cache.size >= REALTIME_MODEL_CATALOG_CACHE_MAX_WORKSPACES) {
|
|
159
|
+
const oldestWorkspaceId = cache.keys().next().value;
|
|
160
|
+
if (oldestWorkspaceId === undefined) return;
|
|
161
|
+
cache.delete(oldestWorkspaceId);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function loadRealtimeModelCatalog(
|
|
166
|
+
client: RealtimeControllerClient,
|
|
167
|
+
workspaceId: string,
|
|
168
|
+
): Promise<RealtimeModelOption[]> | null {
|
|
169
|
+
const load = client.getWorkspaceRealtimeModelCatalog;
|
|
170
|
+
if (!load) return null;
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const cache = realtimeModelCatalogCacheFor(client);
|
|
173
|
+
const entry = cache.get(workspaceId);
|
|
174
|
+
if (entry?.state === "loading") return entry.promise;
|
|
175
|
+
if (entry?.state === "ready" && entry.expiresAt > now) return Promise.resolve(entry.models);
|
|
176
|
+
if (entry) cache.delete(workspaceId);
|
|
177
|
+
pruneRealtimeModelCatalogCache(cache, now);
|
|
178
|
+
|
|
179
|
+
const promise = load
|
|
180
|
+
.call(client, workspaceId)
|
|
181
|
+
.then((response) => response.models.map(toRealtimeModelOption))
|
|
182
|
+
.then((models) => {
|
|
183
|
+
const current = cache.get(workspaceId);
|
|
184
|
+
if (current?.state === "loading" && current.promise === promise) {
|
|
185
|
+
cache.delete(workspaceId);
|
|
186
|
+
cache.set(workspaceId, {
|
|
187
|
+
state: "ready",
|
|
188
|
+
models,
|
|
189
|
+
expiresAt: Date.now() + REALTIME_MODEL_CATALOG_CACHE_TTL_MS,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return models;
|
|
193
|
+
})
|
|
194
|
+
.catch((error: unknown) => {
|
|
195
|
+
const current = cache.get(workspaceId);
|
|
196
|
+
if (current?.state === "loading" && current.promise === promise) cache.delete(workspaceId);
|
|
197
|
+
throw error;
|
|
198
|
+
});
|
|
199
|
+
cache.set(workspaceId, { state: "loading", promise });
|
|
200
|
+
return promise;
|
|
201
|
+
}
|
|
202
|
+
|
|
95
203
|
export type SessionRealtimeControllerFactory = (
|
|
96
204
|
options: CreateSessionRealtimeControllerOptions,
|
|
97
205
|
) => SessionRealtimeController;
|
|
@@ -129,6 +237,8 @@ export function useSessionRealtime(options: {
|
|
|
129
237
|
model?: SessionRealtimeModel | undefined;
|
|
130
238
|
modelAvailable?: boolean | undefined;
|
|
131
239
|
modelUnavailableReason?: string | null | undefined;
|
|
240
|
+
/** Model-visible application context captured with each durable realtime message. */
|
|
241
|
+
getModelContext?: (() => string | undefined) | undefined;
|
|
132
242
|
/** Deterministic browser-test/demo seam. Production hosts should use the SDK default. */
|
|
133
243
|
controllerFactory?: SessionRealtimeControllerFactory | undefined;
|
|
134
244
|
}) {
|
|
@@ -140,8 +250,14 @@ export function useSessionRealtime(options: {
|
|
|
140
250
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
141
251
|
const controllerRef = useRef<SessionRealtimeController | null>(null);
|
|
142
252
|
const controllerModelRef = useRef<SessionRealtimeModel | null>(null);
|
|
253
|
+
const modelContextProviderRef = useRef(options.getModelContext);
|
|
254
|
+
modelContextProviderRef.current = options.getModelContext;
|
|
143
255
|
const [snapshot, setSnapshot] = useState<SessionRealtimeControllerSnapshot>(() => ({
|
|
144
|
-
status: hasStoredSessionRealtimeOwnerProof({
|
|
256
|
+
status: hasStoredSessionRealtimeOwnerProof({
|
|
257
|
+
workspaceId,
|
|
258
|
+
sessionId: options.sessionId,
|
|
259
|
+
model,
|
|
260
|
+
})
|
|
145
261
|
? "recovering"
|
|
146
262
|
: "idle",
|
|
147
263
|
realtimeId: null,
|
|
@@ -180,6 +296,7 @@ export function useSessionRealtime(options: {
|
|
|
180
296
|
sessionId: options.sessionId,
|
|
181
297
|
...(audioRef.current ? { remoteAudio: audioRef.current } : {}),
|
|
182
298
|
model,
|
|
299
|
+
getModelContext: () => modelContextProviderRef.current?.(),
|
|
183
300
|
});
|
|
184
301
|
controllerRef.current = controller;
|
|
185
302
|
controllerModelRef.current = model;
|
|
@@ -293,6 +410,8 @@ export function SessionRealtimeControl(props: {
|
|
|
293
410
|
events: SessionEvent[];
|
|
294
411
|
eventsReady: boolean;
|
|
295
412
|
codexConnected: boolean;
|
|
413
|
+
/** Model-visible application context captured with each durable realtime message. */
|
|
414
|
+
getModelContext?: (() => string | undefined) | undefined;
|
|
296
415
|
realtimeAutostartModel?: SessionRealtimeModel | undefined;
|
|
297
416
|
onRealtimeAutostartConsumed?: (() => void) | undefined;
|
|
298
417
|
/** Host hides dictate (and can choreograph layout) while realtime voice is live. */
|
|
@@ -398,31 +517,52 @@ export function useRealtimeModelSelection(options: {
|
|
|
398
517
|
workspaceId: options.workspaceId,
|
|
399
518
|
});
|
|
400
519
|
const activeModel = options.activeModel;
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
available: options.codexConnected,
|
|
405
|
-
unavailableReason: options.codexConnected ? null : CODEX_LIVE_MODEL.unavailableReason,
|
|
406
|
-
},
|
|
407
|
-
]);
|
|
408
|
-
const [selectedModelId, setSelectedModelId] = useState<SessionRealtimeModel>(
|
|
409
|
-
() => readRealtimeModelPreference(workspaceId) ?? CODEX_LIVE_MODEL.id,
|
|
520
|
+
const initialCachedCatalog = readCachedRealtimeModelCatalog(client, workspaceId);
|
|
521
|
+
const [catalog, setCatalog] = useState<RealtimeModelOption[]>(
|
|
522
|
+
() => initialCachedCatalog ?? realtimeModelFallback(options.codexConnected),
|
|
410
523
|
);
|
|
524
|
+
const catalogScopeRef = useRef({
|
|
525
|
+
client,
|
|
526
|
+
workspaceId,
|
|
527
|
+
source: initialCachedCatalog ? ("cache" as const) : ("fallback" as const),
|
|
528
|
+
});
|
|
529
|
+
const [selectedModelId, setSelectedModelId] = useState<SessionRealtimeModel>(() => {
|
|
530
|
+
const preferred = readRealtimeModelPreference(workspaceId) ?? CODEX_LIVE_MODEL.id;
|
|
531
|
+
if (!initialCachedCatalog) return preferred;
|
|
532
|
+
if (initialCachedCatalog.some((model) => model.id === preferred && model.available)) {
|
|
533
|
+
return preferred;
|
|
534
|
+
}
|
|
535
|
+
return initialCachedCatalog.find((model) => model.available)?.id ?? CODEX_LIVE_MODEL.id;
|
|
536
|
+
});
|
|
411
537
|
|
|
412
538
|
useEffect(() => {
|
|
413
539
|
let disposed = false;
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
540
|
+
const applyCatalog = (models: RealtimeModelOption[]) => {
|
|
541
|
+
catalogScopeRef.current = { client, workspaceId, source: "cache" };
|
|
542
|
+
setCatalog(models);
|
|
543
|
+
setSelectedModelId((current) => {
|
|
544
|
+
if (models.some((model) => model.id === current && model.available)) return current;
|
|
545
|
+
return models.find((model) => model.available)?.id ?? CODEX_LIVE_MODEL.id;
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
const cached = readCachedRealtimeModelCatalog(client, workspaceId);
|
|
549
|
+
if (cached) {
|
|
550
|
+
const scope = catalogScopeRef.current;
|
|
551
|
+
if (
|
|
552
|
+
scope.client !== client ||
|
|
553
|
+
scope.workspaceId !== workspaceId ||
|
|
554
|
+
scope.source !== "cache"
|
|
555
|
+
) {
|
|
556
|
+
applyCatalog(cached);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const loading = loadRealtimeModelCatalog(client, workspaceId);
|
|
561
|
+
if (!loading) return;
|
|
562
|
+
void loading
|
|
563
|
+
.then((models) => {
|
|
419
564
|
if (disposed) return;
|
|
420
|
-
|
|
421
|
-
setCatalog(models);
|
|
422
|
-
setSelectedModelId((current) => {
|
|
423
|
-
if (models.some((model) => model.id === current && model.available)) return current;
|
|
424
|
-
return models.find((model) => model.available)?.id ?? CODEX_LIVE_MODEL.id;
|
|
425
|
-
});
|
|
565
|
+
applyCatalog(models);
|
|
426
566
|
})
|
|
427
567
|
.catch(() => undefined);
|
|
428
568
|
return () => {
|
|
@@ -684,7 +824,13 @@ export function RealtimeVoiceControl(props: {
|
|
|
684
824
|
opacity: 1,
|
|
685
825
|
transition: reduceMotion
|
|
686
826
|
? { duration: 0 }
|
|
687
|
-
: {
|
|
827
|
+
: {
|
|
828
|
+
delay: 0.32,
|
|
829
|
+
type: "spring",
|
|
830
|
+
stiffness: 320,
|
|
831
|
+
damping: 30,
|
|
832
|
+
mass: 0.8,
|
|
833
|
+
},
|
|
688
834
|
}}
|
|
689
835
|
{...(reduceMotion
|
|
690
836
|
? {}
|
|
@@ -1160,7 +1306,11 @@ function statusContent(
|
|
|
1160
1306
|
};
|
|
1161
1307
|
}
|
|
1162
1308
|
if (!canStart && admissionBlocker) {
|
|
1163
|
-
return {
|
|
1309
|
+
return {
|
|
1310
|
+
phase: "unavailable",
|
|
1311
|
+
label: "Voice unavailable",
|
|
1312
|
+
detail: admissionBlocker,
|
|
1313
|
+
};
|
|
1164
1314
|
}
|
|
1165
1315
|
return {
|
|
1166
1316
|
phase: "idle",
|
|
@@ -1268,7 +1418,11 @@ function RealtimeStatusDot(props: { phase: RealtimeVisualPhase; reduceMotion: bo
|
|
|
1268
1418
|
<motion.span
|
|
1269
1419
|
className="absolute inset-0 rounded-full bg-og-status-running"
|
|
1270
1420
|
animate={{ opacity: [0.45, 0], scale: [1, 2.2] }}
|
|
1271
|
-
transition={{
|
|
1421
|
+
transition={{
|
|
1422
|
+
duration: 1.4,
|
|
1423
|
+
ease: "easeOut",
|
|
1424
|
+
repeat: Number.POSITIVE_INFINITY,
|
|
1425
|
+
}}
|
|
1272
1426
|
/>
|
|
1273
1427
|
) : null}
|
|
1274
1428
|
<span
|
|
@@ -1343,6 +1497,7 @@ function isRealtimeModel(value: string | null): value is SessionRealtimeModel {
|
|
|
1343
1497
|
value === "opengeni-gateway/openai/gpt-realtime-2.1" ||
|
|
1344
1498
|
value === "opengeni-gateway/openai/gpt-realtime-mini" ||
|
|
1345
1499
|
value === "opengeni-gateway/xai/grok-voice-think-fast-2.0" ||
|
|
1500
|
+
value === "supergrok/grok-voice-think-fast-2.0" ||
|
|
1346
1501
|
value === "workspace-gateway/openai/gpt-realtime-2.1" ||
|
|
1347
1502
|
value === "workspace-gateway/openai/gpt-realtime-mini" ||
|
|
1348
1503
|
value === "workspace-gateway/xai/grok-voice-think-fast-2.0"
|