@opengeni/api-router 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/access-grant-rls.d.ts +7 -0
  2. package/dist/app.d.ts +2 -2
  3. package/dist/app.js +1 -1
  4. package/dist/azure-live.d.ts +28 -0
  5. package/dist/{chunk-IZEGLLKZ.js → chunk-KKEXIBQG.js} +1188 -387
  6. package/dist/{chunk-IZEGLLKZ.js.map → chunk-KKEXIBQG.js.map} +1 -1
  7. package/dist/codemode.d.ts +3 -3
  8. package/dist/index.js +1 -1
  9. package/dist/integrations/oauth-profiles.d.ts +3 -1
  10. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  11. package/dist/mcp/server.d.ts +1 -1
  12. package/dist/transcription/billing-refusal.d.ts +8 -0
  13. package/dist/transcription/normalize.d.ts +23 -0
  14. package/dist/transcription/providers/azure-mai.d.ts +12 -0
  15. package/dist/transcription/providers/azure-openai.d.ts +4 -0
  16. package/dist/transcription/providers/openai.d.ts +16 -0
  17. package/dist/transcription/service.d.ts +10 -1
  18. package/dist/transcription/wav-duration.d.ts +8 -0
  19. package/package.json +20 -20
  20. package/src/access-grant-rls.ts +18 -10
  21. package/src/app.ts +45 -7
  22. package/src/azure-live.ts +103 -0
  23. package/src/codemode.ts +3 -5
  24. package/src/integrations/oauth-client.ts +1 -0
  25. package/src/integrations/oauth-profiles.ts +17 -6
  26. package/src/routes/editable-artifacts.ts +8 -0
  27. package/src/routes/session-artifact-associations.ts +80 -6
  28. package/src/routes/sessions.ts +185 -25
  29. package/src/routes/transcription-recordings.ts +71 -4
  30. package/src/routes/transcriptions.ts +26 -2
  31. package/src/routes/workspaces.ts +44 -7
  32. package/src/sandbox/channel-a.ts +22 -0
  33. package/src/transcription/billing-refusal.ts +14 -0
  34. package/src/transcription/normalize.ts +77 -0
  35. package/src/transcription/providers/azure-mai.ts +98 -0
  36. package/src/transcription/providers/azure-openai.ts +16 -7
  37. package/src/transcription/providers/openai.ts +79 -5
  38. package/src/transcription/segmenter.ts +7 -1
  39. package/src/transcription/service.ts +215 -10
  40. package/src/transcription/wav-duration.ts +39 -0
@@ -6517,8 +6517,8 @@ import {
6517
6517
  canonicalPublicOrigin as canonicalPublicOrigin3,
6518
6518
  configuredAllowedReasoningEfforts,
6519
6519
  resolveFirstPartyMcpToolPolicy as resolveFirstPartyMcpToolPolicy2,
6520
- resolveVoiceInputProviderRegistry as resolveVoiceInputProviderRegistry2,
6521
- UnsupportedLatencyModeError as UnsupportedLatencyModeError2
6520
+ UnsupportedLatencyModeError as UnsupportedLatencyModeError2,
6521
+ voiceInputPricingIssues
6522
6522
  } from "@opengeni/config";
