@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,472 @@
1
+ import {
2
+ calculateGatewayReportedCostBreakdown,
3
+ calculateGatewayReportedProviderCostMicros,
4
+ calculateModelListUsageCostSnapshot,
5
+ calculateModelUsageCostBreakdown,
6
+ canonicalizeConfiguredModelId,
7
+ configuredModelListPricingSchedules,
8
+ configuredModelPricingSchedules,
9
+ resolveModelProvider,
10
+ OPENGENI_GATEWAY_PROVIDER_ID,
11
+ OPPER_PROVIDER_ID,
12
+ ORGANIZATION_OPPER_PROVIDER_ID,
13
+ WORKSPACE_GATEWAY_PROVIDER_ID,
14
+ WORKSPACE_OPPER_PROVIDER_ID,
15
+ type Settings,
16
+ } from "@opengeni/config";
17
+ import { createHash } from "node:crypto";
18
+ import type { LatencyMode } from "@opengeni/contracts";
19
+ import type { InsightsUsageClassMicros } from "@opengeni/contracts/insights-usage";
20
+ import {
21
+ applyCreditDebitUpToBalance,
22
+ isStoredVerbatimAsLosslessJsonString,
23
+ recordUsageEvent,
24
+ settleSessionUsageReceipt,
25
+ toPostgresLosslessJson,
26
+ type Database,
27
+ } from "@opengeni/db";
28
+ import type { ModelCallUsageNormalization, ModelResponseUsage } from "@opengeni/runtime";
29
+ import type { ModelUsageInput } from "@opengeni/config";
30
+ import {
31
+ recordSessionUsageLimitEvent,
32
+ type SessionUsageLimitMetricsSink,
33
+ } from "../session-usage-limit-metrics";
34
+
35
+ /**
36
+ * Price and settle one authoritative model response: the token-cap usage
37
+ * event, the `model.cost` event, and (for Opengeni-credit models) the
38
+ * idempotent credit debit. Agent turns and stateless single model calls both
39
+ * settle here; `sourceId` is the durable identity every idempotency key
40
+ * derives from, so a retried settlement never charges twice.
41
+ */
42
+
43
+ export type ModelUsageBillingRecord = {
44
+ billingPath: "opengeni_credits" | "external";
45
+ /** Same quantity written to usage_events.model.cost when present; else 0. */
46
+ pricedCostMicros: number;
47
+ /** Hypothetical provider-rate USD micros; never an Opengeni charge. */
48
+ estimatedProviderCostMicros: number | null;
49
+ /** Hypothetical Opengeni credit price at the captured rate; never a debit. */
50
+ equivalentCreditCostMicros: number | null;
51
+ pricingSource: "configured_list_price" | "gateway_reported" | null;
52
+ /** Forward-only provider list class snapshot; older facts/events stay unknown. */
53
+ listByClassMicros?: InsightsUsageClassMicros | null;
54
+ listByClassApprox?: boolean;
55
+ normalizedUsage: ModelCallUsageNormalization;
56
+ upstreamProvider?: string;
57
+ };
58
+
59
+ // Exported for unit testing the external-billing bypass; not part of the activity surface.
60
+
61
+ export type ModelUsageSettlementInput = {
62
+ accountId: string;
63
+ workspaceId: string;
64
+ /** Product model id used for pricing and ledger attribution. */
65
+ model: string;
66
+ chargesOpenGeniCredits: boolean;
67
+ countsTowardTokenCap: boolean;
68
+ creditPolicyRevision?: number | undefined;
69
+ gatewayBilling?: ModelResponseUsage["gatewayBilling"];
70
+ normalizedUsage: ModelCallUsageNormalization;
71
+ /** Durable source identity (for a turn, `${turnId}:${sourceKey}`). */
72
+ sourceId: string;
73
+ sessionId?: string | null;
74
+ /**
75
+ * The session whose task usage limit this response counts against when it
76
+ * differs from the usage attribution `sessionId` (an attempt-authenticated
77
+ * single-model call keeps its existing usage attribution). Defaults to
78
+ * `sessionId`.
79
+ */
80
+ usageLimitSessionId?: string | null;
81
+ turnId?: string | null;
82
+ turnAttemptId?: string | null;
83
+ latencyMode?: LatencyMode;
84
+ /** Non-secret correlation fields added to the credit debit metadata. */
85
+ debitMetadata?: Record<string, unknown>;
86
+ /** Best-effort metrics hook after a committed debit. */
87
+ onCreditsDebited?: (result: { debitedMicros: number; grantDebitedMicros: number }) => void;
88
+ /** Content-free per-task usage limit diagnostics (closed-set labels only). */
89
+ usageLimitMetrics?: SessionUsageLimitMetricsSink | null;
90
+ /**
91
+ * Attempt-scoped custody for a receipt whose write failed. When present, a
92
+ * failed receipt write is handed over (content-free, original identity) and
93
+ * settlement continues, so the response is never lost or replayed; the
94
+ * owner must write held receipts before its next model request. Without
95
+ * custody the failure throws.
96
+ */
97
+ receiptCustody?: ((receipt: HeldSessionUsageReceipt, error: unknown) => void) | null;
98
+ };
99
+
100
+ /** A content-free task usage receipt whose write is still owed. */
101
+ export type HeldSessionUsageReceipt = {
102
+ sessionId: string;
103
+ sourceId: string;
104
+ turnId: string | null;
105
+ amountMicros: number | null;
106
+ pricingSource: "configured_list_price" | "gateway_reported" | null;
107
+ model: string | null;
108
+ };
109
+
110
+ /**
111
+ * The per-task meter amount: the canonical pre-margin estimate, but only when
112
+ * the response actually reported usage. A response without token telemetry
113
+ * (or with zero input and output tokens) and no provider-reported cost is an
114
+ * unknown amount, never a known-free call. Billing and Insights keep their
115
+ * own `estimatedProviderCostMicros` unchanged.
116
+ */
117
+ export function sessionUsageMeterAmountMicros(input: {
118
+ estimatedProviderCostMicros: number | null;
119
+ gatewayReported: boolean;
120
+ inputTokens: number | null;
121
+ outputTokens: number | null;
122
+ }): number | null {
123
+ if (input.estimatedProviderCostMicros === null) return null;
124
+ if (input.gatewayReported) return input.estimatedProviderCostMicros;
125
+ return (input.inputTokens ?? 0) + (input.outputTokens ?? 0) > 0
126
+ ? input.estimatedProviderCostMicros
127
+ : null;
128
+ }
129
+
130
+ /** The receipt table's `source_id` byte bound. */
131
+ export const SESSION_USAGE_RECEIPT_SOURCE_ID_MAX_BYTES = 1100;
132
+
133
+ /**
134
+ * The per-task receipt identity for a producer source. Every identity the
135
+ * event trigger or the legacy import can produce (`<turnId>:<sourceKey>` with
136
+ * a source key of at most 1024 bytes) fits the bound and is used unchanged,
137
+ * so their deduplication is untouched. Only a longer identity, which those
138
+ * paths skip, is replaced by a stable digest, so a long provider response id
139
+ * is recorded once instead of failing the receipt write on every retry.
140
+ *
141
+ * A source key PostgreSQL cannot store as given (NUL, a lone UTF-16
142
+ * surrogate, the lossless codec's own prefix) is stored by the event in its
143
+ * lossless encoding, and the trigger meters that encoded key. The receipt uses
144
+ * the same encoded key, so both writers share one identity instead of the
145
+ * direct write being refused.
146
+ */
147
+ export function sessionUsageReceiptSourceId(sourceId: string, turnId?: string | null): string {
148
+ const turnPrefix = turnId && sourceId.startsWith(`${turnId}:`) ? `${turnId}:` : "";
149
+ const sourceKey = sourceId.slice(turnPrefix.length);
150
+ const stored = isStoredVerbatimAsLosslessJsonString(sourceKey)
151
+ ? sourceId
152
+ : `${turnPrefix}${toPostgresLosslessJson(sourceKey) as string}`;
153
+ if (Buffer.byteLength(stored, "utf8") <= SESSION_USAGE_RECEIPT_SOURCE_ID_MAX_BYTES) {
154
+ return stored;
155
+ }
156
+ return `sha256:${createHash("sha256").update(stored, "utf8").digest("hex")}`;
157
+ }
158
+
159
+ export async function settleModelUsage(
160
+ settings: Settings,
161
+ db: Database,
162
+ input: ModelUsageSettlementInput,
163
+ ): Promise<ModelUsageBillingRecord> {
164
+ const normalizedUsage = input.normalizedUsage;
165
+ const usageContext = {
166
+ ...(input.sessionId ? { sessionId: input.sessionId } : {}),
167
+ ...(input.turnId ? { turnId: input.turnId } : {}),
168
+ ...(input.turnAttemptId ? { turnAttemptId: input.turnAttemptId } : {}),
169
+ };
170
+ const sanitizedUsage = sanitizedModelUsageInput(normalizedUsage);
171
+ const inputTokens = sanitizedUsage.inputTokens ?? 0;
172
+ const outputTokens = sanitizedUsage.outputTokens ?? 0;
173
+ const totalTokens = sanitizedUsage.totalTokens ?? 0;
174
+ const { chargesOpenGeniCredits, countsTowardTokenCap } = input;
175
+ const resolvedGatewayModel = input.gatewayBilling
176
+ ? resolveModelProvider(settings, input.model)
177
+ : undefined;
178
+ const gatewayProviderId = resolvedGatewayModel?.provider.id;
179
+ // Opper reports the exact USD cost of every response (`usage.opper.cost`);
180
+ // the Chat adapter surfaces it with `finalProvider: "opper"`.
181
+ const opperReported =
182
+ (gatewayProviderId === OPPER_PROVIDER_ID ||
183
+ gatewayProviderId === WORKSPACE_OPPER_PROVIDER_ID ||
184
+ gatewayProviderId === ORGANIZATION_OPPER_PROVIDER_ID) &&
185
+ input.gatewayBilling?.finalProvider === "opper";
186
+ const gatewayBilling =
187
+ gatewayProviderId === OPENGENI_GATEWAY_PROVIDER_ID ||
188
+ gatewayProviderId === WORKSPACE_GATEWAY_PROVIDER_ID ||
189
+ opperReported
190
+ ? input.gatewayBilling
191
+ : undefined;
192
+ const allowedProviders = resolvedGatewayModel?.model.requestPolicy?.gateway.only;
193
+ // Scoped Opper rails settle externally; record the exact provider cost only.
194
+ const unpinnedWorkspaceGatewayModel =
195
+ (gatewayProviderId === WORKSPACE_GATEWAY_PROVIDER_ID && allowedProviders === undefined) ||
196
+ (opperReported && gatewayProviderId !== OPPER_PROVIDER_ID);
197
+ if (gatewayBilling && !opperReported) {
198
+ if (
199
+ !unpinnedWorkspaceGatewayModel &&
200
+ (!allowedProviders ||
201
+ !(allowedProviders as readonly string[]).includes(gatewayBilling.finalProvider))
202
+ ) {
203
+ throw new Error(
204
+ `AI Gateway reported unapproved provider ${gatewayBilling.finalProvider} for ${input.model}`,
205
+ );
206
+ }
207
+ if (unpinnedWorkspaceGatewayModel && chargesOpenGeniCredits) {
208
+ throw new Error(
209
+ `Workspace Gateway custom model ${input.model} cannot charge Opengeni credits without pinned pricing`,
210
+ );
211
+ }
212
+ }
213
+ const pricingSchedules = configuredModelPricingSchedules(settings);
214
+ const configuredPricingModel = pricingSchedules[input.model]
215
+ ? input.model
216
+ : input.model.startsWith("codex/") && pricingSchedules[input.model.slice("codex/".length)]
217
+ ? input.model.slice("codex/".length)
218
+ : null;
219
+ const pricingBreakdown = gatewayBilling
220
+ ? unpinnedWorkspaceGatewayModel
221
+ ? {
222
+ providerCostMicros: calculateGatewayReportedProviderCostMicros(
223
+ gatewayBilling.inferenceCostUsd,
224
+ ),
225
+ creditCostMicros: 0,
226
+ }
227
+ : calculateGatewayReportedCostBreakdown(
228
+ settings,
229
+ configuredPricingModel ?? input.model,
230
+ gatewayBilling.inferenceCostUsd,
231
+ { inputTokens },
232
+ )
233
+ : configuredPricingModel
234
+ ? calculateModelUsageCostBreakdown(settings, configuredPricingModel, sanitizedUsage, {
235
+ latencyMode: input.latencyMode ?? "standard",
236
+ })
237
+ : null;
238
+ const hasCompleteCoreTokenTelemetry =
239
+ normalizedUsage.telemetry.inputTokens !== null &&
240
+ normalizedUsage.telemetry.outputTokens !== null;
241
+ // Comparison rates are deliberately separate from debit authority. The
242
+ // current usage frame does not establish geography/service-tier provenance,
243
+ // so forward class splits stay unknown even when a total estimate is priced.
244
+ const listPricingSchedules = configuredModelListPricingSchedules(settings);
245
+ const configuredListPricingModel = listPricingSchedules[input.model]
246
+ ? input.model
247
+ : input.model.startsWith("codex/") && listPricingSchedules[input.model.slice("codex/".length)]
248
+ ? input.model.slice("codex/".length)
249
+ : null;
250
+ const listSnapshot =
251
+ !gatewayBilling && hasCompleteCoreTokenTelemetry && configuredListPricingModel
252
+ ? calculateModelListUsageCostSnapshot(settings, configuredListPricingModel, sanitizedUsage, {
253
+ latencyMode: input.latencyMode ?? "standard",
254
+ priceContextKnown: false,
255
+ })
256
+ : null;
257
+ const listClasses = {
258
+ listByClassMicros: listSnapshot?.listByClassMicros ?? null,
259
+ listByClassApprox: listSnapshot?.listByClassApprox ?? false,
260
+ };
261
+ const estimatedProviderCostMicros = gatewayBilling
262
+ ? (pricingBreakdown?.providerCostMicros ?? null)
263
+ : hasCompleteCoreTokenTelemetry
264
+ ? (listSnapshot?.providerCostMicros ?? pricingBreakdown?.providerCostMicros ?? null)
265
+ : null;
266
+ const equivalentCreditCostMicros =
267
+ pricingBreakdown && !unpinnedWorkspaceGatewayModel
268
+ ? gatewayBilling || hasCompleteCoreTokenTelemetry
269
+ ? pricingBreakdown.creditCostMicros
270
+ : null
271
+ : null;
272
+ const pricingSource = gatewayBilling
273
+ ? ("gateway_reported" as const)
274
+ : estimatedProviderCostMicros !== null
275
+ ? ("configured_list_price" as const)
276
+ : null;
277
+ // Per-task usage limit meter. Synchronous and awaited before any
278
+ // credit/usage write: the canonical pre-margin estimate is recorded once
279
+ // per source identity against the session's task root, for every funding
280
+ // path. A failed write is handed to the caller's custody (which must write
281
+ // it before any further model request) or, without custody, throws; the
282
+ // provider call is never replayed.
283
+ const usageLimitSessionId = input.usageLimitSessionId ?? input.sessionId;
284
+ if (usageLimitSessionId) {
285
+ const meterAmountMicros = sessionUsageMeterAmountMicros({
286
+ estimatedProviderCostMicros,
287
+ gatewayReported: pricingSource === "gateway_reported",
288
+ inputTokens: normalizedUsage.telemetry.inputTokens,
289
+ outputTokens: normalizedUsage.telemetry.outputTokens,
290
+ });
291
+ const held: HeldSessionUsageReceipt = {
292
+ sessionId: usageLimitSessionId,
293
+ sourceId: sessionUsageReceiptSourceId(input.sourceId, input.turnId),
294
+ turnId: input.turnId ?? null,
295
+ amountMicros: meterAmountMicros,
296
+ pricingSource: meterAmountMicros === null ? null : pricingSource,
297
+ model: input.model,
298
+ };
299
+ let receipt: Awaited<ReturnType<typeof settleSessionUsageReceipt>> | null = null;
300
+ try {
301
+ receipt = await settleSessionUsageReceipt(db, {
302
+ accountId: input.accountId,
303
+ workspaceId: input.workspaceId,
304
+ ...held,
305
+ });
306
+ } catch (error) {
307
+ if (!input.receiptCustody) {
308
+ recordSessionUsageLimitEvent(input.usageLimitMetrics, "settlement", "accounting_failure");
309
+ throw error;
310
+ }
311
+ input.receiptCustody(held, error);
312
+ recordSessionUsageLimitEvent(input.usageLimitMetrics, "settlement", "held");
313
+ }
314
+ if (receipt) {
315
+ recordSessionUsageLimitEvent(
316
+ input.usageLimitMetrics,
317
+ "settlement",
318
+ receipt.outcome === "conflict"
319
+ ? "source_conflict"
320
+ : meterAmountMicros === null && receipt.outcome === "recorded"
321
+ ? "unpriced"
322
+ : receipt.outcome,
323
+ );
324
+ }
325
+ }
326
+ // Provider settlement and workspace-facing cost are separate. Externally
327
+ // metered subscription/workspace turns remain exempt from the Opengeni token
328
+ // cap, while a deployment-funded free model still records model.tokens. Every
329
+ // non-credit path records a zero-cost marker and never consults pricing for a
330
+ // debit.
331
+ if (countsTowardTokenCap && totalTokens > 0) {
332
+ await recordUsageEvent(db, {
333
+ accountId: input.accountId,
334
+ workspaceId: input.workspaceId,
335
+ eventType: "model.tokens",
336
+ quantity: totalTokens,
337
+ unit: "tokens",
338
+ sourceResourceType: "model_response",
339
+ sourceResourceId: input.sourceId,
340
+ ...usageContext,
341
+ idempotencyKey: `usage:model.tokens:${input.sourceId}`,
342
+ });
343
+ }
344
+ if (!chargesOpenGeniCredits) {
345
+ await recordUsageEvent(db, {
346
+ accountId: input.accountId,
347
+ workspaceId: input.workspaceId,
348
+ eventType: "model.cost",
349
+ quantity: 0,
350
+ unit: "usd_micros",
351
+ sourceResourceType: "model_response",
352
+ sourceResourceId: input.sourceId,
353
+ ...usageContext,
354
+ idempotencyKey: `usage:model.cost:${input.sourceId}`,
355
+ });
356
+ return {
357
+ billingPath: "external",
358
+ pricedCostMicros: 0,
359
+ estimatedProviderCostMicros,
360
+ equivalentCreditCostMicros,
361
+ pricingSource,
362
+ ...listClasses,
363
+ normalizedUsage,
364
+ ...(gatewayBilling ? { upstreamProvider: gatewayBilling.finalProvider } : {}),
365
+ };
366
+ }
367
+ const shouldDebit = settings.billingMode === "stripe" || settings.usageLimitsMode === "managed";
368
+ if (!shouldDebit || (totalTokens === 0 && !gatewayBilling)) {
369
+ return {
370
+ billingPath: "opengeni_credits",
371
+ pricedCostMicros: 0,
372
+ estimatedProviderCostMicros,
373
+ equivalentCreditCostMicros,
374
+ pricingSource,
375
+ ...listClasses,
376
+ normalizedUsage,
377
+ ...(gatewayBilling ? { upstreamProvider: gatewayBilling.finalProvider } : {}),
378
+ };
379
+ }
380
+ if (!pricingBreakdown) {
381
+ throw new Error(`Missing model pricing for ${input.model}`);
382
+ }
383
+ const costMicros = pricingBreakdown.creditCostMicros;
384
+ await recordUsageEvent(db, {
385
+ accountId: input.accountId,
386
+ workspaceId: input.workspaceId,
387
+ eventType: "model.cost",
388
+ quantity: costMicros,
389
+ unit: "usd_micros",
390
+ sourceResourceType: "model_response",
391
+ sourceResourceId: input.sourceId,
392
+ ...usageContext,
393
+ idempotencyKey: `usage:model.cost:${input.sourceId}`,
394
+ });
395
+ if (costMicros > 0) {
396
+ const result = await applyCreditDebitUpToBalance(db, {
397
+ accountId: input.accountId,
398
+ workspaceId: input.workspaceId,
399
+ type: "model_usage_debit",
400
+ requestedAmountMicros: costMicros,
401
+ modelId: canonicalizeConfiguredModelId(settings, input.model),
402
+ creditPolicyRevision: input.creditPolicyRevision,
403
+ sourceType: "model_response",
404
+ sourceId: input.sourceId,
405
+ idempotencyKey: `credit:model_usage_debit:${input.sourceId}`,
406
+ metadata: {
407
+ model: input.model,
408
+ ...input.debitMetadata,
409
+ latencyMode: input.latencyMode ?? "standard",
410
+ inputTokens,
411
+ outputTokens,
412
+ totalTokens,
413
+ // Additive: the prompt-cache slice of this call's input tokens, so the
414
+ // per-call debit record carries cache efficiency alongside the token
415
+ // counts. 0 when the provider did not report cached tokens.
416
+ cachedTokens: normalizedUsage.telemetry.cachedTokens ?? 0,
417
+ ...(gatewayBilling ? { gatewayProvider: gatewayBilling.finalProvider } : {}),
418
+ },
419
+ });
420
+ try {
421
+ input.onCreditsDebited?.(result);
422
+ } catch {
423
+ // The debit is committed; metrics are best-effort only.
424
+ }
425
+ }
426
+ return {
427
+ billingPath: "opengeni_credits",
428
+ pricedCostMicros: costMicros,
429
+ estimatedProviderCostMicros,
430
+ equivalentCreditCostMicros,
431
+ pricingSource,
432
+ ...listClasses,
433
+ normalizedUsage,
434
+ ...(gatewayBilling ? { upstreamProvider: gatewayBilling.finalProvider } : {}),
435
+ };
436
+ }
437
+
438
+ /** Bounded, validated token counts for pricing; raw provider values never price. */
439
+ export function sanitizedModelUsageInput(normalized: ModelCallUsageNormalization): ModelUsageInput {
440
+ return {
441
+ ...(normalized.telemetry.inputTokens !== null
442
+ ? { inputTokens: normalized.telemetry.inputTokens }
443
+ : {}),
444
+ ...(normalized.telemetry.outputTokens !== null
445
+ ? { outputTokens: normalized.telemetry.outputTokens }
446
+ : {}),
447
+ ...(normalized.totalTokens !== null ? { totalTokens: normalized.totalTokens } : {}),
448
+ ...(normalized.telemetry.cachedTokens !== null ||
449
+ normalized.telemetry.cacheWriteTokens !== null ||
450
+ normalized.cacheWriteTokensByTtl !== undefined
451
+ ? {
452
+ inputTokensDetails: {
453
+ ...(normalized.telemetry.cachedTokens === null
454
+ ? {}
455
+ : { cached_tokens: normalized.telemetry.cachedTokens }),
456
+ ...(normalized.telemetry.cacheWriteTokens === null
457
+ ? {}
458
+ : { cache_write_tokens: normalized.telemetry.cacheWriteTokens }),
459
+ ...(normalized.cacheWriteTokensByTtl?.fiveMinute == null
460
+ ? {}
461
+ : { cache_write_tokens_5m: normalized.cacheWriteTokensByTtl.fiveMinute }),
462
+ ...(normalized.cacheWriteTokensByTtl?.oneHour == null
463
+ ? {}
464
+ : { cache_write_tokens_1h: normalized.cacheWriteTokensByTtl.oneHour }),
465
+ },
466
+ }
467
+ : {}),
468
+ ...(normalized.requestUsageEntries
469
+ ? { requestUsageEntries: normalized.requestUsageEntries }
470
+ : {}),
471
+ };
472
+ }