@opengeni/core 3.0.1 → 3.1.2

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 (257) hide show
  1. package/README.md +4 -4
  2. package/dist/access/index.d.ts +131 -7
  3. package/dist/access/service-initiator.d.ts +3 -0
  4. package/dist/application/archived-session-imports.d.ts +6 -0
  5. package/dist/application/composer-submit.d.ts +1 -1
  6. package/dist/application/connect-operation.d.ts +6 -0
  7. package/dist/application/external-workspace-members.d.ts +13 -1
  8. package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
  9. package/dist/application/modal-native-original-configuration.d.ts +79 -0
  10. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  11. package/dist/application/new-session-drafts.d.ts +29 -2
  12. package/dist/application/organization-privacy.d.ts +57 -0
  13. package/dist/application/prepared-mcp-connection.d.ts +31 -0
  14. package/dist/application/sandbox-recovery.d.ts +58 -0
  15. package/dist/application/session-commands.d.ts +3 -0
  16. package/dist/application/session-tenancy.d.ts +13 -4
  17. package/dist/billing/agent-run-admission.d.ts +21 -0
  18. package/dist/billing/limits.d.ts +27 -1
  19. package/dist/billing/model-call-admission.d.ts +34 -0
  20. package/dist/billing/model-usage-settlement.d.ts +112 -0
  21. package/dist/canonical-human-identities.js +1 -1
  22. package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
  23. package/dist/chunk-7T2SZ7BI.js.map +1 -0
  24. package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
  25. package/dist/chunk-OMJFIIWK.js.map +1 -0
  26. package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
  27. package/dist/chunk-QI2M2B2K.js.map +1 -0
  28. package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
  29. package/dist/chunk-TTWTXQB2.js.map +1 -0
  30. package/dist/codex-model-availability.d.ts +55 -0
  31. package/dist/default-session-model.d.ts +192 -0
  32. package/dist/dependencies.d.ts +41 -7
  33. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  34. package/dist/domain/agent-config-resolution.d.ts +49 -0
  35. package/dist/domain/automations.d.ts +8 -1
  36. package/dist/domain/capabilities.d.ts +112 -8
  37. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  38. package/dist/domain/connector-tool-permissions.d.ts +62 -0
  39. package/dist/domain/direct-model-provider.d.ts +3 -0
  40. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  41. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  42. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  43. package/dist/domain/editable-artifacts/service.d.ts +3 -1
  44. package/dist/domain/editable-artifacts/types.d.ts +1 -1
  45. package/dist/domain/encryption-key.d.ts +12 -0
  46. package/dist/domain/insights-timing.d.ts +16 -0
  47. package/dist/domain/insights-usage.d.ts +27 -0
  48. package/dist/domain/insights.d.ts +21 -3
  49. package/dist/domain/knowledge-files.d.ts +1 -1
  50. package/dist/domain/knowledge-preparation.d.ts +10 -0
  51. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  52. package/dist/domain/knowledge-search.d.ts +15 -76
  53. package/dist/domain/mcp-account-bindings.d.ts +17 -0
  54. package/dist/domain/mcp-account-routes.d.ts +21 -0
  55. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  56. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  57. package/dist/domain/personal-connection-delegations.d.ts +73 -12
  58. package/dist/domain/pr-review.d.ts +121 -6
  59. package/dist/domain/realtime-voice-billing.d.ts +103 -0
  60. package/dist/domain/resources.d.ts +17 -1
  61. package/dist/domain/scheduled-task-access.d.ts +265 -0
  62. package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
  63. package/dist/domain/scheduled-task-audience.d.ts +70 -0
  64. package/dist/domain/scheduled-tasks.d.ts +61 -18
  65. package/dist/domain/session-connection-accounts.d.ts +53 -0
  66. package/dist/domain/session-file-access.d.ts +4 -0
  67. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  68. package/dist/domain/session-start-timing.d.ts +7 -0
  69. package/dist/domain/session-tool-policy.d.ts +102 -4
  70. package/dist/domain/sessions.d.ts +179 -15
  71. package/dist/domain/site-audience.d.ts +52 -0
  72. package/dist/domain/skills.d.ts +29 -1
  73. package/dist/domain/slack-bot.d.ts +46 -1
  74. package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
  75. package/dist/domain/vaults/index.d.ts +7 -0
  76. package/dist/domain/vaults/mapping.d.ts +25 -0
  77. package/dist/domain/vaults/provider-registry.d.ts +72 -0
  78. package/dist/domain/vaults/provider-sync.d.ts +176 -0
  79. package/dist/domain/vaults/provider-writes.d.ts +57 -0
  80. package/dist/domain/vaults/secret-resolver.d.ts +34 -0
  81. package/dist/domain/vaults/types.d.ts +24 -0
  82. package/dist/domain/vaults/vault-service.d.ts +35 -0
  83. package/dist/domain/voice-input-billing.d.ts +77 -0
  84. package/dist/domain/web-search-billing.d.ts +51 -0
  85. package/dist/domain/workspace-credits.d.ts +7 -0
  86. package/dist/editable-artifact-live/application.d.ts +2 -1
  87. package/dist/editable-artifact-live/server.d.ts +5 -1
  88. package/dist/editable-artifact-live/ticket.d.ts +2 -1
  89. package/dist/editable-artifact-live/types.d.ts +9 -0
  90. package/dist/editable-artifact-live.js +2 -2
  91. package/dist/editable-artifacts.js +10 -2
  92. package/dist/goal-admission.d.ts +627 -0
  93. package/dist/host-session-usage-limit.d.ts +15 -0
  94. package/dist/index.d.ts +43 -8
  95. package/dist/index.js +22682 -11743
  96. package/dist/index.js.map +1 -1
  97. package/dist/managed-session.d.ts +110 -7
  98. package/dist/model-call.d.ts +58 -0
  99. package/dist/model-catalog.d.ts +29 -3
  100. package/dist/model-provider-credentials.d.ts +43 -0
  101. package/dist/product-usage-metrics.d.ts +30 -0
  102. package/dist/rigs/index.d.ts +1 -13
  103. package/dist/sandbox/fleet.d.ts +58 -4
  104. package/dist/sandbox/routing.d.ts +42 -2
  105. package/dist/sandbox/runtime-settings.d.ts +27 -17
  106. package/dist/session-authorization.d.ts +10 -0
  107. package/dist/session-usage-limit-metrics.d.ts +60 -0
  108. package/dist/session-usage-limits.d.ts +89 -0
  109. package/dist/site-agent-clamp.d.ts +71 -0
  110. package/dist/site-session-origin.d.ts +39 -2
  111. package/dist/transcription.d.ts +120 -3
  112. package/dist/turn-surface.d.ts +20 -0
  113. package/dist/user-presence.d.ts +32 -0
  114. package/dist/workflow-wake-contract.d.ts +13 -1
  115. package/package.json +15 -22
  116. package/src/access/index.ts +961 -38
  117. package/src/access/service-initiator.ts +50 -0
  118. package/src/application/api-integration-servers.ts +9 -0
  119. package/src/application/archived-session-imports.ts +161 -0
  120. package/src/application/composer-submit.ts +3 -2
  121. package/src/application/connect-authority.ts +3 -3
  122. package/src/application/connect-operation.ts +37 -3
  123. package/src/application/external-continuation.ts +20 -3
  124. package/src/application/external-link-work-admission.ts +3 -0
  125. package/src/application/external-workspace-members.ts +51 -3
  126. package/src/application/modal-native-live-declaration-join.ts +196 -0
  127. package/src/application/modal-native-original-configuration.ts +261 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +172 -55
  130. package/src/application/organization-privacy.ts +161 -0
  131. package/src/application/prepared-mcp-connection.ts +133 -0
  132. package/src/application/sandbox-recovery.ts +78 -0
  133. package/src/application/session-commands.ts +73 -1
  134. package/src/application/session-mcp-credential-rotation.ts +61 -18
  135. package/src/application/session-tenancy.ts +262 -44
  136. package/src/application/user-resource-grants.ts +1 -29
  137. package/src/billing/agent-run-admission.ts +117 -0
  138. package/src/billing/limits.ts +138 -14
  139. package/src/billing/model-call-admission.ts +118 -0
  140. package/src/billing/model-usage-settlement.ts +472 -0
  141. package/src/codex-model-availability.ts +264 -0
  142. package/src/default-session-model.ts +692 -0
  143. package/src/dependencies.ts +48 -6
  144. package/src/developer-setup-credential-ceiling.ts +60 -0
  145. package/src/domain/agent-config-resolution.ts +148 -0
  146. package/src/domain/automations.ts +76 -2
  147. package/src/domain/capabilities.ts +830 -181
  148. package/src/domain/connection-account-selection-error.ts +19 -0
  149. package/src/domain/connector-tool-permissions.ts +572 -0
  150. package/src/domain/direct-model-provider.ts +62 -0
  151. package/src/domain/editable-artifacts/agent-application.ts +44 -6
  152. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  153. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  154. package/src/domain/editable-artifacts/ports.ts +11 -0
  155. package/src/domain/editable-artifacts/service.ts +20 -0
  156. package/src/domain/editable-artifacts/types.ts +1 -1
  157. package/src/domain/encryption-key.ts +26 -0
  158. package/src/domain/file-owner.ts +16 -1
  159. package/src/domain/github-action-policies.ts +1 -1
  160. package/src/domain/github-skill-source.ts +3 -1
  161. package/src/domain/insights-timing.ts +95 -0
  162. package/src/domain/insights-usage.ts +67 -0
  163. package/src/domain/insights.ts +167 -43
  164. package/src/domain/knowledge-files.ts +10 -3
  165. package/src/domain/knowledge-messages.ts +1 -0
  166. package/src/domain/knowledge-preparation.test.ts +276 -0
  167. package/src/domain/knowledge-preparation.ts +147 -0
  168. package/src/domain/knowledge-search.test.ts +617 -5
  169. package/src/domain/knowledge-search.ts +240 -23
  170. package/src/domain/mcp-account-bindings.ts +169 -0
  171. package/src/domain/mcp-account-routes.ts +152 -0
  172. package/src/domain/native-mcp-connection-admission.ts +14 -0
  173. package/src/domain/organization-integration-catalog.ts +88 -0
  174. package/src/domain/personal-connection-delegations.ts +399 -257
  175. package/src/domain/pr-review.ts +24 -8
  176. package/src/domain/realtime-voice-billing.ts +425 -0
  177. package/src/domain/resources.ts +50 -4
  178. package/src/domain/scheduled-task-access.ts +1089 -0
  179. package/src/domain/scheduled-task-account-audit.ts +90 -0
  180. package/src/domain/scheduled-task-audience.ts +249 -0
  181. package/src/domain/scheduled-tasks.ts +1313 -374
  182. package/src/domain/session-connection-accounts.ts +239 -0
  183. package/src/domain/session-file-access.ts +47 -0
  184. package/src/domain/session-native-connection-replacement.ts +64 -0
  185. package/src/domain/session-start-timing.ts +60 -0
  186. package/src/domain/session-tool-policy.ts +489 -14
  187. package/src/domain/sessions.ts +2254 -717
  188. package/src/domain/site-audience.ts +130 -0
  189. package/src/domain/skills.ts +9 -0
  190. package/src/domain/slack-bot.ts +141 -4
  191. package/src/domain/slack-publication-secret-safety.ts +1 -1
  192. package/src/domain/vaults/index.ts +19 -0
  193. package/src/domain/vaults/mapping.ts +190 -0
  194. package/src/domain/vaults/provider-registry.ts +201 -0
  195. package/src/domain/vaults/provider-sync.ts +531 -0
  196. package/src/domain/vaults/provider-writes.ts +288 -0
  197. package/src/domain/vaults/secret-resolver.ts +464 -0
  198. package/src/domain/vaults/types.ts +26 -0
  199. package/src/domain/vaults/vault-service.ts +1316 -0
  200. package/src/domain/voice-input-billing.ts +330 -0
  201. package/src/domain/web-search-billing.ts +169 -0
  202. package/src/domain/workspace-credits.ts +13 -0
  203. package/src/editable-artifact-live/application.ts +8 -1
  204. package/src/editable-artifact-live/server.ts +154 -6
  205. package/src/editable-artifact-live/ticket.ts +86 -1
  206. package/src/editable-artifact-live/types.ts +10 -0
  207. package/src/goal-admission.ts +220 -0
  208. package/src/host-session-usage-limit.ts +25 -0
  209. package/src/index.ts +75 -11
  210. package/src/managed-session.ts +137 -6
  211. package/src/model-call.ts +79 -0
  212. package/src/model-catalog.ts +345 -91
  213. package/src/model-provider-credentials.ts +394 -0
  214. package/src/product-usage-metrics.ts +101 -0
  215. package/src/rigs/index.ts +23 -111
  216. package/src/sandbox/fleet.ts +253 -28
  217. package/src/sandbox/routing.ts +204 -43
  218. package/src/sandbox/runtime-settings.ts +159 -85
  219. package/src/session-authorization.ts +172 -23
  220. package/src/session-usage-limit-metrics.ts +95 -0
  221. package/src/session-usage-limits.ts +415 -0
  222. package/src/site-agent-clamp.ts +226 -0
  223. package/src/site-session-origin.ts +84 -5
  224. package/src/transcription.ts +123 -4
  225. package/src/turn-surface.ts +47 -0
  226. package/src/user-presence.ts +134 -0
  227. package/src/workflow-wake-contract.ts +14 -1
  228. package/dist/application/host-mcp-owner.d.ts +0 -6
  229. package/dist/application/host-mcp-resolvers.d.ts +0 -13
  230. package/dist/chunk-5WZYFL45.js +0 -266
  231. package/dist/chunk-5WZYFL45.js.map +0 -1
  232. package/dist/chunk-H7X52RI2.js.map +0 -1
  233. package/dist/chunk-OF65T3PM.js.map +0 -1
  234. package/dist/chunk-VCZFFEPB.js.map +0 -1
  235. package/dist/chunk-ZYCNLYGH.js.map +0 -1
  236. package/dist/domain/connection-authority.d.ts +0 -97
  237. package/dist/domain/connection-authority.js +0 -169
  238. package/dist/domain/connection-authority.js.map +0 -1
  239. package/dist/domain/environments.d.ts +0 -33
  240. package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
  241. package/dist/domain/host-mcp-task-admission.d.ts +0 -18
  242. package/dist/domain/packs.d.ts +0 -59
  243. package/dist/domain/product-integration-pack.d.ts +0 -44
  244. package/dist/domain/product-integration-skill.gen.d.ts +0 -5
  245. package/dist/remote-mcp-credentials.d.ts +0 -23
  246. package/dist/remote-mcp-credentials.js +0 -11
  247. package/dist/remote-mcp-credentials.js.map +0 -1
  248. package/src/application/host-mcp-owner.ts +0 -41
  249. package/src/application/host-mcp-resolvers.ts +0 -84
  250. package/src/domain/connection-authority.ts +0 -329
  251. package/src/domain/environments.ts +0 -120
  252. package/src/domain/host-mcp-authority-source-admission.ts +0 -25
  253. package/src/domain/host-mcp-task-admission.ts +0 -121
  254. package/src/domain/packs.ts +0 -699
  255. package/src/domain/product-integration-pack.ts +0 -42
  256. package/src/domain/product-integration-skill.gen.ts +0 -52
  257. package/src/remote-mcp-credentials.ts +0 -361
