@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.
@@ -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: ApiRouteDeps, input: LimitCheckInput): Promise<void> {
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, { message: decision.message });
36
+ throw new HTTPException(decision.code === "insufficient_credits" ? 402 : 429, {
37
+ message: decision.message,
38
+ });
35
39
  }
36
40
 
37
- export async function checkLimit(deps: ApiRouteDeps, input: LimitCheckInput): Promise<LimitDecision> {
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({ db: deps.db, settings: deps.settings, workspaceId: input.workspaceId, model: input.model })
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(deps: ApiRouteDeps, input: LimitCheckInput, codexBilled: boolean): Promise<LimitDecision> {
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(deps: ApiRouteDeps, input: LimitCheckInput, codexBilled: boolean): Promise<LimitDecision> {
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("max_monthly_cost_micros_per_account", `monthly model cost limit reached (${limits.maxMonthlyCostMicrosPerAccount} micros)`);
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("max_workspaces_per_account", `workspace limit reached (${limits.maxWorkspacesPerAccount})`);
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("max_api_keys_per_workspace", `API key limit reached (${limits.maxApiKeysPerWorkspace})`);
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("max_schedules_per_workspace", `scheduled task limit reached (${limits.maxSchedulesPerWorkspace})`);
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("max_file_upload_bytes", `file upload exceeds static limit of ${limits.maxFileUploadBytes} bytes`);
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("max_monthly_agent_runs_per_workspace", `monthly agent run limit reached (${limits.maxMonthlyAgentRunsPerWorkspace})`);
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("max_monthly_tokens_per_workspace", `monthly token limit reached (${limits.maxMonthlyTokensPerWorkspace})`);
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("max_document_indexed_chunks_per_workspace", `monthly document indexing limit reached (${limits.maxDocumentIndexedChunksPerWorkspace} chunks)`);
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(deps: ApiRouteDeps, input: {
162
- accountId: string;
163
- workspaceId: string;
164
- subjectId?: string | null;
165
- eventType:
166
- | "agent_run.created"
167
- | "file.uploaded"
168
- | "document.indexed"
169
- | "scheduled_task.fired";
170
- quantity: number;
171
- unit: string;
172
- sourceResourceType: string;
173
- sourceResourceId: string;
174
- idempotencyKey: string;
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: ApiRouteDeps): boolean {
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 action === "agent_run:create"
195
- || action === "tokens:consume"
196
- || action === "file:upload"
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 {
@@ -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: { sessionId: string; eventId: string; workflowId: string }) => Promise<void>;
13
- wakeSessionWorkflow: (input: { accountId: string; workspaceId: string; sessionId: string; workflowId: string }) => Promise<void>;
14
- signalApprovalDecision: (input: { sessionId: string; eventId: string; workflowId: string }) => Promise<void>;
15
- // Interrupt must reach the workflow whether or not an execution is currently
16
- // running: a long-lived session that has gone idle (its workflow returned
17
- // after markSessionIdle) has NO running execution, so a plain
18
- // getHandle().signal() throws WorkflowNotFoundError -> a 500 that leaves an
19
- // operator unable to stop a session. signalWithStart start-or-signals, so it
20
- // needs the session-workflow args (accountId/workspaceId) to start a fresh
21
- // run when none is live; the buffered `interrupt` signal is then honored by
22
- // the workflow's idle-interrupt path (pause goal + mark idle).
23
- signalInterrupt: (input: { accountId: string; workspaceId: string; sessionId: string; eventId: string; workflowId: string }) => Promise<void>;
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: { task: ScheduledTask; agentRunUsageIdempotencyKey?: string; triggerWorkflowId?: string }) => Promise<void>;
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: { accountId: string; workspaceId: string; documentId: string }) => Promise<Document | void>;
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
+ };