@opengeni/core 2.6.3-canary.0 → 2.6.4-canary.0

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.
@@ -0,0 +1,89 @@
1
+ import { type ConfiguredModel, type Settings } from "@opengeni/config";
2
+ import { type ModelAvailabilityV1, type ModelCredentialReadinessV1, type WorkspaceModelPolicyContract } from "@opengeni/contracts";
3
+ import { type Database } from "@opengeni/db";
4
+ export type ResolvedCatalogSettings = {
5
+ settings: Settings;
6
+ source: "code" | "database";
7
+ version: number | null;
8
+ modelNotes: Record<string, string>;
9
+ };
10
+ /**
11
+ * Curated workspace Gateway products and workspace-owned custom slugs share
12
+ * one public prefix. Only the latter have a mutable catalog row whose active
13
+ * generation must be rechecked at a fresh acceptance commit boundary.
14
+ */
15
+ export declare function isWorkspaceGatewayCustomModelId(settings: Settings, modelId: string): boolean;
16
+ export declare function isWorkspaceOpenRouterCustomModelId(settings: Settings, modelId: string): boolean;
17
+ export type WorkspaceCustomModelReference = {
18
+ providerKind: "vercel_gateway" | "openrouter";
19
+ upstreamModelId: string;
20
+ };
21
+ export declare function workspaceCustomModelReference(settings: Settings, modelId: string): WorkspaceCustomModelReference | null;
22
+ export declare function isWorkspaceCustomModelId(settings: Settings, modelId: string): boolean;
23
+ /**
24
+ * Resolve the deployment catalog without making synchronous env settings read
25
+ * Postgres. Database mode fails closed when the singleton is absent or invalid;
26
+ * code mode preserves the already-validated env catalog.
27
+ */
28
+ export declare function resolveCatalogSettings(db: Database, envSettings: Settings): Promise<ResolvedCatalogSettings>;
29
+ /**
30
+ * Resolve the deployment catalog and add only the custom Gateway slugs owned by
31
+ * one workspace. Use this at model-bearing workspace boundaries; public config
32
+ * and deployment-operator surfaces must continue to use `resolveCatalogSettings`.
33
+ */
34
+ export declare function resolveWorkspaceCatalogSettings(db: Database, envSettings: Settings, input: {
35
+ accountId: string;
36
+ workspaceId: string;
37
+ retainedProductModelId?: string | null;
38
+ retainedProductModelIds?: readonly (string | null | undefined)[];
39
+ }): Promise<ResolvedCatalogSettings>;
40
+ export type ModelAvailabilityObservation = {
41
+ status: "available" | "degraded" | "unavailable";
42
+ reason: "not_entitled" | "provider_unhealthy" | null;
43
+ checkedAt: string;
44
+ };
45
+ export type ModelCredentialReadinessObservation = {
46
+ status: "ready";
47
+ checkedAt: string;
48
+ } | {
49
+ status: "not_ready";
50
+ reason: "prerequisites_missing" | "needs_reauth";
51
+ checkedAt: string;
52
+ } | {
53
+ status: "error";
54
+ reason: "resolver_error";
55
+ checkedAt: string;
56
+ };
57
+ export declare const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS: number;
58
+ export type WorkspaceModelSelectionInput = {
59
+ settings: Settings;
60
+ policy: WorkspaceModelPolicyContract | null;
61
+ codexSubscriptionActive: boolean;
62
+ xaiSubscriptionActive?: boolean;
63
+ workspaceGatewayConnectionActive?: boolean;
64
+ workspaceOpenRouterConnectionActive?: boolean;
65
+ workspaceGatewayCustomModels?: readonly {
66
+ upstreamModelId: string;
67
+ label?: string | null;
68
+ }[];
69
+ workspaceOpenRouterCustomModels?: readonly {
70
+ upstreamModelId: string;
71
+ label?: string | null;
72
+ }[];
73
+ credentialReadinessObservations?: Readonly<Record<string, ModelCredentialReadinessObservation>> | undefined;
74
+ observations?: Readonly<Record<string, ModelAvailabilityObservation>> | undefined;
75
+ now?: Date | undefined;
76
+ credentialReadinessMaxAgeMs?: number | undefined;
77
+ };
78
+ export type WorkspaceModelSelection = {
79
+ model: ConfiguredModel;
80
+ credentialReadiness: ModelCredentialReadinessV1;
81
+ policyAllowed: boolean;
82
+ availability: ModelAvailabilityV1;
83
+ };
84
+ /**
85
+ * One shared picker/tool decision. Catalog membership, credential readiness,
86
+ * workspace policy, and optional provider-health observations are evaluated in
87
+ * configured catalog order so every consumer exposes the same selectable set.
88
+ */
89
+ export declare function resolveWorkspaceModelSelection(input: WorkspaceModelSelectionInput): WorkspaceModelSelection[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "2.6.3-canary.0",
3
+ "version": "2.6.4-canary.0",
4
4
  "description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -54,15 +54,15 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@modelcontextprotocol/sdk": "^1.29.0",
57
- "@opengeni/codex": "^0.2.20-canary.0",
58
- "@opengeni/config": "^0.22.5-canary.0",
59
- "@opengeni/contracts": "^2.9.2-canary.0",
60
- "@opengeni/db": "^3.7.3-canary.0",
61
- "@opengeni/documents": "^0.8.13-canary.0",
62
- "@opengeni/events": "^0.4.11-canary.0",
63
- "@opengeni/observability": "^0.8.14-canary.0",
64
- "@opengeni/runtime": "^2.0.1-canary.0",
65
- "@opengeni/storage": "^0.2.115-canary.0",
57
+ "@opengeni/codex": "^0.2.20-canary.1",
58
+ "@opengeni/config": "^0.22.5-canary.1",
59
+ "@opengeni/contracts": "^2.9.2-canary.1",
60
+ "@opengeni/db": "^3.7.4-canary.0",
61
+ "@opengeni/documents": "^0.8.14-canary.0",
62
+ "@opengeni/events": "^0.4.12-canary.0",
63
+ "@opengeni/observability": "^0.8.14-canary.1",
64
+ "@opengeni/runtime": "^2.0.1-canary.1",
65
+ "@opengeni/storage": "^0.2.115-canary.1",
66
66
  "hono": "^4.12.18",
67
67
  "zod": "^4.2.1"
68
68
  },