@@ -0,0 +1,330 @@
1
+ import {
2
+ calculateVoiceInputCost,
3
+ configuredStaticUsageLimits,
4
+ voiceInputCreditBillingActive,
5
+ voiceInputPricingHasTokenRates,
6
+ type Settings,
7
+ type VoiceInputPricing,
8
+ type VoiceInputUsage,
9
+ } from "@opengeni/config";
10
+ import {
11
+ applyCreditDebitAfterUse,
12
+ checkWorkspaceAllowance,
13
+ creditDebitAttributionMetadata,
14
+ getSpendableCreditBalance,
15
+ listUnsettledVoiceTranscriptionCharges,
16
+ recordUsageEvent,
17
+ sumUsageQuantity,
18
+ VOICE_TRANSCRIPTION_DEBIT_TYPE,
19
+ VOICE_TRANSCRIPTION_SOURCE_TYPE,
20
+ voiceTranscriptionSettlementKeys,
21
+ VOICE_CREDIT_USAGE,
22
+ type CreditDebitAttribution,
23
+ type Database,
24
+ } from "@opengeni/db";
25
+ import {
26
+ TranscriptionBillingRefusedError,
27
+ TranscriptionServiceError,
28
+ type TranscriptionBilling,
29
+ type TranscriptionBillingContext,
30
+ } from "../transcription";
31
+ import {
32
+ voiceCreditStanding,
33
+ voiceInsufficientCreditsMessage,
34
+ workspaceCreditsDisabled,
35
+ } from "./realtime-voice-billing";
36
+
37
+ /** Credit debit type and usage source for deployment-funded voice input. */
38
+ export const VOICE_INPUT_DEBIT_TYPE = VOICE_TRANSCRIPTION_DEBIT_TYPE;
39
+ export const VOICE_INPUT_SOURCE_TYPE = VOICE_TRANSCRIPTION_SOURCE_TYPE;
40
+
41
+ /** How the billed quantity was measured; recorded on the ledger entry. */
42
+ export type VoiceInputBillingBasis =
43
+ | "provider_tokens"
44
+ | "provider_duration"
45
+ | "server_duration"
46
+ | "reconciled_receipt";
47
+
48
+ /**
49
+ * Pick the billed measurement. Provider usage wins when this deployment can
50
+ * price it (token usage with token rates, or a reported duration). Otherwise
51
+ * the duration the server measured from WAV bytes it produced bills. Every
52
+ * deployment-funded call carries that server duration, so an unpriced usage
53
+ * shape never leaves a call unbillable. A client-reported duration is never a
54
+ * billing input.
55
+ */
56
+ export function voiceInputBillableUsage(input: {
57
+ pricing: VoiceInputPricing;
58
+ usage: VoiceInputUsage | null;
59
+ trustedDurationSeconds?: number | undefined;
60
+ }): { usage: VoiceInputUsage; basis: VoiceInputBillingBasis } {
61
+ if (input.usage?.kind === "tokens" && voiceInputPricingHasTokenRates(input.pricing)) {
62
+ return { usage: input.usage, basis: "provider_tokens" };
63
+ }
64
+ if (input.usage?.kind === "duration") {
65
+ return { usage: input.usage, basis: "provider_duration" };
66
+ }
67
+ if (
68
+ input.trustedDurationSeconds !== undefined &&
69
+ Number.isFinite(input.trustedDurationSeconds) &&
70
+ input.trustedDurationSeconds >= 0
71
+ ) {
72
+ return {
73
+ usage: { kind: "duration", seconds: input.trustedDurationSeconds },
74
+ basis: "server_duration",
75
+ };
76
+ }
77
+ throw new TranscriptionServiceError({
78
+ code: "provider",
79
+ message: "Transcription usage was not reported.",
80
+ });
81
+ }
82
+
83
+ function initiatingHuman(attribution: CreditDebitAttribution): string | null {
84
+ return attribution.kind === "human" || attribution.kind === "turn"
85
+ ? attribution.initiatingHumanSubjectId
86
+ : null;
87
+ }
88
+
89
+ function startOfUtcMonth(): Date {
90
+ const now = new Date();
91
+ return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
92
+ }
93
+
94
+ /**
95
+ * Same admission shape as knowledge-search and session admission:
96
+ * voice-spendable credits (general plus signup, not model-scoped promotional
97
+ * credits) must be positive, the workspace/member
98
+ * allowance must not be exhausted, and a static monthly cost cap applies.
99
+ * Admission is a read, not a reservation; settlement is post-use and may take
100
+ * an account below zero by at most the concurrent in-flight calls.
101
+ */
102
+ export function createVoiceInputBilling(deps: {
103
+ db: Database;
104
+ settings: Settings;
105
+ /** Reconciliation is best-effort at admission; failures are reported here. */
106
+ onReconcileError?: (error: unknown, scope: { accountId: string; workspaceId: string }) => void;
107
+ /** Minimum spacing of admission-time reconciliation per workspace (default 5 min). */
108
+ reconcileIntervalMilliseconds?: number;
109
+ }): TranscriptionBilling {
110
+ const reconcileInterval = deps.reconcileIntervalMilliseconds ?? 5 * 60_000;
111
+ const lastReconciled = new Map<string, number>();
112
+ const reconcileDue = (key: string) => {
113
+ const now = Date.now();
114
+ const last = lastReconciled.get(key);
115
+ if (last !== undefined && now - last < reconcileInterval) return false;
116
+ if (lastReconciled.size >= 10_000) lastReconciled.clear();
117
+ lastReconciled.set(key, now);
118
+ return true;
119
+ };
120
+ return {
121
+ async admit({ accountId, workspaceId, attribution }) {
122
+ if (!voiceInputCreditBillingActive(deps.settings)) return;
123
+ if (attribution.kind === "unknown") {
124
+ // Not a microphone problem: the caller's credential has no payer the
125
+ // ledger can attribute, so the deployment refuses by policy.
126
+ throw new TranscriptionServiceError({
127
+ code: "policy_blocked",
128
+ status: 403,
129
+ message: "Voice input payer could not be verified.",
130
+ });
131
+ }
132
+ // Deployment-paid voice input spends Opengeni credits, so it honors the
133
+ // workspace switch that turns them off. Nothing was sent yet, so another
134
+ // (subscription) provider may still serve the request.
135
+ if (await workspaceCreditsDisabled(deps.db, workspaceId)) {
136
+ throw new TranscriptionServiceError({
137
+ code: "policy_blocked",
138
+ status: 403,
139
+ fallbackSafe: true,
140
+ message: "Opengeni credits are turned off in this workspace.",
141
+ });
142
+ }
143
+ // Debits whose receipt committed but whose ledger write failed are
144
+ // applied before the balance read, so unsettled use counts against it.
145
+ // Throttled per workspace: failed debits are rare, the scan is not free.
146
+ if (reconcileDue(`${accountId}:${workspaceId}`)) {
147
+ await reconcileUnsettledVoiceInputCharges(deps.db, { accountId, workspaceId }).catch(
148
+ (error: unknown) => {
149
+ deps.onReconcileError?.(error, { accountId, workspaceId });
150
+ },
151
+ );
152
+ }
153
+ const balance = await getSpendableCreditBalance(deps.db, accountId, VOICE_CREDIT_USAGE);
154
+ if (balance.balanceMicros <= 0) {
155
+ throw new TranscriptionBillingRefusedError({
156
+ code: "insufficient_credits",
157
+ message: voiceInsufficientCreditsMessage("Voice input", voiceCreditStanding(balance)),
158
+ });
159
+ }
160
+ const refusal = await checkWorkspaceAllowance(deps.db, {
161
+ accountId,
162
+ workspaceId,
163
+ subjectId: initiatingHuman(attribution),
164
+ });
165
+ if (refusal) {
166
+ throw new TranscriptionBillingRefusedError({
167
+ code: refusal.code,
168
+ message: refusal.message,
169
+ details: {
170
+ scope: refusal.scope,
171
+ resetsAt: refusal.resetsAt,
172
+ ...(refusal.subjectId ? { subjectId: refusal.subjectId } : {}),
173
+ },
174
+ });
175
+ }
176
+ if (
177
+ deps.settings.usageLimitsMode === "static" ||
178
+ deps.settings.usageLimitsMode === "managed"
179
+ ) {
180
+ const cap = configuredStaticUsageLimits(deps.settings).maxMonthlyCostMicrosPerAccount;
181
+ if (cap) {
182
+ const used = await sumUsageQuantity(deps.db, {
183
+ accountId,
184
+ eventType: "model.cost",
185
+ since: startOfUtcMonth(),
186
+ });
187
+ if (used >= cap) {
188
+ throw new TranscriptionBillingRefusedError({
189
+ code: "monthly_model_cost_limit",
190
+ message: "The monthly model cost limit has been reached.",
191
+ });
192
+ }
193
+ }
194
+ }
195
+ },
196
+
197
+ async settle(input) {
198
+ if (!voiceInputCreditBillingActive(deps.settings)) return { creditCostMicros: 0 };
199
+ const { usage, basis } = voiceInputBillableUsage({
200
+ pricing: input.pricing,
201
+ usage: input.usage,
202
+ trustedDurationSeconds: input.billing.trustedDurationSeconds,
203
+ });
204
+ const cost = calculateVoiceInputCost(input.pricing, usage);
205
+ return await settleVoiceInputUsage(deps.db, {
206
+ accountId: input.accountId,
207
+ workspaceId: input.workspaceId,
208
+ providerId: input.providerId,
209
+ model: input.model,
210
+ billing: input.billing,
211
+ usage,
212
+ basis,
213
+ providerCostMicros: cost.providerCostMicros,
214
+ creditCostMicros: cost.creditCostMicros,
215
+ });
216
+ },
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Two commits, in order: the `model.cost` usage receipt (Insights spend,
222
+ * monthly cost cap, member-allowance attribution), then the post-use credit
223
+ * debit. The receipt is the first-writer authority for the amount and the
224
+ * durable record of the charge: if the debit commit fails, the receipt stays
225
+ * and {@link reconcileUnsettledVoiceInputCharges} applies the same debit (same
226
+ * idempotency key) on the workspace's next admission. A retried unit settles
227
+ * exactly once at the originally recorded price.
228
+ */
229
+ export async function settleVoiceInputUsage(
230
+ db: Database,
231
+ input: {
232
+ accountId: string;
233
+ workspaceId: string;
234
+ providerId: string;
235
+ model: string;
236
+ billing: TranscriptionBillingContext;
237
+ usage: VoiceInputUsage;
238
+ basis: VoiceInputBillingBasis;
239
+ providerCostMicros: number;
240
+ creditCostMicros: number;
241
+ },
242
+ ): Promise<{ creditCostMicros: number }> {
243
+ const attribution = input.billing.attribution;
244
+ const keys = voiceTranscriptionSettlementKeys({
245
+ workspaceId: input.workspaceId,
246
+ sourceId: input.billing.sourceId,
247
+ });
248
+ const receipt = await recordUsageEvent(db, {
249
+ accountId: input.accountId,
250
+ workspaceId: input.workspaceId,
251
+ eventType: "model.cost",
252
+ quantity: input.creditCostMicros,
253
+ unit: "usd_micros",
254
+ sourceResourceType: VOICE_TRANSCRIPTION_SOURCE_TYPE,
255
+ sourceResourceId: input.billing.sourceId,
256
+ idempotencyKey: keys.usageIdempotencyKey,
257
+ // The API is the writer; the payer is the trusted attribution snapshot.
258
+ initiator: { kind: "service", subjectId: "api:voice-input" },
259
+ initiatorContext: { creditDebitAttribution: attribution },
260
+ });
261
+ const amount = Number(receipt.quantity);
262
+ if (amount > 0) {
263
+ await applyCreditDebitAfterUse(db, {
264
+ accountId: input.accountId,
265
+ workspaceId: input.workspaceId,
266
+ type: VOICE_TRANSCRIPTION_DEBIT_TYPE,
267
+ amountMicros: amount,
268
+ sourceType: VOICE_TRANSCRIPTION_SOURCE_TYPE,
269
+ sourceId: input.billing.sourceId,
270
+ idempotencyKey: keys.debitIdempotencyKey,
271
+ usage: VOICE_CREDIT_USAGE,
272
+ metadata: {
273
+ providerId: input.providerId,
274
+ model: input.model,
275
+ basis: input.basis,
276
+ providerCostMicros: input.providerCostMicros,
277
+ ...(input.usage.kind === "tokens"
278
+ ? {
279
+ inputTokens: input.usage.inputTokens,
280
+ audioInputTokens: input.usage.audioInputTokens,
281
+ textInputTokens: input.usage.textInputTokens,
282
+ outputTokens: input.usage.outputTokens,
283
+ }
284
+ : { audioMilliseconds: Math.ceil(input.usage.seconds * 1_000) }),
285
+ ...(attribution.kind === "unknown" ? {} : creditDebitAttributionMetadata(attribution)),
286
+ },
287
+ });
288
+ }
289
+ return { creditCostMicros: amount };
290
+ }
291
+
292
+ /**
293
+ * Apply the debit for every voice usage receipt in this workspace whose debit
294
+ * never committed. Idempotent with the inline settlement (same key), bounded
295
+ * per call, and safe to run concurrently.
296
+ */
297
+ export async function reconcileUnsettledVoiceInputCharges(
298
+ db: Database,
299
+ input: { accountId: string; workspaceId: string; minAgeMilliseconds?: number },
300
+ ): Promise<{ reconciled: number }> {
301
+ const pending = await listUnsettledVoiceTranscriptionCharges(db, {
302
+ accountId: input.accountId,
303
+ workspaceId: input.workspaceId,
304
+ ...(input.minAgeMilliseconds !== undefined
305
+ ? { minAgeMilliseconds: input.minAgeMilliseconds }
306
+ : {}),
307
+ });
308
+ for (const charge of pending) {
309
+ await applyCreditDebitAfterUse(db, {
310
+ accountId: input.accountId,
311
+ workspaceId: input.workspaceId,
312
+ type: VOICE_TRANSCRIPTION_DEBIT_TYPE,
313
+ amountMicros: charge.amountMicros,
314
+ sourceType: VOICE_TRANSCRIPTION_SOURCE_TYPE,
315
+ sourceId: charge.sourceId,
316
+ idempotencyKey: voiceTranscriptionSettlementKeys({
317
+ workspaceId: input.workspaceId,
318
+ sourceId: charge.sourceId,
319
+ }).debitIdempotencyKey,
320
+ usage: VOICE_CREDIT_USAGE,
321
+ metadata: {
322
+ basis: "reconciled_receipt" satisfies VoiceInputBillingBasis,
323
+ ...(charge.attribution.kind === "unknown"
324
+ ? {}
325
+ : creditDebitAttributionMetadata(charge.attribution)),
326
+ },
327
+ });
328
+ }
329
+ return { reconciled: pending.length };
330
+ }
@@ -0,0 +1,169 @@
1
+ import { webSearchCreditBillingActive, type Settings } from "@opengeni/config";
2
+ import {
3
+ applyCreditDebitAfterUse,
4
+ checkWorkspaceAllowance,
5
+ creditDebitAttributionForTurn,
6
+ creditDebitAttributionMetadata,
7
+ getSpendableCreditBalance,
8
+ recordUsageEvent,
9
+ withRlsContext,
10
+ type CreditDebitAttribution,
11
+ type Database,
12
+ } from "@opengeni/db";
13
+ import { workspaceCreditsDisabled } from "./workspace-credits";
14
+
15
+ /** Credit ledger type and usage source for deployment-funded web search calls. */
16
+ export const WEB_SEARCH_DEBIT_TYPE = "web_search_debit";
17
+ export const WEB_SEARCH_SOURCE_TYPE = "web_search";
18
+ const WEB_SEARCH_BILLING_INITIATOR = "worker:web-search";
19
+
20
+ /** Same rule as other deployment-funded resources: Stripe or managed limits. */
21
+ export { webSearchCreditBillingActive } from "@opengeni/config";
22
+
23
+ /** Exact attempt that called the tool. Every charge is attributed to its turn. */
24
+ export type WebSearchCallScope = {
25
+ accountId: string;
26
+ workspaceId: string;
27
+ sessionId: string;
28
+ turnId: string;
29
+ attemptId: string;
30
+ };
31
+
32
+ export type WebSearchCallCost = {
33
+ /** Model tool-call or Codemode operation identity, unique within the attempt. */
34
+ operationId: string;
35
+ operation: "search" | "fetch";
36
+ provider: string;
37
+ providerMicros: number;
38
+ creditMicros: number;
39
+ marginBps: number;
40
+ basis: "provider_reported" | "configured_price" | "list_price";
41
+ };
42
+
43
+ export class WebSearchBillingRefusedError extends Error {
44
+ constructor(
45
+ readonly code: "insufficient_credits" | "allowance_exhausted" | "credits_disabled",
46
+ message: string,
47
+ ) {
48
+ super(message);
49
+ this.name = "WebSearchBillingRefusedError";
50
+ }
51
+ }
52
+
53
+ /** Model-facing refusal when the workspace turned Opengeni credits off. */
54
+ export const WEB_SEARCH_CREDITS_DISABLED_MESSAGE =
55
+ "Web search uses Opengeni credits, which are turned off in this workspace.";
56
+
57
+ /**
58
+ * Admission and post-use settlement for paid web search, matching paid
59
+ * Knowledge queries and voice input: admission reads the workspace credit
60
+ * switch, general credits and the workspace/member allowance (a read, not a
61
+ * reservation); settlement records a durable usage receipt and the idempotent
62
+ * debit in one transaction. Calls that cost nothing (free providers, or
63
+ * billing inactive) are never refused. A workspace that turned Opengeni
64
+ * credits off is refused paid calls, and settlement skips the debit (and the
65
+ * cost receipt) for a call that was in flight when the switch turned off or
66
+ * that was admitted as free but reported a provider cost.
67
+ */
68
+ export function createWebSearchBilling(deps: { db: Database; settings: Settings }) {
69
+ const active = webSearchCreditBillingActive(deps.settings);
70
+ const attributionByTurn = new Map<string, Promise<CreditDebitAttribution>>();
71
+ const attributionFor = (scope: WebSearchCallScope) => {
72
+ let attribution = attributionByTurn.get(scope.turnId);
73
+ if (!attribution) {
74
+ attribution = creditDebitAttributionForTurn(deps.db, scope);
75
+ attributionByTurn.set(scope.turnId, attribution);
76
+ attribution.catch(() => attributionByTurn.delete(scope.turnId));
77
+ }
78
+ return attribution;
79
+ };
80
+ return {
81
+ active,
82
+ async admit(scope: WebSearchCallScope, expectedProviderMicros: number): Promise<void> {
83
+ if (!active || expectedProviderMicros <= 0) return;
84
+ if (await workspaceCreditsDisabled(deps.db, scope.workspaceId)) {
85
+ throw new WebSearchBillingRefusedError(
86
+ "credits_disabled",
87
+ WEB_SEARCH_CREDITS_DISABLED_MESSAGE,
88
+ );
89
+ }
90
+ const attribution = await attributionFor(scope);
91
+ // Non-model resources spend general credits only.
92
+ const balance = await getSpendableCreditBalance(deps.db, scope.accountId);
93
+ if (balance.balanceMicros <= 0) {
94
+ throw new WebSearchBillingRefusedError(
95
+ "insufficient_credits",
96
+ "Web search needs Opengeni credits, and this account has none left.",
97
+ );
98
+ }
99
+ const refusal = await checkWorkspaceAllowance(deps.db, {
100
+ accountId: scope.accountId,
101
+ workspaceId: scope.workspaceId,
102
+ subjectId: attribution.kind === "turn" ? attribution.initiatingHumanSubjectId : null,
103
+ });
104
+ if (refusal) throw new WebSearchBillingRefusedError("allowance_exhausted", refusal.message);
105
+ },
106
+ async settle(scope: WebSearchCallScope, cost: WebSearchCallCost): Promise<void> {
107
+ const shared = {
108
+ accountId: scope.accountId,
109
+ workspaceId: scope.workspaceId,
110
+ sessionId: scope.sessionId,
111
+ turnId: scope.turnId,
112
+ turnAttemptId: scope.attemptId,
113
+ sourceResourceType: WEB_SEARCH_SOURCE_TYPE,
114
+ sourceResourceId: `${scope.attemptId}:${cost.operationId}`,
115
+ };
116
+ // Request counts are recorded for every deployment, billed or not, so
117
+ // operators see provider volume.
118
+ await recordUsageEvent(deps.db, {
119
+ ...shared,
120
+ eventType: `web_search.${cost.operation}_requests`,
121
+ quantity: 1,
122
+ unit: "request",
123
+ idempotencyKey: `usage:web_search.${cost.operation}_requests:${shared.sourceResourceId}`,
124
+ });
125
+ if (!active || cost.creditMicros <= 0) return;
126
+ // Admission refused paid calls already; this covers a switch turned off
127
+ // during the provider call and provider-reported cost on a call admitted
128
+ // as free. The workspace spends no credits; the request count stays.
129
+ if (await workspaceCreditsDisabled(deps.db, scope.workspaceId)) return;
130
+ const attribution = await attributionFor(scope);
131
+ await withRlsContext(
132
+ deps.db,
133
+ { accountId: scope.accountId, workspaceId: scope.workspaceId },
134
+ async (tx) => {
135
+ await recordUsageEvent(tx, {
136
+ ...shared,
137
+ eventType: "web_search.cost",
138
+ quantity: cost.creditMicros,
139
+ unit: "usd_micros",
140
+ idempotencyKey: `usage:web_search.cost:${shared.sourceResourceId}`,
141
+ initiator: { kind: "service", subjectId: WEB_SEARCH_BILLING_INITIATOR },
142
+ initiatorContext: { creditDebitAttribution: attribution },
143
+ });
144
+ await applyCreditDebitAfterUse(tx, {
145
+ accountId: scope.accountId,
146
+ workspaceId: scope.workspaceId,
147
+ type: WEB_SEARCH_DEBIT_TYPE,
148
+ amountMicros: cost.creditMicros,
149
+ sourceType: WEB_SEARCH_SOURCE_TYPE,
150
+ sourceId: shared.sourceResourceId,
151
+ idempotencyKey: `credit:${WEB_SEARCH_DEBIT_TYPE}:${shared.sourceResourceId}`,
152
+ metadata: {
153
+ // turnId lets the allowance trigger resolve the causal human
154
+ // from the immutable turn receipt.
155
+ ...creditDebitAttributionMetadata(attribution),
156
+ provider: cost.provider,
157
+ operation: cost.operation,
158
+ providerCostMicros: cost.providerMicros,
159
+ marginBps: cost.marginBps,
160
+ basis: cost.basis,
161
+ },
162
+ });
163
+ },
164
+ );
165
+ },
166
+ };
167
+ }
168
+
169
+ export type WebSearchBilling = ReturnType<typeof createWebSearchBilling>;
@@ -0,0 +1,13 @@
1
+ import { getWorkspaceModelPolicy, type Database } from "@opengeni/db";
2
+
3
+ /**
4
+ * True when the workspace turned Opengeni credits off. Every meter that would
5
+ * spend credits (credit-billed models, voice, web search, knowledge
6
+ * embeddings) honors this one switch.
7
+ */
8
+ export async function workspaceCreditsDisabled(
9
+ db: Database,
10
+ workspaceId: string,
11
+ ): Promise<boolean> {
12
+ return (await getWorkspaceModelPolicy(db, workspaceId))?.allowCreditModels === false;
13
+ }
@@ -19,7 +19,10 @@ import type {
19
19
  EditableArtifactLiveServer,
20
20
  OpenEditableArtifactLiveInput,
21
21
  } from "./server";
22
- import type { EditableArtifactLiveTicket } from "./types";
22
+ import type {
23
+ EditableArtifactLiveSourceSessionAuthority,
24
+ EditableArtifactLiveTicket,
25
+ } from "./types";
23
26
 
24
27
  export type EditableArtifactLiveCompatibilityRequest = Readonly<{
25
28
  artifact: EditableArtifact;
@@ -83,6 +86,7 @@ export type MintEditableArtifactApplicationTicketInput = ReadEditableArtifactApp
83
86
  commandProtocolVersion: number;
84
87
  committedTransactionProtocolVersion: number;
85
88
  allowEdit: boolean;
89
+ sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
86
90
  }>;
87
91
 
88
92
  /** Exact application seam used by standalone API and embedding hosts. */
@@ -176,6 +180,9 @@ export class EditableArtifactApplication implements EditableArtifactApplicationP
176
180
  modality: artifact.modality,
177
181
  actor: input.actor,
178
182
  allowEdit: input.allowEdit,
183
+ ...(input.sourceSessionAuthority
184
+ ? { sourceSessionAuthority: input.sourceSessionAuthority }
185
+ : {}),
179
186
  });
180
187
  }
181
188