@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.
- package/README.md +4 -4
- package/dist/access/index.d.ts +131 -7
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +1 -1
- package/dist/application/connect-operation.d.ts +6 -0
- package/dist/application/external-workspace-members.d.ts +13 -1
- package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
- package/dist/application/modal-native-original-configuration.d.ts +79 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +29 -2
- package/dist/application/organization-privacy.d.ts +57 -0
- package/dist/application/prepared-mcp-connection.d.ts +31 -0
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +3 -0
- package/dist/application/session-tenancy.d.ts +13 -4
- package/dist/billing/agent-run-admission.d.ts +21 -0
- package/dist/billing/limits.d.ts +27 -1
- package/dist/billing/model-call-admission.d.ts +34 -0
- package/dist/billing/model-usage-settlement.d.ts +112 -0
- package/dist/canonical-human-identities.js +1 -1
- package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
- package/dist/chunk-7T2SZ7BI.js.map +1 -0
- package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
- package/dist/chunk-OMJFIIWK.js.map +1 -0
- package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
- package/dist/chunk-QI2M2B2K.js.map +1 -0
- package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
- package/dist/chunk-TTWTXQB2.js.map +1 -0
- package/dist/codex-model-availability.d.ts +55 -0
- package/dist/default-session-model.d.ts +192 -0
- package/dist/dependencies.d.ts +41 -7
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +8 -1
- package/dist/domain/capabilities.d.ts +112 -8
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +62 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +3 -1
- package/dist/domain/editable-artifacts/types.d.ts +1 -1
- package/dist/domain/encryption-key.d.ts +12 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +21 -3
- package/dist/domain/knowledge-files.d.ts +1 -1
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -76
- package/dist/domain/mcp-account-bindings.d.ts +17 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/personal-connection-delegations.d.ts +73 -12
- package/dist/domain/pr-review.d.ts +121 -6
- package/dist/domain/realtime-voice-billing.d.ts +103 -0
- package/dist/domain/resources.d.ts +17 -1
- package/dist/domain/scheduled-task-access.d.ts +265 -0
- package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
- package/dist/domain/scheduled-task-audience.d.ts +70 -0
- package/dist/domain/scheduled-tasks.d.ts +61 -18
- package/dist/domain/session-connection-accounts.d.ts +53 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +102 -4
- package/dist/domain/sessions.d.ts +179 -15
- package/dist/domain/site-audience.d.ts +52 -0
- package/dist/domain/skills.d.ts +29 -1
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
- package/dist/domain/vaults/index.d.ts +7 -0
- package/dist/domain/vaults/mapping.d.ts +25 -0
- package/dist/domain/vaults/provider-registry.d.ts +72 -0
- package/dist/domain/vaults/provider-sync.d.ts +176 -0
- package/dist/domain/vaults/provider-writes.d.ts +57 -0
- package/dist/domain/vaults/secret-resolver.d.ts +34 -0
- package/dist/domain/vaults/types.d.ts +24 -0
- package/dist/domain/vaults/vault-service.d.ts +35 -0
- package/dist/domain/voice-input-billing.d.ts +77 -0
- package/dist/domain/web-search-billing.d.ts +51 -0
- package/dist/domain/workspace-credits.d.ts +7 -0
- package/dist/editable-artifact-live/application.d.ts +2 -1
- package/dist/editable-artifact-live/server.d.ts +5 -1
- package/dist/editable-artifact-live/ticket.d.ts +2 -1
- package/dist/editable-artifact-live/types.d.ts +9 -0
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -2
- package/dist/goal-admission.d.ts +627 -0
- package/dist/host-session-usage-limit.d.ts +15 -0
- package/dist/index.d.ts +43 -8
- package/dist/index.js +22682 -11743
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +110 -7
- package/dist/model-call.d.ts +58 -0
- package/dist/model-catalog.d.ts +29 -3
- package/dist/model-provider-credentials.d.ts +43 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +1 -13
- package/dist/sandbox/fleet.d.ts +58 -4
- package/dist/sandbox/routing.d.ts +42 -2
- package/dist/sandbox/runtime-settings.d.ts +27 -17
- package/dist/session-authorization.d.ts +10 -0
- package/dist/session-usage-limit-metrics.d.ts +60 -0
- package/dist/session-usage-limits.d.ts +89 -0
- package/dist/site-agent-clamp.d.ts +71 -0
- package/dist/site-session-origin.d.ts +39 -2
- package/dist/transcription.d.ts +120 -3
- package/dist/turn-surface.d.ts +20 -0
- package/dist/user-presence.d.ts +32 -0
- package/dist/workflow-wake-contract.d.ts +13 -1
- package/package.json +15 -22
- package/src/access/index.ts +961 -38
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +9 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +3 -2
- package/src/application/connect-authority.ts +3 -3
- package/src/application/connect-operation.ts +37 -3
- package/src/application/external-continuation.ts +20 -3
- package/src/application/external-link-work-admission.ts +3 -0
- package/src/application/external-workspace-members.ts +51 -3
- package/src/application/modal-native-live-declaration-join.ts +196 -0
- package/src/application/modal-native-original-configuration.ts +261 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +172 -55
- package/src/application/organization-privacy.ts +161 -0
- package/src/application/prepared-mcp-connection.ts +133 -0
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +73 -1
- package/src/application/session-mcp-credential-rotation.ts +61 -18
- package/src/application/session-tenancy.ts +262 -44
- package/src/application/user-resource-grants.ts +1 -29
- package/src/billing/agent-run-admission.ts +117 -0
- package/src/billing/limits.ts +138 -14
- package/src/billing/model-call-admission.ts +118 -0
- package/src/billing/model-usage-settlement.ts +472 -0
- package/src/codex-model-availability.ts +264 -0
- package/src/default-session-model.ts +692 -0
- package/src/dependencies.ts +48 -6
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +76 -2
- package/src/domain/capabilities.ts +830 -181
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +572 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +44 -6
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +20 -0
- package/src/domain/editable-artifacts/types.ts +1 -1
- package/src/domain/encryption-key.ts +26 -0
- package/src/domain/file-owner.ts +16 -1
- package/src/domain/github-action-policies.ts +1 -1
- package/src/domain/github-skill-source.ts +3 -1
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +167 -43
- package/src/domain/knowledge-files.ts +10 -3
- package/src/domain/knowledge-messages.ts +1 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +617 -5
- package/src/domain/knowledge-search.ts +240 -23
- package/src/domain/mcp-account-bindings.ts +169 -0
- package/src/domain/mcp-account-routes.ts +152 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/personal-connection-delegations.ts +399 -257
- package/src/domain/pr-review.ts +24 -8
- package/src/domain/realtime-voice-billing.ts +425 -0
- package/src/domain/resources.ts +50 -4
- package/src/domain/scheduled-task-access.ts +1089 -0
- package/src/domain/scheduled-task-account-audit.ts +90 -0
- package/src/domain/scheduled-task-audience.ts +249 -0
- package/src/domain/scheduled-tasks.ts +1313 -374
- package/src/domain/session-connection-accounts.ts +239 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +60 -0
- package/src/domain/session-tool-policy.ts +489 -14
- package/src/domain/sessions.ts +2254 -717
- package/src/domain/site-audience.ts +130 -0
- package/src/domain/skills.ts +9 -0
- package/src/domain/slack-bot.ts +141 -4
- package/src/domain/slack-publication-secret-safety.ts +1 -1
- package/src/domain/vaults/index.ts +19 -0
- package/src/domain/vaults/mapping.ts +190 -0
- package/src/domain/vaults/provider-registry.ts +201 -0
- package/src/domain/vaults/provider-sync.ts +531 -0
- package/src/domain/vaults/provider-writes.ts +288 -0
- package/src/domain/vaults/secret-resolver.ts +464 -0
- package/src/domain/vaults/types.ts +26 -0
- package/src/domain/vaults/vault-service.ts +1316 -0
- package/src/domain/voice-input-billing.ts +330 -0
- package/src/domain/web-search-billing.ts +169 -0
- package/src/domain/workspace-credits.ts +13 -0
- package/src/editable-artifact-live/application.ts +8 -1
- package/src/editable-artifact-live/server.ts +154 -6
- package/src/editable-artifact-live/ticket.ts +86 -1
- package/src/editable-artifact-live/types.ts +10 -0
- package/src/goal-admission.ts +220 -0
- package/src/host-session-usage-limit.ts +25 -0
- package/src/index.ts +75 -11
- package/src/managed-session.ts +137 -6
- package/src/model-call.ts +79 -0
- package/src/model-catalog.ts +345 -91
- package/src/model-provider-credentials.ts +394 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +23 -111
- package/src/sandbox/fleet.ts +253 -28
- package/src/sandbox/routing.ts +204 -43
- package/src/sandbox/runtime-settings.ts +159 -85
- package/src/session-authorization.ts +172 -23
- package/src/session-usage-limit-metrics.ts +95 -0
- package/src/session-usage-limits.ts +415 -0
- package/src/site-agent-clamp.ts +226 -0
- package/src/site-session-origin.ts +84 -5
- package/src/transcription.ts +123 -4
- package/src/turn-surface.ts +47 -0
- package/src/user-presence.ts +134 -0
- package/src/workflow-wake-contract.ts +14 -1
- package/dist/application/host-mcp-owner.d.ts +0 -6
- package/dist/application/host-mcp-resolvers.d.ts +0 -13
- package/dist/chunk-5WZYFL45.js +0 -266
- package/dist/chunk-5WZYFL45.js.map +0 -1
- package/dist/chunk-H7X52RI2.js.map +0 -1
- package/dist/chunk-OF65T3PM.js.map +0 -1
- package/dist/chunk-VCZFFEPB.js.map +0 -1
- package/dist/chunk-ZYCNLYGH.js.map +0 -1
- package/dist/domain/connection-authority.d.ts +0 -97
- package/dist/domain/connection-authority.js +0 -169
- package/dist/domain/connection-authority.js.map +0 -1
- package/dist/domain/environments.d.ts +0 -33
- package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
- package/dist/domain/host-mcp-task-admission.d.ts +0 -18
- package/dist/domain/packs.d.ts +0 -59
- package/dist/domain/product-integration-pack.d.ts +0 -44
- package/dist/domain/product-integration-skill.gen.d.ts +0 -5
- package/dist/remote-mcp-credentials.d.ts +0 -23
- package/dist/remote-mcp-credentials.js +0 -11
- package/dist/remote-mcp-credentials.js.map +0 -1
- package/src/application/host-mcp-owner.ts +0 -41
- package/src/application/host-mcp-resolvers.ts +0 -84
- package/src/domain/connection-authority.ts +0 -329
- package/src/domain/environments.ts +0 -120
- package/src/domain/host-mcp-authority-source-admission.ts +0 -25
- package/src/domain/host-mcp-task-admission.ts +0 -121
- package/src/domain/packs.ts +0 -699
- package/src/domain/product-integration-pack.ts +0 -42
- package/src/domain/product-integration-skill.gen.ts +0 -52
- 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
|
+
}
|