@opengeni/sdk 0.13.0 → 0.20.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 +66 -17
- package/dist/index.d.ts +610 -85
- package/dist/index.js +730 -221
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +384 -71
- package/src/errors.ts +13 -0
- package/src/index.ts +77 -2
- package/src/stream.ts +3 -0
- package/src/transcription.ts +496 -0
- package/src/types.ts +465 -60
- package/src/workspace-control-stream.ts +101 -0
package/src/errors.ts
CHANGED
|
@@ -11,6 +11,19 @@ export class OpenGeniApiError extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** The browser bundle and API disagree about their state-changing wire contract. */
|
|
15
|
+
export class OpenGeniApiContractMismatchError extends Error {
|
|
16
|
+
readonly expected: string;
|
|
17
|
+
readonly actual: string;
|
|
18
|
+
|
|
19
|
+
constructor(expected: string, actual: string) {
|
|
20
|
+
super(`OpenGeni API contract mismatch: client expects ${expected}, API serves ${actual}`);
|
|
21
|
+
this.name = "OpenGeniApiContractMismatchError";
|
|
22
|
+
this.expected = expected;
|
|
23
|
+
this.actual = actual;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
/** Error for an unrecoverable event-stream condition (not a transient drop). */
|
|
15
28
|
export class OpenGeniStreamError extends Error {
|
|
16
29
|
constructor(message: string) {
|
package/src/index.ts
CHANGED
|
@@ -2,10 +2,17 @@ export { OpenGeniClient } from "./client";
|
|
|
2
2
|
export type {
|
|
3
3
|
FetchLike,
|
|
4
4
|
OpenGeniClientOptions,
|
|
5
|
+
OpenGeniRequestOptions,
|
|
5
6
|
SendMessageInput,
|
|
6
7
|
SteerMessageResult,
|
|
8
|
+
WorkspaceControlEventPage,
|
|
7
9
|
} from "./client";
|
|
8
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
OpenGeniApiContractMismatchError,
|
|
12
|
+
OpenGeniApiError,
|
|
13
|
+
OpenGeniStreamError,
|
|
14
|
+
isRetryableStreamError,
|
|
15
|
+
} from "./errors";
|
|
9
16
|
export {
|
|
10
17
|
formatSseEvent,
|
|
11
18
|
proxySessionEventStream,
|
|
@@ -43,7 +50,43 @@ export type {
|
|
|
43
50
|
StreamConnectionState,
|
|
44
51
|
StreamSessionEventsOptions,
|
|
45
52
|
} from "./stream";
|
|
46
|
-
export {
|
|
53
|
+
export { streamWorkspaceControlEvents } from "./workspace-control-stream";
|
|
54
|
+
export type { WorkspaceControlStreamTransport } from "./workspace-control-stream";
|
|
55
|
+
export {
|
|
56
|
+
DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY,
|
|
57
|
+
authorizeTranscriptionAdapter,
|
|
58
|
+
createTranscriptionSessionRequest,
|
|
59
|
+
resolveWorkspaceTranscriptionPolicy,
|
|
60
|
+
} from "./transcription";
|
|
61
|
+
export type {
|
|
62
|
+
TranscriptionAdapter,
|
|
63
|
+
TranscriptionAdapterDescriptor,
|
|
64
|
+
TranscriptionAdapterStartContext,
|
|
65
|
+
TranscriptionAuthorization,
|
|
66
|
+
TranscriptionCredentialMode,
|
|
67
|
+
TranscriptionDiagnostic,
|
|
68
|
+
TranscriptionErrorCode,
|
|
69
|
+
TranscriptionEvent,
|
|
70
|
+
TranscriptionEventListener,
|
|
71
|
+
TranscriptionLifecycleStatus,
|
|
72
|
+
TranscriptionPolicyBlockReason,
|
|
73
|
+
TranscriptionResultMetadata,
|
|
74
|
+
TranscriptionSession,
|
|
75
|
+
TranscriptionSessionRequest,
|
|
76
|
+
TranscriptionSpeaker,
|
|
77
|
+
TranscriptionTargetSelection,
|
|
78
|
+
TranscriptionTimeSpan,
|
|
79
|
+
TranscriptionWord,
|
|
80
|
+
WorkspaceTranscriptionPolicy,
|
|
81
|
+
WorkspaceTranscriptionTarget,
|
|
82
|
+
} from "./transcription";
|
|
83
|
+
export {
|
|
84
|
+
KNOWN_PERMISSIONS,
|
|
85
|
+
KNOWN_USAGE_EVENT_TYPES,
|
|
86
|
+
OPENGENI_API_CONTRACT_HEADER,
|
|
87
|
+
OPENGENI_API_CONTRACT_REVISION,
|
|
88
|
+
SESSION_EVENT_TYPES,
|
|
89
|
+
} from "./types";
|
|
47
90
|
export type {
|
|
48
91
|
AccessContext,
|
|
49
92
|
AccessGrant,
|
|
@@ -128,14 +171,21 @@ export type {
|
|
|
128
171
|
EntitlementValue,
|
|
129
172
|
EntitlementsMode,
|
|
130
173
|
FileAsset,
|
|
174
|
+
HumanInputAnswer,
|
|
175
|
+
HumanInputOption,
|
|
176
|
+
HumanInputQuestion,
|
|
177
|
+
HumanInputQuestionKind,
|
|
178
|
+
HumanInputResponse,
|
|
131
179
|
FileDownloadUrlResponse,
|
|
132
180
|
FileResourceRef,
|
|
133
181
|
FileStatus,
|
|
134
182
|
FileUploadData,
|
|
135
183
|
GetPackResponse,
|
|
136
184
|
GitHubAppInfo,
|
|
185
|
+
GitHubInstallationBinding,
|
|
137
186
|
GitHubRepositoriesResponse,
|
|
138
187
|
GitHubRepository,
|
|
188
|
+
GitHubRepositoryScope,
|
|
139
189
|
GoalSpec,
|
|
140
190
|
IntegrationClientMetadata,
|
|
141
191
|
KnownPermission,
|
|
@@ -148,6 +198,8 @@ export type {
|
|
|
148
198
|
KnowledgeSourceKind,
|
|
149
199
|
KnowledgeSourceRef,
|
|
150
200
|
GitCredentialProvider,
|
|
201
|
+
GitCredentialBindingId,
|
|
202
|
+
GitRepositoryAccess,
|
|
151
203
|
ListApiKeysResponse,
|
|
152
204
|
ListConnectionsResponse,
|
|
153
205
|
ListPacksResponse,
|
|
@@ -191,6 +243,16 @@ export type {
|
|
|
191
243
|
SessionQueueMutationResponse,
|
|
192
244
|
SessionQueueSnapshot,
|
|
193
245
|
SessionControlResponse,
|
|
246
|
+
ComposerDraft,
|
|
247
|
+
DeleteSessionQueueItemRequest,
|
|
248
|
+
EditSessionQueueItemRequest,
|
|
249
|
+
EffectiveControlBlocker,
|
|
250
|
+
EffectiveControlResumeOption,
|
|
251
|
+
EffectiveSessionControl,
|
|
252
|
+
MoveSessionQueueItemRequest,
|
|
253
|
+
SaveComposerDraftRequest,
|
|
254
|
+
SessionCommandReceipt,
|
|
255
|
+
SteerSessionQueueItemRequest,
|
|
194
256
|
WorkspaceInferenceControlResponse,
|
|
195
257
|
SessionSystemUpdate,
|
|
196
258
|
SessionSystemUpdateKind,
|
|
@@ -221,16 +283,27 @@ export type {
|
|
|
221
283
|
ViewerHeartbeatRequest,
|
|
222
284
|
ViewerHeartbeatResponse,
|
|
223
285
|
SessionEvent,
|
|
286
|
+
SessionEventListOptions,
|
|
287
|
+
SessionEventPage,
|
|
288
|
+
SessionEventPayloadMode,
|
|
289
|
+
SessionEventReadDirection,
|
|
290
|
+
SessionEventReadMode,
|
|
291
|
+
SessionEventSemanticClass,
|
|
224
292
|
SessionEventType,
|
|
225
293
|
SessionGoal,
|
|
226
294
|
SessionGoalCreatedBy,
|
|
227
295
|
SessionGoalStatus,
|
|
296
|
+
SessionHumanInputRequest,
|
|
228
297
|
SessionStatus,
|
|
229
298
|
SessionStatusChangedPayload,
|
|
230
299
|
SessionStructuredCapabilities,
|
|
231
300
|
SessionTurn,
|
|
301
|
+
ServiceTurnInitiator,
|
|
302
|
+
ServiceTurnInitiatorContext,
|
|
232
303
|
SessionTurnSource,
|
|
233
304
|
SessionTurnStatus,
|
|
305
|
+
SubmitHumanInputResponseRequest,
|
|
306
|
+
TurnInitiator,
|
|
234
307
|
ToolAuthNeededPayload,
|
|
235
308
|
UpdateConnectionRequest,
|
|
236
309
|
// Channel-A structured services (P4.4) — A1 payloads + A2 request/response.
|
|
@@ -304,8 +377,10 @@ export type {
|
|
|
304
377
|
UsageEvent,
|
|
305
378
|
UsageEventType,
|
|
306
379
|
UserApprovalDecisionEventInput,
|
|
380
|
+
UserHumanInputResponseEventInput,
|
|
307
381
|
UserMessageEventInput,
|
|
308
382
|
Workspace,
|
|
383
|
+
WorkspaceControlEvent,
|
|
309
384
|
VariableSet,
|
|
310
385
|
VariableSetVariableMetadata,
|
|
311
386
|
Rig,
|
package/src/stream.ts
CHANGED
|
@@ -34,6 +34,8 @@ export type StreamSessionEventsOptions = {
|
|
|
34
34
|
* reconnects = N+1 total open-stream calls). Defaults to unlimited.
|
|
35
35
|
*/
|
|
36
36
|
maxReconnectAttempts?: number;
|
|
37
|
+
/** Await authoritative client reconciliation before exposing `live`. */
|
|
38
|
+
beforeLive?: (() => void | Promise<void>) | undefined;
|
|
37
39
|
onStateChange?: (state: StreamConnectionState) => void;
|
|
38
40
|
};
|
|
39
41
|
|
|
@@ -75,6 +77,7 @@ export async function* streamSessionEvents(
|
|
|
75
77
|
everConnected = true;
|
|
76
78
|
failedAttempts = 0;
|
|
77
79
|
delayMs = baseDelayMs;
|
|
80
|
+
await options.beforeLive?.();
|
|
78
81
|
options.onStateChange?.("live");
|
|
79
82
|
for await (const message of parseSseStream(body)) {
|
|
80
83
|
// Re-check after every yield resumption: an abort from the consumer
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework- and transport-agnostic speech-to-text capability contract.
|
|
3
|
+
*
|
|
4
|
+
* Audio transport, microphone access, credentials, and provider SDKs belong to
|
|
5
|
+
* host-supplied adapters. This module deliberately contains no browser globals
|
|
6
|
+
* and no provider implementation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type TranscriptionCredentialMode = "managed" | "byok";
|
|
10
|
+
|
|
11
|
+
export type WorkspaceTranscriptionTarget = {
|
|
12
|
+
provider: string;
|
|
13
|
+
model: string | null;
|
|
14
|
+
credentialMode: TranscriptionCredentialMode;
|
|
15
|
+
/** Workspace-scoped connection reference. This is never a secret. */
|
|
16
|
+
credentialConnectionId: string | null;
|
|
17
|
+
region: string | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type WorkspaceTranscriptionPolicy = {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
/** Exact admin-accepted policy identity; required whenever enabled. */
|
|
23
|
+
acceptanceId: string | null;
|
|
24
|
+
primary: WorkspaceTranscriptionTarget | null;
|
|
25
|
+
/** Explicit language preference. Required when automatic detection is not accepted. */
|
|
26
|
+
language: string | null;
|
|
27
|
+
/** Whether the accepted adapter may automatically detect the spoken language. */
|
|
28
|
+
autoDetectLanguage: boolean;
|
|
29
|
+
/** Whether the accepted adapter may identify distinct speakers. */
|
|
30
|
+
diarization: {
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
maxSpeakers: number | null;
|
|
33
|
+
};
|
|
34
|
+
retention: {
|
|
35
|
+
mode: "none" | "provider-policy";
|
|
36
|
+
maxDays: number | null;
|
|
37
|
+
};
|
|
38
|
+
privacy: {
|
|
39
|
+
allowProviderLogging: boolean;
|
|
40
|
+
allowProviderTraining: boolean;
|
|
41
|
+
};
|
|
42
|
+
fallback: {
|
|
43
|
+
mode: "disabled" | "explicit";
|
|
44
|
+
targets: WorkspaceTranscriptionTarget[];
|
|
45
|
+
};
|
|
46
|
+
cost: {
|
|
47
|
+
currency: "USD";
|
|
48
|
+
maxPerHour: number | null;
|
|
49
|
+
maxPerMonth: number | null;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY: WorkspaceTranscriptionPolicy = {
|
|
54
|
+
enabled: false,
|
|
55
|
+
acceptanceId: null,
|
|
56
|
+
primary: null,
|
|
57
|
+
language: null,
|
|
58
|
+
autoDetectLanguage: false,
|
|
59
|
+
diarization: { enabled: false, maxSpeakers: null },
|
|
60
|
+
retention: { mode: "none", maxDays: null },
|
|
61
|
+
privacy: { allowProviderLogging: false, allowProviderTraining: false },
|
|
62
|
+
fallback: { mode: "disabled", targets: [] },
|
|
63
|
+
cost: { currency: "USD", maxPerHour: null, maxPerMonth: null },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type TranscriptionAdapterDescriptor = {
|
|
67
|
+
provider: string;
|
|
68
|
+
model: string | null;
|
|
69
|
+
credentialMode: TranscriptionCredentialMode;
|
|
70
|
+
region: string | null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type TranscriptionTargetSelection =
|
|
74
|
+
| { kind: "primary" }
|
|
75
|
+
| { kind: "fallback"; index: number };
|
|
76
|
+
|
|
77
|
+
export type TranscriptionPolicyBlockReason =
|
|
78
|
+
| "disabled"
|
|
79
|
+
| "unaccepted"
|
|
80
|
+
| "target_missing"
|
|
81
|
+
| "fallback_disabled"
|
|
82
|
+
| "fallback_unaccepted"
|
|
83
|
+
| "provider_mismatch"
|
|
84
|
+
| "model_mismatch"
|
|
85
|
+
| "credential_mode_mismatch"
|
|
86
|
+
| "region_mismatch";
|
|
87
|
+
|
|
88
|
+
export type TranscriptionAuthorization =
|
|
89
|
+
| {
|
|
90
|
+
authorized: true;
|
|
91
|
+
acceptanceId: string;
|
|
92
|
+
target: WorkspaceTranscriptionTarget;
|
|
93
|
+
selection: TranscriptionTargetSelection;
|
|
94
|
+
}
|
|
95
|
+
| { authorized: false; reason: TranscriptionPolicyBlockReason };
|
|
96
|
+
|
|
97
|
+
export type TranscriptionLifecycleStatus =
|
|
98
|
+
| "idle"
|
|
99
|
+
| "requesting-permission"
|
|
100
|
+
| "listening"
|
|
101
|
+
| "reconnecting"
|
|
102
|
+
| "cancelling"
|
|
103
|
+
| "closed"
|
|
104
|
+
| "error";
|
|
105
|
+
|
|
106
|
+
export type TranscriptionErrorCode =
|
|
107
|
+
| "permission_denied"
|
|
108
|
+
| "not_supported"
|
|
109
|
+
| "network"
|
|
110
|
+
| "provider"
|
|
111
|
+
| "policy_blocked"
|
|
112
|
+
| "timeout"
|
|
113
|
+
| "cancelled"
|
|
114
|
+
| "unknown";
|
|
115
|
+
|
|
116
|
+
export type TranscriptionTimeSpan = {
|
|
117
|
+
startMilliseconds: number;
|
|
118
|
+
endMilliseconds: number;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type TranscriptionSpeaker = {
|
|
122
|
+
/** Provider-neutral identity stable within the local transcription session. */
|
|
123
|
+
id: string;
|
|
124
|
+
label?: string | undefined;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type TranscriptionWord = {
|
|
128
|
+
text: string;
|
|
129
|
+
span: TranscriptionTimeSpan;
|
|
130
|
+
confidence?: number | undefined;
|
|
131
|
+
speaker?: TranscriptionSpeaker | undefined;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** Optional result detail; adapters omit fields their provider cannot supply. */
|
|
135
|
+
export type TranscriptionResultMetadata = {
|
|
136
|
+
detectedLanguage?: string | undefined;
|
|
137
|
+
span?: TranscriptionTimeSpan | undefined;
|
|
138
|
+
confidence?: number | undefined;
|
|
139
|
+
speaker?: TranscriptionSpeaker | undefined;
|
|
140
|
+
words?: TranscriptionWord[] | undefined;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export type TranscriptionDiagnostic = {
|
|
144
|
+
operation: "start" | "session" | "cancel" | "close";
|
|
145
|
+
code: TranscriptionErrorCode;
|
|
146
|
+
/** Diagnostic-only detail. React sanitizes and bounds this before forwarding it. */
|
|
147
|
+
detail: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
type TranscriptionEventBase = {
|
|
151
|
+
/** Stable across reconnects and explicitly accepted fallback attempts. */
|
|
152
|
+
localSessionId: string;
|
|
153
|
+
/** Adapter-monotonic across the entire local session, including replay. */
|
|
154
|
+
sequence: number;
|
|
155
|
+
occurredAt: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export type TranscriptionEvent =
|
|
159
|
+
| (TranscriptionEventBase & { type: "permission.requested" })
|
|
160
|
+
| (TranscriptionEventBase & {
|
|
161
|
+
type: "session.opened";
|
|
162
|
+
providerSessionId: string;
|
|
163
|
+
})
|
|
164
|
+
| (TranscriptionEventBase & {
|
|
165
|
+
type: "transcript.partial";
|
|
166
|
+
segmentId: string;
|
|
167
|
+
text: string;
|
|
168
|
+
metadata?: TranscriptionResultMetadata | undefined;
|
|
169
|
+
})
|
|
170
|
+
| (TranscriptionEventBase & {
|
|
171
|
+
type: "transcript.final";
|
|
172
|
+
segmentId: string;
|
|
173
|
+
text: string;
|
|
174
|
+
/** Stable provider/coordinator acceptance identity used for dedupe. */
|
|
175
|
+
providerAcceptanceId: string;
|
|
176
|
+
metadata?: TranscriptionResultMetadata | undefined;
|
|
177
|
+
})
|
|
178
|
+
| (TranscriptionEventBase & {
|
|
179
|
+
type: "usage";
|
|
180
|
+
audioMilliseconds: number;
|
|
181
|
+
costUsd: number | null;
|
|
182
|
+
})
|
|
183
|
+
| (TranscriptionEventBase & {
|
|
184
|
+
type: "session.reconnecting";
|
|
185
|
+
attempt: number;
|
|
186
|
+
reason: string;
|
|
187
|
+
})
|
|
188
|
+
| (TranscriptionEventBase & {
|
|
189
|
+
type: "session.error";
|
|
190
|
+
code: TranscriptionErrorCode;
|
|
191
|
+
recoverable: boolean;
|
|
192
|
+
})
|
|
193
|
+
| (TranscriptionEventBase & {
|
|
194
|
+
type: "session.closed";
|
|
195
|
+
reason: "completed" | "cancelled" | "error" | "replaced";
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export type TranscriptionSessionRequest = {
|
|
199
|
+
localSessionId: string;
|
|
200
|
+
policyAcceptanceId: string;
|
|
201
|
+
selection: TranscriptionTargetSelection;
|
|
202
|
+
target: WorkspaceTranscriptionTarget;
|
|
203
|
+
language: string | null;
|
|
204
|
+
autoDetectLanguage: boolean;
|
|
205
|
+
diarization: WorkspaceTranscriptionPolicy["diarization"];
|
|
206
|
+
retention: WorkspaceTranscriptionPolicy["retention"];
|
|
207
|
+
privacy: WorkspaceTranscriptionPolicy["privacy"];
|
|
208
|
+
cost: WorkspaceTranscriptionPolicy["cost"];
|
|
209
|
+
/** A replacement/reconnect adapter must emit events above this floor. */
|
|
210
|
+
sequenceFloor: number;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export type TranscriptionEventListener = (event: TranscriptionEvent) => void;
|
|
214
|
+
|
|
215
|
+
export type TranscriptionAdapterStartContext = {
|
|
216
|
+
/** Aborted on local cancellation, policy replacement, timeout, or unmount. */
|
|
217
|
+
signal: AbortSignal;
|
|
218
|
+
/** Non-UI observability seam; callers receive only bounded, redacted detail. */
|
|
219
|
+
reportDiagnostic: (diagnostic: TranscriptionDiagnostic) => void;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export type TranscriptionSession = {
|
|
223
|
+
readonly localSessionId: string;
|
|
224
|
+
cancel(reason?: string): Promise<void>;
|
|
225
|
+
close(): Promise<void>;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export type TranscriptionAdapter = {
|
|
229
|
+
readonly descriptor: TranscriptionAdapterDescriptor;
|
|
230
|
+
start(
|
|
231
|
+
request: TranscriptionSessionRequest,
|
|
232
|
+
listener: TranscriptionEventListener,
|
|
233
|
+
context: TranscriptionAdapterStartContext,
|
|
234
|
+
): Promise<TranscriptionSession>;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** Invalid or absent settings always resolve to the fail-closed default. */
|
|
238
|
+
export function resolveWorkspaceTranscriptionPolicy(
|
|
239
|
+
settings: unknown,
|
|
240
|
+
): WorkspaceTranscriptionPolicy {
|
|
241
|
+
if (!isRecord(settings)) return cloneDefaultPolicy();
|
|
242
|
+
const candidate = settings.transcription;
|
|
243
|
+
if (!isWorkspaceTranscriptionPolicy(candidate)) return cloneDefaultPolicy();
|
|
244
|
+
return {
|
|
245
|
+
...candidate,
|
|
246
|
+
primary: candidate.primary ? normalizeTarget(candidate.primary) : null,
|
|
247
|
+
language: candidate.language?.trim() ?? null,
|
|
248
|
+
diarization: { ...candidate.diarization },
|
|
249
|
+
retention: { ...candidate.retention },
|
|
250
|
+
privacy: { ...candidate.privacy },
|
|
251
|
+
fallback: {
|
|
252
|
+
mode: candidate.fallback.mode,
|
|
253
|
+
targets: candidate.fallback.targets.map(normalizeTarget),
|
|
254
|
+
},
|
|
255
|
+
cost: { ...candidate.cost },
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Speech authorization is intentionally independent from turn model policy.
|
|
261
|
+
* Every selected adapter must match one exact admin-accepted target.
|
|
262
|
+
*/
|
|
263
|
+
export function authorizeTranscriptionAdapter(
|
|
264
|
+
policy: WorkspaceTranscriptionPolicy,
|
|
265
|
+
descriptor: TranscriptionAdapterDescriptor,
|
|
266
|
+
selection: TranscriptionTargetSelection = { kind: "primary" },
|
|
267
|
+
): TranscriptionAuthorization {
|
|
268
|
+
if (!isWorkspaceTranscriptionPolicy(policy)) {
|
|
269
|
+
return { authorized: false, reason: "unaccepted" };
|
|
270
|
+
}
|
|
271
|
+
if (!policy.enabled) return { authorized: false, reason: "disabled" };
|
|
272
|
+
if (!policy.acceptanceId) return { authorized: false, reason: "unaccepted" };
|
|
273
|
+
let target: WorkspaceTranscriptionTarget | null | undefined;
|
|
274
|
+
if (selection.kind === "primary") {
|
|
275
|
+
target = policy.primary;
|
|
276
|
+
} else {
|
|
277
|
+
if (policy.fallback.mode !== "explicit") {
|
|
278
|
+
return { authorized: false, reason: "fallback_disabled" };
|
|
279
|
+
}
|
|
280
|
+
target = policy.fallback.targets[selection.index];
|
|
281
|
+
if (!target) return { authorized: false, reason: "fallback_unaccepted" };
|
|
282
|
+
}
|
|
283
|
+
if (!target) return { authorized: false, reason: "target_missing" };
|
|
284
|
+
const acceptedTarget = normalizeTarget(target);
|
|
285
|
+
if (acceptedTarget.provider !== descriptor.provider) {
|
|
286
|
+
return { authorized: false, reason: "provider_mismatch" };
|
|
287
|
+
}
|
|
288
|
+
if (acceptedTarget.model !== descriptor.model) {
|
|
289
|
+
return { authorized: false, reason: "model_mismatch" };
|
|
290
|
+
}
|
|
291
|
+
if (acceptedTarget.credentialMode !== descriptor.credentialMode) {
|
|
292
|
+
return { authorized: false, reason: "credential_mode_mismatch" };
|
|
293
|
+
}
|
|
294
|
+
if (acceptedTarget.region !== descriptor.region) {
|
|
295
|
+
return { authorized: false, reason: "region_mismatch" };
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
authorized: true,
|
|
299
|
+
acceptanceId: policy.acceptanceId,
|
|
300
|
+
target: acceptedTarget,
|
|
301
|
+
selection,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function createTranscriptionSessionRequest(input: {
|
|
306
|
+
policy: WorkspaceTranscriptionPolicy;
|
|
307
|
+
adapter: TranscriptionAdapter;
|
|
308
|
+
localSessionId: string;
|
|
309
|
+
selection?: TranscriptionTargetSelection | undefined;
|
|
310
|
+
sequenceFloor?: number | undefined;
|
|
311
|
+
}): TranscriptionSessionRequest | null {
|
|
312
|
+
const sequenceFloor = input.sequenceFloor ?? 0;
|
|
313
|
+
if (!Number.isSafeInteger(sequenceFloor) || sequenceFloor < 0) return null;
|
|
314
|
+
const authorization = authorizeTranscriptionAdapter(
|
|
315
|
+
input.policy,
|
|
316
|
+
input.adapter.descriptor,
|
|
317
|
+
input.selection,
|
|
318
|
+
);
|
|
319
|
+
if (!authorization.authorized) return null;
|
|
320
|
+
return {
|
|
321
|
+
localSessionId: input.localSessionId,
|
|
322
|
+
policyAcceptanceId: authorization.acceptanceId,
|
|
323
|
+
selection: authorization.selection,
|
|
324
|
+
target: { ...authorization.target },
|
|
325
|
+
language: input.policy.language?.trim() ?? null,
|
|
326
|
+
autoDetectLanguage: input.policy.autoDetectLanguage,
|
|
327
|
+
diarization: { ...input.policy.diarization },
|
|
328
|
+
retention: { ...input.policy.retention },
|
|
329
|
+
privacy: { ...input.policy.privacy },
|
|
330
|
+
cost: { ...input.policy.cost },
|
|
331
|
+
sequenceFloor,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function cloneDefaultPolicy(): WorkspaceTranscriptionPolicy {
|
|
336
|
+
return {
|
|
337
|
+
...DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY,
|
|
338
|
+
diarization: { ...DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY.diarization },
|
|
339
|
+
retention: { ...DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY.retention },
|
|
340
|
+
privacy: { ...DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY.privacy },
|
|
341
|
+
fallback: { mode: "disabled", targets: [] },
|
|
342
|
+
cost: { ...DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY.cost },
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isWorkspaceTranscriptionPolicy(value: unknown): value is WorkspaceTranscriptionPolicy {
|
|
347
|
+
if (!isRecord(value) || typeof value.enabled !== "boolean") return false;
|
|
348
|
+
if (
|
|
349
|
+
!hasOnlyKeys(value, [
|
|
350
|
+
"enabled",
|
|
351
|
+
"acceptanceId",
|
|
352
|
+
"primary",
|
|
353
|
+
"language",
|
|
354
|
+
"autoDetectLanguage",
|
|
355
|
+
"diarization",
|
|
356
|
+
"retention",
|
|
357
|
+
"privacy",
|
|
358
|
+
"fallback",
|
|
359
|
+
"cost",
|
|
360
|
+
])
|
|
361
|
+
) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
if (!(value.acceptanceId === null || isUuid(value.acceptanceId))) return false;
|
|
365
|
+
if (!(value.primary === null || isTarget(value.primary))) return false;
|
|
366
|
+
if (!(value.language === null || isBoundedString(value.language, 64))) return false;
|
|
367
|
+
if (typeof value.autoDetectLanguage !== "boolean") return false;
|
|
368
|
+
if (
|
|
369
|
+
!isRecord(value.diarization) ||
|
|
370
|
+
!hasOnlyKeys(value.diarization, ["enabled", "maxSpeakers"]) ||
|
|
371
|
+
typeof value.diarization.enabled !== "boolean" ||
|
|
372
|
+
!(
|
|
373
|
+
value.diarization.maxSpeakers === null ||
|
|
374
|
+
(isBoundedInteger(value.diarization.maxSpeakers, 100) && value.diarization.maxSpeakers >= 2)
|
|
375
|
+
)
|
|
376
|
+
) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
if (!value.diarization.enabled && value.diarization.maxSpeakers !== null) return false;
|
|
380
|
+
if (!isRecord(value.retention) || !hasOnlyKeys(value.retention, ["mode", "maxDays"])) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
if (value.retention.mode !== "none" && value.retention.mode !== "provider-policy") return false;
|
|
384
|
+
if (!(value.retention.maxDays === null || isBoundedInteger(value.retention.maxDays, 3650))) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
if (
|
|
388
|
+
!isRecord(value.privacy) ||
|
|
389
|
+
!hasOnlyKeys(value.privacy, ["allowProviderLogging", "allowProviderTraining"]) ||
|
|
390
|
+
typeof value.privacy.allowProviderLogging !== "boolean" ||
|
|
391
|
+
typeof value.privacy.allowProviderTraining !== "boolean"
|
|
392
|
+
) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
if (!isRecord(value.fallback) || !hasOnlyKeys(value.fallback, ["mode", "targets"])) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
if (value.fallback.mode !== "disabled" && value.fallback.mode !== "explicit") return false;
|
|
399
|
+
if (
|
|
400
|
+
!Array.isArray(value.fallback.targets) ||
|
|
401
|
+
value.fallback.targets.length > 8 ||
|
|
402
|
+
!value.fallback.targets.every(isTarget)
|
|
403
|
+
) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (value.fallback.mode === "disabled" && value.fallback.targets.length !== 0) return false;
|
|
407
|
+
if (value.fallback.mode === "explicit" && value.fallback.targets.length === 0) return false;
|
|
408
|
+
if (
|
|
409
|
+
!isRecord(value.cost) ||
|
|
410
|
+
!hasOnlyKeys(value.cost, ["currency", "maxPerHour", "maxPerMonth"]) ||
|
|
411
|
+
value.cost.currency !== "USD"
|
|
412
|
+
) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
if (!isNullableBoundedNumber(value.cost.maxPerHour, 10_000)) return false;
|
|
416
|
+
if (!isNullableBoundedNumber(value.cost.maxPerMonth, 1_000_000)) return false;
|
|
417
|
+
if (value.enabled && (!value.acceptanceId || !value.primary)) return false;
|
|
418
|
+
if (value.enabled && !value.autoDetectLanguage && value.language === null) return false;
|
|
419
|
+
if (value.autoDetectLanguage && value.language !== null) return false;
|
|
420
|
+
const targets = [value.primary, ...value.fallback.targets].filter(
|
|
421
|
+
(target): target is WorkspaceTranscriptionTarget => target !== null,
|
|
422
|
+
);
|
|
423
|
+
if (new Set(targets.map(targetKey)).size !== targets.length) return false;
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function targetKey(target: WorkspaceTranscriptionTarget): string {
|
|
428
|
+
return [
|
|
429
|
+
target.provider.trim(),
|
|
430
|
+
target.model?.trim() ?? "",
|
|
431
|
+
target.credentialMode,
|
|
432
|
+
target.credentialConnectionId ?? "",
|
|
433
|
+
target.region?.trim() ?? "",
|
|
434
|
+
].join("\u0000");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function isTarget(value: unknown): value is WorkspaceTranscriptionTarget {
|
|
438
|
+
if (!isRecord(value)) return false;
|
|
439
|
+
if (
|
|
440
|
+
!hasOnlyKeys(value, ["provider", "model", "credentialMode", "credentialConnectionId", "region"])
|
|
441
|
+
) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (!isBoundedString(value.provider, 128)) return false;
|
|
445
|
+
if (!(value.model === null || isBoundedString(value.model, 256))) return false;
|
|
446
|
+
if (value.credentialMode !== "managed" && value.credentialMode !== "byok") return false;
|
|
447
|
+
if (value.provider.trim() === "azure-speech" && value.credentialMode !== "byok") return false;
|
|
448
|
+
if (!(value.credentialConnectionId === null || isUuid(value.credentialConnectionId))) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (!(value.region === null || isBoundedString(value.region, 128))) return false;
|
|
452
|
+
if (value.credentialMode === "byok" && value.credentialConnectionId === null) return false;
|
|
453
|
+
if (value.credentialMode === "managed" && value.credentialConnectionId !== null) return false;
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function normalizeTarget(target: WorkspaceTranscriptionTarget): WorkspaceTranscriptionTarget {
|
|
458
|
+
return {
|
|
459
|
+
provider: target.provider.trim(),
|
|
460
|
+
model: target.model?.trim() ?? null,
|
|
461
|
+
credentialMode: target.credentialMode,
|
|
462
|
+
credentialConnectionId: target.credentialConnectionId,
|
|
463
|
+
region: target.region?.trim() ?? null,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
468
|
+
return typeof value === "object" && value !== null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function hasOnlyKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
472
|
+
const accepted = new Set(keys);
|
|
473
|
+
return Object.keys(value).every((key) => accepted.has(key));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isBoundedString(value: unknown, maximum: number): value is string {
|
|
477
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= maximum;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function isUuid(value: unknown): value is string {
|
|
481
|
+
return (
|
|
482
|
+
typeof value === "string" &&
|
|
483
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function isBoundedInteger(value: unknown, maximum: number): value is number {
|
|
488
|
+
return Number.isInteger(value) && (value as number) >= 0 && (value as number) <= maximum;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function isNullableBoundedNumber(value: unknown, maximum: number): boolean {
|
|
492
|
+
return (
|
|
493
|
+
value === null ||
|
|
494
|
+
(typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= maximum)
|
|
495
|
+
);
|
|
496
|
+
}
|