@opengeni/sdk 0.15.0 → 0.23.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/src/index.ts CHANGED
@@ -2,8 +2,10 @@ 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
10
  export {
9
11
  OpenGeniApiContractMismatchError,
@@ -50,11 +52,41 @@ export type {
50
52
  } from "./stream";
51
53
  export { streamWorkspaceControlEvents } from "./workspace-control-stream";
52
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";
53
83
  export {
54
84
  KNOWN_PERMISSIONS,
55
85
  KNOWN_USAGE_EVENT_TYPES,
56
86
  OPENGENI_API_CONTRACT_HEADER,
57
87
  OPENGENI_API_CONTRACT_REVISION,
88
+ RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
89
+ RETAINED_OUTPUT_MAX_PAGE_BYTES,
58
90
  SESSION_EVENT_TYPES,
59
91
  } from "./types";
60
92
  export type {
@@ -91,12 +123,28 @@ export type {
91
123
  CapabilityUnavailableReason,
92
124
  ClientConfig,
93
125
  ClientModel,
126
+ ModelAvailabilityV1,
127
+ ModelBillingAttributionV1,
128
+ ModelCapabilitiesV1,
129
+ ModelCapabilityStateV1,
130
+ ModelCapabilitySupportV1,
131
+ ModelCredentialReadinessV1,
132
+ ModelCredentialSourceV1,
133
+ ModelPricingScheduleV1,
134
+ ModelPricingV1,
135
+ WorkspaceModelCatalogModel,
136
+ WorkspaceModelCatalogResponse,
94
137
  CodexAccount,
138
+ CodexAccountOverview,
95
139
  CodexAccountsResponse,
140
+ CodexAllocatorUpdate,
96
141
  CodexAccountSwitchedPayload,
97
142
  CodexConnectionStatus,
98
143
  CodexConnectStart,
99
144
  CodexConnectPoll,
145
+ CodexOverviewResponse,
146
+ CodexResetCredit,
147
+ CodexResetRedemptionRecovery,
100
148
  CodexRotationSettings,
101
149
  CodexUsage,
102
150
  CodexUsageMap,
@@ -141,14 +189,21 @@ export type {
141
189
  EntitlementValue,
142
190
  EntitlementsMode,
143
191
  FileAsset,
192
+ HumanInputAnswer,
193
+ HumanInputOption,
194
+ HumanInputQuestion,
195
+ HumanInputQuestionKind,
196
+ HumanInputResponse,
144
197
  FileDownloadUrlResponse,
145
198
  FileResourceRef,
146
199
  FileStatus,
147
200
  FileUploadData,
148
201
  GetPackResponse,
149
202
  GitHubAppInfo,
203
+ GitHubInstallationBinding,
150
204
  GitHubRepositoriesResponse,
151
205
  GitHubRepository,
206
+ GitHubRepositoryScope,
152
207
  GoalSpec,
153
208
  IntegrationClientMetadata,
154
209
  KnownPermission,
@@ -161,6 +216,8 @@ export type {
161
216
  KnowledgeSourceKind,
162
217
  KnowledgeSourceRef,
163
218
  GitCredentialProvider,
219
+ GitCredentialBindingId,
220
+ GitRepositoryAccess,
164
221
  ListApiKeysResponse,
165
222
  ListConnectionsResponse,
166
223
  ListPacksResponse,
@@ -173,6 +230,13 @@ export type {
173
230
  Permission,
174
231
  ProductAccessMode,
175
232
  ReasoningEffort,
233
+ RetainedArtifactContent,
234
+ RetainedArtifactContentOptions,
235
+ RetainedArtifactMetadata,
236
+ RetainedArtifactReference,
237
+ RetainedArtifactUnavailable,
238
+ RetainedOutputKind,
239
+ RetainedOutputUnavailableReason,
176
240
  RecordingAvailablePayload,
177
241
  RecordingCodec,
178
242
  RecordingContentType,
@@ -201,6 +265,7 @@ export type {
201
265
  SessionCapabilities,
202
266
  SessionListResponse,
203
267
  SessionLineageResponse,
268
+ SessionEffectiveToolPolicy,
204
269
  SessionQueueMutationResponse,
205
270
  SessionQueueSnapshot,
206
271
  SessionControlResponse,
@@ -221,8 +286,10 @@ export type {
221
286
  SessionSummary,
222
287
  LineageNode,
223
288
  SessionMcpCredentialUpdateInput,
289
+ SessionMcpApprovalPolicy,
224
290
  SessionMcpServerInput,
225
291
  SessionMcpServerMetadata,
292
+ SessionToolPolicy,
226
293
  // Per-surface capability cell aliases (views of SessionCapabilities).
227
294
  FileSystemCapability,
228
295
  TerminalCapability,
@@ -244,16 +311,31 @@ export type {
244
311
  ViewerHeartbeatRequest,
245
312
  ViewerHeartbeatResponse,
246
313
  SessionEvent,
314
+ SessionEventCompactResult,
315
+ SessionEventCompactResultOptions,
316
+ SessionEventListOptions,
317
+ SessionEventLatestClass,
318
+ SessionEventPage,
319
+ SessionEventPayloadMode,
320
+ SessionEventReadDirection,
321
+ SessionEventReadMode,
322
+ SessionEventResultMode,
323
+ SessionEventSemanticClass,
247
324
  SessionEventType,
248
325
  SessionGoal,
249
326
  SessionGoalCreatedBy,
250
327
  SessionGoalStatus,
328
+ SessionHumanInputRequest,
251
329
  SessionStatus,
252
330
  SessionStatusChangedPayload,
253
331
  SessionStructuredCapabilities,
254
332
  SessionTurn,
333
+ ServiceTurnInitiator,
334
+ ServiceTurnInitiatorContext,
255
335
  SessionTurnSource,
256
336
  SessionTurnStatus,
337
+ SubmitHumanInputResponseRequest,
338
+ TurnInitiator,
257
339
  ToolAuthNeededPayload,
258
340
  UpdateConnectionRequest,
259
341
  // Channel-A structured services (P4.4) — A1 payloads + A2 request/response.
@@ -316,6 +398,8 @@ export type {
316
398
  UpdateKnowledgeMemoryRequest,
317
399
  UpdateScheduledTaskRequest,
318
400
  UpdateSessionGoalRequest,
401
+ UpdateSessionMcpApprovalPolicyRequest,
402
+ UpdateSessionMcpApprovalPolicyResponse,
319
403
  UpdateSessionPinRequest,
320
404
  UpdateSessionRequest,
321
405
  UpdateVariableSetRequest,
@@ -327,6 +411,7 @@ export type {
327
411
  UsageEvent,
328
412
  UsageEventType,
329
413
  UserApprovalDecisionEventInput,
414
+ UserHumanInputResponseEventInput,
330
415
  UserMessageEventInput,
331
416
  Workspace,
332
417
  WorkspaceControlEvent,
@@ -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
+ }