@@ -1,4 +1,8 @@
1
- import { configuredStaticUsageLimits, resolveModelProvider } from "@opengeni/config";
1
+ import {
2
+ configuredStaticUsageLimits,
3
+ resolveModelProviderForTurn,
4
+ type Settings,
5
+ } from "@opengeni/config";
2
6
  import type {
3
7
  LimitAction,
4
8
  LimitDecision,
@@ -26,14 +30,35 @@ export type LimitCheckInput = {
26
30
  workspaceId?: string;
27
31
  action: LimitAction;
28
32
  quantity?: number;
29
- // The turn's model id, when the action represents an agent turn. Externally
30
- // billed models consume ZERO OpenGeni credits, so the credit-balance +
31
- // model-cost + token gates are skipped. Connected subscriptions still require
32
- // live workspace readiness; an explicitly anonymous deployment route is
33
- // statically ready by definition. Non-model infra actions leave this undefined.
33
+ // The turn's model id, when the action represents an agent turn. The model's
34
+ // deployment cost controls credit/cost gates; upstream metering independently
35
+ // controls the token cap. Connected subscriptions still require live workspace
36
+ // readiness. Non-model infra actions leave this undefined.
34
37
  model?: string | null;
35
38
  };
36
39
 
40
+ export function modelFundingForAdmission(
41
+ settings: Settings,
42
+ model: string | null | undefined,
43
+ codexBilled: boolean,
44
+ ): { fundedWithoutCredits: boolean; countsTowardTokenCap: boolean } {
45
+ const resolvedModel = model ? resolveModelProviderForTurn(settings, model)?.model : null;
46
+ const codexSubscriptionModel =
47
+ resolvedModel?.credentialSource.kind === "connected_subscription" &&
48
+ resolvedModel.credentialSource.provider === "codex";
49
+ return {
50
+ // A Codex namespace/definition never bypasses credits by itself: the live
51
+ // workspace credential predicate above remains authoritative. SuperGrok's
52
+ // static overlay is likewise secret-free; its worker/provider admission
53
+ // owns live account selection before any upstream request can occur.
54
+ fundedWithoutCredits:
55
+ codexBilled ||
56
+ (resolvedModel != null && !codexSubscriptionModel && resolvedModel.cost !== "credits"),
57
+ countsTowardTokenCap:
58
+ !codexBilled && resolvedModel != null && resolvedModel.billing.upstreamPayer === "deployment",
59
+ };
60
+ }
61
+
37
62
  export async function requireLimit(deps: LimitDependencies, input: LimitCheckInput): Promise<void> {
38
63
  const decision = await checkLimit(deps, input);
39
64
  if (decision.allowed) {
@@ -59,25 +84,22 @@ export async function checkLimit(
59
84
  model: input.model,
60
85
  })
61
86
  : false;
62
- const credentialFreeExternal = input.model
63
- ? (() => {
64
- const resolved = resolveModelProvider(deps.settings, input.model);
65
- return (
66
- resolved?.model.billing.metering === "external" &&
67
- resolved.model.credentialSource.kind === "deployment" &&
68
- resolved.model.credentialSource.mechanism === "none"
69
- );
70
- })()
71
- : false;
72
- const externallyBilled = codexBilled || credentialFreeExternal;
73
- const creditDecision = await checkCreditBalance(deps, input, externallyBilled);
87
+ const { fundedWithoutCredits, countsTowardTokenCap } = modelFundingForAdmission(
88
+ deps.settings,
89
+ input.model,
90
+ codexBilled,
91
+ );
92
+ const creditDecision = await checkCreditBalance(deps, input, fundedWithoutCredits);
74
93
  if (!creditDecision.allowed) {
75
94
  return creditDecision;
76
95
  }
77
96
  if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
78
97
  return { allowed: true };
79
98
  }
80
- return await checkStaticCaps(deps, input, externallyBilled);
99
+ return await checkStaticCaps(deps, input, {
100
+ fundedWithoutCredits,
101
+ countsTowardTokenCap,
102
+ });
81
103
  }
