@opengeni/sdk 3.3.2 → 3.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 +1007 -72
- package/dist/adapters/express.d.ts +31 -0
- package/dist/adapters/express.js +87 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/hono.d.ts +16 -0
- package/dist/adapters/hono.js +8 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/next.d.ts +29 -0
- package/dist/adapters/next.js +22 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/allowance-refusal.d.ts +2 -0
- package/dist/allowance-refusal.js +50 -0
- package/dist/allowance-refusal.js.map +1 -0
- package/dist/app-links.d.ts +42 -0
- package/dist/app-links.js +66 -0
- package/dist/app-links.js.map +1 -0
- package/dist/artifact-catalog.d.ts +40 -0
- package/dist/artifact-client.d.ts +20 -8
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +8 -6
- package/dist/automations.js +8 -2
- package/dist/automations.js.map +1 -1
- package/dist/azure-live-transport.d.ts +32 -0
- package/dist/browser.d.ts +7 -2
- package/dist/browser.js +27 -5
- package/dist/chat/fold.d.ts +75 -0
- package/dist/chat/handler.d.ts +41 -0
- package/dist/chat/http.d.ts +67 -0
- package/dist/chat/ids.d.ts +34 -0
- package/dist/chat/index.d.ts +9 -0
- package/dist/chat/index.js +1369 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/openai.d.ts +30 -0
- package/dist/chat/opengeni.d.ts +129 -0
- package/dist/chat/types.d.ts +207 -0
- package/dist/chat/vercel.d.ts +49 -0
- package/dist/chats.d.ts +8 -0
- package/dist/chunk-4AHR4KGA.js +301 -0
- package/dist/chunk-4AHR4KGA.js.map +1 -0
- package/dist/chunk-6GYGMLX4.js +332 -0
- package/dist/chunk-6GYGMLX4.js.map +1 -0
- package/dist/chunk-7EWT3PV6.js +368 -0
- package/dist/chunk-7EWT3PV6.js.map +1 -0
- package/dist/chunk-FTWVVPR6.js +1665 -0
- package/dist/chunk-FTWVVPR6.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-KGEABAID.js +71 -0
- package/dist/chunk-KGEABAID.js.map +1 -0
- package/dist/{chunk-YKZME56C.js → chunk-KWADQ227.js} +3 -3
- package/dist/{chunk-YKZME56C.js.map → chunk-KWADQ227.js.map} +1 -1
- package/dist/{chunk-JUS2UYRP.js → chunk-NCU5OAN7.js} +27 -2
- package/dist/chunk-NCU5OAN7.js.map +1 -0
- package/dist/{chunk-YXQX7RQT.js → chunk-OI5YLASH.js} +73 -2
- package/dist/chunk-OI5YLASH.js.map +1 -0
- package/dist/chunk-OV3POE62.js +359 -0
- package/dist/chunk-OV3POE62.js.map +1 -0
- package/dist/chunk-P7UWKORG.js +561 -0
- package/dist/chunk-P7UWKORG.js.map +1 -0
- package/dist/chunk-QGEIL4TR.js +195 -0
- package/dist/chunk-QGEIL4TR.js.map +1 -0
- package/dist/{chunk-TRNI7KEO.js → chunk-S2NLS3ES.js} +3669 -1281
- package/dist/chunk-S2NLS3ES.js.map +1 -0
- package/dist/chunk-UM4ZXXJR.js +9 -0
- package/dist/chunk-UM4ZXXJR.js.map +1 -0
- package/dist/{chunk-QT3KFVDP.js → chunk-UME3X7CP.js} +28 -32
- package/dist/chunk-UME3X7CP.js.map +1 -0
- package/dist/chunk-VT4YM4XQ.js +210 -0
- package/dist/chunk-VT4YM4XQ.js.map +1 -0
- package/dist/chunk-WSYYXONG.js +1602 -0
- package/dist/chunk-WSYYXONG.js.map +1 -0
- package/dist/{chunk-C2H7HBNP.js → chunk-WVCJRSGJ.js} +119 -14
- package/dist/chunk-WVCJRSGJ.js.map +1 -0
- package/dist/chunk-X5ITQVXE.js +15 -0
- package/dist/chunk-X5ITQVXE.js.map +1 -0
- package/dist/chunk-XA7HVIHI.js +126 -0
- package/dist/chunk-XA7HVIHI.js.map +1 -0
- package/dist/{chunk-KKRO2VCP.js → chunk-YJCHB7R3.js} +60 -12
- package/dist/chunk-YJCHB7R3.js.map +1 -0
- package/dist/chunk-YKIHNLWD.js +153 -0
- package/dist/chunk-YKIHNLWD.js.map +1 -0
- package/dist/chunk-ZDUBPV33.js +277 -0
- package/dist/chunk-ZDUBPV33.js.map +1 -0
- package/dist/client.d.ts +1068 -118
- package/dist/codex-realtime-controller.d.ts +13 -0
- package/dist/codex-realtime-controller.js +4 -2
- package/dist/codex-realtime-v3-wire.d.ts +3 -2
- package/dist/codex-realtime.d.ts +5 -1
- package/dist/company-brain.d.ts +1 -11
- package/dist/core.d.ts +7 -2
- package/dist/core.js +25 -6
- package/dist/deprecation.d.ts +47 -0
- package/dist/document-authority-client.d.ts +24 -2
- package/dist/document-authority.js +7 -5
- package/dist/editable-artifacts/index.d.ts +1 -1
- package/dist/editable-artifacts/types.d.ts +12 -2
- package/dist/editable-artifacts.js +19 -3
- package/dist/editable-artifacts.js.map +1 -1
- package/dist/embedding-client.d.ts +129 -0
- package/dist/errors.d.ts +76 -1
- package/dist/external-vault-receiver.d.ts +203 -0
- package/dist/external-vault-receiver.js +564 -0
- package/dist/external-vault-receiver.js.map +1 -0
- package/dist/feedback.d.ts +23 -0
- package/dist/gateway-realtime-transport.js +1 -1
- package/dist/github-repositories.d.ts +10 -0
- package/dist/github-repositories.js +36 -0
- package/dist/github-repositories.js.map +1 -0
- package/dist/index.d.ts +62 -11
- package/dist/index.js +557 -338
- package/dist/index.js.map +1 -1
- package/dist/insights-usage.d.ts +33 -0
- package/dist/insights-usage.js +7 -0
- package/dist/insights-usage.js.map +1 -0
- package/dist/interaction.d.ts +165 -71
- package/dist/interaction.js +2 -2
- package/dist/knowledge.d.ts +7 -0
- package/dist/knowledge.js +20 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/known-tools.d.ts +4 -0
- package/dist/message-links.d.ts +68 -0
- package/dist/model-context.d.ts +68 -0
- package/dist/model-display.d.ts +2 -0
- package/dist/model-display.js +17 -0
- package/dist/model-display.js.map +1 -0
- package/dist/model-picker-order.d.ts +1 -0
- package/dist/model-picker-order.js +10 -0
- package/dist/model-picker-order.js.map +1 -0
- package/dist/organization-integration-policy.d.ts +26 -0
- package/dist/organization-integration-policy.js +26 -0
- package/dist/organization-integration-policy.js.map +1 -0
- package/dist/pr-review-client.d.ts +1 -1
- package/dist/pr-review.d.ts +1 -1
- package/dist/pr-review.js.map +1 -1
- package/dist/preference-registry.d.ts +1 -1
- package/dist/prepared-mcp-setup.d.ts +5 -0
- package/dist/proxy.d.ts +13 -11
- package/dist/realtime.d.ts +1 -1
- package/dist/realtime.js +253 -7
- package/dist/realtime.js.map +1 -1
- package/dist/session-history-import.d.ts +22 -0
- package/dist/session-history-import.js +36 -0
- package/dist/session-history-import.js.map +1 -0
- package/dist/session-list-entries.d.ts +1 -0
- package/dist/session-list-entries.js +7 -0
- package/dist/session-list-entries.js.map +1 -0
- package/dist/session-message-search.d.ts +53 -0
- package/dist/session-proxy-client.d.ts +28 -0
- package/dist/session-proxy.d.ts +334 -0
- package/dist/session-proxy.js +24 -0
- package/dist/session-proxy.js.map +1 -0
- package/dist/session-titles.d.ts +2 -28
- package/dist/session-usage-limits.d.ts +10 -0
- package/dist/session-usage-limits.js +43 -0
- package/dist/session-usage-limits.js.map +1 -0
- package/dist/shared-catalog-loader.d.ts +34 -0
- package/dist/site-ai.d.ts +35 -0
- package/dist/site-bridge-host.d.ts +112 -0
- package/dist/site-bridge-host.js +286 -0
- package/dist/site-bridge-host.js.map +1 -0
- package/dist/site-browser-entry.d.ts +1 -0
- package/dist/site-browser-runtime.gen.d.ts +1 -0
- package/dist/site-data.d.ts +178 -0
- package/dist/site-document.d.ts +5 -0
- package/dist/site-document.js +20 -0
- package/dist/site-document.js.map +1 -0
- package/dist/site-document.test.d.ts +1 -0
- package/dist/site-http.d.ts +17 -0
- package/dist/site-sql.d.ts +31 -0
- package/dist/site-tool-bridge.d.ts +57 -0
- package/dist/site.d.ts +186 -0
- package/dist/site.js +63 -0
- package/dist/site.js.map +1 -0
- package/dist/skills.d.ts +75 -0
- package/dist/sse.d.ts +1 -1
- package/dist/stream.d.ts +2 -0
- package/dist/tenant-workspaces.d.ts +47 -0
- package/dist/tenant-workspaces.js +8 -0
- package/dist/tenant-workspaces.js.map +1 -0
- package/dist/tool-auth.d.ts +87 -0
- package/dist/tool-auth.js +23 -0
- package/dist/tool-auth.js.map +1 -0
- package/dist/tool-display-metadata.d.ts +3 -0
- package/dist/tools-37DCTCNZ.js +12 -0
- package/dist/tools-37DCTCNZ.js.map +1 -0
- package/dist/tools.d.ts +81 -0
- package/dist/transcription.d.ts +1 -1
- package/dist/types.d.ts +2807 -583
- package/dist/usage-allowances.d.ts +36 -0
- package/dist/usage-allowances.js +80 -0
- package/dist/usage-allowances.js.map +1 -0
- package/dist/vault-types.d.ts +324 -0
- package/dist/vaults-api.d.ts +65 -0
- package/dist/workspace-artifacts.d.ts +47 -5
- package/dist/workspace-instruction-policies.d.ts +2 -2
- package/dist/workspace-integrations.d.ts +74 -0
- package/dist/workspace-integrations.js +67 -0
- package/dist/workspace-integrations.js.map +1 -0
- package/dist/workspace-state.d.ts +8 -71
- package/package.json +103 -5
- package/src/adapters/express.ts +136 -0
- package/src/adapters/hono.ts +18 -0
- package/src/adapters/next.ts +42 -0
- package/src/allowance-refusal.ts +5 -0
- package/src/app-links.ts +117 -0
- package/src/artifact-catalog.ts +51 -0
- package/src/artifact-client.ts +40 -49
- package/src/artifacts.ts +1 -0
- package/src/automations-client.ts +8 -2
- package/src/azure-live-transport.ts +286 -0
- package/src/browser.ts +37 -0
- package/src/chat/fold.ts +431 -0
- package/src/chat/handler.ts +240 -0
- package/src/chat/http.ts +257 -0
- package/src/chat/ids.ts +85 -0
- package/src/chat/index.ts +88 -0
- package/src/chat/openai.ts +439 -0
- package/src/chat/opengeni.ts +737 -0
- package/src/chat/types.ts +219 -0
- package/src/chat/vercel.ts +204 -0
- package/src/chats.ts +16 -0
- package/src/client.ts +5697 -1833
- package/src/codex-realtime-controller.ts +163 -8
- package/src/codex-realtime-v3-wire.ts +4 -3
- package/src/codex-realtime-v3.ts +12 -3
- package/src/codex-realtime.ts +11 -3
- package/src/company-brain.ts +0 -40
- package/src/core.ts +20 -0
- package/src/deprecation.ts +149 -0
- package/src/document-authority-client.ts +104 -1
- package/src/editable-artifacts/controller.ts +36 -2
- package/src/editable-artifacts/index.ts +1 -0
- package/src/editable-artifacts/types.ts +29 -16
- package/src/embedding-client.ts +519 -0
- package/src/errors.ts +303 -4
- package/src/external-vault-receiver.ts +914 -0
- package/src/feedback.ts +20 -0
- package/src/gateway-realtime-transport.ts +3 -3
- package/src/github-repositories.ts +60 -0
- package/src/index.ts +603 -65
- package/src/insights-usage.ts +64 -0
- package/src/interaction.ts +315 -93
- package/src/knowledge.ts +72 -0
- package/src/known-tools.ts +279 -0
- package/src/message-links.ts +178 -0
- package/src/model-context.ts +113 -0
- package/src/model-display.ts +11 -0
- package/src/model-picker-order.ts +6 -0
- package/src/organization-integration-policy.ts +54 -0
- package/src/pr-review-client.ts +1 -1
- package/src/pr-review.ts +1 -1
- package/src/preference-registry.ts +2 -0
- package/src/prepared-mcp-setup.ts +123 -0
- package/src/proxy.ts +23 -12
- package/src/realtime.ts +15 -6
- package/src/session-history-import.ts +84 -0
- package/src/session-list-entries.ts +1 -0
- package/src/session-message-search.ts +47 -0
- package/src/session-proxy-client.ts +68 -0
- package/src/session-proxy.ts +2098 -0
- package/src/session-titles.ts +8 -82
- package/src/session-usage-limits.ts +71 -0
- package/src/shared-catalog-loader.ts +169 -0
- package/src/site-ai.ts +284 -0
- package/src/site-bridge-host.ts +422 -0
- package/src/site-browser-entry.ts +6 -0
- package/src/site-browser-runtime.gen.ts +2 -0
- package/src/site-data.ts +372 -0
- package/src/site-document.test.ts +64 -0
- package/src/site-document.ts +18 -0
- package/src/site-http.ts +171 -0
- package/src/site-sql.ts +153 -0
- package/src/site-tool-bridge.ts +401 -0
- package/src/site.ts +878 -0
- package/src/skills.ts +69 -0
- package/src/sse.ts +9 -3
- package/src/stream.ts +33 -10
- package/src/tenant-workspaces.ts +148 -0
- package/src/tool-auth.ts +301 -0
- package/src/tool-display-metadata.ts +27 -0
- package/src/tools.ts +452 -0
- package/src/transcription.ts +3 -0
- package/src/types.ts +3170 -582
- package/src/usage-allowances.ts +189 -0
- package/src/vault-types.ts +379 -0
- package/src/vaults-api.ts +339 -0
- package/src/workspace-artifacts.ts +55 -6
- package/src/workspace-instruction-policies.ts +2 -1
- package/src/workspace-integrations.ts +402 -0
- package/src/workspace-state.ts +8 -82
- package/dist/chunk-C2H7HBNP.js.map +0 -1
- package/dist/chunk-C4DJPZRU.js +0 -119
- package/dist/chunk-C4DJPZRU.js.map +0 -1
- package/dist/chunk-JUS2UYRP.js.map +0 -1
- package/dist/chunk-KKRO2VCP.js.map +0 -1
- package/dist/chunk-QT3KFVDP.js.map +0 -1
- package/dist/chunk-TRNI7KEO.js.map +0 -1
- package/dist/chunk-VYCTL62C.js.map +0 -1
- package/dist/chunk-YXQX7RQT.js.map +0 -1
|
@@ -47,7 +47,7 @@ export type { SessionRealtimeLifecycleProjection } from "./codex-realtime-lifecy
|
|
|
47
47
|
const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
48
48
|
const OUTBOUND_SYNC_INTERVAL_MS = 1_000;
|
|
49
49
|
export const CODEX_REALTIME_NEGOTIATION_TIMEOUT_MS = 20_000;
|
|
50
|
-
//
|
|
50
|
+
// Opengeni policy: rotate conservatively without asserting an upstream lifetime.
|
|
51
51
|
const DEFAULT_CONNECTION_ROTATION_INTERVAL_MS = 15 * 60_000;
|
|
52
52
|
const DEFAULT_RECONNECT_BACKOFF_MS = [250, 1_000, 2_000, 5_000] as const;
|
|
53
53
|
const MAX_BROWSER_TIMEOUT_MS = 2_147_483_647;
|
|
@@ -120,8 +120,39 @@ 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
|
+
const message = apiErrorMessage(error);
|
|
146
|
+
return {
|
|
147
|
+
code: error.code,
|
|
148
|
+
message:
|
|
149
|
+
message ??
|
|
150
|
+
(error.code === "insufficient_credits"
|
|
151
|
+
? "Live voice needs Opengeni credits. Add credits to continue."
|
|
152
|
+
: "Live voice is unavailable right now."),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
125
156
|
export type CodexRealtimeControllerClient = {
|
|
126
157
|
beginSessionRealtime(
|
|
127
158
|
workspaceId: string,
|
|
@@ -178,6 +209,7 @@ export type CodexRealtimeOwnerStorage = Pick<Storage, "getItem" | "setItem" | "r
|
|
|
178
209
|
|
|
179
210
|
/** Canonical browser-owner storage namespace for a public realtime model. */
|
|
180
211
|
export function sessionRealtimeOwnerStorageNamespace(model: SessionRealtimeModel): string {
|
|
212
|
+
if (model === "opengeni-azure/gpt-live-1") return "azure-live-owner";
|
|
181
213
|
if (model === "gpt-live-1-boulder-alpha") return "codex-realtime-owner";
|
|
182
214
|
if (model === "supergrok/grok-voice-think-fast-2.0") return "xai-realtime-owner";
|
|
183
215
|
return "gateway-realtime-owner";
|
|
@@ -355,6 +387,7 @@ export function createCodexRealtimeController(
|
|
|
355
387
|
reconnectAttempt: 0,
|
|
356
388
|
diagnostic: null,
|
|
357
389
|
error: null,
|
|
390
|
+
refusal: null,
|
|
358
391
|
};
|
|
359
392
|
let active: ConnectionRuntime | null = null;
|
|
360
393
|
let pendingAbort: AbortController | null = null;
|
|
@@ -371,6 +404,9 @@ export function createCodexRealtimeController(
|
|
|
371
404
|
let generation = 0;
|
|
372
405
|
let reconnectAttempt = 0;
|
|
373
406
|
let recoveryTerminal = false;
|
|
407
|
+
// Whether this mode ever reached a live provider connection. A definitive
|
|
408
|
+
// failure before that ends the mode instead of leaving an empty call open.
|
|
409
|
+
let connectedInMode = false;
|
|
374
410
|
let mutationTail = Promise.resolve();
|
|
375
411
|
let connectionTask: Promise<void> | null = null;
|
|
376
412
|
const acceptedDelegationItemIds = new Set(
|
|
@@ -699,6 +735,12 @@ export function createCodexRealtimeController(
|
|
|
699
735
|
if (closed || stopping || isAbortError(error)) return;
|
|
700
736
|
const message = safeError(error);
|
|
701
737
|
if (error instanceof CodexRealtimeMicrophoneError) {
|
|
738
|
+
if (error.code !== "track_ended" && !connectedInMode) {
|
|
739
|
+
// No conversation exists yet: end the call and say how to fix the
|
|
740
|
+
// microphone instead of holding an empty "reconnecting" call open.
|
|
741
|
+
await endAfterFailure(microphoneFailureMessage(error), null);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
702
744
|
if (error.code === "track_ended" && state.mode?.state === "active") {
|
|
703
745
|
reconnectAttempt += 1;
|
|
704
746
|
publish({
|
|
@@ -720,6 +762,11 @@ export function createCodexRealtimeController(
|
|
|
720
762
|
transitionEnded("Realtime owner no longer exists");
|
|
721
763
|
return;
|
|
722
764
|
}
|
|
765
|
+
const refusal = codexRealtimeRefusal(error);
|
|
766
|
+
if (refusal) {
|
|
767
|
+
await endForRefusal(refusal);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
723
770
|
if (error instanceof OpenGeniApiError && error.status === 409 && error.retryable && owner) {
|
|
724
771
|
try {
|
|
725
772
|
const reconciled = await begin(owner, true, false);
|
|
@@ -737,6 +784,9 @@ export function createCodexRealtimeController(
|
|
|
737
784
|
return;
|
|
738
785
|
}
|
|
739
786
|
}
|
|
787
|
+
} else if (error instanceof OpenGeniApiError && !error.retryable && !connectedInMode) {
|
|
788
|
+
await endAfterFailure(message, null);
|
|
789
|
+
return;
|
|
740
790
|
} else if (error instanceof OpenGeniApiError && !error.retryable) {
|
|
741
791
|
stopTimers();
|
|
742
792
|
if (!active) releaseMicrophone();
|
|
@@ -883,6 +933,15 @@ export function createCodexRealtimeController(
|
|
|
883
933
|
});
|
|
884
934
|
let connected: CodexRealtimeWebrtcSession;
|
|
885
935
|
try {
|
|
936
|
+
// Persist a fragment provider's tail before the replacement broker reads
|
|
937
|
+
// its startup history, while the old connection still owns the ledger.
|
|
938
|
+
if (active?.transport.drain) {
|
|
939
|
+
await active.transport.drain();
|
|
940
|
+
await active.bridge.flush();
|
|
941
|
+
}
|
|
942
|
+
if (closed || stopping || abort.signal.aborted || pendingGeneration !== targetGeneration) {
|
|
943
|
+
throw abort.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
944
|
+
}
|
|
886
945
|
const media = await ensureMicrophone(replaceMicrophone, abort.signal);
|
|
887
946
|
const operationId = randomUUID();
|
|
888
947
|
const commonTransportInput = {
|
|
@@ -1016,6 +1075,7 @@ export function createCodexRealtimeController(
|
|
|
1016
1075
|
onFatal: (fatal) => onBridgeFatal(targetGeneration, bridge, fatal),
|
|
1017
1076
|
});
|
|
1018
1077
|
active = { generation: targetGeneration, transport: connected, bridge };
|
|
1078
|
+
connectedInMode = true;
|
|
1019
1079
|
connected.setOutputMuted(state.outputMuted);
|
|
1020
1080
|
recoveryTerminal = false;
|
|
1021
1081
|
pendingAbort = null;
|
|
@@ -1098,6 +1158,7 @@ export function createCodexRealtimeController(
|
|
|
1098
1158
|
};
|
|
1099
1159
|
|
|
1100
1160
|
const heartbeat = async (): Promise<void> => {
|
|
1161
|
+
let stopInstruction: CodexRealtimeRefusal | null = null;
|
|
1101
1162
|
await exclusive(async () => {
|
|
1102
1163
|
const current = state.mode;
|
|
1103
1164
|
if (!owner || !current || current.state !== "active") {
|
|
@@ -1121,6 +1182,43 @@ export function createCodexRealtimeController(
|
|
|
1121
1182
|
return;
|
|
1122
1183
|
}
|
|
1123
1184
|
publish({ mode: result.mode, realtimeId: result.mode.id, error: null });
|
|
1185
|
+
stopInstruction = result.stop ?? null;
|
|
1186
|
+
});
|
|
1187
|
+
// The server stopped extending the lease (for example, out of credits):
|
|
1188
|
+
// drain and end gracefully inside the remaining lease.
|
|
1189
|
+
const instruction = stopInstruction as CodexRealtimeRefusal | null;
|
|
1190
|
+
stopInstruction = null;
|
|
1191
|
+
if (instruction && !stopping && !closed) await endForRefusal(instruction);
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* End the call because Opengeni refused it, keep final speech, and leave a
|
|
1196
|
+
* terminal, non-retrying state that explains why.
|
|
1197
|
+
*/
|
|
1198
|
+
const endForRefusal = async (refusal: CodexRealtimeRefusal): Promise<void> =>
|
|
1199
|
+
await endAfterFailure(refusal.message, refusal);
|
|
1200
|
+
|
|
1201
|
+
const endAfterFailure = async (
|
|
1202
|
+
message: string,
|
|
1203
|
+
refusal: CodexRealtimeRefusal | null,
|
|
1204
|
+
): Promise<void> => {
|
|
1205
|
+
try {
|
|
1206
|
+
await controller.stop();
|
|
1207
|
+
} catch {
|
|
1208
|
+
// The lease is no longer extended; a failed graceful end lapses on its own.
|
|
1209
|
+
stopping = false;
|
|
1210
|
+
closeBrowserResources();
|
|
1211
|
+
clearOwner();
|
|
1212
|
+
}
|
|
1213
|
+
recoveryTerminal = false;
|
|
1214
|
+
publish({
|
|
1215
|
+
status: "error",
|
|
1216
|
+
realtimeId: null,
|
|
1217
|
+
mode: null,
|
|
1218
|
+
bridge: null,
|
|
1219
|
+
diagnostic: diagnostic("terminal_stop", message, false),
|
|
1220
|
+
error: message,
|
|
1221
|
+
refusal,
|
|
1124
1222
|
});
|
|
1125
1223
|
};
|
|
1126
1224
|
|
|
@@ -1176,6 +1274,7 @@ export function createCodexRealtimeController(
|
|
|
1176
1274
|
closed = false;
|
|
1177
1275
|
stopping = false;
|
|
1178
1276
|
recoveryTerminal = false;
|
|
1277
|
+
connectedInMode = false;
|
|
1179
1278
|
const record: OwnerRecord = {
|
|
1180
1279
|
version: OWNER_RECORD_VERSION,
|
|
1181
1280
|
workspaceId: options.workspaceId,
|
|
@@ -1186,11 +1285,12 @@ export function createCodexRealtimeController(
|
|
|
1186
1285
|
};
|
|
1187
1286
|
owner = record;
|
|
1188
1287
|
storage?.setItem(storageKey, JSON.stringify(record));
|
|
1189
|
-
publish({ status: "starting", error: null, diagnostic: null });
|
|
1288
|
+
publish({ status: "starting", error: null, diagnostic: null, refusal: null });
|
|
1190
1289
|
try {
|
|
1191
1290
|
await begin(record, false);
|
|
1192
1291
|
} catch (error) {
|
|
1193
1292
|
closeBrowserResources(false);
|
|
1293
|
+
const refusal = codexRealtimeRefusal(error);
|
|
1194
1294
|
const failedMode = state.mode as SessionRealtimeMode | null;
|
|
1195
1295
|
if (failedMode?.state === "active") {
|
|
1196
1296
|
await handleConnectionFailure(error, "reconnect", false);
|
|
@@ -1200,9 +1300,13 @@ export function createCodexRealtimeController(
|
|
|
1200
1300
|
status: "error",
|
|
1201
1301
|
realtimeId: null,
|
|
1202
1302
|
mode: null,
|
|
1203
|
-
error: safeError(error),
|
|
1303
|
+
error: refusal?.message ?? safeError(error),
|
|
1304
|
+
...(refusal
|
|
1305
|
+
? { refusal, diagnostic: diagnostic("terminal_stop", refusal.message, false) }
|
|
1306
|
+
: {}),
|
|
1204
1307
|
});
|
|
1205
1308
|
}
|
|
1309
|
+
if (refusal) return;
|
|
1206
1310
|
throw error;
|
|
1207
1311
|
}
|
|
1208
1312
|
},
|
|
@@ -1211,6 +1315,8 @@ export function createCodexRealtimeController(
|
|
|
1211
1315
|
if (recoveryTerminal && state.mode?.state === "active") return;
|
|
1212
1316
|
const record = readOwnerRecord(storage, storageKey, options);
|
|
1213
1317
|
if (!record) {
|
|
1318
|
+
// Keep a terminal failure readable after the server records the end.
|
|
1319
|
+
if (state.status === "error" && !state.mode) return;
|
|
1214
1320
|
transitionEnded();
|
|
1215
1321
|
return;
|
|
1216
1322
|
}
|
|
@@ -1234,7 +1340,10 @@ export function createCodexRealtimeController(
|
|
|
1234
1340
|
if (lifecycle.state === "ended") {
|
|
1235
1341
|
const record = readOwnerRecord(storage, storageKey, options);
|
|
1236
1342
|
if (record && record.operationId !== lifecycle.operationId) {
|
|
1237
|
-
|
|
1343
|
+
// An earlier call's end is not news while this browser is starting
|
|
1344
|
+
// its own call (for example, waiting on the microphone prompt):
|
|
1345
|
+
// re-beginning would advance the lease and fail the pending start.
|
|
1346
|
+
if (connectionTask || state.status === "active" || state.status === "starting") return;
|
|
1238
1347
|
closed = false;
|
|
1239
1348
|
stopping = false;
|
|
1240
1349
|
owner = record;
|
|
@@ -1250,6 +1359,9 @@ export function createCodexRealtimeController(
|
|
|
1250
1359
|
await connectionTask;
|
|
1251
1360
|
return;
|
|
1252
1361
|
}
|
|
1362
|
+
// The server's end of a call that failed to start must not wipe the
|
|
1363
|
+
// failure the user still needs to read.
|
|
1364
|
+
if (state.status === "error" && !state.mode) return;
|
|
1253
1365
|
if (state.realtimeId === null || lifecycle.realtimeId === state.realtimeId) {
|
|
1254
1366
|
transitionEnded(`Realtime ended: ${lifecycle.reason}`);
|
|
1255
1367
|
}
|
|
@@ -1259,11 +1371,22 @@ export function createCodexRealtimeController(
|
|
|
1259
1371
|
if (state.status === "active" && state.realtimeId === lifecycle.realtimeId) return;
|
|
1260
1372
|
const record = readOwnerRecord(storage, storageKey, options);
|
|
1261
1373
|
if (!record || record.operationId !== lifecycle.operationId) {
|
|
1262
|
-
closeBrowserResources();
|
|
1263
|
-
owner = null;
|
|
1264
1374
|
const leaseExpiresAt = Date.parse(lifecycle.leaseExpiresAt);
|
|
1265
1375
|
const remainingLeaseMs = leaseExpiresAt - now().getTime();
|
|
1266
|
-
|
|
1376
|
+
const leaseExpired = Number.isFinite(remainingLeaseMs) && remainingLeaseMs <= 0;
|
|
1377
|
+
// An earlier call whose lease already ran out (its end not yet in the
|
|
1378
|
+
// events) is not another owner: it must not fail the call this
|
|
1379
|
+
// browser is starting.
|
|
1380
|
+
if (
|
|
1381
|
+
leaseExpired &&
|
|
1382
|
+
lifecycle.realtimeId !== state.realtimeId &&
|
|
1383
|
+
(connectionTask || state.status === "starting")
|
|
1384
|
+
) {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
closeBrowserResources();
|
|
1388
|
+
owner = null;
|
|
1389
|
+
if (leaseExpired) {
|
|
1267
1390
|
transitionEnded("Realtime lease expired");
|
|
1268
1391
|
return;
|
|
1269
1392
|
}
|
|
@@ -1337,6 +1460,7 @@ export function createCodexRealtimeController(
|
|
|
1337
1460
|
connectionTask = null;
|
|
1338
1461
|
void retiredTask?.catch(() => undefined);
|
|
1339
1462
|
try {
|
|
1463
|
+
await active?.transport.drain?.();
|
|
1340
1464
|
await active?.bridge.sealAndFlush();
|
|
1341
1465
|
closeBrowserResources();
|
|
1342
1466
|
let current = state.mode;
|
|
@@ -1708,6 +1832,37 @@ function isAbortError(error: unknown): boolean {
|
|
|
1708
1832
|
return error instanceof Error && error.name === "AbortError";
|
|
1709
1833
|
}
|
|
1710
1834
|
|
|
1835
|
+
function microphoneFailureMessage(error: CodexRealtimeMicrophoneError): string {
|
|
1836
|
+
switch (error.code) {
|
|
1837
|
+
case "permission_denied":
|
|
1838
|
+
return "Microphone access is blocked. Allow it in site settings, then try again.";
|
|
1839
|
+
case "device_not_found":
|
|
1840
|
+
return "No microphone was found. Connect one, then try again.";
|
|
1841
|
+
case "device_unavailable":
|
|
1842
|
+
return "Your microphone is busy or unavailable. Close other apps using it, then try again.";
|
|
1843
|
+
default:
|
|
1844
|
+
return /timed out/i.test(error.message)
|
|
1845
|
+
? "Microphone access wasn't granted in time. Allow it when your browser asks, then try again."
|
|
1846
|
+
: "The microphone could not start. Try again.";
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
/** The server's own message, without the transport prefix or reference id. */
|
|
1851
|
+
function apiErrorMessage(error: OpenGeniApiError): string | null {
|
|
1852
|
+
try {
|
|
1853
|
+
const body = JSON.parse(error.body) as Record<string, unknown>;
|
|
1854
|
+
const nested =
|
|
1855
|
+
body.error && typeof body.error === "object" ? (body.error as Record<string, unknown>) : body;
|
|
1856
|
+
return typeof nested.message === "string" && nested.message ? nested.message : null;
|
|
1857
|
+
} catch {
|
|
1858
|
+
return null;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1711
1862
|
function safeError(error: unknown): string {
|
|
1712
|
-
|
|
1863
|
+
if (error instanceof OpenGeniApiError) {
|
|
1864
|
+
const message = apiErrorMessage(error);
|
|
1865
|
+
if (message) return /[.!?]$/.test(message) ? message : `${message}.`;
|
|
1866
|
+
}
|
|
1867
|
+
return error instanceof Error ? error.message : "Live voice failed in this browser.";
|
|
1713
1868
|
}
|
|
@@ -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
|
@@ -316,7 +316,7 @@ export function createCodexRealtimeV3Bridge(
|
|
|
316
316
|
if (closed || fatal) break;
|
|
317
317
|
|
|
318
318
|
for (const entry of result.outbound) {
|
|
319
|
-
// This is
|
|
319
|
+
// This is Opengeni's durable browser-delivery acknowledgment. The
|
|
320
320
|
// provider send below remains at-least-once because pinned V3 exposes
|
|
321
321
|
// no provider receipt and providerAckSequences is never populated.
|
|
322
322
|
if (entry.clientAckedAt === null && !clientReceivedSequences.has(entry.sequence)) {
|
|
@@ -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
|
};
|
|
@@ -85,7 +89,7 @@ export type CodexRealtimeWebrtcSession = {
|
|
|
85
89
|
* Complete the browser half of native connected-Codex GPT-Live V3 negotiation.
|
|
86
90
|
* Provider credentials never enter this boundary: `negotiate` sends SDP,
|
|
87
91
|
* public session configuration, and the active browser-owner proof only to the
|
|
88
|
-
*
|
|
92
|
+
* Opengeni API.
|
|
89
93
|
*/
|
|
90
94
|
export async function startCodexRealtimeWebrtc(
|
|
91
95
|
options: StartCodexRealtimeWebrtcOptions,
|
|
@@ -260,8 +264,12 @@ export async function startCodexRealtimeWebrtc(
|
|
|
260
264
|
{ signal: options.signal },
|
|
261
265
|
);
|
|
262
266
|
throwIfAborted(options.signal);
|
|
263
|
-
if (
|
|
264
|
-
|
|
267
|
+
if (
|
|
268
|
+
answer.version !== "v3" ||
|
|
269
|
+
answer.model !== (options.expectedModel ?? "gpt-live-1-boulder-alpha") ||
|
|
270
|
+
!answer.sdp
|
|
271
|
+
) {
|
|
272
|
+
throw new Error("Opengeni returned an incompatible Codex realtime answer");
|
|
265
273
|
}
|
|
266
274
|
await peerConnection.setRemoteDescription({
|
|
267
275
|
type: "answer",
|
package/src/company-brain.ts
CHANGED
|
@@ -2,23 +2,11 @@ import type {
|
|
|
2
2
|
CompanyBrainContextReceiptPage as ContractCompanyBrainContextReceiptPage,
|
|
3
3
|
CompanyBrainKnowledgeProposalPage as ContractCompanyBrainKnowledgeProposalPage,
|
|
4
4
|
CompanyBrainOkfPackage as ContractCompanyBrainOkfPackage,
|
|
5
|
-
KnowledgeBrowseRequest as ContractKnowledgeBrowseRequest,
|
|
6
|
-
KnowledgeBrowseResponse as ContractKnowledgeBrowseResponse,
|
|
7
|
-
KnowledgeGetResponse as ContractKnowledgeGetResponse,
|
|
8
|
-
KnowledgeRecord as ContractKnowledgeRecord,
|
|
9
|
-
KnowledgeSearchRequest as ContractKnowledgeSearchRequest,
|
|
10
|
-
KnowledgeSearchResponse as ContractKnowledgeSearchResponse,
|
|
11
5
|
} from "@opengeni/contracts";
|
|
12
6
|
|
|
13
7
|
export type CompanyBrainOkfPackage = ContractCompanyBrainOkfPackage;
|
|
14
8
|
export type CompanyBrainContextReceiptPage = ContractCompanyBrainContextReceiptPage;
|
|
15
9
|
export type CompanyBrainKnowledgeProposalPage = ContractCompanyBrainKnowledgeProposalPage;
|
|
16
|
-
export type CompanyBrainKnowledgeSearchRequest = ContractKnowledgeSearchRequest;
|
|
17
|
-
export type CompanyBrainKnowledgeSearchResponse = ContractKnowledgeSearchResponse;
|
|
18
|
-
export type CompanyBrainKnowledgeBrowseRequest = ContractKnowledgeBrowseRequest;
|
|
19
|
-
export type CompanyBrainKnowledgeBrowseResponse = ContractKnowledgeBrowseResponse;
|
|
20
|
-
export type CompanyBrainKnowledgeGetResponse = ContractKnowledgeGetResponse;
|
|
21
|
-
export type CompanyBrainKnowledgeRecord = ContractKnowledgeRecord;
|
|
22
10
|
|
|
23
11
|
export type CompanyBrainContextReceiptListOptions = {
|
|
24
12
|
attemptId?: string;
|
|
@@ -38,34 +26,6 @@ function inspectorPath(workspaceId: string, suffix: string): string {
|
|
|
38
26
|
return `/v1/workspaces/${workspaceId}/company-brain/${suffix}`;
|
|
39
27
|
}
|
|
40
28
|
|
|
41
|
-
/** Human-authorized, permission-first Knowledge search for Company Brain inspectors. */
|
|
42
|
-
export function searchCompanyBrainKnowledge(
|
|
43
|
-
client: CompanyBrainInspectorTransport,
|
|
44
|
-
workspaceId: string,
|
|
45
|
-
request: CompanyBrainKnowledgeSearchRequest,
|
|
46
|
-
): Promise<CompanyBrainKnowledgeSearchResponse> {
|
|
47
|
-
return client.requestJson("POST", inspectorPath(workspaceId, "knowledge/search"), request);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function getCompanyBrainKnowledge(
|
|
51
|
-
client: CompanyBrainInspectorTransport,
|
|
52
|
-
workspaceId: string,
|
|
53
|
-
recordId: string,
|
|
54
|
-
): Promise<CompanyBrainKnowledgeGetResponse> {
|
|
55
|
-
return client.requestJson(
|
|
56
|
-
"GET",
|
|
57
|
-
`${inspectorPath(workspaceId, "knowledge/record")}?id=${encodeURIComponent(recordId)}`,
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function browseCompanyBrainKnowledge(
|
|
62
|
-
client: CompanyBrainInspectorTransport,
|
|
63
|
-
workspaceId: string,
|
|
64
|
-
request: CompanyBrainKnowledgeBrowseRequest = {},
|
|
65
|
-
): Promise<CompanyBrainKnowledgeBrowseResponse> {
|
|
66
|
-
return client.requestJson("POST", inspectorPath(workspaceId, "knowledge/browse"), request);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
29
|
export function listCompanyBrainContextReceipts(
|
|
70
30
|
client: CompanyBrainInspectorTransport,
|
|
71
31
|
workspaceId: string,
|
package/src/core.ts
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
/** Backward-compatible non-artifact client surface. */
|
|
2
2
|
export { OpenGeniDocumentAuthorityClient as OpenGeniCoreClient } from "./document-authority-client";
|
|
3
|
+
export {
|
|
4
|
+
OpenGeniToolCallError,
|
|
5
|
+
OpenGeniToolReapprovalRequiredError,
|
|
6
|
+
OpenGeniToolsClient,
|
|
7
|
+
} from "./tools";
|
|
8
|
+
export type {
|
|
9
|
+
OpenGeniGeneratedTools,
|
|
10
|
+
OpenGeniDynamicToolNode,
|
|
11
|
+
OpenGeniToolCallOptions,
|
|
12
|
+
OpenGeniToolFunction,
|
|
13
|
+
OpenGeniToolsFacade,
|
|
14
|
+
OpenGeniWorkspaceTools,
|
|
15
|
+
} from "./tools";
|
|
3
16
|
export type {
|
|
4
17
|
FetchLike,
|
|
18
|
+
FetchResponse,
|
|
5
19
|
GetSessionOptions,
|
|
6
20
|
OpenGeniClientOptions,
|
|
7
21
|
OpenGeniRequestOptions,
|
|
@@ -13,9 +27,15 @@ export type {
|
|
|
13
27
|
export {
|
|
14
28
|
OpenGeniApiContractMismatchError,
|
|
15
29
|
OpenGeniApiError,
|
|
30
|
+
OpenGeniSetupError,
|
|
31
|
+
OpenGeniSecureContextRequiredError,
|
|
16
32
|
OpenGeniSessionListCursorError,
|
|
17
33
|
OpenGeniStreamError,
|
|
34
|
+
formatErrorMessage,
|
|
18
35
|
isRetryableStreamError,
|
|
19
36
|
} from "./errors";
|
|
37
|
+
export type { OpenGeniSecureContextRequiredReason } from "./errors";
|
|
38
|
+
export { parseDeprecationNotice } from "./deprecation";
|
|
39
|
+
export type { OpenGeniDeprecationHandler, OpenGeniDeprecationNotice } from "./deprecation";
|
|
20
40
|
export { resolveWorkspaceVoiceInputEnabled } from "./transcription";
|
|
21
41
|
export { OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION } from "./types";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deprecation notices advertised by the Opengeni API.
|
|
3
|
+
*
|
|
4
|
+
* Per the public API compatibility policy, a route whose behaviour is scheduled
|
|
5
|
+
* for removal answers with the standard `Deprecation` (RFC 9745) and `Sunset`
|
|
6
|
+
* (RFC 8594) headers plus `Link: <notes>; rel="deprecation"`. The client
|
|
7
|
+
* surfaces them through `OpenGeniClientOptions.onDeprecation`, or by default
|
|
8
|
+
* as a one-time `console.warn` per route, so an integrator pinned to an older
|
|
9
|
+
* SDK learns about a removal well before it happens.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type OpenGeniDeprecationNotice = {
|
|
13
|
+
/** Request method, upper case. */
|
|
14
|
+
method: string;
|
|
15
|
+
/** Request path with ids collapsed (`/v1/workspaces/:id/...`), stable per route. */
|
|
16
|
+
route: string;
|
|
17
|
+
/** Exact request path. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** When the deprecation was announced, if the header carried a date. */
|
|
20
|
+
deprecatedAt: Date | null;
|
|
21
|
+
/** Earliest removal on the server, if advertised. */
|
|
22
|
+
sunset: Date | null;
|
|
23
|
+
/** Migration notes (`rel="deprecation"`), if advertised. */
|
|
24
|
+
link: string | null;
|
|
25
|
+
/** Replacement (`rel="successor-version"`), if advertised. */
|
|
26
|
+
successor: string | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** `false` silences notices; a function receives each route's notice once per client. */
|
|
30
|
+
export type OpenGeniDeprecationHandler = (notice: OpenGeniDeprecationNotice) => void;
|
|
31
|
+
|
|
32
|
+
type HeaderBag = { get(name: string): string | null };
|
|
33
|
+
|
|
34
|
+
const ID_SEGMENT =
|
|
35
|
+
/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\d+|[A-Za-z0-9_-]{20,})$/i;
|
|
36
|
+
|
|
37
|
+
function routeKey(path: string): string {
|
|
38
|
+
return path
|
|
39
|
+
.split("/")
|
|
40
|
+
.map((segment) => (ID_SEGMENT.test(segment) ? ":id" : segment))
|
|
41
|
+
.join("/");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseDeprecationDate(value: string): Date | null {
|
|
45
|
+
const trimmed = value.trim();
|
|
46
|
+
// RFC 9745 structured date `@<unix seconds>`; older drafts used `true` or an HTTP-date.
|
|
47
|
+
const structured = /^@(-?\d+)$/.exec(trimmed);
|
|
48
|
+
if (structured) return new Date(Number(structured[1]) * 1000);
|
|
49
|
+
const parsed = Date.parse(trimmed);
|
|
50
|
+
return Number.isNaN(parsed) ? null : new Date(parsed);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseLinks(value: string | null): Map<string, string> {
|
|
54
|
+
const links = new Map<string, string>();
|
|
55
|
+
if (!value) return links;
|
|
56
|
+
for (const match of value.matchAll(/<([^>]*)>\s*((?:;\s*[^;,]+)*)/g)) {
|
|
57
|
+
const rel = /;\s*rel="?([^";,]+)"?/i.exec(match[2] ?? "")?.[1];
|
|
58
|
+
if (!rel) continue;
|
|
59
|
+
for (const name of rel.toLowerCase().split(/\s+/)) {
|
|
60
|
+
if (!links.has(name)) links.set(name, match[1]!);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return links;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Read a deprecation notice from response headers; `null` when none is advertised. */
|
|
67
|
+
export function parseDeprecationNotice(
|
|
68
|
+
method: string,
|
|
69
|
+
url: string,
|
|
70
|
+
headers: HeaderBag,
|
|
71
|
+
): OpenGeniDeprecationNotice | null {
|
|
72
|
+
const deprecation = headers.get("deprecation");
|
|
73
|
+
const sunsetHeader = headers.get("sunset");
|
|
74
|
+
if (!deprecation && !sunsetHeader) return null;
|
|
75
|
+
let path = url;
|
|
76
|
+
try {
|
|
77
|
+
path = new URL(url, "http://opengeni.invalid").pathname;
|
|
78
|
+
} catch {
|
|
79
|
+
// Keep the raw value; the notice is advisory.
|
|
80
|
+
}
|
|
81
|
+
const sunset = sunsetHeader ? Date.parse(sunsetHeader) : Number.NaN;
|
|
82
|
+
const links = parseLinks(headers.get("link"));
|
|
83
|
+
return {
|
|
84
|
+
method: method.toUpperCase(),
|
|
85
|
+
route: routeKey(path),
|
|
86
|
+
path,
|
|
87
|
+
deprecatedAt: deprecation ? parseDeprecationDate(deprecation) : null,
|
|
88
|
+
sunset: Number.isNaN(sunset) ? null : new Date(sunset),
|
|
89
|
+
link: links.get("deprecation") ?? links.get("sunset") ?? null,
|
|
90
|
+
successor: links.get("successor-version") ?? null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const warnedRoutes = new Set<string>();
|
|
95
|
+
|
|
96
|
+
/** The default handler: one `console.warn` per route per process. */
|
|
97
|
+
export function warnDeprecationOnce(notice: OpenGeniDeprecationNotice): void {
|
|
98
|
+
const key = `${notice.method} ${notice.route}`;
|
|
99
|
+
if (warnedRoutes.has(key)) return;
|
|
100
|
+
warnedRoutes.add(key);
|
|
101
|
+
const sunset = notice.sunset ? ` and will be removed after ${notice.sunset.toISOString()}` : "";
|
|
102
|
+
const notes = notice.link ? ` Migration notes: ${notice.link}.` : "";
|
|
103
|
+
console.warn(
|
|
104
|
+
`[@opengeni/sdk] ${key} is deprecated${sunset}.${notes} Upgrade @opengeni/sdk, or pass onDeprecation to handle this notice.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type FetchFunction<R extends { headers: HeaderBag }> = (
|
|
109
|
+
input: string | URL | Request,
|
|
110
|
+
init?: RequestInit,
|
|
111
|
+
) => Promise<R>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Wrap a fetch implementation so every response is inspected for a deprecation
|
|
115
|
+
* notice. The handler runs at most once per route for this wrapper and never
|
|
116
|
+
* affects the request: a throwing handler is swallowed.
|
|
117
|
+
*/
|
|
118
|
+
export function withDeprecationNotices<R extends { headers: HeaderBag }>(
|
|
119
|
+
fetchImpl: FetchFunction<R>,
|
|
120
|
+
handler: OpenGeniDeprecationHandler | false | undefined,
|
|
121
|
+
): FetchFunction<R> {
|
|
122
|
+
if (handler === false) return fetchImpl;
|
|
123
|
+
const notify = handler ?? warnDeprecationOnce;
|
|
124
|
+
const seen = new Set<string>();
|
|
125
|
+
return async (input, init) => {
|
|
126
|
+
const response = await fetchImpl(input, init);
|
|
127
|
+
try {
|
|
128
|
+
const headers = response.headers;
|
|
129
|
+
if (headers && (headers.get("deprecation") || headers.get("sunset"))) {
|
|
130
|
+
const method =
|
|
131
|
+
init?.method ??
|
|
132
|
+
(typeof Request !== "undefined" && input instanceof Request ? input.method : "GET");
|
|
133
|
+
const url =
|
|
134
|
+
typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
135
|
+
const notice = parseDeprecationNotice(method, url, headers);
|
|
136
|
+
if (notice) {
|
|
137
|
+
const key = `${notice.method} ${notice.route}`;
|
|
138
|
+
if (!seen.has(key)) {
|
|
139
|
+
seen.add(key);
|
|
140
|
+
notify(notice);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Deprecation notices are advisory and must never fail a request.
|
|
146
|
+
}
|
|
147
|
+
return response;
|
|
148
|
+
};
|
|
149
|
+
}
|