@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,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 {
|
|
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
|
|