82
104
 
83
105
  async function checkCreditBalance(
@@ -101,10 +123,14 @@ async function checkCreditBalance(
101
123
  async function checkStaticCaps(
102
124
  deps: LimitDependencies,
103
125
  input: LimitCheckInput,
104
- externallyBilled: boolean,
126
+ funding: { fundedWithoutCredits: boolean; countsTowardTokenCap: boolean },
105
127
  ): Promise<LimitDecision> {
106
128
  const limits = configuredStaticUsageLimits(deps.settings);
107
- if (limits.maxMonthlyCostMicrosPerAccount && isCostlyAction(input.action) && !externallyBilled) {
129
+ if (
130
+ limits.maxMonthlyCostMicrosPerAccount &&
131
+ isCostlyAction(input.action) &&
132
+ !funding.fundedWithoutCredits
133
+ ) {
108
134
  const used = await sumUsageQuantity(deps.db, {
109
135
  accountId: input.accountId,
110
136
  eventType: "model.cost",
@@ -185,7 +211,11 @@ async function checkStaticCaps(
185
211
  );
186
212
  }
187
213
  case "tokens:consume": {
188
- if (externallyBilled || !limits.maxMonthlyTokensPerWorkspace || !input.workspaceId) {
214
+ if (
215
+ !funding.countsTowardTokenCap ||
216
+ !limits.maxMonthlyTokensPerWorkspace ||
217
+ !input.workspaceId
218
+ ) {
189
219
  return { allowed: true };
190
220
  }
191
221
  const used = await sumUsageQuantity(deps.db, {
@@ -19,6 +19,7 @@ import type { ManagedAuthSessionAdapter } from "./managed-auth-session-sets";
19
19
  import type { ApiSandboxClient, ResumeBoxByIdInput, ResumedSandboxSession } from "./sandbox-types";
20
20
  import type { TranscriptionSegmenter, TranscriptionService } from "./transcription";
21
21
  import type { EditableArtifactApplicationPort } from "./editable-artifact-live";
22
+ import type { ResolvedCatalogSettings } from "./model-catalog";
22
23
  import type {
23
24
  EditableArtifactAgentApplication,
24
25
  EditableArtifactDurableExportService,
@@ -129,6 +130,13 @@ export type ManagedEmailTransport = {
129
130
 
130
131
  export type AppDependencies = {
131
132
  settings: Settings;
133
+ /**
134
+ * Original deployment settings when `settings` is already overlaid with a
135
+ * deployment/workspace catalog snapshot. Model-bearing request adapters set
136
+ * this marker so core admission never feeds a synthetic reviewed provider
137
+ * back through deployment validation.
138
+ */
139
+ catalogSourceSettings?: Settings;
132
140
  db: Database;
133
141
  /**
134
142
  * Host-composed editable artifact engine. Standalone startup binds the same
@@ -225,6 +233,7 @@ export type AppDependencies = {
225
233
  export type ObjectStorageDependency = ReturnType<typeof createObjectStorage>;
226
234
 
227
235
  export type ApiRouteDeps = AppDependencies & {
236
+ resolveCatalogSettings: () => Promise<ResolvedCatalogSettings>;
228
237
  managedEmailTransport: ManagedEmailTransport;
229
238
  objectStorage: ObjectStorageDependency;
230
239
  githubStateSecret: string;
@@ -244,7 +253,12 @@ export type ApiRouteDeps = AppDependencies & {
244
253
  */
245
254
  export type AcceptSessionUserMessageDependencies = Pick<
246
255
  AppDependencies,
247
- "settings" | "db" | "bus" | "sessionAuthorization" | "schedulePromptPostCommit"
256
+ | "settings"
257
+ | "catalogSourceSettings"
258
+ | "db"
259
+ | "bus"
260
+ | "sessionAuthorization"
261
+ | "schedulePromptPostCommit"
248
262
  > & {
249
263
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
250
264
  objectStorage: ObjectStorageDependency;
@@ -34,6 +34,8 @@ import {
34
34
  getSandbox,
35
35
  getSessionTurnXaiProviderAccountAuthoritySnapshot,
36
36
  getSession,
37
+ lockActiveWorkspaceGatewayCustomModelForAdmission,
38
+ lockActiveWorkspaceOpenRouterCustomModelForAdmission,
37
39
  nestedPostgresSqlState,
38
40
  requireWorkspace,
39
41
  scopedKnowledgeScopeKey,
@@ -54,6 +56,7 @@ import {
54
56
  } from "../session-authorization";
55
57
  import type { SessionWorkflowClient } from "../dependencies";
56
58
  import type { ObjectStorageDependency } from "../dependencies";
59
+ import { workspaceCustomModelReference } from "../model-catalog";
57
60
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
58
61
  import { validateVariableSetAttachment } from "./environments";
59
62
  import {
@@ -99,6 +102,35 @@ export function scheduledTaskToolsProvided(rawPayload: unknown): boolean {
99
102
  );
100
103
  }
101
104
 
105
+ function workspaceCustomModelCommitGuard(input: {
106
+ settings: Settings;
107
+ accountId: string;
108
+ workspaceId: string;
109
+ modelId: string;
110
+ }): ((tx: Database) => Promise<void>) | undefined {
111
+ const reference = workspaceCustomModelReference(input.settings, input.modelId);
112
+ if (!reference) return undefined;
113
+ return async (tx: Database): Promise<void> => {
114
+ const active =
115
+ reference.providerKind === "openrouter"
116
+ ? await lockActiveWorkspaceOpenRouterCustomModelForAdmission(tx, {
117
+ accountId: input.accountId,
118
+ workspaceId: input.workspaceId,
119
+ upstreamModelId: reference.upstreamModelId,
120
+ })
121
+ : await lockActiveWorkspaceGatewayCustomModelForAdmission(tx, {
122
+ accountId: input.accountId,
123
+ workspaceId: input.workspaceId,
124
+ upstreamModelId: reference.upstreamModelId,
125
+ });
126
+ if (!active) {
127
+ throw new HTTPException(422, {
128
+ message: `model is not available: ${input.modelId}`,
129
+ });
130
+ }
131
+ };
132
+ }
133
+
102
134
  export function scheduledConnectionSurfaceEligibility(
103
135
  settings: Settings,
104
136
  target: Pick<Session, "firstPartyMcpTools" | "firstPartyMcpPermissions"> | null,
@@ -146,7 +178,7 @@ export async function createValidatedScheduledTask(input: {
146
178
  action: knowledgeAction,
147
179
  });
148
180
  }
149
- const agentConfig = knowledgeAction
181
+ const agentConfig: ScheduledTaskAgentConfig = knowledgeAction
150
182
  ? input.payload.agentConfig
151
183
  : await validateScheduledTaskAgentConfig({
152
184
  ...input,
@@ -235,6 +267,15 @@ export async function createValidatedScheduledTask(input: {
235
267
  workspaceId: input.grant.workspaceId,
236
268
  subjectId: input.grant.subjectId,
237
269
  });
270
+ const beforeCreateCommit =
271
+ !knowledgeAction && input.payload.runMode !== "existing_session"
272
+ ? workspaceCustomModelCommitGuard({
273
+ settings: input.settings,
274
+ accountId: input.grant.accountId,
275
+ workspaceId: input.grant.workspaceId,
276
+ modelId: agentConfig.model ?? input.settings.openaiModel,
277
+ })
278
+ : undefined;
238
279
  return await withScheduledTaskAuthorityWriteErrors(() =>
239
280
  createScheduledTask(input.db, {
240
281
  id,
@@ -257,6 +298,7 @@ export async function createValidatedScheduledTask(input: {
257
298
  variableSetId: input.payload.variableSetId ?? null,
258
299
  rigId: input.payload.rigId ?? null,
259
300
  metadata: input.payload.metadata,
301
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
260
302
  }),
261
303
  );
262
304
  }
@@ -734,6 +776,15 @@ export async function validatedScheduledTaskUpdate(input: {
734
776
  (input.payload.metadata !== undefined &&
735
777
  !isDeepStrictEqual(input.payload.metadata, input.existing.metadata)) ||
736
778
  (input.existing.status === "paused" && input.payload.status === "active");
779
+ if (materialExecutionChange && nextRunMode !== "existing_session") {
780
+ const beforeUpdateCommit = workspaceCustomModelCommitGuard({
781
+ settings: input.settings,
782
+ accountId: input.existing.accountId,
783
+ workspaceId: input.existing.workspaceId,
784
+ modelId: nextAgentConfig.model ?? input.settings.openaiModel,
785
+ });
786
+ if (beforeUpdateCommit) update.beforeUpdateCommit = beforeUpdateCommit;
787
+ }
737
788
  const existingXaiAuthority = await getScheduledTaskXaiProviderAccountAuthoritySnapshot(
738
789
  input.db,
739
790
  input.existing.workspaceId,