@opengeni/core 0.4.6 → 0.4.8
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/dist/index.d.ts +293 -32
- package/dist/index.js +1750 -472
- package/dist/index.js.map +1 -1
- package/package.json +17 -17
- package/src/access/index.ts +136 -57
- package/src/application/session-commands.ts +527 -0
- package/src/billing/limits.ts +76 -34
- package/src/dependencies.ts +67 -14
- package/src/domain/capabilities.ts +380 -181
- package/src/domain/environments.ts +58 -38
- package/src/domain/packs.ts +49 -16
- package/src/domain/resources.ts +71 -28
- package/src/domain/scheduled-tasks.ts +107 -41
- package/src/domain/sessions.ts +531 -255
- package/src/domain/workspace-members.ts +6 -2
- package/src/index.ts +3 -0
- package/src/rigs/index.ts +540 -0
- package/src/sandbox/fleet.ts +97 -18
- package/src/sandbox/routing.ts +6 -1
- package/src/sandbox-types.ts +17 -5
- package/src/workflow-wake-contract.ts +6 -0
package/src/billing/limits.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
import { HTTPException } from "hono/http-exception";
|
|
13
13
|
import type { ApiRouteDeps } from "../dependencies";
|
|
14
14
|
|
|
15
|
+
export type LimitDependencies = Pick<ApiRouteDeps, "db" | "settings">;
|
|
16
|
+
|
|
15
17
|
export type LimitCheckInput = {
|
|
16
18
|
accountId: string;
|
|
17
19
|
workspaceId?: string;
|
|
@@ -26,20 +28,30 @@ export type LimitCheckInput = {
|
|
|
26
28
|
model?: string | null;
|
|
27
29
|
};
|
|
28
30
|
|
|
29
|
-
export async function requireLimit(deps:
|
|
31
|
+
export async function requireLimit(deps: LimitDependencies, input: LimitCheckInput): Promise<void> {
|
|
30
32
|
const decision = await checkLimit(deps, input);
|
|
31
33
|
if (decision.allowed) {
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
|
-
throw new HTTPException(decision.code === "insufficient_credits" ? 402 : 429, {
|
|
36
|
+
throw new HTTPException(decision.code === "insufficient_credits" ? 402 : 429, {
|
|
37
|
+
message: decision.message,
|
|
38
|
+
});
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
export async function checkLimit(
|
|
41
|
+
export async function checkLimit(
|
|
42
|
+
deps: LimitDependencies,
|
|
43
|
+
input: LimitCheckInput,
|
|
44
|
+
): Promise<LimitDecision> {
|
|
38
45
|
// Resolve the canonical codex-billed predicate ONCE. Returns false for any
|
|
39
46
|
// action that carries no model (infra caps) or any codex/<slug> model without
|
|
40
47
|
// an active credential — so the bypass never triggers on the prefix alone.
|
|
41
48
|
const codexBilled = input.workspaceId
|
|
42
|
-
? await isCodexBilledTurn({
|
|
49
|
+
? await isCodexBilledTurn({
|
|
50
|
+
db: deps.db,
|
|
51
|
+
settings: deps.settings,
|
|
52
|
+
workspaceId: input.workspaceId,
|
|
53
|
+
model: input.model,
|
|
54
|
+
})
|
|
43
55
|
: false;
|
|
44
56
|
const creditDecision = await checkCreditBalance(deps, input, codexBilled);
|
|
45
57
|
if (!creditDecision.allowed) {
|
|
@@ -51,7 +63,11 @@ export async function checkLimit(deps: ApiRouteDeps, input: LimitCheckInput): Pr
|
|
|
51
63
|
return await checkStaticCaps(deps, input, codexBilled);
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
async function checkCreditBalance(
|
|
66
|
+
async function checkCreditBalance(
|
|
67
|
+
deps: LimitDependencies,
|
|
68
|
+
input: LimitCheckInput,
|
|
69
|
+
codexBilled: boolean,
|
|
70
|
+
): Promise<LimitDecision> {
|
|
55
71
|
if (codexBilled) {
|
|
56
72
|
return { allowed: true }; // paid by the user's ChatGPT/Codex plan — zero OpenGeni credits
|
|
57
73
|
}
|
|
@@ -65,7 +81,11 @@ async function checkCreditBalance(deps: ApiRouteDeps, input: LimitCheckInput, co
|
|
|
65
81
|
return { allowed: false, code: "insufficient_credits", message: "insufficient OpenGeni credits" };
|
|
66
82
|
}
|
|
67
83
|
|
|
68
|
-
async function checkStaticCaps(
|
|
84
|
+
async function checkStaticCaps(
|
|
85
|
+
deps: LimitDependencies,
|
|
86
|
+
input: LimitCheckInput,
|
|
87
|
+
codexBilled: boolean,
|
|
88
|
+
): Promise<LimitDecision> {
|
|
69
89
|
const limits = configuredStaticUsageLimits(deps.settings);
|
|
70
90
|
if (limits.maxMonthlyCostMicrosPerAccount && isCostlyAction(input.action) && !codexBilled) {
|
|
71
91
|
const used = await sumUsageQuantity(deps.db, {
|
|
@@ -74,7 +94,10 @@ async function checkStaticCaps(deps: ApiRouteDeps, input: LimitCheckInput, codex
|
|
|
74
94
|
since: startOfUtcMonth(),
|
|
75
95
|
});
|
|
76
96
|
if (used >= limits.maxMonthlyCostMicrosPerAccount) {
|
|
77
|
-
return blocked(
|
|
97
|
+
return blocked(
|
|
98
|
+
"max_monthly_cost_micros_per_account",
|
|
99
|
+
`monthly model cost limit reached (${limits.maxMonthlyCostMicrosPerAccount} micros)`,
|
|
100
|
+
);
|
|
78
101
|
}
|
|
79
102
|
}
|
|
80
103
|
switch (input.action) {
|
|
@@ -85,7 +108,10 @@ async function checkStaticCaps(deps: ApiRouteDeps, input: LimitCheckInput, codex
|
|
|
85
108
|
const count = await countWorkspacesForAccount(deps.db, input.accountId);
|
|
86
109
|
return count < limits.maxWorkspacesPerAccount
|
|
87
110
|
? { allowed: true }
|
|
88
|
-
: blocked(
|
|
111
|
+
: blocked(
|
|
112
|
+
"max_workspaces_per_account",
|
|
113
|
+
`workspace limit reached (${limits.maxWorkspacesPerAccount})`,
|
|
114
|
+
);
|
|
89
115
|
}
|
|
90
116
|
case "api_key:create": {
|
|
91
117
|
if (!limits.maxApiKeysPerWorkspace || !input.workspaceId) {
|
|
@@ -94,7 +120,10 @@ async function checkStaticCaps(deps: ApiRouteDeps, input: LimitCheckInput, codex
|
|
|
94
120
|
const count = await countActiveApiKeysForWorkspace(deps.db, input.workspaceId);
|
|
95
121
|
return count < limits.maxApiKeysPerWorkspace
|
|
96
122
|
? { allowed: true }
|
|
97
|
-
: blocked(
|
|
123
|
+
: blocked(
|
|
124
|
+
"max_api_keys_per_workspace",
|
|
125
|
+
`API key limit reached (${limits.maxApiKeysPerWorkspace})`,
|
|
126
|
+
);
|
|
98
127
|
}
|
|
99
128
|
case "schedule:create": {
|
|
100
129
|
if (!limits.maxSchedulesPerWorkspace || !input.workspaceId) {
|
|
@@ -103,7 +132,10 @@ async function checkStaticCaps(deps: ApiRouteDeps, input: LimitCheckInput, codex
|
|
|
103
132
|
const count = await countScheduledTasksForWorkspace(deps.db, input.workspaceId);
|
|
104
133
|
return count < limits.maxSchedulesPerWorkspace
|
|
105
134
|
? { allowed: true }
|
|
106
|
-
: blocked(
|
|
135
|
+
: blocked(
|
|
136
|
+
"max_schedules_per_workspace",
|
|
137
|
+
`scheduled task limit reached (${limits.maxSchedulesPerWorkspace})`,
|
|
138
|
+
);
|
|
107
139
|
}
|
|
108
140
|
case "file:upload": {
|
|
109
141
|
if (!limits.maxFileUploadBytes || !input.quantity) {
|
|
@@ -111,7 +143,10 @@ async function checkStaticCaps(deps: ApiRouteDeps, input: LimitCheckInput, codex
|
|
|
111
143
|
}
|
|
112
144
|
return input.quantity <= limits.maxFileUploadBytes
|
|
113
145
|
? { allowed: true }
|
|
114
|
-
: blocked(
|
|
146
|
+
: blocked(
|
|
147
|
+
"max_file_upload_bytes",
|
|
148
|
+
`file upload exceeds static limit of ${limits.maxFileUploadBytes} bytes`,
|
|
149
|
+
);
|
|
115
150
|
}
|
|
116
151
|
case "agent_run:create": {
|
|
117
152
|
if (!limits.maxMonthlyAgentRunsPerWorkspace || !input.workspaceId) {
|
|
@@ -125,7 +160,10 @@ async function checkStaticCaps(deps: ApiRouteDeps, input: LimitCheckInput, codex
|
|
|
125
160
|
const requested = input.quantity ?? 0;
|
|
126
161
|
return used + requested <= limits.maxMonthlyAgentRunsPerWorkspace
|
|
127
162
|
? { allowed: true }
|
|
128
|
-
: blocked(
|
|
163
|
+
: blocked(
|
|
164
|
+
"max_monthly_agent_runs_per_workspace",
|
|
165
|
+
`monthly agent run limit reached (${limits.maxMonthlyAgentRunsPerWorkspace})`,
|
|
166
|
+
);
|
|
129
167
|
}
|
|
130
168
|
case "tokens:consume": {
|
|
131
169
|
if (codexBilled || !limits.maxMonthlyTokensPerWorkspace || !input.workspaceId) {
|
|
@@ -139,7 +177,10 @@ async function checkStaticCaps(deps: ApiRouteDeps, input: LimitCheckInput, codex
|
|
|
139
177
|
const requested = input.quantity ?? 0;
|
|
140
178
|
return used + requested <= limits.maxMonthlyTokensPerWorkspace
|
|
141
179
|
? { allowed: true }
|
|
142
|
-
: blocked(
|
|
180
|
+
: blocked(
|
|
181
|
+
"max_monthly_tokens_per_workspace",
|
|
182
|
+
`monthly token limit reached (${limits.maxMonthlyTokensPerWorkspace})`,
|
|
183
|
+
);
|
|
143
184
|
}
|
|
144
185
|
case "document:index": {
|
|
145
186
|
if (!limits.maxDocumentIndexedChunksPerWorkspace || !input.workspaceId) {
|
|
@@ -153,26 +194,28 @@ async function checkStaticCaps(deps: ApiRouteDeps, input: LimitCheckInput, codex
|
|
|
153
194
|
const requested = input.quantity ?? 0;
|
|
154
195
|
return used + requested <= limits.maxDocumentIndexedChunksPerWorkspace
|
|
155
196
|
? { allowed: true }
|
|
156
|
-
: blocked(
|
|
197
|
+
: blocked(
|
|
198
|
+
"max_document_indexed_chunks_per_workspace",
|
|
199
|
+
`monthly document indexing limit reached (${limits.maxDocumentIndexedChunksPerWorkspace} chunks)`,
|
|
200
|
+
);
|
|
157
201
|
}
|
|
158
202
|
}
|
|
159
203
|
}
|
|
160
204
|
|
|
161
|
-
export async function recordWorkspaceUsage(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
|
167
|
-
| "file.uploaded"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}): Promise<void> {
|
|
205
|
+
export async function recordWorkspaceUsage(
|
|
206
|
+
deps: LimitDependencies,
|
|
207
|
+
input: {
|
|
208
|
+
accountId: string;
|
|
209
|
+
workspaceId: string;
|
|
210
|
+
subjectId?: string | null;
|
|
211
|
+
eventType: "agent_run.created" | "file.uploaded" | "document.indexed" | "scheduled_task.fired";
|
|
212
|
+
quantity: number;
|
|
213
|
+
unit: string;
|
|
214
|
+
sourceResourceType: string;
|
|
215
|
+
sourceResourceId: string;
|
|
216
|
+
idempotencyKey: string;
|
|
217
|
+
},
|
|
218
|
+
): Promise<void> {
|
|
176
219
|
await recordUsageEvent(deps.db, {
|
|
177
220
|
accountId: input.accountId,
|
|
178
221
|
workspaceId: input.workspaceId,
|
|
@@ -186,15 +229,14 @@ export async function recordWorkspaceUsage(deps: ApiRouteDeps, input: {
|
|
|
186
229
|
});
|
|
187
230
|
}
|
|
188
231
|
|
|
189
|
-
function usesCreditLimits(deps:
|
|
232
|
+
function usesCreditLimits(deps: LimitDependencies): boolean {
|
|
190
233
|
return deps.settings.billingMode === "stripe" || deps.settings.usageLimitsMode === "managed";
|
|
191
234
|
}
|
|
192
235
|
|
|
193
236
|
function isCostlyAction(action: LimitAction): boolean {
|
|
194
|
-
return
|
|
195
|
-
|| action === "tokens:consume"
|
|
196
|
-
|
|
197
|
-
|| action === "document:index";
|
|
237
|
+
return (
|
|
238
|
+
action === "agent_run:create" || action === "tokens:consume" || action === "document:index"
|
|
239
|
+
);
|
|
198
240
|
}
|
|
199
241
|
|
|
200
242
|
function blocked(code: string, message: string): LimitDecision {
|
package/src/dependencies.ts
CHANGED
|
@@ -9,26 +9,62 @@ import type { ManagedAuth } from "./managed-auth-type";
|
|
|
9
9
|
import type { ApiSandboxClient, ResumeBoxByIdInput, ResumedSandboxSession } from "./sandbox-types";
|
|
10
10
|
|
|
11
11
|
export type SessionWorkflowClient = {
|
|
12
|
-
signalUserMessage: (input: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
signalUserMessage: (input: {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
eventId: string;
|
|
15
|
+
workflowId: string;
|
|
16
|
+
}) => Promise<void>;
|
|
17
|
+
wakeSessionWorkflow: (input: {
|
|
18
|
+
accountId: string;
|
|
19
|
+
workspaceId: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
workflowId: string;
|
|
22
|
+
wakeRevision: number;
|
|
23
|
+
interruptionRequested?: boolean;
|
|
24
|
+
}) => Promise<void>;
|
|
25
|
+
/** Trigger one bounded drain of already-committed workflow-wake revisions. */
|
|
26
|
+
requestSessionWorkflowWakeDispatch: () => Promise<void>;
|
|
27
|
+
// Dedicated, revision-carrying nudge for a durable Codex capacity waiter.
|
|
28
|
+
// Optional for embedded/back-compat clients: callers may fall back to the
|
|
29
|
+
// generic queueChanged wake because Postgres wakeRevision is authoritative.
|
|
30
|
+
signalCodexCapacity?: (input: {
|
|
31
|
+
accountId: string;
|
|
32
|
+
workspaceId: string;
|
|
33
|
+
sessionId: string;
|
|
34
|
+
workflowId: string;
|
|
35
|
+
wakeRevision: number;
|
|
36
|
+
workflowWakeRevision: number;
|
|
37
|
+
}) => Promise<void>;
|
|
38
|
+
signalApprovalDecision: (input: {
|
|
39
|
+
accountId: string;
|
|
40
|
+
workspaceId: string;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
eventId: string;
|
|
43
|
+
workflowId: string;
|
|
44
|
+
workflowWakeRevision: number;
|
|
45
|
+
}) => Promise<void>;
|
|
24
46
|
syncScheduledTask: (input: { task: ScheduledTask }) => Promise<void>;
|
|
25
47
|
deleteScheduledTaskSchedule: (input: { temporalScheduleId: string }) => Promise<void>;
|
|
26
|
-
triggerScheduledTask: (input: {
|
|
48
|
+
triggerScheduledTask: (input: {
|
|
49
|
+
task: ScheduledTask;
|
|
50
|
+
agentRunUsageIdempotencyKey?: string;
|
|
51
|
+
triggerWorkflowId?: string;
|
|
52
|
+
}) => Promise<void>;
|
|
53
|
+
startRigVerification: (input: {
|
|
54
|
+
workspaceId: string;
|
|
55
|
+
changeId?: string;
|
|
56
|
+
versionId?: string;
|
|
57
|
+
workflowId?: string;
|
|
58
|
+
}) => Promise<void>;
|
|
27
59
|
check?: () => Promise<void>;
|
|
28
60
|
};
|
|
29
61
|
|
|
30
62
|
export type DocumentIndexClient = {
|
|
31
|
-
indexDocument: (input: {
|
|
63
|
+
indexDocument: (input: {
|
|
64
|
+
accountId: string;
|
|
65
|
+
workspaceId: string;
|
|
66
|
+
documentId: string;
|
|
67
|
+
}) => Promise<Document | void>;
|
|
32
68
|
};
|
|
33
69
|
|
|
34
70
|
export type AppDependencies = {
|
|
@@ -36,6 +72,8 @@ export type AppDependencies = {
|
|
|
36
72
|
db: Database;
|
|
37
73
|
bus: EventBus;
|
|
38
74
|
workflowClient: SessionWorkflowClient;
|
|
75
|
+
/** Optional provider override for deterministic API/object-storage tests. */
|
|
76
|
+
objectStorage?: ObjectStorageDependency;
|
|
39
77
|
documentIndexer?: DocumentIndexClient;
|
|
40
78
|
documentServices?: DocumentServices;
|
|
41
79
|
observability?: Observability;
|
|
@@ -70,3 +108,18 @@ export type ApiRouteDeps = AppDependencies & {
|
|
|
70
108
|
// resumeBoxById (it throws SandboxResumeError when sandboxBackend=none).
|
|
71
109
|
resumeBoxById: (input: ResumeBoxByIdInput) => Promise<ResumedSandboxSession>;
|
|
72
110
|
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The exact dependency slice used by `acceptSessionUserMessage`.
|
|
114
|
+
*
|
|
115
|
+
* Keeping this narrower than `ApiRouteDeps` lets control-plane callers reuse
|
|
116
|
+
* the canonical admission path without constructing unrelated HTTP, document,
|
|
117
|
+
* or sandbox services. The public API still passes its `ApiRouteDeps` superset.
|
|
118
|
+
*/
|
|
119
|
+
export type AcceptSessionUserMessageDependencies = Pick<
|
|
120
|
+
AppDependencies,
|
|
121
|
+
"settings" | "db" | "bus"
|
|
122
|
+
> & {
|
|
123
|
+
workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
|
|
124
|
+
objectStorage: ObjectStorageDependency;
|
|
125
|
+
};
|