6523
6523
  import {
6524
6524
  AGENT_CAPABILITY_IDS,
@@ -6532,6 +6532,7 @@ import {
6532
6532
  resolveWorkspaceMemoryEnabled,
6533
6533
  resolveWorkspaceMemoryPromptMode,
6534
6534
  VOICE_INPUT_ACCEPTED_MIME_TYPES as VOICE_INPUT_ACCEPTED_MIME_TYPES2,
6535
+ WorkspaceVoiceInputSettings as WorkspaceVoiceInputSettings3,
6535
6536
  TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS as TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS2,
6536
6537
  ToolGatewayApprovalRequest as ToolGatewayApprovalRequest2,
6537
6538
  ToolGatewayCallRequest as ToolGatewayCallRequest2
@@ -7065,8 +7066,8 @@ import {
7065
7066
  CodexCompactionV2ProviderLockedError,
7066
7067
  ManagedAuthActorLeaseOutcomeUnknownError,
7067
7068
  hasPermission as hasPermission34,
7068
- requireAccessGrant as requireAccessGrant54,
7069
- requireAccessGrantAuthorization as requireAccessGrantAuthorization40,
7069
+ requireAccessGrant as requireAccessGrant53,
7070
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization41,
7070
7071
  accountScopedApiKeyWorkspaceAuthority as accountScopedApiKeyWorkspaceAuthority10,
7071
7072
  requireAccessContext as requireAccessContext15,
7072
7073
  requirePermission as requirePermission13,
@@ -8515,7 +8516,7 @@ function startManagedSignInNotificationDelivery(db, transport) {
8515
8516
  if (active) return;
8516
8517
  active = true;
8517
8518
  try {
8518
- for (let count = 0; count < 10; count++)
8519
+ for (let count2 = 0; count2 < 10; count2++)
8519
8520
  if (await deliverManagedSignInNotification(db, transport) === "outcome_unknown") break;
8520
8521
  } finally {
8521
8522
  active = false;
@@ -10277,9 +10278,9 @@ function dispositionFilenameParameters(filename) {
10277
10278
  const cleaned = Array.from(
10278
10279
  filename.replace(/[\u0000-\u001f\u007f/\\]+/g, "_").replace(/[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g, "_").trim()
10279
10280
  ).slice(0, DISPOSITION_FILENAME_MAX_CHARS).join("");
10280
- const ascii2 = cleaned.replace(/[^A-Za-z0-9._ -]+/g, "_").replace(/\s+/g, " ").trim().slice(0, DISPOSITION_FILENAME_MAX_CHARS) || "download";
10281
- if (!cleaned || ascii2 === cleaned) return `filename="${ascii2}"`;
10282
- return `filename="${ascii2}"; filename*=UTF-8''${encodeExtendedParameterValue(cleaned)}`;
10281
+ const ascii3 = cleaned.replace(/[^A-Za-z0-9._ -]+/g, "_").replace(/\s+/g, " ").trim().slice(0, DISPOSITION_FILENAME_MAX_CHARS) || "download";
10282
+ if (!cleaned || ascii3 === cleaned) return `filename="${ascii3}"`;
10283
+ return `filename="${ascii3}"; filename*=UTF-8''${encodeExtendedParameterValue(cleaned)}`;
10283
10284
  }
10284
10285
  function userContentDispositionHeaders(contentType, filename) {
10285
10286
  if (!isActiveUserContentType(contentType)) return {};
@@ -10817,13 +10818,18 @@ import {
10817
10818
  import {
10818
10819
  freezeAgentLearningPolicy,
10819
10820
  withSessionRlsActorContext,
10820
- withCreditDebitAttribution
10821
+ withCreditDebitAttribution,
10822
+ currentCreditDebitAttribution
10821
10823
  } from "@opengeni/db";
10822
10824
  import { HTTPException as HTTPException21 } from "hono/http-exception";
10825
+ function creditDebitAttributionForGrant(grant) {
10826
+ if (grant.principalKind === "agent_attempt") return currentCreditDebitAttribution();
10827
+ return grant.principalKind === "human_session" ? { kind: "human", initiatingHumanSubjectId: grant.subjectId } : ["service", "api_key", "configured_key", "mcp_gateway"].includes(grant.principalKind ?? "") ? { kind: "service" } : { kind: "unknown" };
10828
+ }
10823
10829
  async function withAccessGrantSessionRlsContext(deps, grant, fn) {
10824
10830
  if (grant.principalKind !== "agent_attempt") {
10825
10831
  return await withCreditDebitAttribution(
10826
- grant.principalKind === "human_session" ? { kind: "human", initiatingHumanSubjectId: grant.subjectId } : grant.principalKind === "service" || grant.principalKind === "api_key" || grant.principalKind === "configured_key" || grant.principalKind === "mcp_gateway" ? { kind: "service" } : { kind: "unknown" },
10832
+ creditDebitAttributionForGrant(grant),
10827
10833
  () => withSessionRlsActorContext({ subjectId: grant.subjectId }, fn)
10828
10834
  );
10829
10835
  }
@@ -11270,6 +11276,13 @@ function registerEditableArtifactRoutes(app, deps) {
11270
11276
  const body3 = await parseBoundedMaterializationRequest(context);
11271
11277
  const scope = editableArtifactScope({ accountId: grant.accountId, workspaceId });
11272
11278
  const actor = editableArtifactActorForGrant(grant, body3.replicaId);
11279
+ if (!deps.settings.artifactMaterializerDeployed) {
11280
+ throw new ApiHttpError(503, {
11281
+ code: "upstream_unavailable",
11282
+ message: "File export is not available on this deployment.",
11283
+ retryable: false
11284
+ });
11285
+ }
11273
11286
  try {
11274
11287
  const result = await requireEditableArtifactExports(deps).enqueueMaterialization({
11275
11288
  scope,
@@ -12498,6 +12511,7 @@ import {
12498
12511
  SandboxResumeIdentityMismatchError,
12499
12512
  SandboxResumeIdentityUnavailableError,
12500
12513
  RoutingActiveRouteChangedError,
12514
+ RoutingMutationOutputRejectedError,
12501
12515
  RoutingWorkspaceRootChangedError,
12502
12516
  SelfhostedWorkspaceRootChangedError,
12503
12517
  ChannelAConflictError,
@@ -13594,6 +13608,20 @@ function mapChannelAError(error, waitSignal) {
13594
13608
  });
13595
13609
  if (error instanceof SandboxResumeIdentityMismatchError || error instanceof SandboxResumeIdentityUnavailableError)
13596
13610
  return new HTTPException22(409, { message: error.message });
13611
+ if (error instanceof RoutingMutationOutputRejectedError) {
13612
+ const revoked = error.reasonCode === "authority_revoked";
13613
+ return new ApiHttpError(revoked ? 403 : 409, {
13614
+ code: revoked ? "forbidden" : "conflict",
13615
+ message: error.message,
13616
+ retryable: false,
13617
+ outcomeUnknown: false,
13618
+ details: {
13619
+ code: error.code,
13620
+ reasonCode: error.reasonCode,
13621
+ physicalOutcome: "resolved"
13622
+ }
13623
+ });
13624
+ }
13597
13625
  if (error instanceof RoutingActiveRouteChangedError || error instanceof RoutingWorkspaceRootChangedError || error instanceof SelfhostedWorkspaceRootChangedError || error instanceof ChannelAFileSystemRouteChangedError)
13598
13626
  return new ApiHttpError(409, {
13599
13627
  code: "conflict",
@@ -13660,6 +13688,13 @@ function channelAOperationFailureDiagnostic(error, waitSignal) {
13660
13688
  errorCode: "sandbox_channel_a_cancelled"
13661
13689
  };
13662
13690
  }
13691
+ if (error instanceof RoutingMutationOutputRejectedError) {
13692
+ return {
13693
+ reason: "request_rejected",
13694
+ status: error.reasonCode === "authority_revoked" ? 403 : 409,
13695
+ errorCode: "sandbox_channel_a_operation_failed"
13696
+ };
13697
+ }
13663
13698
  if (error instanceof SandboxProviderReadLockUnavailableError) {
13664
13699
  return {
13665
13700
  reason: "provider_read_busy",
@@ -15507,8 +15542,8 @@ function tokenHash(value) {
15507
15542
  function mcpOAuthRegistrationClientKey(c, trust) {
15508
15543
  return `mcp-oauth-registration:${trustedRequestSourceRateLimitKey(c, trust)}`;
15509
15544
  }
15510
- function expiresIn(seconds) {
15511
- return new Date(Date.now() + seconds * 1e3);
15545
+ function expiresIn(seconds2) {
15546
+ return new Date(Date.now() + seconds2 * 1e3);
15512
15547
  }
15513
15548
  function authorizationRedirect(redirectUri, params) {
15514
15549
  const url = new URL(redirectUri);
@@ -17002,7 +17037,11 @@ import {
17002
17037
  import { OPENGENI_PERSONAL_SLACK_MCP_URL } from "@opengeni/contracts";
17003
17038
  import { HTTPException as HTTPException31 } from "hono/http-exception";
17004
17039
  import { z as z25 } from "zod";
17005
- import { GMAIL_REST_MCP_TOOLS, gmailToolSupportsScopes } from "@opengeni/runtime/gmail-rest-mcp";
17040
+ import {
17041
+ GMAIL_REST_MCP_TOOLS,
17042
+ gmailToolAvailableOnDeployment,
17043
+ gmailToolSupportsScopes
17044
+ } from "@opengeni/runtime/gmail-rest-mcp";
17006
17045
 
17007
17046
  // src/integrations/provider-domain.ts
17008
17047
  import { HTTPException as HTTPException30 } from "hono/http-exception";
@@ -17137,10 +17176,12 @@ var OFFICIAL_GMAIL_PROFILE = {
17137
17176
  }
17138
17177
  return { gmailEmail: payload.emailAddress };
17139
17178
  },
17140
- toolsForScopes: (scopes) => {
17141
- return GMAIL_REST_MCP_TOOLS.filter((tool) => gmailToolSupportsScopes(tool.name, scopes)).map(
17142
- ({ name, description }) => ({ name, ...description ? { description } : {} })
17143
- );
17179
+ toolsForScopes: (scopes, deployment) => {
17180
+ return GMAIL_REST_MCP_TOOLS.filter(
17181
+ (tool) => gmailToolSupportsScopes(tool.name, scopes) && gmailToolAvailableOnDeployment(tool.name, {
17182
+ watchTopicName: deployment.gmailWatchTopicName
17183
+ })
17184
+ ).map(({ name, description }) => ({ name, ...description ? { description } : {} }));
17144
17185
  }
17145
17186
  }
17146
17187
  };
@@ -19122,7 +19163,8 @@ async function verifyMcpToolsListNonFatal(observability, settings, state, token,
19122
19163
  grantRejected = local.isRejectedGrant?.(payload) ?? false;
19123
19164
  providerIdentity = local.validateIdentity(payload);
19124
19165
  const verifiedTools = local.toolsForScopes(
19125
- grantedScopes(token.scopeText, state.authorizeScopes, profile)
19166
+ grantedScopes(token.scopeText, state.authorizeScopes, profile),
19167
+ settings
19126
19168
  );
19127
19169
  if (local.required && verifiedTools.length === 0)
19128
19170
  throw new Error("Connector verification did not report an authorized tool scope");
@@ -20502,15 +20544,15 @@ import { COMMAND_OUTPUT_MAX_BYTES } from "@opengeni/db/session-background-comman
20502
20544
  var COMMAND_READ_MAX_WAIT_SECONDS = 50;
20503
20545
  var COMMAND_WAIT_DEFAULT_SECONDS = 45;
20504
20546
  async function readCommandWithWait(input) {
20505
- const seconds = input.waitSeconds ?? 0;
20506
- if (!Number.isInteger(seconds) || seconds < 0 || seconds > COMMAND_READ_MAX_WAIT_SECONDS) {
20547
+ const seconds2 = input.waitSeconds ?? 0;
20548
+ if (!Number.isInteger(seconds2) || seconds2 < 0 || seconds2 > COMMAND_READ_MAX_WAIT_SECONDS) {
20507
20549
  throw new Error("waitSeconds must be between 0 and 50");
20508
20550
  }
20509
20551
  if (input.maxOutputBytes !== void 0 && (!Number.isInteger(input.maxOutputBytes) || input.maxOutputBytes < 4 || input.maxOutputBytes > COMMAND_OUTPUT_MAX_BYTES)) {
20510
20552
  throw new Error("maxOutputBytes must be between 4 and 65536");
20511
20553
  }
20512
20554
  const started2 = Date.now();
20513
- const deadline = started2 + seconds * 1e3;
20555
+ const deadline = started2 + seconds2 * 1e3;
20514
20556
  let unsubscribe;
20515
20557
  let wake;
20516
20558
  let changed2 = false;
@@ -20521,7 +20563,7 @@ async function readCommandWithWait(input) {
20521
20563
  };
20522
20564
  input.signal?.addEventListener("abort", notify);
20523
20565
  try {
20524
- if (seconds > 0) {
20566
+ if (seconds2 > 0) {
20525
20567
  try {
20526
20568
  unsubscribe = await input.subscribe((events) => {
20527
20569
  if (events.some(
@@ -20541,7 +20583,7 @@ async function readCommandWithWait(input) {
20541
20583
  return {
20542
20584
  ...result,
20543
20585
  waitedMs: Date.now() - started2,
20544
- timedOut: seconds > 0 && expired && !result.terminal && result.chunks.length === 0 && !result.hasMore,
20586
+ timedOut: seconds2 > 0 && expired && !result.terminal && result.chunks.length === 0 && !result.hasMore,
20545
20587
  aborted: input.signal?.aborted ?? false,
20546
20588
  liveFanout
20547
20589
  };
@@ -22021,7 +22063,7 @@ function buildScheduledTaskDetailMcp(task, budget, reducedForBytes) {
22021
22063
  deliveredCount: resources.length,
22022
22064
  originalBytes: jsonBytes(task.agentConfig.resources),
22023
22065
  truncatedIdentityFieldCount: resourceProjections.reduce(
22024
- (count, projection) => count + projection.truncatedFieldCount,
22066
+ (count2, projection) => count2 + projection.truncatedFieldCount,
22025
22067
  0
22026
22068
  )
22027
22069
  },
@@ -22030,7 +22072,7 @@ function buildScheduledTaskDetailMcp(task, budget, reducedForBytes) {
22030
22072
  deliveredCount: tools.length,
22031
22073
  originalBytes: jsonBytes(task.agentConfig.tools),
22032
22074
  truncatedIdentityFieldCount: toolProjections.reduce(
22033
- (count, projection) => count + projection.truncatedFieldCount,
22075
+ (count2, projection) => count2 + projection.truncatedFieldCount,
22034
22076
  0
22035
22077
  )
22036
22078
  },
@@ -25033,9 +25075,9 @@ function slackRetryAfterMs(response) {
25033
25075
  if (response.status !== 429) return null;
25034
25076
  const raw = response.headers.get("retry-after");
25035
25077
  if (!raw || !/^\d+$/.test(raw)) return null;
25036
- const seconds = Number(raw);
25037
- if (!Number.isSafeInteger(seconds) || seconds < 1) return null;
25038
- return Math.min(seconds, 3600) * 1e3;
25078
+ const seconds2 = Number(raw);
25079
+ if (!Number.isSafeInteger(seconds2) || seconds2 < 1) return null;
25080
+ return Math.min(seconds2, 3600) * 1e3;
25039
25081
  }
25040
25082
  function assertOpenGeniSlackBotScopes(grantedScopes2) {
25041
25083
  const policy = evaluateOpenGeniSlackBotScopes(grantedScopes2);
@@ -25613,11 +25655,11 @@ function nextSlackFilesListPage(payload, requested, returnedFileCount) {
25613
25655
  if (responseMetadata && Object.hasOwn(responseMetadata, "next_cursor")) {
25614
25656
  throw new SlackBotProviderError("invalid_files_paging");
25615
25657
  }
25616
- const count = paging?.count;
25658
+ const count2 = paging?.count;
25617
25659
  const total = paging?.total;
25618
25660
  const page2 = paging?.page;
25619
25661
  const pages = paging?.pages;
25620
- if (!positiveSafeInt(count) || !nonNegativeSafeInt(total) || !positiveSafeInt(page2) || !nonNegativeSafeInt(pages) || count !== requested.count || page2 !== requested.page || returnedFileCount > count || pages !== (total === 0 ? 0 : Math.ceil(total / count)) || page2 > Math.max(1, pages)) {
25662
+ if (!positiveSafeInt(count2) || !nonNegativeSafeInt(total) || !positiveSafeInt(page2) || !nonNegativeSafeInt(pages) || count2 !== requested.count || page2 !== requested.page || returnedFileCount > count2 || pages !== (total === 0 ? 0 : Math.ceil(total / count2)) || page2 > Math.max(1, pages)) {
25621
25663
  throw new SlackBotProviderError("invalid_files_paging");
25622
25664
  }
25623
25665
  if (page2 >= pages) return null;
@@ -83461,12 +83503,12 @@ function creditScopeMetadata(scope) {
83461
83503
  }
83462
83504
  function creditScopeFromMetadata(metadata) {
83463
83505
  if (!metadata || !Object.keys(metadata).some((key) => key.startsWith(prefix))) return void 0;
83464
- const count = Number(metadata[countKey]);
83465
- if (!Number.isInteger(count) || count < 1 || count > 24) {
83506
+ const count2 = Number(metadata[countKey]);
83507
+ if (!Number.isInteger(count2) || count2 < 1 || count2 > 24) {
83466
83508
  throw new Error("Invalid promotional credit snapshot");
83467
83509
  }
83468
83510
  let encoded = "";
83469
- for (let index = 0; index < count; index++) {
83511
+ for (let index = 0; index < count2; index++) {
83470
83512
  const chunk = metadata[`${prefix}${index}`];
83471
83513
  if (!chunk) throw new Error("Incomplete promotional credit snapshot");
83472
83514
  encoded += chunk;
@@ -100182,219 +100224,10 @@ function registerScheduledTaskRoutes(app, deps) {
100182
100224
  });
100183
100225
  }
100184
100226
 
100185
- // src/routes/sessions.ts
100186
- import { getRetainedProviderCommand } from "@opengeni/db/retained-provider-commands";
100187
- import { assertGoalResumeAllowed as assertGoalResumeAllowed2, GoalResumeBlockedError } from "@opengeni/core";
100188
- import { UnsupportedLatencyModeError } from "@opengeni/config";
100189
- import { searchSessionMessagesForSubject, SessionMessageSearchCursorError } from "@opengeni/db";
100190
- import { listSessionEventSlices } from "@opengeni/db/session-event-slices";
100191
- import * as sessionPreviewSchema from "@opengeni/db/schema";
100192
- import { SessionMessageSearchRequest } from "@opengeni/contracts";
100193
- import { scheduledSessionIds } from "@opengeni/db";
100194
- import { isVerifiedDelegatedHumanAuthorization as isVerifiedDelegatedHumanAuthorization4, withSiteSessionOrigin as withSiteSessionOrigin3 } from "@opengeni/core";
100195
- import { freezeSessionRealtimeConnectionAccounts } from "@opengeni/core";
100196
- import { SandboxRecoveryRequest } from "@opengeni/contracts";
100197
- import { getManagedHumanSandboxRecovery, consentManagedHumanSandboxRecovery } from "@opengeni/core";
100198
- import { SandboxRecoveryConflictError } from "@opengeni/db";
100199
- import { getSessionCodexAccounts } from "@opengeni/db";
100200
- import { getToolActionReview, getToolReviewDetailsPage } from "@opengeni/db";
100201
- import {
100202
- AcknowledgeStreamRequest,
100203
- ApplySessionGoalRevisionRequest,
100204
- ListSessionGoalRevisionsQuery,
100205
- RejectSessionGoalRevisionRequest,
100206
- RollbackSessionGoalRevisionRequest,
100207
- ActivateCodexRealtimeConnectionRequest,
100208
- AttachViewerRequest,
100209
- BeginSessionRealtimeRequest,
100210
- ClearSessionContextRequest,
100211
- CodexRealtimeWebrtcRequest,
100212
- GatewayRealtimeConnectRequest,
100213
- ClientSessionEvent,
100214
- CompactSessionContextRequest,
100215
- CreateSessionRequest as CreateSessionRequest2,
100216
- DeleteSessionQueueItemRequest,
100217
- EditSessionQueueItemRequest,
100218
- EndSessionRealtimeRequest,
100219
- FsDeleteRequest,
100220
- FsListBatchRequest,
100221
- FsListRequest,
100222
- FsMkdirRequest,
100223
- FsMoveRequest,
100224
- FsReadRequest,
100225
- FsWriteRequest,
100226
- ForkSessionRequest,
100227
- HumanInputRequestStatus,
100228
- GitDiffRequest,
100229
- GitReadBatchRequest,
100230
- GitLogRequest,
100231
- GitShowRequest,
100232
- GitStatusRequest,
100233
- MoveSessionQueueItemRequest,
100234
- PtyCloseRequest,
100235
- PtyOpenRequest,
100236
- PtyResizeRequest,
100237
- PtyWriteRequest,
100238
- PublishSandboxFileArtifactRequest,
100239
- RenewSessionRealtimeRequest,
100240
- SyncSessionRealtimeLedgerRequest,
100241
- SessionControlRequest,
100242
- SessionRetryRequest,
100243
- SESSION_EVENT_RAW_DELTA_TYPES as SESSION_EVENT_RAW_DELTA_TYPES2,
100244
- SessionEventPayloadMode as SessionEventPayloadMode2,
100245
- SessionEventReadDirection as SessionEventReadDirection2,
100246
- SessionEventReadMode as SessionEventReadMode2,
100247
- SessionEventLatestClass as SessionEventLatestClass2,
100248
- SessionEventResultMode as SessionEventResultMode2,
100249
- SessionScopeSubjectId,
100250
- SessionEventSemanticClass as SessionEventSemanticClass2,
100251
- SessionEventType as SessionEventType2,
100252
- SessionMcpServerId,
100253
- SaveNewSessionDraftRequest,
100254
- compactSessionEventResult as compactSessionEventResult3,
100255
- sessionEventLatestClassToSemanticClass as sessionEventLatestClassToSemanticClass2,
100256
- SaveComposerDraftRequest,
100257
- SubmitComposerDraftRequest,
100258
- SteerSessionQueueItemRequest,
100259
- SteerSessionMessageRequest,
100260
- TerminalExecRequest,
100261
- UpdateSessionChannelRequest,
100262
- UpdateSessionAttentionRequest,
100263
- UpdateSessionArchiveRequest,
100264
- UpdateSessionPinRequest,
100265
- UpdateSessionGoalRequest,
100266
- UpdateSessionMcpApprovalPolicyRequest,
100267
- UpdateSessionRequest,
100268
- UpdateSessionVariableSetsRequest,
100269
- UpdateSessionVisibilityRequest,
100270
- UpdateSessionToolPolicyRequest,
100271
- UpdateSessionAgentRequest,
100272
- AgentConfigError as AgentConfigError2,
100273
- ViewerHeartbeatRequest,
100274
- WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
100275
- WORK_CLAIM_CANONICAL_KEY_MAX_BYTES as WORK_CLAIM_CANONICAL_KEY_MAX_BYTES2,
100276
- WORK_CLAIM_DISCOVERY_LIMIT as WORK_CLAIM_DISCOVERY_LIMIT2,
100277
- WORK_CLAIM_NAMESPACE_MAX_BYTES as WORK_CLAIM_NAMESPACE_MAX_BYTES2,
100278
- WORK_DISCOVERY_QUERY_MAX_CHARS as WORK_DISCOVERY_QUERY_MAX_CHARS2,
100279
- WORK_DISCOVERY_RECENT_HOURS_MAX as WORK_DISCOVERY_RECENT_HOURS_MAX2,
100280
- WorkClaimSubjectFilter as WorkClaimSubjectFilterSchema,
100281
- WorkClaimSubjectType as WorkClaimSubjectType2,
100282
- normalizeWorkClaimCanonicalKey,
100283
- normalizeWorkClaimNamespace,
100284
- workspaceControlUtf8Bytes
100285
- } from "@opengeni/contracts";
100286
- import { streamTokenDegraded } from "@opengeni/config";
100287
- import {
100288
- recordAuditEvent as recordAuditEvent13,
100289
- acceptSessionApprovalDecision as acceptSessionApprovalDecision2,
100290
- acceptSessionHumanInputResponse as acceptSessionHumanInputResponse2,
100291
- clearSessionGoal,
100292
- clearSessionContext,
100293
- getOpenPtySession,
100294
- getEnrollment as getEnrollment2,
100295
- getLiveEnrollmentConnection as getLiveEnrollmentConnection7,
100296
- getRetainedProcess,
100297
- getSandbox as getSandbox4,
100298
- getSession as getSession9,
100299
- readActiveSandbox as readActiveSandbox3,
100300
- deleteSessionTreeIfQuiescent,
100301
- getSessionEvent,
100302
- getSessionForSubject as getSessionForSubject2,
100303
- getSessionGoal as getSessionGoal2,
100304
- getLatestSessionModelContext,
100305
- getSessionHumanInputRequest,
100306
- getSessionGoalWithContinuation,
100307
- getSessionGoalRevision,
100308
- getSessionQueueSnapshot as getSessionQueueSnapshot2,
100309
- getStreamAcknowledgment,
100310
- insertPtySession,
100311
- listSessionEventPage as listSessionEventPage3,
100312
- listSessionHumanInputRequests,
100313
- listSessionGoalRevisionPage,
100314
- listSessionGoalRevisions,
100315
- rejectSessionGoalRevisionWithEvent,
100316
- listSessionIdsInGroup,
100317
- listSessionDiscoverySummaries as listSessionDiscoverySummaries2,
100318
- listSessionDiscoveryAncestorPaths,
100319
- listSessionsForSubject,
100320
- listSessionEntriesForSubject,
100321
- getLatestStartedSessionTurn,
100322
- listSessionTurns,
100323
- projectEffectiveControlForRelatedAccess as projectEffectiveControlForRelatedAccess2,
100324
- projectSessionForRelatedAccess as projectSessionForRelatedAccess2,
100325
- recordStreamAcknowledgment,
100326
- requestSessionCompaction,
100327
- switchSessionCodexAccount,
100328
- setSessionChannel as setSessionChannel2,
100329
- updateSessionVariableSets,
100330
- ChannelNotFoundError,
100331
- setSessionAttention,
100332
- setSessionArchive,
100333
- setSessionPin,
100334
- SessionAttentionVersionConflictError,
100335
- SessionArchiveVersionConflictError,
100336
- SessionPinVersionConflictError,
100337
- SessionPinAccessError,
100338
- SessionListAccessError,
100339
- SessionListCursorError,
100340
- SessionListCursorExpiredError,
100341
- decodeSessionListCursor,
100342
- revokeViewer,
100343
- setSessionGoalStatusWithEvent as setSessionGoalStatusWithEvent2,
100344
- upsertSessionGoalWithEvent as upsertSessionGoalWithEvent2,
100345
- updatePtySessionActivity,
100346
- QueueCommandConflictError,
100347
- beginSessionRealtimeInTransaction,
100348
- activateSessionRealtimeConnectionInTransaction,
100349
- claimSessionRealtimeConnectionInTransaction,
100350
- completeSessionRealtimeConnectionInTransaction,
100351
- endSessionRealtimeInTransaction,
100352
- failSessionRealtimeConnectionInTransaction,
100353
- NewSessionDraftConflictError,
100354
- SessionCommandIdempotencyError,
100355
- SessionControlConflictError,
100356
- SessionRealtimeConflictError,
100357
- SessionToolPolicyVersionConflictError,
100358
- SessionContextBusyError,
100359
- SessionVariableSetSelectionUnavailableError,
100360
- SessionCreateConnectionSelectionUnavailableError as SessionCreateConnectionSelectionUnavailableError2,
100361
- workspaceControlRequestLockTimeoutMs,
100362
- SessionTenancyAccessError,
100363
- SessionTenancyConflictError,
100364
- SessionTenancyInvalidRequestError,
100365
- SessionTenancyNotActivatedError,
100366
- HumanInputResponseValidationError as HumanInputResponseValidationError2,
100367
- SkillHumanResponseError,
100368
- latestWorkspaceCapture,
100369
- sessionLatestWorkspaceCapture,
100370
- renewSessionRealtimeInTransaction,
100371
- syncSessionRealtimeLedgerInTransaction,
100372
- withWorkspaceSessionActivityRls,
100373
- withWorkspaceRls,
100374
- workspaceCaptureAtRevision
100375
- } from "@opengeni/db";
100376
- import {
100377
- backgroundCommandActivityForSessions,
100378
- listSessionBackgroundCommands,
100379
- requestSessionBackgroundCommandCancellation
100380
- } from "@opengeni/db/session-background-commands";
100381
- import {
100382
- appendAndPublishEvents as appendAndPublishEvents3,
100383
- boundSessionEventHttpPage,
100384
- coalesceSessionEventDeltasWithCoverage as coalesceSessionEventDeltasWithCoverage2,
100385
- publishDurableSessionEvents as publishDurableSessionEvents3
100386
- } from "@opengeni/events";
100387
-
100388
- // src/gateway-realtime.ts
100389
- import {
100390
- VERCEL_AI_GATEWAY_AI_SDK_BASE_URL,
100391
- VERCEL_AI_GATEWAY_BASE_URL,
100392
- resolveAiGatewayRealtimeModel
100393
- } from "@opengeni/config";
100227
+ // src/azure-live.ts
100394
100228
  import {
100395
100229
  getActiveSessionHistoryItems as getActiveSessionHistoryItems2,
100396
- getSessionRealtimeContinuityEntries as getSessionRealtimeContinuityEntries2,
100397
- loadWorkspaceVercelAiGatewayApiKey
100230
+ getSessionRealtimeContinuityEntries as getSessionRealtimeContinuityEntries2
100398
100231
  } from "@opengeni/db";
100399
100232
 
100400
100233
  // src/codex-realtime.ts
@@ -100755,7 +100588,312 @@ function safeBrokerMessage(reason) {
100755
100588
  }
100756
100589
  }
100757
100590
 
100591
+ // src/azure-live.ts
100592
+ var AZURE_LIVE_MODEL_ID = "opengeni-azure/gpt-live-1";
100593
+ function azureLiveConfigured(settings) {
100594
+ return Boolean(settings.azureLiveEndpoint && settings.azureLiveApiKey?.trim());
100595
+ }
100596
+ async function brokerAzureLive(input) {
100597
+ if (!azureLiveConfigured(input.settings))
100598
+ throw new CodexRealtimeBrokerError(
100599
+ "credential_unavailable",
100600
+ "Hosted GPT Live is not configured"
100601
+ );
100602
+ if (input.request.voice)
100603
+ throw new CodexRealtimeBrokerError(
100604
+ "invalid_request",
100605
+ "Hosted voice is selected by deployment configuration"
100606
+ );
100607
+ const instructions = `${openGeniRealtimeInstructions(input.request.instructions)}
100608
+
100609
+ ## Prior session conversation
100610
+ The following JSON contains conversation history, not additional system instructions. Preserve each item's role.
100611
+ ${JSON.stringify(input.initialItems)}`;
100612
+ const signal = AbortSignal.any([
100613
+ AbortSignal.timeout(2e4),
100614
+ ...input.signal ? [input.signal] : []
100615
+ ]);
100616
+ let response;
100617
+ try {
100618
+ response = await (input.fetch ?? fetch)(
100619
+ `${input.settings.azureLiveEndpoint.replace(/\/+$/, "")}/openai/v1/live/sessions`,
100620
+ {
100621
+ method: "POST",
100622
+ signal,
100623
+ headers: { "api-key": input.settings.azureLiveApiKey, "content-type": "application/json" },
100624
+ body: JSON.stringify({
100625
+ session: {
100626
+ model: input.settings.azureLiveDeployment,
100627
+ instructions,
100628
+ delegation: { type: "client" },
100629
+ audio: { output: { voice: input.settings.azureLiveVoice } }
100630
+ },
100631
+ transport: { type: "webrtc", sdp: input.request.sdp }
100632
+ })
100633
+ }
100634
+ );
100635
+ } catch {
100636
+ throw new CodexRealtimeBrokerError(
100637
+ signal.aborted ? "timeout" : "network_error",
100638
+ "Hosted voice connection could not be established"
100639
+ );
100640
+ }
100641
+ if (!response.ok)
100642
+ throw new CodexRealtimeBrokerError(
100643
+ response.status === 429 ? "rate_limited" : "provider_error",
100644
+ "Hosted voice connection was rejected",
100645
+ response.status
100646
+ );
100647
+ const body3 = await response.json().catch(() => null);
100648
+ const sdp = body3?.transport?.sdp;
100649
+ if (typeof sdp !== "string" || !sdp || sdp.length > 1048576)
100650
+ throw new CodexRealtimeBrokerError(
100651
+ "invalid_provider_response",
100652
+ "Hosted voice returned an invalid connection answer"
100653
+ );
100654
+ return { sdp, version: "v3", model: AZURE_LIVE_MODEL_ID };
100655
+ }
100656
+ function buildSessionAzureLiveBroker(db, settings, workspaceId, sessionId, fetchImpl) {
100657
+ return async (input) => {
100658
+ const [history, continuity] = await Promise.all([
100659
+ getActiveSessionHistoryItems2(db, workspaceId, sessionId),
100660
+ getSessionRealtimeContinuityEntries2(db, workspaceId, sessionId)
100661
+ ]);
100662
+ return brokerAzureLive({
100663
+ settings,
100664
+ ...input,
100665
+ initialItems: projectSessionRealtimeInitialItems(history, continuity),
100666
+ ...fetchImpl ? { fetch: fetchImpl } : {}
100667
+ });
100668
+ };
100669
+ }
100670
+
100671
+ // src/routes/sessions.ts
100672
+ import { getRetainedProviderCommand } from "@opengeni/db/retained-provider-commands";
100673
+ import { assertGoalResumeAllowed as assertGoalResumeAllowed2, GoalResumeBlockedError } from "@opengeni/core";
100674
+ import { UnsupportedLatencyModeError } from "@opengeni/config";
100675
+ import { searchSessionMessagesForSubject, SessionMessageSearchCursorError } from "@opengeni/db";
100676
+ import { listSessionEventSlices } from "@opengeni/db/session-event-slices";
100677
+ import * as sessionPreviewSchema from "@opengeni/db/schema";
100678
+ import { SessionMessageSearchRequest } from "@opengeni/contracts";
100679
+ import { scheduledSessionIds } from "@opengeni/db";
100680
+ import { isVerifiedDelegatedHumanAuthorization as isVerifiedDelegatedHumanAuthorization4, withSiteSessionOrigin as withSiteSessionOrigin3 } from "@opengeni/core";
100681
+ import { freezeSessionRealtimeConnectionAccounts } from "@opengeni/core";
100682
+ import {
100683
+ createRealtimeVoiceBilling,
100684
+ RealtimeVoiceUnavailableError,
100685
+ TranscriptionBillingRefusedError
100686
+ } from "@opengeni/core";
100687
+
100688
+ // src/transcription/billing-refusal.ts
100689
+ function transcriptionBillingRefusal(c, error) {
100690
+ return c.json({ code: error.code, message: error.message, ...error.details }, error.status);
100691
+ }
100692
+
100693
+ // src/routes/sessions.ts
100694
+ import { SandboxRecoveryRequest } from "@opengeni/contracts";
100695
+ import { getManagedHumanSandboxRecovery, consentManagedHumanSandboxRecovery } from "@opengeni/core";
100696
+ import { SandboxRecoveryConflictError } from "@opengeni/db";
100697
+ import { getSessionCodexAccounts } from "@opengeni/db";
100698
+ import { getToolActionReview, getToolReviewDetailsPage } from "@opengeni/db";
100699
+ import {
100700
+ AcknowledgeStreamRequest,
100701
+ ApplySessionGoalRevisionRequest,
100702
+ ListSessionGoalRevisionsQuery,
100703
+ RejectSessionGoalRevisionRequest,
100704
+ RollbackSessionGoalRevisionRequest,
100705
+ ActivateCodexRealtimeConnectionRequest,
100706
+ AttachViewerRequest,
100707
+ BeginSessionRealtimeRequest,
100708
+ ClearSessionContextRequest,
100709
+ CodexRealtimeWebrtcRequest,
100710
+ GatewayRealtimeConnectRequest,
100711
+ ClientSessionEvent,
100712
+ CompactSessionContextRequest,
100713
+ CreateSessionRequest as CreateSessionRequest2,
100714
+ DeleteSessionQueueItemRequest,
100715
+ EditSessionQueueItemRequest,
100716
+ EndSessionRealtimeRequest,
100717
+ FsDeleteRequest,
100718
+ FsListBatchRequest,
100719
+ FsListRequest,
100720
+ FsMkdirRequest,
100721
+ FsMoveRequest,
100722
+ FsReadRequest,
100723
+ FsWriteRequest,
100724
+ ForkSessionRequest,
100725
+ HumanInputRequestStatus,
100726
+ GitDiffRequest,
100727
+ GitReadBatchRequest,
100728
+ GitLogRequest,
100729
+ GitShowRequest,
100730
+ GitStatusRequest,
100731
+ MoveSessionQueueItemRequest,
100732
+ PtyCloseRequest,
100733
+ PtyOpenRequest,
100734
+ PtyResizeRequest,
100735
+ PtyWriteRequest,
100736
+ PublishSandboxFileArtifactRequest,
100737
+ RenewSessionRealtimeRequest,
100738
+ SyncSessionRealtimeLedgerRequest,
100739
+ SessionControlRequest,
100740
+ SessionRetryRequest,
100741
+ SESSION_EVENT_RAW_DELTA_TYPES as SESSION_EVENT_RAW_DELTA_TYPES2,
100742
+ SessionEventPayloadMode as SessionEventPayloadMode2,
100743
+ SessionEventReadDirection as SessionEventReadDirection2,
100744
+ SessionEventReadMode as SessionEventReadMode2,
100745
+ SessionEventLatestClass as SessionEventLatestClass2,
100746
+ SessionEventResultMode as SessionEventResultMode2,
100747
+ SessionScopeSubjectId,
100748
+ SessionEventSemanticClass as SessionEventSemanticClass2,
100749
+ SessionEventType as SessionEventType2,
100750
+ SessionMcpServerId,
100751
+ SaveNewSessionDraftRequest,
100752
+ compactSessionEventResult as compactSessionEventResult3,
100753
+ sessionEventLatestClassToSemanticClass as sessionEventLatestClassToSemanticClass2,
100754
+ SaveComposerDraftRequest,
100755
+ SubmitComposerDraftRequest,
100756
+ SteerSessionQueueItemRequest,
100757
+ SteerSessionMessageRequest,
100758
+ TerminalExecRequest,
100759
+ UpdateSessionChannelRequest,
100760
+ UpdateSessionAttentionRequest,
100761
+ UpdateSessionArchiveRequest,
100762
+ UpdateSessionPinRequest,
100763
+ UpdateSessionGoalRequest,
100764
+ UpdateSessionMcpApprovalPolicyRequest,
100765
+ UpdateSessionRequest,
100766
+ UpdateSessionVariableSetsRequest,
100767
+ UpdateSessionVisibilityRequest,
100768
+ UpdateSessionToolPolicyRequest,
100769
+ UpdateSessionAgentRequest,
100770
+ AgentConfigError as AgentConfigError2,
100771
+ ViewerHeartbeatRequest,
100772
+ WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
100773
+ WORK_CLAIM_CANONICAL_KEY_MAX_BYTES as WORK_CLAIM_CANONICAL_KEY_MAX_BYTES2,
100774
+ WORK_CLAIM_DISCOVERY_LIMIT as WORK_CLAIM_DISCOVERY_LIMIT2,
100775
+ WORK_CLAIM_NAMESPACE_MAX_BYTES as WORK_CLAIM_NAMESPACE_MAX_BYTES2,
100776
+ WORK_DISCOVERY_QUERY_MAX_CHARS as WORK_DISCOVERY_QUERY_MAX_CHARS2,
100777
+ WORK_DISCOVERY_RECENT_HOURS_MAX as WORK_DISCOVERY_RECENT_HOURS_MAX2,
100778
+ WorkClaimSubjectFilter as WorkClaimSubjectFilterSchema,
100779
+ WorkClaimSubjectType as WorkClaimSubjectType2,
100780
+ normalizeWorkClaimCanonicalKey,
100781
+ normalizeWorkClaimNamespace,
100782
+ workspaceControlUtf8Bytes
100783
+ } from "@opengeni/contracts";
100784
+ import { streamTokenDegraded } from "@opengeni/config";
100785
+ import {
100786
+ recordAuditEvent as recordAuditEvent13,
100787
+ acceptSessionApprovalDecision as acceptSessionApprovalDecision2,
100788
+ acceptSessionHumanInputResponse as acceptSessionHumanInputResponse2,
100789
+ clearSessionGoal,
100790
+ clearSessionContext,
100791
+ getOpenPtySession,
100792
+ getEnrollment as getEnrollment2,
100793
+ getLiveEnrollmentConnection as getLiveEnrollmentConnection7,
100794
+ getRetainedProcess,
100795
+ getSandbox as getSandbox4,
100796
+ getSession as getSession9,
100797
+ readActiveSandbox as readActiveSandbox3,
100798
+ deleteSessionTreeIfQuiescent,
100799
+ getSessionEvent,
100800
+ getSessionForSubject as getSessionForSubject2,
100801
+ getSessionGoal as getSessionGoal2,
100802
+ getLatestSessionModelContext,
100803
+ getSessionHumanInputRequest,
100804
+ getSessionGoalWithContinuation,
100805
+ getSessionGoalRevision,
100806
+ getSessionQueueSnapshot as getSessionQueueSnapshot2,
100807
+ getStreamAcknowledgment,
100808
+ insertPtySession,
100809
+ listSessionEventPage as listSessionEventPage3,
100810
+ listSessionHumanInputRequests,
100811
+ listSessionGoalRevisionPage,
100812
+ listSessionGoalRevisions,
100813
+ rejectSessionGoalRevisionWithEvent,
100814
+ listSessionIdsInGroup,
100815
+ listSessionDiscoverySummaries as listSessionDiscoverySummaries2,
100816
+ listSessionDiscoveryAncestorPaths,
100817
+ listSessionsForSubject,
100818
+ listSessionEntriesForSubject,
100819
+ getLatestStartedSessionTurn,
100820
+ listSessionTurns,
100821
+ projectEffectiveControlForRelatedAccess as projectEffectiveControlForRelatedAccess2,
100822
+ projectSessionForRelatedAccess as projectSessionForRelatedAccess2,
100823
+ recordStreamAcknowledgment,
100824
+ requestSessionCompaction,
100825
+ switchSessionCodexAccount,
100826
+ setSessionChannel as setSessionChannel2,
100827
+ updateSessionVariableSets,
100828
+ ChannelNotFoundError,
100829
+ setSessionAttention,
100830
+ setSessionArchive,
100831
+ setSessionPin,
100832
+ SessionAttentionVersionConflictError,
100833
+ SessionArchiveVersionConflictError,
100834
+ SessionPinVersionConflictError,
100835
+ SessionPinAccessError,
100836
+ SessionListAccessError,
100837
+ SessionListCursorError,
100838
+ SessionListCursorExpiredError,
100839
+ decodeSessionListCursor,
100840
+ revokeViewer,
100841
+ setSessionGoalStatusWithEvent as setSessionGoalStatusWithEvent2,
100842
+ upsertSessionGoalWithEvent as upsertSessionGoalWithEvent2,
100843
+ updatePtySessionActivity,
100844
+ QueueCommandConflictError,
100845
+ beginSessionRealtimeInTransaction,
100846
+ activateSessionRealtimeConnectionInTransaction,
100847
+ claimSessionRealtimeConnectionInTransaction,
100848
+ completeSessionRealtimeConnectionInTransaction,
100849
+ endSessionRealtimeInTransaction,
100850
+ failSessionRealtimeConnectionInTransaction,
100851
+ NewSessionDraftConflictError,
100852
+ SessionCommandIdempotencyError,
100853
+ SessionControlConflictError,
100854
+ SessionRealtimeConflictError,
100855
+ SessionToolPolicyVersionConflictError,
100856
+ SessionContextBusyError,
100857
+ SessionVariableSetSelectionUnavailableError,
100858
+ SessionCreateConnectionSelectionUnavailableError as SessionCreateConnectionSelectionUnavailableError2,
100859
+ workspaceControlRequestLockTimeoutMs,
100860
+ SessionTenancyAccessError,
100861
+ SessionTenancyConflictError,
100862
+ SessionTenancyInvalidRequestError,
100863
+ SessionTenancyNotActivatedError,
100864
+ HumanInputResponseValidationError as HumanInputResponseValidationError2,
100865
+ SkillHumanResponseError,
100866
+ latestWorkspaceCapture,
100867
+ sessionLatestWorkspaceCapture,
100868
+ renewSessionRealtimeInTransaction,
100869
+ syncSessionRealtimeLedgerInTransaction,
100870
+ withWorkspaceSessionActivityRls,
100871
+ withWorkspaceRls,
100872
+ workspaceCaptureAtRevision
100873
+ } from "@opengeni/db";
100874
+ import {
100875
+ backgroundCommandActivityForSessions,
100876
+ listSessionBackgroundCommands,
100877
+ requestSessionBackgroundCommandCancellation
100878
+ } from "@opengeni/db/session-background-commands";
100879
+ import {
100880
+ appendAndPublishEvents as appendAndPublishEvents3,
100881
+ boundSessionEventHttpPage,
100882
+ coalesceSessionEventDeltasWithCoverage as coalesceSessionEventDeltasWithCoverage2,
100883
+ publishDurableSessionEvents as publishDurableSessionEvents3
100884
+ } from "@opengeni/events";
100885
+
100758
100886
  // src/gateway-realtime.ts
100887
+ import {
100888
+ VERCEL_AI_GATEWAY_AI_SDK_BASE_URL,
100889
+ VERCEL_AI_GATEWAY_BASE_URL,
100890
+ resolveAiGatewayRealtimeModel
100891
+ } from "@opengeni/config";
100892
+ import {
100893
+ getActiveSessionHistoryItems as getActiveSessionHistoryItems3,
100894
+ getSessionRealtimeContinuityEntries as getSessionRealtimeContinuityEntries3,
100895
+ loadWorkspaceVercelAiGatewayApiKey
100896
+ } from "@opengeni/db";
100759
100897
  var GatewayRealtimeBrokerError = class extends Error {
100760
100898
  constructor(code, message, providerStatus = null) {
100761
100899
  super(message);
@@ -100780,8 +100918,8 @@ async function createGatewayRealtimeConnectionSecret(input) {
100780
100918
  );
100781
100919
  }
100782
100920
  const [history, continuity, minted] = await Promise.all([
100783
- getActiveSessionHistoryItems2(input.db, input.workspaceId, input.sessionId),
100784
- getSessionRealtimeContinuityEntries2(input.db, input.workspaceId, input.sessionId),
100921
+ getActiveSessionHistoryItems3(input.db, input.workspaceId, input.sessionId),
100922
+ getSessionRealtimeContinuityEntries3(input.db, input.workspaceId, input.sessionId),
100785
100923
  mintGatewayClientSecret({
100786
100924
  apiKey,
100787
100925
  upstreamModelId: resolved.upstreamModelId,
@@ -100844,8 +100982,8 @@ async function mintGatewayClientSecret(input) {
100844
100982
  // src/xai-realtime.ts
100845
100983
  import { SUPERGROK_REALTIME_MODEL_ID } from "@opengeni/config";
100846
100984
  import {
100847
- getActiveSessionHistoryItems as getActiveSessionHistoryItems3,
100848
- getSessionRealtimeContinuityEntries as getSessionRealtimeContinuityEntries3,
100985
+ getActiveSessionHistoryItems as getActiveSessionHistoryItems4,
100986
+ getSessionRealtimeContinuityEntries as getSessionRealtimeContinuityEntries4,
100849
100987
  getXaiSessionAccountPin,
100850
100988
  resolveXaiProviderAccountAuthoritySnapshotForAcceptance as resolveXaiProviderAccountAuthoritySnapshotForAcceptance3,
100851
100989
  setXaiSessionAccountPin
@@ -101054,8 +101192,8 @@ async function createXaiRealtimeConnectionSecret(input) {
101054
101192
  );
101055
101193
  }
101056
101194
  const [history, continuity, minted] = await Promise.all([
101057
- getActiveSessionHistoryItems3(input.db, input.workspaceId, input.sessionId),
101058
- getSessionRealtimeContinuityEntries3(input.db, input.workspaceId, input.sessionId),
101195
+ getActiveSessionHistoryItems4(input.db, input.workspaceId, input.sessionId),
101196
+ getSessionRealtimeContinuityEntries4(input.db, input.workspaceId, input.sessionId),
101059
101197
  mintXaiClientSecret({
101060
101198
  auth,
101061
101199
  fetchImpl: input.fetchImpl ?? fetch
@@ -101495,6 +101633,46 @@ function registerSessionRoutes(app, deps) {
101495
101633
  observability: deps.observability
101496
101634
  };
101497
101635
  const workspaceCaptureManifestCache = new WorkspaceCaptureManifestCache();
101636
+ const realtimeVoiceBilling = createRealtimeVoiceBilling({ db, settings });
101637
+ const realtimeVoiceRefusalResponse = (c, error) => {
101638
+ if (error instanceof TranscriptionBillingRefusedError) {
101639
+ return transcriptionBillingRefusal(c, error);
101640
+ }
101641
+ if (error instanceof RealtimeVoiceUnavailableError) {
101642
+ return c.json(
101643
+ {
101644
+ error: {
101645
+ status: 409,
101646
+ code: "realtime_voice_unavailable",
101647
+ message: error.message,
101648
+ retryable: false,
101649
+ details: { reason: error.code }
101650
+ }
101651
+ },
101652
+ 409
101653
+ );
101654
+ }
101655
+ return null;
101656
+ };
101657
+ const settleRealtimeVoice = async (input) => {
101658
+ try {
101659
+ return await realtimeVoiceBilling.settle({
101660
+ workspaceId: input.workspaceId,
101661
+ sessionId: input.sessionId,
101662
+ realtimeId: input.realtimeId,
101663
+ callerSubjectId: input.grant.subjectId,
101664
+ attribution: input.attribution
101665
+ });
101666
+ } catch (error) {
101667
+ console.error("realtime voice settlement failed", {
101668
+ workspaceId: input.workspaceId,
101669
+ sessionId: input.sessionId,
101670
+ realtimeId: input.realtimeId,
101671
+ error: error instanceof Error ? error.message : String(error)
101672
+ });
101673
+ return null;
101674
+ }
101675
+ };
101498
101676
  const withSiteCommandOrigin = (c, workspaceId, run) => withOptionalSiteCommandOrigin(
101499
101677
  db,
101500
101678
  workspaceId,
@@ -102298,6 +102476,18 @@ function registerSessionRoutes(app, deps) {
102298
102476
  message: "invalid session realtime request"
102299
102477
  });
102300
102478
  }
102479
+ try {
102480
+ await realtimeVoiceBilling.admit({
102481
+ accountId: grant.accountId,
102482
+ workspaceId,
102483
+ model: parsed.data.model,
102484
+ attribution: creditDebitAttributionForGrant(grant)
102485
+ });
102486
+ } catch (error) {
102487
+ const refused = realtimeVoiceRefusalResponse(c, error);
102488
+ if (refused) return refused;
102489
+ throw error;
102490
+ }
102301
102491
  try {
102302
102492
  const result = await withWorkspaceSessionActivityRls(db, workspaceId, async (scopedDb) => {
102303
102493
  const accounts = await freezeSessionRealtimeConnectionAccounts({
@@ -102342,6 +102532,14 @@ function registerSessionRoutes(app, deps) {
102342
102532
  });
102343
102533
  }
102344
102534
  try {
102535
+ const settlement = await settleRealtimeVoice({
102536
+ grant,
102537
+ attribution: creditDebitAttributionForGrant(grant),
102538
+ workspaceId,
102539
+ sessionId,
102540
+ realtimeId
102541
+ });
102542
+ const stop = settlement?.stop ?? null;
102345
102543
  const result = await withWorkspaceSessionActivityRls(
102346
102544
  db,
102347
102545
  workspaceId,
@@ -102350,12 +102548,17 @@ function registerSessionRoutes(app, deps) {
102350
102548
  sessionId,
102351
102549
  realtimeId,
102352
102550
  ownerSubjectId: grant.subjectId,
102353
- ...parsed.data
102551
+ ...parsed.data,
102552
+ extendLease: stop === null
102354
102553
  })
102355
102554
  );
102356
102555
  await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
102357
102556
  c.header("cache-control", "private, no-store");
102358
- return c.json({ mode: result.mode, replay: result.replay });
102557
+ return c.json({
102558
+ mode: result.mode,
102559
+ replay: result.replay,
102560
+ ...stop && result.mode.state === "active" ? { stop: { code: stop.code, message: stop.message } } : {}
102561
+ });
102359
102562
  } catch (error) {
102360
102563
  throw sessionRealtimeHttpError(error);
102361
102564
  }
@@ -102390,6 +102593,13 @@ function registerSessionRoutes(app, deps) {
102390
102593
  })
102391
102594
  );
102392
102595
  await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
102596
+ await settleRealtimeVoice({
102597
+ grant,
102598
+ attribution: creditDebitAttributionForGrant(grant),
102599
+ workspaceId,
102600
+ sessionId,
102601
+ realtimeId
102602
+ });
102393
102603
  c.header("cache-control", "private, no-store");
102394
102604
  return c.json({ mode: result.mode, replay: result.replay });
102395
102605
  } catch (error) {
@@ -102425,21 +102635,31 @@ function registerSessionRoutes(app, deps) {
102425
102635
  const claim = await withWorkspaceRls(
102426
102636
  db,
102427
102637
  workspaceId,
102428
- async (scopedDb) => scopedDb.transaction(
102429
- async (tx) => claimSessionRealtimeConnectionInTransaction(tx, {
102430
- workspaceId,
102431
- sessionId,
102432
- realtimeId,
102433
- operationId,
102434
- ownerSubjectId: grant.subjectId,
102435
- browserInstanceId,
102436
- ownerKey,
102437
- expectedVersion,
102438
- expectedConnectionEpoch,
102439
- rotate,
102440
- promotionMode: browserActivation === "required" ? "staged" : "legacy"
102441
- })
102442
- )
102638
+ async (scopedDb) => scopedDb.transaction(async (tx) => {
102639
+ const claimed = await claimSessionRealtimeConnectionInTransaction(
102640
+ tx,
102641
+ {
102642
+ workspaceId,
102643
+ sessionId,
102644
+ realtimeId,
102645
+ operationId,
102646
+ ownerSubjectId: grant.subjectId,
102647
+ browserInstanceId,
102648
+ ownerKey,
102649
+ expectedVersion,
102650
+ expectedConnectionEpoch,
102651
+ rotate,
102652
+ promotionMode: browserActivation === "required" ? "staged" : "legacy"
102653
+ }
102654
+ );
102655
+ if (claimed.mode.model !== "gpt-live-1-boulder-alpha" && claimed.mode.model !== AZURE_LIVE_MODEL_ID) {
102656
+ throw new CodexRealtimeBrokerError(
102657
+ "invalid_request",
102658
+ "This voice model does not use WebRTC negotiation"
102659
+ );
102660
+ }
102661
+ return claimed;
102662
+ })
102443
102663
  );
102444
102664
  if (claim.replay) {
102445
102665
  if (claim.connection.state !== "ready" && claim.connection.state !== "active" || !claim.connection.sdpAnswer) {
@@ -102470,7 +102690,7 @@ function registerSessionRoutes(app, deps) {
102470
102690
  return c.json({
102471
102691
  sdp: claim.connection.sdpAnswer,
102472
102692
  version: "v3",
102473
- model: "gpt-live-1-boulder-alpha",
102693
+ model: claim.mode.model,
102474
102694
  connectionId: claim.connection.id,
102475
102695
  connectionEpoch: claim.connection.connectionEpoch,
102476
102696
  startupFenceSequence: claim.connection.startupFenceSequence,
@@ -102478,13 +102698,34 @@ function registerSessionRoutes(app, deps) {
102478
102698
  replay: true
102479
102699
  });
102480
102700
  }
102481
- const broker = buildSessionCodexRealtimeBroker(
102482
- db,
102483
- settings,
102484
- workspaceId,
102485
- sessionId,
102486
- deps.codexFetch
102487
- );
102701
+ try {
102702
+ await realtimeVoiceBilling.admit({
102703
+ accountId: grant.accountId,
102704
+ workspaceId,
102705
+ model: claim.mode.model,
102706
+ attribution: creditDebitAttributionForGrant(grant)
102707
+ });
102708
+ } catch (error) {
102709
+ const refused = realtimeVoiceRefusalResponse(c, error);
102710
+ if (!refused) throw error;
102711
+ await withWorkspaceRls(
102712
+ db,
102713
+ workspaceId,
102714
+ async (scopedDb) => scopedDb.transaction(
102715
+ async (tx) => failSessionRealtimeConnectionInTransaction(tx, {
102716
+ workspaceId,
102717
+ sessionId,
102718
+ realtimeId,
102719
+ connectionId: claim.connection.id,
102720
+ operationId,
102721
+ connectionEpoch: claim.connection.connectionEpoch,
102722
+ failureCode: "billing_refused"
102723
+ })
102724
+ )
102725
+ ).catch(() => void 0);
102726
+ return refused;
102727
+ }
102728
+ const broker = (claim.mode.model === AZURE_LIVE_MODEL_ID ? buildSessionAzureLiveBroker : buildSessionCodexRealtimeBroker)(db, settings, workspaceId, sessionId, deps.codexFetch);
102488
102729
  try {
102489
102730
  const answer = await broker({
102490
102731
  request: providerRequest,
@@ -102524,6 +102765,13 @@ function registerSessionRoutes(app, deps) {
102524
102765
  })
102525
102766
  )
102526
102767
  ) : null;
102768
+ await settleRealtimeVoice({
102769
+ grant,
102770
+ attribution: creditDebitAttributionForGrant(grant),
102771
+ workspaceId,
102772
+ sessionId,
102773
+ realtimeId
102774
+ });
102527
102775
  return c.json({
102528
102776
  ...answer,
102529
102777
  connectionId: completed.connection.id,
@@ -102626,6 +102874,12 @@ function registerSessionRoutes(app, deps) {
102626
102874
  "Realtime Gateway tokens are single-use; reconnect with a new operation"
102627
102875
  );
102628
102876
  }
102877
+ await realtimeVoiceBilling.admit({
102878
+ accountId: grant.accountId,
102879
+ workspaceId,
102880
+ model: claim.mode.model,
102881
+ attribution: creditDebitAttributionForGrant(grant)
102882
+ });
102629
102883
  const secret = await createGatewayRealtimeConnectionSecret({
102630
102884
  db,
102631
102885
  settings,
@@ -102651,6 +102905,13 @@ function registerSessionRoutes(app, deps) {
102651
102905
  )
102652
102906
  );
102653
102907
  connectionCompleted = true;
102908
+ await settleRealtimeVoice({
102909
+ grant,
102910
+ attribution: creditDebitAttributionForGrant(grant),
102911
+ workspaceId,
102912
+ sessionId,
102913
+ realtimeId
102914
+ });
102654
102915
  return c.json({
102655
102916
  ...secret,
102656
102917
  connectionId: completed.connection.id,
@@ -102673,11 +102934,13 @@ function registerSessionRoutes(app, deps) {
102673
102934
  connectionId: claimed.connection.id,
102674
102935
  operationId,
102675
102936
  connectionEpoch: claimed.connection.connectionEpoch,
102676
- failureCode: error instanceof GatewayRealtimeBrokerError ? error.code : "gateway_error"
102937
+ failureCode: error instanceof GatewayRealtimeBrokerError ? error.code : error instanceof TranscriptionBillingRefusedError || error instanceof RealtimeVoiceUnavailableError ? "billing_refused" : "gateway_error"
102677
102938
  })
102678
102939
  )
102679
102940
  ).catch(() => void 0);
102680
102941
  }
102942
+ const refused = realtimeVoiceRefusalResponse(c, error);
102943
+ if (refused) return refused;
102681
102944
  if (error instanceof SessionRealtimeConflictError) throw sessionRealtimeHttpError(error);
102682
102945
  if (!(error instanceof GatewayRealtimeBrokerError)) throw error;
102683
102946
  const status = error.code === "credential_unavailable" ? 409 : 502;
@@ -106798,6 +107061,11 @@ function socialHttpException(error) {
106798
107061
  }
106799
107062
 
106800
107063
  // src/routes/workspaces.ts
107064
+ import {
107065
+ createRealtimeVoiceBilling as createRealtimeVoiceBilling2,
107066
+ deploymentRealtimeVoice,
107067
+ realtimeVoiceOfferProblem
107068
+ } from "@opengeni/core";
106801
107069
  import { withDirectModelProviders } from "@opengeni/config";
106802
107070
  import { listConnectionsMetadata as listConnectionsMetadata6 } from "@opengeni/db";
106803
107071
  import {
@@ -106959,13 +107227,13 @@ function recordWorkspaceDeleteObservation(observability, identity2, observation)
106959
107227
  labels: { outcome: observation.outcome }
106960
107228
  });
106961
107229
  }
106962
- for (const [kind, count] of Object.entries(observation.inventory ?? {})) {
107230
+ for (const [kind, count2] of Object.entries(observation.inventory ?? {})) {
106963
107231
  observability.observeHistogram({
106964
107232
  name: "opengeni_workspace_delete_inventory_rows",
106965
107233
  help: "Rows or live owners observed by workspace deletion preflight inventory class.",
106966
107234
  labels: { kind },
106967
107235
  buckets: INVENTORY_BUCKETS,
106968
- value: Math.max(0, Math.floor(count ?? 0))
107236
+ value: Math.max(0, Math.floor(count2 ?? 0))
106969
107237
  });
106970
107238
  }
106971
107239
  observability.info("Workspace deletion phase", {
@@ -107735,16 +108003,36 @@ function registerWorkspaceRoutes(app, deps) {
107735
108003
  return credentialReady ? { available: true, unavailableReason: null } : { available: false, unavailableReason: credentialReason };
107736
108004
  };
107737
108005
  const gatewayModels = Object.values(AI_GATEWAY_REALTIME_MODELS);
107738
- const models = [
107739
- ...gatewayModels.map((model, index) => ({
108006
+ const managedCandidates = [
108007
+ {
108008
+ id: AZURE_LIVE_MODEL_ID,
108009
+ label: "GPT Live 1",
108010
+ description: "Realtime voice with session delegation"
108011
+ },
108012
+ ...gatewayModels.map((model) => ({
107740
108013
  id: model.managedModelId,
107741
108014
  label: model.label,
108015
+ description: model.description
108016
+ }))
108017
+ ].filter((model) => {
108018
+ const voice = deploymentRealtimeVoice(deps.settings, model.id);
108019
+ return voice !== null && realtimeVoiceOfferProblem(deps.settings, voice) === null;
108020
+ }).filter(
108021
+ (model, _index, offered) => offered.some((candidate) => candidate.id === AZURE_LIVE_MODEL_ID) ? model.id === AZURE_LIVE_MODEL_ID : true
108022
+ );
108023
+ const hasCredits = managedCandidates.length > 0 && await createRealtimeVoiceBilling2({
108024
+ db: deps.db,
108025
+ settings: deps.settings
108026
+ }).hasSpendableCredits(grant.accountId);
108027
+ const models = [
108028
+ ...managedCandidates.map((model, index) => ({
108029
+ ...model,
107742
108030
  provider: "OpenGeni",
107743
- description: model.description,
107744
- ...availability(
107745
- Boolean(deps.settings.vercelAiGatewayApiKey),
107746
- "Opengeni Gateway voice is not configured"
107747
- ),
108031
+ ...hasCredits ? { available: true, unavailableReason: null, unavailableCode: null } : {
108032
+ available: false,
108033
+ unavailableReason: "Add Opengeni credits to use live voice",
108034
+ unavailableCode: "insufficient_credits"
108035
+ },
107748
108036
  recommended: index === 0
107749
108037
  })),
107750
108038
  {
@@ -111549,7 +111837,11 @@ import {
111549
111837
  resolveWorkspaceVoiceInputEnabled as resolveWorkspaceVoiceInputEnabled2,
111550
111838
  WorkspaceVoiceInputSettings as WorkspaceVoiceInputSettings2
111551
111839
  } from "@opengeni/contracts";
111552
- import { requireAccessGrant as requireAccessGrant50, TranscriptionServiceError as TranscriptionServiceError2 } from "@opengeni/core";
111840
+ import {
111841
+ requireAccessGrant as requireAccessGrant50,
111842
+ TranscriptionBillingRefusedError as TranscriptionBillingRefusedError3,
111843
+ TranscriptionServiceError as TranscriptionServiceError2
111844
+ } from "@opengeni/core";
111553
111845
  import { getWorkspace as getWorkspace10 } from "@opengeni/db";
111554
111846
 
111555
111847
  // src/routes/transcription-recordings.ts
@@ -111567,6 +111859,7 @@ import {
111567
111859
  normalizeMimeType,
111568
111860
  requireAccessGrant as requireAccessGrant49,
111569
111861
  TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS,
111862
+ TranscriptionBillingRefusedError as TranscriptionBillingRefusedError2,
111570
111863
  TranscriptionServiceError
111571
111864
  } from "@opengeni/core";
111572
111865
  import { retryWhileMissing as retryWhileMissing7 } from "@opengeni/storage";
@@ -111598,6 +111891,38 @@ import {
111598
111891
  } from "@opengeni/db";
111599
111892
  import { getWorkspace as getWorkspace9 } from "@opengeni/db";
111600
111893
 
111894
+ // src/transcription/wav-duration.ts
111895
+ function wavDurationSeconds(bytes) {
111896
+ if (bytes.byteLength < 12) return null;
111897
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
111898
+ if (ascii2(bytes, 0) !== "RIFF" || ascii2(bytes, 8) !== "WAVE") return null;
111899
+ let offset = 12;
111900
+ let byteRate = null;
111901
+ while (offset + 8 <= bytes.byteLength) {
111902
+ const id = ascii2(bytes, offset);
111903
+ const size = view.getUint32(offset + 4, true);
111904
+ const body3 = offset + 8;
111905
+ if (id === "fmt " && body3 + 12 <= bytes.byteLength) {
111906
+ byteRate = view.getUint32(body3 + 8, true);
111907
+ } else if (id === "data") {
111908
+ if (!byteRate) return null;
111909
+ const available = bytes.byteLength - body3;
111910
+ const dataBytes = size === 0 || size === 4294967295 ? available : Math.min(size, available);
111911
+ return dataBytes / byteRate;
111912
+ }
111913
+ offset = body3 + size + size % 2;
111914
+ }
111915
+ return null;
111916
+ }
111917
+ function ascii2(bytes, offset) {
111918
+ return String.fromCharCode(
111919
+ bytes[offset] ?? 0,
111920
+ bytes[offset + 1] ?? 0,
111921
+ bytes[offset + 2] ?? 0,
111922
+ bytes[offset + 3] ?? 0
111923
+ );
111924
+ }
111925
+
111601
111926
  // src/transcription/segmenter.ts
111602
111927
  import { spawn } from "child_process";
111603
111928
  import { createWriteStream } from "fs";
@@ -111629,7 +111954,7 @@ function createFfmpegTranscriptionSegmenter(input) {
111629
111954
  true
111630
111955
  );
111631
111956
  }
111632
- if (!Number.isSafeInteger(request.providerSegmentSeconds) || request.providerSegmentSeconds <= 0 || !Number.isSafeInteger(request.totalDurationMilliseconds) || request.totalDurationMilliseconds <= 0) {
111957
+ if (!Number.isSafeInteger(request.providerSegmentSeconds) || request.providerSegmentSeconds <= 0 || !Number.isSafeInteger(request.totalDurationMilliseconds) || request.totalDurationMilliseconds <= 0 || request.maxDecodeSeconds !== void 0 && (!Number.isSafeInteger(request.maxDecodeSeconds) || request.maxDecodeSeconds <= 0)) {
111633
111958
  throw new TranscriptionSegmenterError(
111634
111959
  "Audio segmentation bounds are invalid",
111635
111960
  "invalid_audio",
@@ -111661,6 +111986,8 @@ function createFfmpegTranscriptionSegmenter(input) {
111661
111986
  "-y",
111662
111987
  "-i",
111663
111988
  inputPath,
111989
+ // Output-side ceiling: audio past it is never decoded or written.
111990
+ ...request.maxDecodeSeconds !== void 0 ? ["-t", String(request.maxDecodeSeconds)] : [],
111664
111991
  "-map",
111665
111992
  "0:a:0",
111666
111993
  "-vn",
@@ -112123,7 +112450,9 @@ function registerResumableTranscriptionRoutes(app, deps) {
112123
112450
  const service = deps.transcription;
112124
112451
  if (!deps.objectStorage || !service || !await service.available({
112125
112452
  workspaceId: authority.workspaceId,
112126
- subjectId: authority.subjectId
112453
+ subjectId: authority.subjectId,
112454
+ preferredProvider: authority.voiceInput?.preferredProvider,
112455
+ fallbackEnabled: authority.voiceInput?.fallbackEnabled
112127
112456
  })) {
112128
112457
  return c.json({ code: "unavailable" }, 503);
112129
112458
  }
@@ -112166,6 +112495,31 @@ function registerResumableTranscriptionRoutes(app, deps) {
112166
112495
  false
112167
112496
  );
112168
112497
  }
112498
+ const segmentProviderId = claim.segment.providerId && claim.segment.providerId !== "host" ? claim.segment.providerId : null;
112499
+ if (segmentProviderId && service.admit) {
112500
+ try {
112501
+ await service.admit({
112502
+ providerId: segmentProviderId,
112503
+ accountId: authority.accountId,
112504
+ workspaceId: authority.workspaceId,
112505
+ attribution: authority.attribution
112506
+ });
112507
+ } catch (error) {
112508
+ if (!(error instanceof TranscriptionBillingRefusedError2)) throw error;
112509
+ await failTranscriptionRecordingSegment(deps.db, {
112510
+ workspaceId: authority.workspaceId,
112511
+ subjectId: authority.subjectId,
112512
+ recordingId: uuidParam3(c, "recordingId"),
112513
+ segmentNumber,
112514
+ attemptId,
112515
+ fallbackProviderId: null,
112516
+ // The real refusal, so a reloaded recording still explains it.
112517
+ errorCode: error.code,
112518
+ retryable: true
112519
+ }).catch(() => null);
112520
+ return transcriptionBillingRefusal(c, error);
112521
+ }
112522
+ }
112169
112523
  const providerStartedAt = /* @__PURE__ */ new Date();
112170
112524
  const providerDeadlineAt = new Date(
112171
112525
  providerStartedAt.getTime() + TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS
@@ -112188,7 +112542,17 @@ function registerResumableTranscriptionRoutes(app, deps) {
112188
112542
  durationSeconds: claim.segment.durationMilliseconds / 1e3,
112189
112543
  requestId: attemptId,
112190
112544
  providerDeadlineAt,
112191
- ...claim.segment.providerId && claim.segment.providerId !== "host" ? { providerId: claim.segment.providerId } : {}
112545
+ deferBillingSettlement: true,
112546
+ ...segmentProviderId ? { providerId: segmentProviderId } : {},
112547
+ billing: {
112548
+ // One settlement per prepared segment of this exact recording,
112549
+ // however many provider attempts it takes. The recording id is
112550
+ // client-chosen, so the unit id also binds its server creation
112551
+ // time and the server-produced segment bytes.
112552
+ sourceId: `${claim.recording.recording.id}:${Date.parse(claim.recording.recording.createdAt)}:${segmentNumber}:${claim.segment.sha256}`,
112553
+ attribution: authority.attribution,
112554
+ trustedDurationSeconds: wavDurationSeconds(stored.bytes) ?? void 0
112555
+ }
112192
112556
  });
112193
112557
  const completed = await completeTranscriptionRecordingSegment(deps.db, {
112194
112558
  workspaceId: authority.workspaceId,
@@ -112200,6 +112564,7 @@ function registerResumableTranscriptionRoutes(app, deps) {
112200
112564
  languages: result.languages,
112201
112565
  providerId: claim.segment.providerId ?? result.providerId
112202
112566
  });
112567
+ await result.settleBilling?.().catch(() => void 0);
112203
112568
  return c.json(await cleanupTerminalObjects(deps, authority, completed));
112204
112569
  } catch (error) {
112205
112570
  if (authority && attemptId && segmentNumber !== null) {
@@ -112223,6 +112588,9 @@ function registerResumableTranscriptionRoutes(app, deps) {
112223
112588
  errorCode: failure2.code,
112224
112589
  retryable: failure2.retryable
112225
112590
  }).catch(() => null);
112591
+ if (error instanceof TranscriptionBillingRefusedError2) {
112592
+ return transcriptionBillingRefusal(c, error);
112593
+ }
112226
112594
  if (persisted) return c.json(persisted);
112227
112595
  }
112228
112596
  return routeError2(c, error);
@@ -112269,7 +112637,8 @@ async function requireRecordingAuthority(c, deps, requirePolicy) {
112269
112637
  voiceInput: WorkspaceVoiceInputSettings.safeParse(workspace?.settings.voiceInput).data,
112270
112638
  accountId: grant.accountId,
112271
112639
  workspaceId,
112272
- subjectId: grant.subjectId
112640
+ subjectId: grant.subjectId,
112641
+ attribution: creditDebitAttributionForGrant(grant)
112273
112642
  };
112274
112643
  }
112275
112644
  async function resumableAvailable(deps, workspaceId) {
@@ -112340,6 +112709,9 @@ function processingFailure(error) {
112340
112709
  if (error instanceof TranscriptionSegmenterError) {
112341
112710
  return { code: error.code, retryable: error.retryable };
112342
112711
  }
112712
+ if (error instanceof TranscriptionBillingRefusedError2) {
112713
+ return { code: error.code, retryable: true };
112714
+ }
112343
112715
  if (error instanceof TranscriptionServiceError) {
112344
112716
  return {
112345
112717
  code: error.code,
@@ -112355,6 +112727,9 @@ function routeError2(c, error) {
112355
112727
  if (error instanceof TranscriptionRecordingNotFoundError) {
112356
112728
  return c.json({ code: "not_found" }, 404);
112357
112729
  }
112730
+ if (error instanceof TranscriptionBillingRefusedError2) {
112731
+ return transcriptionBillingRefusal(c, error);
112732
+ }
112358
112733
  if (error instanceof TranscriptionRecordingConflictError) {
112359
112734
  return c.json({ code: "conflict" }, 409);
112360
112735
  }
@@ -112478,12 +112853,21 @@ function registerTranscriptionRoutes(app, deps) {
112478
112853
  }
112479
112854
  const preferences = WorkspaceVoiceInputSettings2.safeParse(workspace.settings.voiceInput).data;
112480
112855
  const service = deps.transcription;
112481
- if (!service || !await service.available({ workspaceId, subjectId: grant.subjectId })) {
112856
+ if (!service || !await service.available({
112857
+ workspaceId,
112858
+ subjectId: grant.subjectId,
112859
+ preferredProvider: preferences?.preferredProvider,
112860
+ fallbackEnabled: preferences?.fallbackEnabled
112861
+ })) {
112482
112862
  return c.json({ code: "unavailable" }, 503);
112483
112863
  }
112484
112864
  try {
112485
112865
  const body3 = await audioRequest(c.req.raw, service.limits().maxSizeBytes);
112486
112866
  const result = await service.transcribe({
112867
+ billing: {
112868
+ sourceId: crypto.randomUUID(),
112869
+ attribution: creditDebitAttributionForGrant(grant)
112870
+ },
112487
112871
  preferredProvider: preferences?.preferredProvider,
112488
112872
  fallbackEnabled: preferences?.fallbackEnabled,
112489
112873
  workspaceId,
@@ -112504,6 +112888,9 @@ function registerTranscriptionRoutes(app, deps) {
112504
112888
  if (error instanceof TranscriptionServiceError2) {
112505
112889
  return c.json({ code: error.code }, error.status);
112506
112890
  }
112891
+ if (error instanceof TranscriptionBillingRefusedError3) {
112892
+ return transcriptionBillingRefusal(c, error);
112893
+ }
112507
112894
  if (isAbort3(error)) return c.json({ code: "cancelled" }, 499);
112508
112895
  return c.json({ code: "unknown" }, 500);
112509
112896
  }
@@ -112609,17 +112996,23 @@ function isAbort3(error) {
112609
112996
 
112610
112997
  // src/routes/session-artifact-associations.ts
112611
112998
  import {
112612
- requireAccessGrant as requireAccessGrant51,
112999
+ fileOwnerContextForAccess as fileOwnerContextForAccess6,
113000
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization37,
112613
113001
  requirePermission as requirePermission12,
112614
113002
  requireSessionAuthorization as requireSessionAuthorization15,
112615
113003
  SessionAuthorizationDeniedError as SessionAuthorizationDeniedError15,
112616
113004
  SessionAuthorizationUnavailableError as SessionAuthorizationUnavailableError13
112617
113005
  } from "@opengeni/core";
112618
113006
  import {
113007
+ getGeneratedImageArtifact as getGeneratedImageArtifact3,
113008
+ getGeneratedVideoArtifact as getGeneratedVideoArtifact2,
113009
+ getRetainedFileArtifact as getRetainedFileArtifact2,
112619
113010
  hasEditableArtifactSessionLink as hasEditableArtifactSessionLink2,
112620
113011
  hasWorkspaceArtifactSessionLink,
113012
+ listArtifactCatalogCandidates as listArtifactCatalogCandidates2,
112621
113013
  transactionallyAuthorizeEditableArtifactActor,
112622
- withRlsContext as withRlsContext3
113014
+ withRlsContext as withRlsContext3,
113015
+ withSessionRlsActorContext as withSessionRlsActorContext10
112623
113016
  } from "@opengeni/db";
112624
113017
  import { HTTPException as HTTPException107 } from "hono/http-exception";
112625
113018
  import { z as z55 } from "zod";
@@ -112630,12 +113023,18 @@ function registerSessionArtifactAssociationRoutes(app, deps) {
112630
113023
  "/v1/workspaces/:workspaceId/sessions/:sessionId/artifact-associations/:artifactId",
112631
113024
  async (context) => {
112632
113025
  const workspaceId = context.req.param("workspaceId");
112633
- const grant = await requireAccessGrant51(context, deps, workspaceId, "artifacts:read");
112634
- requirePermission12(grant, "sessions:read");
112635
113026
  const sessionId = context.req.param("sessionId");
112636
113027
  const artifactId2 = context.req.param("artifactId");
112637
- const kind = EditableArtifactId.safeParse(artifactId2).success ? "editable" : SessionId2.safeParse(artifactId2).success ? "site" : null;
112638
113028
  const requestedKind = context.req.query("kind");
113029
+ const kind = EditableArtifactId.safeParse(artifactId2).success ? "editable" : SessionId2.safeParse(artifactId2).success ? requestedKind === "retained" ? "retained" : "site" : null;
113030
+ const authorization = await requireAccessGrantAuthorization37(
113031
+ context,
113032
+ deps,
113033
+ workspaceId,
113034
+ kind === "retained" ? "files:read" : "artifacts:read"
113035
+ );
113036
+ const grant = authorization.grant;
113037
+ requirePermission12(grant, "sessions:read");
112639
113038
  if (!SessionId2.safeParse(sessionId).success || !kind || requestedKind !== void 0 && requestedKind !== kind) {
112640
113039
  throw new HTTPException107(404, { message: "Artifact association not found" });
112641
113040
  }
@@ -112647,7 +113046,18 @@ function registerSessionArtifactAssociationRoutes(app, deps) {
112647
113046
  surface: "http"
112648
113047
  });
112649
113048
  const scope = { accountId: grant.accountId, workspaceId };
112650
- const linked = kind === "editable" ? await hasEditableArtifactSessionLink2(deps.db, scope, sessionId, artifactId2) : await hasWorkspaceArtifactSessionLink(deps.db, workspaceId, sessionId, artifactId2);
113049
+ const linked = kind === "editable" ? await hasEditableArtifactSessionLink2(deps.db, scope, sessionId, artifactId2) : kind === "retained" ? await hasRetainedArtifactSessionLink(
113050
+ deps,
113051
+ authorization,
113052
+ workspaceId,
113053
+ sessionId,
113054
+ artifactId2.toLowerCase()
113055
+ ) : await hasWorkspaceArtifactSessionLink(
113056
+ deps.db,
113057
+ workspaceId,
113058
+ sessionId,
113059
+ artifactId2
113060
+ );
112651
113061
  if (!linked) throw new HTTPException107(404, { message: "Artifact association not found" });
112652
113062
  if (kind === "editable") {
112653
113063
  const decision = await withRlsContext3(
@@ -112679,6 +113089,38 @@ function registerSessionArtifactAssociationRoutes(app, deps) {
112679
113089
  }
112680
113090
  );
112681
113091
  }
113092
+ async function hasRetainedArtifactSessionLink(deps, authorization, workspaceId, sessionId, artifactId2) {
113093
+ const image = await getGeneratedImageArtifact3(deps.db, workspaceId, artifactId2);
113094
+ if (image) return image.sessionId === sessionId;
113095
+ const video = await getGeneratedVideoArtifact2(deps.db, workspaceId, artifactId2);
113096
+ if (video) return !video.artifact.deletedAt && video.artifact.sessionId === sessionId;
113097
+ const file = await getRetainedFileArtifact2(deps.db, workspaceId, artifactId2);
113098
+ if (!file) return false;
113099
+ const grant = authorization.grant;
113100
+ const actor = await fileOwnerContextForAccess6(deps, authorization, "files:read");
113101
+ const candidates = await withSessionRlsActorContext10(
113102
+ actor,
113103
+ () => listArtifactCatalogCandidates2(
113104
+ deps.db,
113105
+ { accountId: grant.accountId, workspaceId },
113106
+ {
113107
+ query: {
113108
+ sourceSessionId: sessionId,
113109
+ q: file.file.filename.slice(0, 200),
113110
+ sort: "newest",
113111
+ status: "active",
113112
+ limit: 100
113113
+ },
113114
+ kinds: ["file", "image"],
113115
+ snapshotAt: (/* @__PURE__ */ new Date()).toISOString(),
113116
+ limit: 201
113117
+ }
113118
+ )
113119
+ );
113120
+ return candidates.some(
113121
+ (candidate) => candidate.origin === "sandbox_file" && candidate.id === artifactId2
113122
+ );
113123
+ }
112682
113124
 
112683
113125
  // src/routes/video-generation.ts
112684
113126
  import {
@@ -112689,7 +113131,7 @@ import {
112689
113131
  } from "@opengeni/contracts";
112690
113132
  import {
112691
113133
  getVideoGenerationOperationSummary,
112692
- withSessionRlsActorContext as withSessionRlsActorContext10,
113134
+ withSessionRlsActorContext as withSessionRlsActorContext11,
112693
113135
  getWorkspaceVercelAiGatewayConnectionMetadata,
112694
113136
  getWorkspaceVideoGenerationPolicy,
112695
113137
  updateWorkspaceVideoGenerationPolicy,
@@ -112697,9 +113139,9 @@ import {
112697
113139
  workspaceXaiSubscriptionActive as workspaceXaiSubscriptionActive2
112698
113140
  } from "@opengeni/db";
112699
113141
  import {
112700
- requireAccessGrant as requireAccessGrant52,
112701
- requireAccessGrantAuthorization as requireAccessGrantAuthorization37,
112702
- fileOwnerContextForAccess as fileOwnerContextForAccess6,
113142
+ requireAccessGrant as requireAccessGrant51,
113143
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization38,
113144
+ fileOwnerContextForAccess as fileOwnerContextForAccess7,
112703
113145
  requireWorkspaceSettingsGrant as requireWorkspaceSettingsGrant6,
112704
113146
  VIDEO_GENERATION_MODEL_CATALOG,
112705
113147
  videoGenerationModelSupportsFundingSource,
@@ -112709,7 +113151,7 @@ import { HTTPException as HTTPException108 } from "hono/http-exception";
112709
113151
  function registerVideoGenerationRoutes(app, deps) {
112710
113152
  app.get("/v1/workspaces/:workspaceId/video-generation", async (c) => {
112711
113153
  const workspaceId = c.req.param("workspaceId");
112712
- const grant = await requireAccessGrant52(c, deps, workspaceId, "workspace:read");
113154
+ const grant = await requireAccessGrant51(c, deps, workspaceId, "workspace:read");
112713
113155
  const [policy, connection, supergrokConfigured] = await Promise.all([
112714
113156
  getWorkspaceVideoGenerationPolicy(deps.db, workspaceId),
112715
113157
  getWorkspaceVercelAiGatewayConnectionMetadata(deps.db, workspaceId),
@@ -112783,9 +113225,9 @@ function registerVideoGenerationRoutes(app, deps) {
112783
113225
  });
112784
113226
  app.get("/v1/workspaces/:workspaceId/video-generation/operations/:operationId", async (c) => {
112785
113227
  const workspaceId = c.req.param("workspaceId");
112786
- const access = await requireAccessGrantAuthorization37(c, deps, workspaceId, "workspace:read");
112787
- const actor = await fileOwnerContextForAccess6(deps, access, "workspace:read");
112788
- const summary = await withSessionRlsActorContext10(
113228
+ const access = await requireAccessGrantAuthorization38(c, deps, workspaceId, "workspace:read");
113229
+ const actor = await fileOwnerContextForAccess7(deps, access, "workspace:read");
113230
+ const summary = await withSessionRlsActorContext11(
112789
113231
  actor,
112790
113232
  () => getVideoGenerationOperationSummary(deps.db, workspaceId, c.req.param("operationId"))
112791
113233
  );
@@ -113983,7 +114425,7 @@ function registerManagedSignInMethodRoutes(app, deps) {
113983
114425
  select provider_id as provider, (provider_id <> 'credential' or length(password)>0) as usable from auth_identities where user_id=${actor.authUserId}`
113984
114426
  );
113985
114427
  const connected = (provider) => accounts.some((a) => a.provider === provider && a.usable) && projection.loginBindings.some((b) => b.providerId === provider && b.status === "active");
113986
- const count = ["credential", "google", "github"].filter(
114428
+ const count2 = ["credential", "google", "github"].filter(
113987
114429
  (p) => connected(p) && configured(deps, p)
113988
114430
  ).length;
113989
114431
  return context.json(
@@ -113996,7 +114438,7 @@ function registerManagedSignInMethodRoutes(app, deps) {
113996
114438
  provider,
113997
114439
  connected: connected(provider),
113998
114440
  available: configured(deps, provider),
113999
- canDisconnect: provider !== "credential" && connected(provider) && count - (configured(deps, provider) ? 1 : 0) > 0,
114441
+ canDisconnect: provider !== "credential" && connected(provider) && count2 - (configured(deps, provider) ? 1 : 0) > 0,
114000
114442
  implicitRelinkingSuppressed: !connected(provider) && projection.loginBindings.some(
114001
114443
  (b) => b.providerId === provider && b.status !== "active"
114002
114444
  )
@@ -115321,7 +115763,7 @@ import {
115321
115763
  organizationWorkspaceInScope as organizationWorkspaceInScope4,
115322
115764
  hasPermission as hasPermission31,
115323
115765
  requireAccessContext as requireAccessContext13,
115324
- requireAccessGrantAuthorization as requireAccessGrantAuthorization38,
115766
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization39,
115325
115767
  requireSessionAuthorizationListScope as requireSessionAuthorizationListScope3,
115326
115768
  SessionAuthorizationDeniedError as SessionAuthorizationDeniedError16,
115327
115769
  SessionAuthorizationUnavailableError as SessionAuthorizationUnavailableError14,
@@ -115474,7 +115916,7 @@ function registerOrganizationSessionRoutes(app, deps) {
115474
115916
  async function listWorkspaceSessionPage(c, deps, input) {
115475
115917
  let authorization;
115476
115918
  try {
115477
- authorization = await requireAccessGrantAuthorization38(
115919
+ authorization = await requireAccessGrantAuthorization39(
115478
115920
  c,
115479
115921
  deps,
115480
115922
  input.workspace.id,
@@ -116084,7 +116526,7 @@ import {
116084
116526
  import {
116085
116527
  issueManagedHumanUserResourceGrant,
116086
116528
  listManagedHumanUserResourceAuthorities,
116087
- requireAccessGrantAuthorization as requireAccessGrantAuthorization39,
116529
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization40,
116088
116530
  revokeManagedHumanUserResourceGrant,
116089
116531
  SessionAuthorizationDeniedError as SessionAuthorizationDeniedError17,
116090
116532
  SessionAuthorizationUnavailableError as SessionAuthorizationUnavailableError15,
@@ -116124,7 +116566,7 @@ function registerUserResourceAuthorityRoutes(app, deps) {
116124
116566
  });
116125
116567
  if (!query.success) throw new HTTPException116(422, { message: "explicit scope=user required" });
116126
116568
  const workspaceId = context.req.param("workspaceId");
116127
- const access = await requireAccessGrantAuthorization39(
116569
+ const access = await requireAccessGrantAuthorization40(
116128
116570
  context,
116129
116571
  deps,
116130
116572
  workspaceId,
@@ -116152,7 +116594,7 @@ function registerUserResourceAuthorityRoutes(app, deps) {
116152
116594
  const authorityId = z62.string().uuid().safeParse(context.req.param("authorityId"));
116153
116595
  if (!authorityId.success) throw new HTTPException116(422, { message: "invalid authority id" });
116154
116596
  const request = await body2(context, IssueUserResourceGrantRequest);
116155
- const access = await requireAccessGrantAuthorization39(
116597
+ const access = await requireAccessGrantAuthorization40(
116156
116598
  context,
116157
116599
  deps,
116158
116600
  workspaceId,
@@ -116182,7 +116624,7 @@ function registerUserResourceAuthorityRoutes(app, deps) {
116182
116624
  const grantId = z62.string().uuid().safeParse(context.req.param("grantId"));
116183
116625
  if (!grantId.success) throw new HTTPException116(422, { message: "invalid grant id" });
116184
116626
  const workspaceId = context.req.param("workspaceId");
116185
- const access = await requireAccessGrantAuthorization39(
116627
+ const access = await requireAccessGrantAuthorization40(
116186
116628
  context,
116187
116629
  deps,
116188
116630
  workspaceId,
@@ -116202,28 +116644,94 @@ function registerUserResourceAuthorityRoutes(app, deps) {
116202
116644
  }
116203
116645
 
116204
116646
  // src/transcription/service.ts
116205
- import { resolveVoiceInputProviderRegistry } from "@opengeni/config";
116647
+ import {
116648
+ resolveVoiceInputProviderRegistry,
116649
+ voiceInputCreditBillingActive
116650
+ } from "@opengeni/config";
116206
116651
  import { VOICE_INPUT_ACCEPTED_MIME_TYPES } from "@opengeni/contracts";
116652
+ import { voiceTranscriptionSettlementKeys } from "@opengeni/db";
116207
116653
  import {
116654
+ createVoiceInputBilling,
116208
116655
  filenameForMimeType,
116209
116656
  isAcceptedMimeType as isAcceptedMimeType2,
116210
116657
  normalizeMimeType as normalizeMimeType2,
116211
116658
  TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS as TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS2,
116212
- TranscriptionServiceError as TranscriptionServiceError6
116659
+ TranscriptionServiceError as TranscriptionServiceError7
116213
116660
  } from "@opengeni/core";
116214
116661
 
116215
- // src/transcription/providers/openai.ts
116662
+ // src/transcription/normalize.ts
116216
116663
  import { TranscriptionServiceError as TranscriptionServiceError3 } from "@opengeni/core";
116664
+ function createFfmpegAudioNormalizer(input) {
116665
+ const segmenter = createFfmpegTranscriptionSegmenter({ ffmpegPath: input.ffmpegPath });
116666
+ return async ({ audio, mimeType, maxDurationSeconds, signal }) => {
116667
+ if (!Number.isFinite(maxDurationSeconds) || maxDurationSeconds <= 0) {
116668
+ throw new TranscriptionServiceError3({
116669
+ code: "unavailable",
116670
+ message: "Voice input duration limit is not configured."
116671
+ });
116672
+ }
116673
+ const decodeSeconds = Math.ceil(maxDurationSeconds) + 1;
116674
+ const segmentSeconds = decodeSeconds + 1;
116675
+ let normalized;
116676
+ try {
116677
+ for await (const segment of segmenter.segment({
116678
+ sourceMimeType: mimeType,
116679
+ chunks: async function* () {
116680
+ yield audio;
116681
+ }(),
116682
+ providerSegmentSeconds: segmentSeconds,
116683
+ totalDurationMilliseconds: segmentSeconds * 1e3,
116684
+ maxDecodeSeconds: decodeSeconds,
116685
+ ...signal ? { signal } : {}
116686
+ })) {
116687
+ if (normalized) {
116688
+ throw new TranscriptionServiceError3({ code: "too_large", message: "Audio is too long." });
116689
+ }
116690
+ normalized = segment.bytes;
116691
+ }
116692
+ } catch (error) {
116693
+ if (error instanceof TranscriptionSegmenterError) {
116694
+ throw new TranscriptionServiceError3({
116695
+ code: error.code === "unknown" ? "invalid_audio" : error.code,
116696
+ message: "Audio could not be prepared for transcription.",
116697
+ retryable: error.retryable
116698
+ });
116699
+ }
116700
+ throw error;
116701
+ }
116702
+ const durationSeconds = normalized ? wavDurationSeconds(normalized) : null;
116703
+ if (!normalized || durationSeconds === null || !(durationSeconds > 0)) {
116704
+ throw new TranscriptionServiceError3({
116705
+ code: "invalid_audio",
116706
+ message: "Audio could not be decoded."
116707
+ });
116708
+ }
116709
+ if (durationSeconds > decodeSeconds - 0.02) {
116710
+ throw new TranscriptionServiceError3({ code: "too_large", message: "Audio is too long." });
116711
+ }
116712
+ return { bytes: normalized, durationSeconds };
116713
+ };
116714
+ }
116715
+
116716
+ // src/transcription/providers/openai.ts
116717
+ import {
116718
+ voiceInputModelReportsDurationOnly
116719
+ } from "@opengeni/config";
116720
+ import { TranscriptionServiceError as TranscriptionServiceError4 } from "@opengeni/core";
116217
116721
  function createOpenAiTranscriptionProvider(input) {
116218
116722
  const fetchImpl = input.fetch ?? fetch;
116219
116723
  return {
116220
116724
  id: "openai",
116221
116725
  supportsServerDeadline: true,
116726
+ deploymentFunded: { model: input.model, pricing: input.pricing ?? null },
116222
116727
  available: () => true,
116223
116728
  async transcribe({ audio, mimeType, filename, requestId, signal }) {
116224
116729
  const form = new FormData();
116225
116730
  form.append("file", audioBlob(audio, mimeType), filename);
116226
116731
  form.append("model", input.model);
116732
+ if (voiceInputModelReportsDurationOnly(input.model)) {
116733
+ form.append("response_format", "verbose_json");
116734
+ }
116227
116735
  let response;
116228
116736
  try {
116229
116737
  response = await fetchImpl(`${input.baseUrl}/audio/transcriptions`, {
@@ -116241,50 +116749,89 @@ function createOpenAiTranscriptionProvider(input) {
116241
116749
  }
116242
116750
  if (!response.ok) throw responseError(response.status);
116243
116751
  const body3 = await response.json().catch(() => null);
116244
- if (!body3 || typeof body3.text !== "string") {
116245
- throw new TranscriptionServiceError3({
116752
+ const parsed = parseTranscriptionResponseBody(body3);
116753
+ if (!parsed) {
116754
+ throw new TranscriptionServiceError4({
116246
116755
  code: "provider",
116247
116756
  message: "Invalid transcription response."
116248
116757
  });
116249
116758
  }
116250
- return {
116251
- text: body3.text,
116252
- languages: typeof body3.language === "string" && body3.language ? [body3.language] : []
116253
- };
116759
+ return parsed;
116254
116760
  }
116255
116761
  };
116256
116762
  }
116763
+ function parseTranscriptionResponseBody(body3) {
116764
+ if (!body3 || typeof body3 !== "object") return null;
116765
+ const record6 = body3;
116766
+ if (typeof record6.text !== "string") return null;
116767
+ const languages = typeof record6.language === "string" && record6.language ? [record6.language] : Array.isArray(record6.languages) ? record6.languages.flatMap((language) => {
116768
+ const code = typeof language === "string" ? language : language?.code;
116769
+ return typeof code === "string" && code ? [code] : [];
116770
+ }) : [];
116771
+ return { text: record6.text, languages, usage: parseTranscriptionUsage(record6) };
116772
+ }
116773
+ function parseTranscriptionUsage(body3) {
116774
+ const usage = body3.usage;
116775
+ if (usage && typeof usage === "object") {
116776
+ const record6 = usage;
116777
+ if (record6.type === "tokens") {
116778
+ const inputTokens = count(record6.input_tokens);
116779
+ const outputTokens = count(record6.output_tokens);
116780
+ if (inputTokens !== null && outputTokens !== null) {
116781
+ const details = record6.input_token_details && typeof record6.input_token_details === "object" ? record6.input_token_details : {};
116782
+ return {
116783
+ kind: "tokens",
116784
+ inputTokens,
116785
+ audioInputTokens: count(details.audio_tokens) ?? 0,
116786
+ textInputTokens: count(details.text_tokens) ?? 0,
116787
+ outputTokens
116788
+ };
116789
+ }
116790
+ }
116791
+ if (record6.type === "duration" && seconds(record6.seconds) !== null) {
116792
+ return { kind: "duration", seconds: seconds(record6.seconds) };
116793
+ }
116794
+ }
116795
+ const duration = seconds(body3.duration);
116796
+ return duration === null ? null : { kind: "duration", seconds: duration };
116797
+ }
116798
+ function count(value) {
116799
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
116800
+ }
116801
+ function seconds(value) {
116802
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value < 86400 ? value : null;
116803
+ }
116257
116804
  function audioBlob(audio, mimeType) {
116258
116805
  return new Blob([Uint8Array.from(audio).buffer], { type: mimeType });
116259
116806
  }
116260
116807
  function responseError(status) {
116261
116808
  if (status === 401 || status === 403) {
116262
- return new TranscriptionServiceError3({
116809
+ return new TranscriptionServiceError4({
116263
116810
  fallbackSafe: true,
116264
116811
  code: "unavailable",
116265
116812
  message: "Transcription is unavailable."
116266
116813
  });
116267
116814
  }
116268
116815
  if (status === 413) {
116269
- return new TranscriptionServiceError3({
116816
+ return new TranscriptionServiceError4({
116270
116817
  code: "too_large",
116271
116818
  message: "Audio is too large."
116272
116819
  });
116273
116820
  }
116274
116821
  if (status === 400 || status === 422) {
116275
- return new TranscriptionServiceError3({
116822
+ return new TranscriptionServiceError4({
116276
116823
  code: "invalid_audio",
116277
116824
  message: "Audio could not be transcribed."
116278
116825
  });
116279
116826
  }
116280
116827
  if (status === 408 || status === 504) {
116281
- return new TranscriptionServiceError3({
116828
+ return new TranscriptionServiceError4({
116282
116829
  code: "timeout",
116283
116830
  message: "Transcription timed out.",
116284
116831
  retryable: true
116285
116832
  });
116286
116833
  }
116287
- return new TranscriptionServiceError3({
116834
+ return new TranscriptionServiceError4({
116288
116835
  code: status >= 500 ? "unavailable" : "provider",
116289
116836
  message: "Transcription provider failed.",
116290
116837
  retryable: status >= 500
@@ -116292,29 +116839,105 @@ function responseError(status) {
116292
116839
  }
116293
116840
  function fetchError(error) {
116294
116841
  if (error instanceof DOMException && error.name === "AbortError") {
116295
- return new TranscriptionServiceError3({
116842
+ return new TranscriptionServiceError4({
116296
116843
  code: "cancelled",
116297
116844
  message: "Transcription was cancelled."
116298
116845
  });
116299
116846
  }
116300
- return new TranscriptionServiceError3({
116847
+ return new TranscriptionServiceError4({
116301
116848
  code: "network",
116302
116849
  message: "Transcription provider is unreachable.",
116303
116850
  retryable: true
116304
116851
  });
116305
116852
  }
116306
116853
 
116854
+ // src/transcription/providers/azure-mai.ts
116855
+ function createMaiTranscriptionProvider(input) {
116856
+ const fetchImpl = input.fetch ?? fetch;
116857
+ const normalize = createFfmpegAudioNormalizer({ ffmpegPath: input.ffmpegPath });
116858
+ return {
116859
+ id: "azure-mai",
116860
+ supportsServerDeadline: true,
116861
+ deploymentFunded: { model: input.model, pricing: input.pricing ?? null },
116862
+ available: () => Boolean(input.apiKey),
116863
+ async transcribe({ audio, mimeType, filename, requestId, signal }) {
116864
+ let bytes = audio;
116865
+ let contentType = mimeType;
116866
+ let name = filename;
116867
+ if (!["audio/wav", "audio/x-wav", "audio/mpeg", "audio/flac"].includes(mimeType)) {
116868
+ const normalized = await normalize({
116869
+ audio,
116870
+ mimeType,
116871
+ maxDurationSeconds: 600,
116872
+ ...signal ? { signal } : {}
116873
+ });
116874
+ bytes = normalized.bytes;
116875
+ contentType = "audio/wav";
116876
+ name = "audio.wav";
116877
+ }
116878
+ const form = new FormData();
116879
+ form.append("audio", new Blob([Uint8Array.from(bytes).buffer], { type: contentType }), name);
116880
+ form.append(
116881
+ "definition",
116882
+ JSON.stringify({ enhancedMode: { enabled: true, model: input.model } })
116883
+ );
116884
+ let response;
116885
+ try {
116886
+ response = await fetchImpl(
116887
+ `${input.endpoint.replace(/\/+$/, "")}/speechtotext/transcriptions:transcribe?api-version=${encodeURIComponent(input.apiVersion)}`,
116888
+ {
116889
+ method: "POST",
116890
+ headers: {
116891
+ "Ocp-Apim-Subscription-Key": input.apiKey,
116892
+ "x-opengeni-request-id": requestId
116893
+ },
116894
+ body: form,
116895
+ ...signal ? { signal } : {}
116896
+ }
116897
+ );
116898
+ } catch (error) {
116899
+ throw fetchError(error);
116900
+ }
116901
+ if (!response.ok) throw responseError(response.status);
116902
+ const body3 = await response.json().catch(() => null);
116903
+ if (!body3 || !Array.isArray(body3.combinedPhrases) || !body3.combinedPhrases.every(
116904
+ (phrase) => typeof phrase === "object" && phrase !== null && "text" in phrase && typeof phrase.text === "string"
116905
+ )) {
116906
+ throw responseError(502);
116907
+ }
116908
+ return {
116909
+ usage: typeof body3.durationMilliseconds === "number" && Number.isFinite(body3.durationMilliseconds) && body3.durationMilliseconds >= 0 ? { kind: "duration", seconds: body3.durationMilliseconds / 1e3 } : null,
116910
+ text: body3.combinedPhrases.map((phrase) => phrase.text).join("\n"),
116911
+ languages: [
116912
+ ...new Set(
116913
+ (Array.isArray(body3.phrases) ? body3.phrases : []).flatMap(
116914
+ (phrase) => typeof phrase?.locale === "string" && phrase.locale ? [phrase.locale] : []
116915
+ )
116916
+ )
116917
+ ]
116918
+ };
116919
+ }
116920
+ };
116921
+ }
116922
+
116307
116923
  // src/transcription/providers/azure-openai.ts
116924
+ import { voiceInputModelReportsDurationOnly as voiceInputModelReportsDurationOnly2 } from "@opengeni/config";
116308
116925
  function createAzureOpenAiTranscriptionProvider(input) {
116309
116926
  const fetchImpl = input.fetch ?? fetch;
116310
- const url = `${input.endpoint}/openai/deployments/${encodeURIComponent(input.deployment)}/audio/transcriptions?api-version=${encodeURIComponent(input.apiVersion)}`;
116927
+ const model = input.model ?? input.deployment;
116928
+ const endpoint = input.endpoint.replace(/\/+$/, "");
116929
+ const url = `${endpoint}/openai/deployments/${encodeURIComponent(input.deployment)}/audio/transcriptions?api-version=${encodeURIComponent(input.apiVersion)}`;
116311
116930
  return {
116312
116931
  id: "azure-openai",
116313
116932
  supportsServerDeadline: true,
116933
+ deploymentFunded: { model, pricing: input.pricing ?? null },
116314
116934
  available: () => Boolean(input.apiKey || input.adToken),
116315
116935
  async transcribe({ audio, mimeType, filename, requestId, signal }) {
116316
116936
  const form = new FormData();
116317
116937
  form.append("file", new Blob([Uint8Array.from(audio).buffer], { type: mimeType }), filename);
116938
+ if (voiceInputModelReportsDurationOnly2(model)) {
116939
+ form.append("response_format", "verbose_json");
116940
+ }
116318
116941
  const headers = input.apiKey ? { "api-key": input.apiKey, "x-opengeni-request-id": requestId } : { Authorization: `Bearer ${input.adToken}`, "x-opengeni-request-id": requestId };
116319
116942
  let response;
116320
116943
  try {
@@ -116329,13 +116952,11 @@ function createAzureOpenAiTranscriptionProvider(input) {
116329
116952
  }
116330
116953
  if (!response.ok) throw responseError(response.status);
116331
116954
  const body3 = await response.json().catch(() => null);
116332
- if (!body3 || typeof body3.text !== "string") {
116955
+ const parsed = parseTranscriptionResponseBody(body3);
116956
+ if (!parsed) {
116333
116957
  throw responseError(502);
116334
116958
  }
116335
- return {
116336
- text: body3.text,
116337
- languages: typeof body3.language === "string" && body3.language ? [body3.language] : []
116338
- };
116959
+ return parsed;
116339
116960
  }
116340
116961
  };
116341
116962
  }
@@ -116343,7 +116964,7 @@ function createAzureOpenAiTranscriptionProvider(input) {
116343
116964
  // src/transcription/providers/codex-subscription.ts
116344
116965
  import { CODEX_CLIENT_VERSION as CODEX_CLIENT_VERSION4, CODEX_ORIGINATOR } from "@opengeni/codex/constants";
116345
116966
  import {
116346
- TranscriptionServiceError as TranscriptionServiceError4
116967
+ TranscriptionServiceError as TranscriptionServiceError5
116347
116968
  } from "@opengeni/core";
116348
116969
  import { buildCodexTokenResolver as buildCodexTokenResolver4, listCodexAccountStatuses as listCodexAccountStatuses3 } from "@opengeni/db";
116349
116970
  var TRANSCRIBE_URL = "https://chatgpt.com/backend-api/transcribe";
@@ -116369,7 +116990,7 @@ function createCodexSubscriptionTranscriptionProvider(input) {
116369
116990
  (candidate) => candidate.isActive && candidate.status === "active"
116370
116991
  );
116371
116992
  if (!account) {
116372
- throw new TranscriptionServiceError4({
116993
+ throw new TranscriptionServiceError5({
116373
116994
  fallbackSafe: true,
116374
116995
  code: "unavailable",
116375
116996
  message: "Transcription is unavailable."
@@ -116380,7 +117001,7 @@ function createCodexSubscriptionTranscriptionProvider(input) {
116380
117001
  try {
116381
117002
  token = await resolver.getToken();
116382
117003
  } catch {
116383
- throw new TranscriptionServiceError4({
117004
+ throw new TranscriptionServiceError5({
116384
117005
  fallbackSafe: true,
116385
117006
  code: "unavailable",
116386
117007
  message: "Transcription is unavailable."
@@ -116431,7 +117052,7 @@ function createCodexSubscriptionTranscriptionProvider(input) {
116431
117052
 
116432
117053
  // src/transcription/providers/xai-subscription.ts
116433
117054
  import {
116434
- TranscriptionServiceError as TranscriptionServiceError5
117055
+ TranscriptionServiceError as TranscriptionServiceError6
116435
117056
  } from "@opengeni/core";
116436
117057
  import { workspaceXaiSubscriptionActive as workspaceXaiSubscriptionActive3 } from "@opengeni/db";
116437
117058
  import {
@@ -116480,7 +117101,7 @@ function createXaiSubscriptionTranscriptionProvider(input) {
116480
117101
  ...input.fetch ? { fetch: input.fetch } : {}
116481
117102
  });
116482
117103
  } catch {
116483
- throw new TranscriptionServiceError5({
117104
+ throw new TranscriptionServiceError6({
116484
117105
  code: "unavailable",
116485
117106
  fallbackSafe: true,
116486
117107
  message: "Transcription is unavailable."
@@ -116511,7 +117132,7 @@ function createXaiSubscriptionTranscriptionProvider(input) {
116511
117132
  try {
116512
117133
  return await (refresh ? auth.context.refresh() : auth.context.getToken());
116513
117134
  } catch {
116514
- throw new TranscriptionServiceError5({
117135
+ throw new TranscriptionServiceError6({
116515
117136
  code: "unavailable",
116516
117137
  message: "Reconnect the SuperGrok account to use transcription.",
116517
117138
  fallbackSafe: true
@@ -116528,7 +117149,7 @@ function createXaiSubscriptionTranscriptionProvider(input) {
116528
117149
  response = await request(token.accessToken);
116529
117150
  }
116530
117151
  } catch (error) {
116531
- if (error instanceof TranscriptionServiceError5) throw error;
117152
+ if (error instanceof TranscriptionServiceError6) throw error;
116532
117153
  throw fetchError(error);
116533
117154
  }
116534
117155
  if (!response.ok) throw responseError(response.status);
@@ -116581,6 +117202,12 @@ function createTranscriptionService(input) {
116581
117202
  ...config,
116582
117203
  ...input.fetch ? { fetch: input.fetch } : {}
116583
117204
  });
117205
+ case "azure-mai":
117206
+ return createMaiTranscriptionProvider({
117207
+ ...config,
117208
+ ffmpegPath: input.settings.voiceInputFfmpegPath,
117209
+ ...input.fetch ? { fetch: input.fetch } : {}
117210
+ });
116584
117211
  case "azure-openai":
116585
117212
  return createAzureOpenAiTranscriptionProvider({
116586
117213
  ...config,
@@ -116611,32 +117238,113 @@ function createTranscriptionService(input) {
116611
117238
  };
116612
117239
  const providerRequestTimeoutMilliseconds = input.providerRequestTimeoutMilliseconds ?? TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS2;
116613
117240
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
117241
+ const log = input.log ?? ((message, attributes) => {
117242
+ console.error(message, attributes);
117243
+ });
117244
+ const billing = input.billing ?? createVoiceInputBilling({
117245
+ db: input.db,
117246
+ settings: input.settings,
117247
+ onReconcileError: (error, scope) => {
117248
+ log("Voice input charge reconciliation failed", {
117249
+ ...scope,
117250
+ errorClass: error instanceof Error ? error.name : "UnknownError",
117251
+ error: boundedErrorMessage(error)
117252
+ });
117253
+ }
117254
+ });
117255
+ let normalizer = input.normalizeAudio;
117256
+ const normalizeAudio = (request) => {
117257
+ normalizer ??= createFfmpegAudioNormalizer({ ffmpegPath: input.settings.voiceInputFfmpegPath });
117258
+ return normalizer(request);
117259
+ };
117260
+ const retryDelays = input.settlementRetryDelaysMilliseconds ?? [2e3, 15e3, 6e4];
117261
+ const settleWithoutFailing = async (settlement) => {
117262
+ try {
117263
+ return (await billing.settle(settlement)).creditCostMicros;
117264
+ } catch (error) {
117265
+ reportUnsettled(settlement, error, 0, retryDelays.length > 0 && !deterministic(error));
117266
+ if (!deterministic(error)) scheduleSettlementRetry(settlement, 0);
117267
+ return 0;
117268
+ }
117269
+ };
117270
+ const deterministic = (error) => error instanceof TranscriptionServiceError7;
117271
+ const scheduleSettlementRetry = (settlement, attempt) => {
117272
+ const delay = retryDelays[attempt];
117273
+ if (delay === void 0) return;
117274
+ const timer = setTimeout(() => {
117275
+ void billing.settle(settlement).then(
117276
+ () => {
117277
+ log("Voice input charge settled on retry", {
117278
+ ...settlementAttributes(settlement),
117279
+ attempt: attempt + 1
117280
+ });
117281
+ },
117282
+ (error) => {
117283
+ const more = attempt + 1 < retryDelays.length;
117284
+ reportUnsettled(settlement, error, attempt + 1, more);
117285
+ if (more) scheduleSettlementRetry(settlement, attempt + 1);
117286
+ }
117287
+ );
117288
+ }, delay);
117289
+ timer.unref?.();
117290
+ };
117291
+ const reportUnsettled = (settlement, error, attempt, willRetry) => {
117292
+ log(
117293
+ willRetry ? "Voice input charge is unsettled after provider success; retrying" : "Voice input charge is UNSETTLED after provider success; manual reconciliation needed unless its usage receipt committed",
117294
+ {
117295
+ ...settlementAttributes(settlement),
117296
+ attempt,
117297
+ willRetry,
117298
+ errorClass: error instanceof Error ? error.name : "UnknownError",
117299
+ error: boundedErrorMessage(error)
117300
+ }
117301
+ );
117302
+ };
117303
+ const creditBillingActive = voiceInputCreditBillingActive(input.settings);
117304
+ const chargeable = (provider) => creditBillingActive && provider.deploymentFunded !== void 0;
116614
117305
  return {
116615
117306
  limits: () => limits,
116616
117307
  async available(context) {
116617
- return (await Promise.all(providers.map((provider) => provider.available(context)))).some(
116618
- Boolean
117308
+ return Boolean(
117309
+ await firstAvailable(orderedProviders(providers, context ?? {}), context ?? {})
116619
117310
  );
116620
117311
  },
117312
+ async availableProviderIds(context) {
117313
+ const available = await Promise.all(
117314
+ providers.map(async (provider) => {
117315
+ try {
117316
+ return await provider.available(context) ? provider.id : null;
117317
+ } catch {
117318
+ return null;
117319
+ }
117320
+ })
117321
+ );
117322
+ return available.filter((id) => id !== null);
117323
+ },
116621
117324
  async selectProvider(context) {
116622
117325
  return (await firstAvailable(orderedProviders(providers, context), context))?.id ?? null;
116623
117326
  },
117327
+ async admit({ providerId, accountId, workspaceId, attribution }) {
117328
+ const provider = providers.find((candidate) => candidate.id === providerId);
117329
+ if (!provider || !chargeable(provider)) return;
117330
+ await billing.admit({ accountId, workspaceId, attribution });
117331
+ },
116624
117332
  async transcribe(request) {
116625
117333
  const mimeType = normalizeMimeType2(request.mimeType);
116626
117334
  if (!isAcceptedMimeType2(mimeType, limits.acceptedMimeTypes)) {
116627
- throw new TranscriptionServiceError6({
117335
+ throw new TranscriptionServiceError7({
116628
117336
  code: "not_supported",
116629
117337
  message: "Unsupported audio format."
116630
117338
  });
116631
117339
  }
116632
117340
  if (request.audio.byteLength > limits.maxSizeBytes) {
116633
- throw new TranscriptionServiceError6({
117341
+ throw new TranscriptionServiceError7({
116634
117342
  code: "too_large",
116635
117343
  message: "Audio is too large."
116636
117344
  });
116637
117345
  }
116638
117346
  if (request.durationSeconds !== void 0 && (!Number.isFinite(request.durationSeconds) || request.durationSeconds < 0 || request.durationSeconds > limits.maxDurationSeconds)) {
116639
- throw new TranscriptionServiceError6({
117347
+ throw new TranscriptionServiceError7({
116640
117348
  code: "invalid_audio",
116641
117349
  message: "Invalid audio duration."
116642
117350
  });
@@ -116646,22 +117354,51 @@ function createTranscriptionService(input) {
116646
117354
  subjectId: request.subjectId
116647
117355
  }) : await firstAvailable(orderedProviders(providers, request), request);
116648
117356
  if (!provider) {
116649
- throw new TranscriptionServiceError6({
117357
+ throw new TranscriptionServiceError7({
116650
117358
  fallbackSafe: true,
116651
117359
  code: "unavailable",
116652
117360
  message: "Transcription is unavailable."
116653
117361
  });
116654
117362
  }
116655
117363
  if (provider.supportsServerDeadline !== true) {
116656
- throw new TranscriptionServiceError6({
117364
+ throw new TranscriptionServiceError7({
116657
117365
  code: "unavailable",
116658
117366
  message: "Transcription provider does not support bounded requests."
116659
117367
  });
116660
117368
  }
117369
+ if (chargeable(provider)) {
117370
+ if (!provider.deploymentFunded?.pricing || !request.billing) {
117371
+ throw new TranscriptionServiceError7({
117372
+ code: "unavailable",
117373
+ message: "Transcription billing is not configured."
117374
+ });
117375
+ }
117376
+ await billing.admit({
117377
+ accountId: request.accountId,
117378
+ workspaceId: request.workspaceId,
117379
+ attribution: request.billing.attribution
117380
+ });
117381
+ }
117382
+ let audio = request.audio;
117383
+ let providerMimeType = mimeType;
117384
+ let billingContext = request.billing;
117385
+ let audioSeconds = request.durationSeconds ?? 0;
117386
+ if (chargeable(provider) && billingContext?.trustedDurationSeconds === void 0) {
117387
+ const normalized = await normalizeAudio({
117388
+ audio,
117389
+ mimeType,
117390
+ maxDurationSeconds: limits.maxDurationSeconds,
117391
+ ...request.signal ? { signal: request.signal } : {}
117392
+ });
117393
+ audio = normalized.bytes;
117394
+ providerMimeType = "audio/wav";
117395
+ audioSeconds = normalized.durationSeconds;
117396
+ billingContext = { ...billingContext, trustedDurationSeconds: normalized.durationSeconds };
117397
+ }
116661
117398
  const startedAt = performance.now();
116662
117399
  const remainingMilliseconds = request.providerDeadlineAt ? remainingTranscriptionProviderRequestMilliseconds(request.providerDeadlineAt, now()) : providerRequestTimeoutMilliseconds;
116663
117400
  if (request.providerDeadlineAt && (!Number.isFinite(remainingMilliseconds) || remainingMilliseconds <= 0)) {
116664
- throw new TranscriptionServiceError6({
117401
+ throw new TranscriptionServiceError7({
116665
117402
  code: "timeout",
116666
117403
  message: "Transcription provider deadline expired.",
116667
117404
  retryable: true
@@ -116671,9 +117408,9 @@ function createTranscriptionService(input) {
116671
117408
  let result;
116672
117409
  try {
116673
117410
  result = await provider.transcribe({
116674
- audio: request.audio,
116675
- mimeType,
116676
- filename: filenameForMimeType(mimeType),
117411
+ audio,
117412
+ mimeType: providerMimeType,
117413
+ filename: filenameForMimeType(providerMimeType),
116677
117414
  workspaceId: request.workspaceId,
116678
117415
  accountId: request.accountId,
116679
117416
  subjectId: request.subjectId,
@@ -116681,7 +117418,7 @@ function createTranscriptionService(input) {
116681
117418
  signal: deadline.signal
116682
117419
  });
116683
117420
  if (deadline.timedOut && !request.signal?.aborted) {
116684
- throw new TranscriptionServiceError6({
117421
+ throw new TranscriptionServiceError7({
116685
117422
  code: "timeout",
116686
117423
  message: "Transcription provider timed out.",
116687
117424
  retryable: true
@@ -116689,13 +117426,13 @@ function createTranscriptionService(input) {
116689
117426
  }
116690
117427
  } catch (error) {
116691
117428
  if (deadline.timedOut && !request.signal?.aborted) {
116692
- throw new TranscriptionServiceError6({
117429
+ throw new TranscriptionServiceError7({
116693
117430
  code: "timeout",
116694
117431
  message: "Transcription provider timed out.",
116695
117432
  retryable: true
116696
117433
  });
116697
117434
  }
116698
- if (error instanceof TranscriptionServiceError6 && error.fallbackSafe && !request.providerId && request.fallbackEnabled !== false && !request.signal?.aborted) {
117435
+ if (error instanceof TranscriptionServiceError7 && error.fallbackSafe && !request.providerId && request.fallbackEnabled !== false && !request.signal?.aborted) {
116699
117436
  const excludedProviders = [...request.excludedProviders ?? [], provider.id];
116700
117437
  if (await firstAvailable(
116701
117438
  orderedProviders(providers, { ...request, excludedProviders }),
@@ -116708,11 +117445,30 @@ function createTranscriptionService(input) {
116708
117445
  } finally {
116709
117446
  deadline.dispose();
116710
117447
  }
117448
+ const latencyMs = Math.round(performance.now() - startedAt);
117449
+ let creditCostMicros = 0;
117450
+ const settle = async () => {
117451
+ if (chargeable(provider) && provider.deploymentFunded?.pricing && billingContext) {
117452
+ creditCostMicros = await settleWithoutFailing({
117453
+ accountId: request.accountId,
117454
+ workspaceId: request.workspaceId,
117455
+ providerId: provider.id,
117456
+ model: provider.deploymentFunded.model,
117457
+ pricing: provider.deploymentFunded.pricing,
117458
+ usage: result.usage ?? null,
117459
+ billing: billingContext
117460
+ });
117461
+ }
117462
+ };
117463
+ if (!request.deferBillingSettlement) await settle();
116711
117464
  return {
116712
- ...result,
117465
+ text: result.text,
117466
+ languages: result.languages,
116713
117467
  providerId: provider.id,
116714
- audioSeconds: request.durationSeconds ?? 0,
116715
- latencyMs: Math.round(performance.now() - startedAt)
117468
+ audioSeconds,
117469
+ latencyMs,
117470
+ creditCostMicros,
117471
+ ...request.deferBillingSettlement ? { settleBilling: settle } : {}
116716
117472
  };
116717
117473
  }
116718
117474
  };
@@ -116767,6 +117523,23 @@ async function exactAvailable(providers, providerId, context) {
116767
117523
  const provider = providers.find((candidate) => candidate.id === providerId);
116768
117524
  return provider && await provider.available(context) ? provider : null;
116769
117525
  }
117526
+ function settlementAttributes(settlement) {
117527
+ const keys2 = voiceTranscriptionSettlementKeys({
117528
+ workspaceId: settlement.workspaceId,
117529
+ sourceId: settlement.billing.sourceId
117530
+ });
117531
+ return {
117532
+ accountId: settlement.accountId,
117533
+ workspaceId: settlement.workspaceId,
117534
+ providerId: settlement.providerId,
117535
+ sourceId: settlement.billing.sourceId,
117536
+ usageIdempotencyKey: keys2.usageIdempotencyKey,
117537
+ debitIdempotencyKey: keys2.debitIdempotencyKey
117538
+ };
117539
+ }
117540
+ function boundedErrorMessage(error) {
117541
+ return (error instanceof Error ? error.message : String(error)).slice(0, 500);
117542
+ }
116770
117543
 
116771
117544
  // src/integrations/slack-tool-review.ts
116772
117545
  function slackToolReviewCanDecide(review) {
@@ -116936,7 +117709,7 @@ import {
116936
117709
  getActorNewSessionModelChoice,
116937
117710
  hasPermission as hasPermission33,
116938
117711
  requireAccessContext as requireAccessContext14,
116939
- requireAccessGrant as requireAccessGrant53,
117712
+ requireAccessGrant as requireAccessGrant52,
116940
117713
  requireSessionAuthorizationListScope as requireSessionAuthorizationListScope4,
116941
117714
  resolveWorkspaceCatalogSettings as resolveWorkspaceCatalogSettings9
116942
117715
  } from "@opengeni/core";
@@ -118103,7 +118876,7 @@ function registerSlackInteractionRoutes(app, deps) {
118103
118876
  );
118104
118877
  app.get("/v1/workspaces/:workspaceId/members/access-requests/slack", async (c) => {
118105
118878
  const workspaceId = c.req.param("workspaceId");
118106
- await requireAccessGrant53(c, deps, workspaceId, "members:manage");
118879
+ await requireAccessGrant52(c, deps, workspaceId, "members:manage");
118107
118880
  const workspace = await getWorkspace11(deps.db, workspaceId);
118108
118881
  const requests = await listPendingSlackUserLinkAccessRequests(deps.db, workspaceId);
118109
118882
  return c.json(
@@ -118119,7 +118892,7 @@ function registerSlackInteractionRoutes(app, deps) {
118119
118892
  "/v1/workspaces/:workspaceId/members/access-requests/slack/:requestId/approve",
118120
118893
  async (c) => {
118121
118894
  const workspaceId = c.req.param("workspaceId");
118122
- const grant = await requireAccessGrant53(c, deps, workspaceId, "members:manage");
118895
+ const grant = await requireAccessGrant52(c, deps, workspaceId, "members:manage");
118123
118896
  const payload = await parseRequestJson(c, ApproveSlackUserLinkAccessRequest);
118124
118897
  const requestId = c.req.param("requestId");
118125
118898
  const requestTeam = await slackUserLinkAccessRequestTeam(deps.db, {
@@ -118159,7 +118932,7 @@ function registerSlackInteractionRoutes(app, deps) {
118159
118932
  "/v1/workspaces/:workspaceId/members/access-requests/slack/:requestId/deny",
118160
118933
  async (c) => {
118161
118934
  const workspaceId = c.req.param("workspaceId");
118162
- const grant = await requireAccessGrant53(c, deps, workspaceId, "members:manage");
118935
+ const grant = await requireAccessGrant52(c, deps, workspaceId, "members:manage");
118163
118936
  const payload = await parseRequestJson(c, SlackUserLinkAccessMutationRequest);
118164
118937
  try {
118165
118938
  const request = await denySlackUserLinkAccessRequest(deps.db, {
@@ -118182,7 +118955,7 @@ function registerSlackInteractionRoutes(app, deps) {
118182
118955
  );
118183
118956
  app.post("/v1/workspaces/:workspaceId/integrations/slack/user-links", async (c) => {
118184
118957
  const workspaceId = c.req.param("workspaceId");
118185
- const grant = await requireAccessGrant53(c, deps, workspaceId, "sessions:create");
118958
+ const grant = await requireAccessGrant52(c, deps, workspaceId, "sessions:create");
118186
118959
  const body3 = record5(await c.req.json().catch(() => null));
118187
118960
  const linkToken = boundedString(body3?.linkToken, 2048);
118188
118961
  const signingSecret = deps.settings.slackSigningSecret;
@@ -118238,7 +119011,7 @@ function registerSlackInteractionRoutes(app, deps) {
118238
119011
  "/v1/workspaces/:workspaceId/integrations/slack/user-links/:slackUserId",
118239
119012
  async (c) => {
118240
119013
  const workspaceId = c.req.param("workspaceId");
118241
- await requireAccessGrant53(c, deps, workspaceId, "connections:write");
119014
+ await requireAccessGrant52(c, deps, workspaceId, "connections:write");
118242
119015
  const connectionId = boundedString(c.req.query("connectionId"), 64);
118243
119016
  if (!connectionId) throw new HTTPException117(400, { message: "connectionId is required" });
118244
119017
  return c.json({
@@ -118253,7 +119026,7 @@ function registerSlackInteractionRoutes(app, deps) {
118253
119026
  );
118254
119027
  app.get("/v1/workspaces/:workspaceId/integrations/slack/channel-routes", async (c) => {
118255
119028
  const workspaceId = c.req.param("workspaceId");
118256
- const grant = await requireAccessGrant53(c, deps, workspaceId, "workspace:admin");
119029
+ const grant = await requireAccessGrant52(c, deps, workspaceId, "workspace:admin");
118257
119030
  const connectionId = boundedString(c.req.query("connectionId"), 64);
118258
119031
  if (!connectionId) throw new HTTPException117(400, { message: "connectionId is required" });
118259
119032
  const routes = await listSlackChannelRoutes(
@@ -118284,7 +119057,7 @@ function registerSlackInteractionRoutes(app, deps) {
118284
119057
  });
118285
119058
  app.put("/v1/workspaces/:workspaceId/integrations/slack/channel-routes", async (c) => {
118286
119059
  const workspaceId = c.req.param("workspaceId");
118287
- const grant = await requireAccessGrant53(c, deps, workspaceId, "workspace:admin");
119060
+ const grant = await requireAccessGrant52(c, deps, workspaceId, "workspace:admin");
118288
119061
  const payload = await parseRequestJson(c, UpdateSlackChannelRoutesRequest);
118289
119062
  const home = { accountId: grant.accountId, workspaceId };
118290
119063
  const installation = await listSlackInstallationBindings2(deps.db, home);
@@ -118339,7 +119112,7 @@ function registerSlackInteractionRoutes(app, deps) {
118339
119112
  });
118340
119113
  app.get("/v1/workspaces/:workspaceId/integrations/slack/reaction-channels", async (c) => {
118341
119114
  const workspaceId = c.req.param("workspaceId");
118342
- const grant = await requireAccessGrant53(c, deps, workspaceId, "workspace:admin");
119115
+ const grant = await requireAccessGrant52(c, deps, workspaceId, "workspace:admin");
118343
119116
  const connectionId = boundedString(c.req.query("connectionId"), 64);
118344
119117
  if (!connectionId) throw new HTTPException117(400, { message: "connectionId is required" });
118345
119118
  const cursor = boundedString(c.req.query("cursor"), 1024);
@@ -122244,10 +123017,23 @@ function createAppComposition(deps) {
122244
123017
  }, MANAGED_AUTH_REAPER_INTERVAL_MS);
122245
123018
  timer.unref?.();
122246
123019
  }
123020
+ if (deps.transcription === void 0) {
123021
+ for (const issue of voiceInputPricingIssues(deps.settings)) {
123022
+ observability.error(
123023
+ `Voice input provider ${issue.providerId} is unavailable: ${issue.message}`,
123024
+ {
123025
+ providerId: issue.providerId,
123026
+ env: issue.env,
123027
+ reason: issue.reason
123028
+ }
123029
+ );
123030
+ }
123031
+ }
122247
123032
  const transcription = deps.transcription === void 0 ? createTranscriptionService({
122248
123033
  settings: deps.settings,
122249
123034
  db: deps.db,
122250
- ...deps.codexFetch ? { codexFetch: deps.codexFetch } : {}
123035
+ ...deps.codexFetch ? { codexFetch: deps.codexFetch } : {},
123036
+ log: (message, attributes) => observability.error(message, attributes)
122251
123037
  }) : deps.transcription;
122252
123038
  const transcriptionSegmenter = deps.transcriptionSegmenter === void 0 ? createFfmpegTranscriptionSegmenter({
122253
123039
  ffmpegPath: deps.settings.voiceInputFfmpegPath
@@ -122357,6 +123143,8 @@ function createAppComposition(deps) {
122357
123143
  "X-OpenGeni-Actor-Epoch",
122358
123144
  "X-OpenGeni-Correlation-Id",
122359
123145
  "X-OpenGeni-Session-Csrf",
123146
+ // Session scope for a session proxy; the API itself ignores it.
123147
+ "X-OpenGeni-Session-Id",
122360
123148
  "X-OpenGeni-Site-Id",
122361
123149
  "X-OpenGeni-Site-Version",
122362
123150
  "X-OpenGeni-Subject",
@@ -122803,6 +123591,8 @@ function createAppComposition(deps) {
122803
123591
  creditsAvailable: false
122804
123592
  });
122805
123593
  let modelSelectionForbidden = false;
123594
+ let voiceInputAvailable = false;
123595
+ let voiceInputProviders = [];
122806
123596
  const requestedWorkspaceId = c.req.query("workspaceId");
122807
123597
  if (requestedWorkspaceId !== void 0 && requestedWorkspaceId.trim() === "") {
122808
123598
  throw new HTTPException118(422, { message: "workspaceId must not be empty" });
@@ -122821,7 +123611,7 @@ function createAppComposition(deps) {
122821
123611
  }
122822
123612
  const workspaceId = requestedWorkspaceId ?? context?.defaultWorkspaceId;
122823
123613
  if (context && workspaceId) {
122824
- const grant = await requireAccessGrant54(c, routeDeps, workspaceId);
123614
+ const grant = await requireAccessGrant53(c, routeDeps, workspaceId);
122825
123615
  await withAccessGrantSessionRlsContext(routeDeps, grant, async () => {
122826
123616
  catalogSettings = (await resolveWorkspaceCatalogSettings10(deps.db, deps.settings, {
122827
123617
  accountId: grant.accountId,
@@ -122838,6 +123628,19 @@ function createAppComposition(deps) {
122838
123628
  selections = [];
122839
123629
  }
122840
123630
  const workspace = await getWorkspace12(deps.db, workspaceId);
123631
+ const voicePreferences = WorkspaceVoiceInputSettings3.safeParse(
123632
+ workspace?.settings.voiceInput
123633
+ ).data;
123634
+ const voiceContext = {
123635
+ workspaceId,
123636
+ subjectId: grant.subjectId,
123637
+ preferredProvider: voicePreferences?.preferredProvider,
123638
+ fallbackEnabled: voicePreferences?.fallbackEnabled
123639
+ };
123640
+ if (hasPermission34(grant.permissions, "sessions:create") && transcription) {
123641
+ voiceInputProviders = await transcription.availableProviderIds?.(voiceContext) ?? [];
123642
+ voiceInputAvailable = voiceInputProviders.length > 0 && await Promise.resolve(transcription.available(voiceContext)).catch(() => false);
123643
+ }
122841
123644
  defaultSelection = await resolveDefaultSessionModelForSelections2(deps.db, {
122842
123645
  settings: catalogSettings,
122843
123646
  accountId: grant.accountId,
@@ -122893,14 +123696,12 @@ function createAppComposition(deps) {
122893
123696
  maxSizeBytes: objectStorage?.maxSinglePutSizeBytes ?? 5e9
122894
123697
  },
122895
123698
  voiceInput: {
122896
- providers: resolveVoiceInputProviderRegistry2(deps.settings).map(
122897
- (provider) => provider.id
122898
- ),
122899
- available: await transcription?.available() ?? false,
123699
+ providers: voiceInputProviders,
123700
+ available: voiceInputAvailable,
122900
123701
  maxDurationSeconds: deps.settings.voiceInputMaxDurationSeconds,
122901
123702
  maxSizeBytes: deps.settings.voiceInputMaxSizeBytes,
122902
123703
  acceptedMimeTypes: [...VOICE_INPUT_ACCEPTED_MIME_TYPES2],
122903
- ...deps.settings.voiceInputResumableEnabled && objectStorage && transcription && transcriptionSegmenter && await transcription.available() && await transcriptionSegmenter.available() ? {
123704
+ ...deps.settings.voiceInputResumableEnabled && objectStorage && transcription && transcriptionSegmenter && voiceInputAvailable && await transcriptionSegmenter.available() ? {
122904
123705
  resumable: {
122905
123706
  maxDurationSeconds: deps.settings.voiceInputResumableMaxDurationSeconds,
122906
123707
  maxSizeBytes: deps.settings.voiceInputResumableMaxSizeBytes,
@@ -123009,7 +123810,7 @@ function createAppComposition(deps) {
123009
123810
  app.use("/v1/workspaces/:workspaceId/*", async (c, next) => {
123010
123811
  const workspaceId = c.req.param("workspaceId");
123011
123812
  if (workspaceRequestRequiresCodexAccountPrevalidation(c.req.raw)) {
123012
- const grant2 = await requireAccessGrant54(c, routeDeps, workspaceId);
123813
+ const grant2 = await requireAccessGrant53(c, routeDeps, workspaceId);
123013
123814
  await validateCodexAccountTarget(c.req.raw);
123014
123815
  await withAccessGrantSessionRlsContext(routeDeps, grant2, next);
123015
123816
  return;
@@ -123019,7 +123820,7 @@ function createAppComposition(deps) {
123019
123820
  return;
123020
123821
  }
123021
123822
  const permission = c.req.method === "PUT" && /^\/v1\/workspaces\/[^/]+\/artifact-catalog\/[^/]+\/[^/]+\/pin$/.test(c.req.path) ? "artifacts:publish" : void 0;
123022
- const grant = await requireAccessGrant54(c, routeDeps, workspaceId, permission);
123823
+ const grant = await requireAccessGrant53(c, routeDeps, workspaceId, permission);
123023
123824
  await withAccessGrantSessionRlsContext(routeDeps, grant, next);
123024
123825
  });
123025
123826
  app.all("/v1/workspaces/:workspaceId/mcp", async (c) => {
@@ -123174,7 +123975,7 @@ function createAppComposition(deps) {
123174
123975
  });
123175
123976
  app.get("/v1/workspaces/:workspaceId/tools/catalog", async (c) => {
123176
123977
  const workspaceId = c.req.param("workspaceId");
123177
- const authorization = await requireAccessGrantAuthorization40(
123978
+ const authorization = await requireAccessGrantAuthorization41(
123178
123979
  c,
123179
123980
  routeDeps,
123180
123981
  workspaceId,
@@ -123190,7 +123991,7 @@ function createAppComposition(deps) {
123190
123991
  });
123191
123992
  app.post("/v1/workspaces/:workspaceId/tools/calls", async (c) => {
123192
123993
  const workspaceId = c.req.param("workspaceId");
123193
- const authorization = await requireAccessGrantAuthorization40(
123994
+ const authorization = await requireAccessGrantAuthorization41(
123194
123995
  c,
123195
123996
  routeDeps,
123196
123997
  workspaceId,
@@ -123219,7 +124020,7 @@ function createAppComposition(deps) {
123219
124020
  });
123220
124021
  app.post("/v1/workspaces/:workspaceId/tools/approvals", async (c) => {
123221
124022
  const workspaceId = c.req.param("workspaceId");
123222
- const authorization = await requireAccessGrantAuthorization40(
124023
+ const authorization = await requireAccessGrantAuthorization41(
123223
124024
  c,
123224
124025
  routeDeps,
123225
124026
  workspaceId,
@@ -123248,7 +124049,7 @@ function createAppComposition(deps) {
123248
124049
  });
123249
124050
  app.get("/v1/workspaces/:workspaceId/tools/declarations", async (c) => {
123250
124051
  const workspaceId = c.req.param("workspaceId");
123251
- const authorization = await requireAccessGrantAuthorization40(
124052
+ const authorization = await requireAccessGrantAuthorization41(
123252
124053
  c,
123253
124054
  routeDeps,
123254
124055
  workspaceId,
@@ -123264,7 +124065,7 @@ function createAppComposition(deps) {
123264
124065
  });
123265
124066
  app.all("/v1/workspaces/:workspaceId/codemode/sdk/*", async (c) => {
123266
124067
  const workspaceId = c.req.param("workspaceId");
123267
- const grant = await requireAccessGrant54(c, routeDeps, workspaceId);
124068
+ const grant = await requireAccessGrant53(c, routeDeps, workspaceId);
123268
124069
  const url = new URL(c.req.url);
123269
124070
  const prefix2 = `/v1/workspaces/${workspaceId}/codemode/sdk`;
123270
124071
  let forwarded;
@@ -123282,7 +124083,7 @@ function createAppComposition(deps) {
123282
124083
  });
123283
124084
  app.get("/v1/workspaces/:workspaceId/codemode/catalog", async (c) => {
123284
124085
  const workspaceId = c.req.param("workspaceId");
123285
- const grant = await requireAccessGrant54(c, routeDeps, workspaceId);
124086
+ const grant = await requireAccessGrant53(c, routeDeps, workspaceId);
123286
124087
  if (!isCodemodeGrant(grant)) {
123287
124088
  throw new HTTPException118(403, { message: "Codemode access denied" });
123288
124089
  }
@@ -123294,7 +124095,7 @@ function createAppComposition(deps) {
123294
124095
  });
123295
124096
  app.post("/v1/workspaces/:workspaceId/codemode/calls", async (c) => {
123296
124097
  const workspaceId = c.req.param("workspaceId");
123297
- const grant = await requireAccessGrant54(c, routeDeps, workspaceId);
124098
+ const grant = await requireAccessGrant53(c, routeDeps, workspaceId);
123298
124099
  if (!isCodemodeGrant(grant)) {
123299
124100
  throw new HTTPException118(403, { message: "Codemode access denied" });
123300
124101
  }
@@ -123314,7 +124115,7 @@ function createAppComposition(deps) {
123314
124115
  });
123315
124116
  app.get("/v1/workspaces/:workspaceId/codemode/calls/:operationId", async (c) => {
123316
124117
  const workspaceId = c.req.param("workspaceId");
123317
- const grant = await requireAccessGrant54(c, routeDeps, workspaceId);
124118
+ const grant = await requireAccessGrant53(c, routeDeps, workspaceId);
123318
124119
  if (!isCodemodeGrant(grant)) {
123319
124120
  throw new HTTPException118(403, { message: "Codemode access denied" });
123320
124121
  }
@@ -123511,7 +124312,7 @@ async function validateCodexAccountTarget(request) {
123511
124312
  }
123512
124313
  }
123513
124314
  async function requireMcpAccessGrantAuthorization(c, deps, workspaceId) {
123514
- const authorization = await requireAccessGrantAuthorization40(c, deps, workspaceId);
124315
+ const authorization = await requireAccessGrantAuthorization41(c, deps, workspaceId);
123515
124316
  const grant = authorization.grant;
123516
124317
  if (hasPermission34(grant.permissions, "workspace:read")) {
123517
124318
  return authorization;
@@ -124905,4 +125706,4 @@ export {
124905
125706
  withDefaultEnabledCapabilityMcpTools,
124906
125707
  workflowIdForSession4 as workflowIdForSession
124907
125708
  };
124908
- //# sourceMappingURL=chunk-IZEGLLKZ.js.map
125709
+ //# sourceMappingURL=chunk-KKEXIBQG.js.map