@opengeni/core 4.0.1 → 4.0.3-canary.36199476632001
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/application/new-session-drafts.d.ts +21 -5
- package/dist/billing/limits.d.ts +4 -0
- package/dist/default-session-model.d.ts +110 -0
- package/dist/domain/capabilities.d.ts +2 -1
- package/dist/domain/knowledge-messages.d.ts +4 -4
- package/dist/domain/knowledge-preparation.d.ts +2 -1
- package/dist/domain/knowledge-search.d.ts +11 -76
- package/dist/domain/pr-review.d.ts +2 -2
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/sessions.d.ts +9 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +929 -384
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/application/new-session-drafts.ts +101 -22
- package/src/application/session-mcp-credential-rotation.ts +57 -15
- package/src/billing/limits.ts +24 -3
- package/src/default-session-model.ts +467 -0
- package/src/domain/capabilities.ts +23 -7
- package/src/domain/knowledge-preparation.test.ts +25 -0
- package/src/domain/knowledge-preparation.ts +35 -14
- package/src/domain/knowledge-search.test.ts +331 -5
- package/src/domain/knowledge-search.ts +172 -23
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/sessions.ts +42 -7
- package/src/index.ts +1 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalizeConfiguredModelId,
|
|
3
|
+
type ConfiguredModel,
|
|
4
|
+
type Settings,
|
|
5
|
+
} from "@opengeni/config";
|
|
6
|
+
import {
|
|
7
|
+
resolveWorkspaceSessionDefaults,
|
|
8
|
+
type DefaultModelSelection,
|
|
9
|
+
type ReasoningEffort,
|
|
10
|
+
type ScheduledTask,
|
|
11
|
+
type WorkspaceSessionDefaults,
|
|
12
|
+
type XaiProviderAccountAuthoritySnapshotV1,
|
|
13
|
+
} from "@opengeni/contracts";
|
|
14
|
+
import {
|
|
15
|
+
getScheduledTaskXaiProviderAccountAuthoritySnapshot,
|
|
16
|
+
getSession,
|
|
17
|
+
getWorkspace,
|
|
18
|
+
getWorkspaceConnectionModelRestrictions,
|
|
19
|
+
getWorkspaceModelPolicy,
|
|
20
|
+
listOrganizationModelProviderCustomModelsForWorkspace,
|
|
21
|
+
listWorkspaceGatewayCustomModels,
|
|
22
|
+
listWorkspaceOpenRouterCustomModels,
|
|
23
|
+
organizationModelProviderConnectionActiveForWorkspace,
|
|
24
|
+
workspaceCodexSubscriptionActive,
|
|
25
|
+
workspaceOpenRouterConnectionActive,
|
|
26
|
+
workspaceVercelAiGatewayConnectionActive,
|
|
27
|
+
organizationHoldsCredits,
|
|
28
|
+
workspaceXaiSubscriptionActive,
|
|
29
|
+
workspaceXaiSubscriptionActiveForAuthority,
|
|
30
|
+
XaiAuthorityPoolInactiveError,
|
|
31
|
+
type ConnectionModelRestrictions,
|
|
32
|
+
type Database,
|
|
33
|
+
} from "@opengeni/db";
|
|
34
|
+
import {
|
|
35
|
+
resolveWorkspaceModelSelection,
|
|
36
|
+
type WorkspaceModelSelection,
|
|
37
|
+
type WorkspaceModelSelectionInput,
|
|
38
|
+
} from "./model-catalog";
|
|
39
|
+
|
|
40
|
+
const REASONING_EFFORT_ORDER: readonly ReasoningEffort[] = [
|
|
41
|
+
"none",
|
|
42
|
+
"minimal",
|
|
43
|
+
"low",
|
|
44
|
+
"medium",
|
|
45
|
+
"high",
|
|
46
|
+
"xhigh",
|
|
47
|
+
"max",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function supportedReasoningEfforts(model: ConfiguredModel): ReasoningEffort[] {
|
|
51
|
+
const efforts = model.capabilities.reasoning.efforts;
|
|
52
|
+
return REASONING_EFFORT_ORDER.filter((effort) => efforts.includes(effort));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The model's own default effort, matching the web picker's choice. */
|
|
56
|
+
export function defaultReasoningEffortForConfiguredModel(
|
|
57
|
+
model: ConfiguredModel,
|
|
58
|
+
fallback: ReasoningEffort,
|
|
59
|
+
): ReasoningEffort {
|
|
60
|
+
const options = supportedReasoningEfforts(model);
|
|
61
|
+
if (options.length === 0) return fallback;
|
|
62
|
+
const configured = model.capabilities.reasoning.defaultEffort;
|
|
63
|
+
if (configured && options.includes(configured)) return configured;
|
|
64
|
+
return options[0]!;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The highest supported effort at or below `preferred`. */
|
|
68
|
+
export function clampReasoningEffortForConfiguredModel(
|
|
69
|
+
model: ConfiguredModel,
|
|
70
|
+
preferred: ReasoningEffort,
|
|
71
|
+
fallback: ReasoningEffort,
|
|
72
|
+
): ReasoningEffort {
|
|
73
|
+
const options = supportedReasoningEfforts(model);
|
|
74
|
+
if (options.length === 0) return fallback;
|
|
75
|
+
if (options.includes(preferred)) return preferred;
|
|
76
|
+
const ceiling = REASONING_EFFORT_ORDER.indexOf(preferred);
|
|
77
|
+
const below = options.filter((effort) => REASONING_EFFORT_ORDER.indexOf(effort) <= ceiling);
|
|
78
|
+
return below.at(-1) ?? defaultReasoningEffortForConfiguredModel(model, fallback);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findSelection(
|
|
82
|
+
selections: readonly WorkspaceModelSelection[],
|
|
83
|
+
modelId: string,
|
|
84
|
+
): WorkspaceModelSelection | undefined {
|
|
85
|
+
return (
|
|
86
|
+
selections.find((selection) => selection.model.id === modelId) ??
|
|
87
|
+
selections.find((selection) => selection.model.aliases.includes(modelId))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type DefaultSessionModelInput = {
|
|
92
|
+
/** Catalog-resolved settings: `openaiModel` is the deployment default. */
|
|
93
|
+
settings: Settings;
|
|
94
|
+
/** This workspace's selection, in operator catalog order. */
|
|
95
|
+
selections: readonly WorkspaceModelSelection[];
|
|
96
|
+
workspaceDefaults: WorkspaceSessionDefaults | null;
|
|
97
|
+
/**
|
|
98
|
+
* True while the organization holds a positive OpenGeni credit balance from
|
|
99
|
+
* any source, the verified-signup trial grant included (see
|
|
100
|
+
* `organizationHoldsCredits`).
|
|
101
|
+
*/
|
|
102
|
+
creditsAvailable: boolean;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function creditsCandidate(
|
|
106
|
+
input: Pick<DefaultSessionModelInput, "settings" | "selections">,
|
|
107
|
+
): DefaultModelSelection | null {
|
|
108
|
+
const fallbackEffort = input.settings.openaiReasoningEffort;
|
|
109
|
+
const deployment = findSelection(input.selections, input.settings.openaiModel);
|
|
110
|
+
if (deployment?.availability.selectable && deployment.model.cost === "credits") return null;
|
|
111
|
+
const configured = findSelection(input.selections, input.settings.creditsDefaultModel);
|
|
112
|
+
if (configured?.availability.selectable && configured.model.cost === "credits") {
|
|
113
|
+
return {
|
|
114
|
+
model: configured.model.id,
|
|
115
|
+
reasoningEffort: clampReasoningEffortForConfiguredModel(
|
|
116
|
+
configured.model,
|
|
117
|
+
input.settings.creditsDefaultReasoningEffort,
|
|
118
|
+
fallbackEffort,
|
|
119
|
+
),
|
|
120
|
+
source: "credits",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const first = input.selections.find(
|
|
124
|
+
(selection) => selection.availability.selectable && selection.model.cost === "credits",
|
|
125
|
+
);
|
|
126
|
+
return first
|
|
127
|
+
? {
|
|
128
|
+
model: first.model.id,
|
|
129
|
+
reasoningEffort: defaultReasoningEffortForConfiguredModel(first.model, fallbackEffort),
|
|
130
|
+
source: "credits",
|
|
131
|
+
}
|
|
132
|
+
: null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Default model policy for a new chat or scheduled task that names no model.
|
|
137
|
+
*
|
|
138
|
+
* Precedence, first match wins:
|
|
139
|
+
*
|
|
140
|
+
* 1. `workspace`: the saved workspace default (`settings.sessionDefaults`)
|
|
141
|
+
* while it is selectable in this workspace, its saved effort clamped to
|
|
142
|
+
* what the model supports today.
|
|
143
|
+
* 2. `subscription`: the first selectable connected-subscription model
|
|
144
|
+
* (ChatGPT/Codex, then SuperGrok) in operator catalog order. The
|
|
145
|
+
* deployment default wins inside this step when it is itself a selectable
|
|
146
|
+
* subscription model.
|
|
147
|
+
* 3. `credits`: while the organization holds a positive OpenGeni credit
|
|
148
|
+
* balance (a purchase, a grant, or the verified-signup trial grant), the
|
|
149
|
+
* configured credits default (`OPENGENI_CREDITS_DEFAULT_MODEL`, effort
|
|
150
|
+
* clamped to what the model supports), or the first selectable
|
|
151
|
+
* credits-billed model when that one is not selectable. Skipped when the
|
|
152
|
+
* deployment default is already a selectable credits-billed model, so an
|
|
153
|
+
* operator's paid default is never replaced.
|
|
154
|
+
* 4. `deployment`: the deployment default with the deployment reasoning effort.
|
|
155
|
+
*
|
|
156
|
+
* An explicit model on the request, the scheduled task, or the person's
|
|
157
|
+
* new-chat draft is never passed through this function.
|
|
158
|
+
*/
|
|
159
|
+
export function selectDefaultSessionModel(input: DefaultSessionModelInput): DefaultModelSelection {
|
|
160
|
+
const fallbackEffort = input.settings.openaiReasoningEffort;
|
|
161
|
+
if (input.workspaceDefaults) {
|
|
162
|
+
const saved = findSelection(input.selections, input.workspaceDefaults.model);
|
|
163
|
+
if (saved?.availability.selectable) {
|
|
164
|
+
return {
|
|
165
|
+
model: saved.model.id,
|
|
166
|
+
reasoningEffort: clampReasoningEffortForConfiguredModel(
|
|
167
|
+
saved.model,
|
|
168
|
+
input.workspaceDefaults.reasoningEffort,
|
|
169
|
+
fallbackEffort,
|
|
170
|
+
),
|
|
171
|
+
source: "workspace",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const deployment = findSelection(input.selections, input.settings.openaiModel);
|
|
176
|
+
const subscription =
|
|
177
|
+
deployment?.availability.selectable && deployment.model.cost === "subscription"
|
|
178
|
+
? deployment
|
|
179
|
+
: input.selections.find(
|
|
180
|
+
(selection) =>
|
|
181
|
+
selection.availability.selectable && selection.model.cost === "subscription",
|
|
182
|
+
);
|
|
183
|
+
if (subscription) {
|
|
184
|
+
return {
|
|
185
|
+
model: subscription.model.id,
|
|
186
|
+
reasoningEffort: defaultReasoningEffortForConfiguredModel(subscription.model, fallbackEffort),
|
|
187
|
+
source: "subscription",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (input.creditsAvailable) {
|
|
191
|
+
const credits = creditsCandidate(input);
|
|
192
|
+
if (credits) return credits;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
model:
|
|
196
|
+
deployment?.model.id ??
|
|
197
|
+
canonicalizeConfiguredModelId(input.settings, input.settings.openaiModel),
|
|
198
|
+
reasoningEffort: fallbackEffort,
|
|
199
|
+
source: "deployment",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The default this workspace would use while its organization holds a
|
|
205
|
+
* positive OpenGeni credit balance. Null when the deployment does not bill
|
|
206
|
+
* credits.
|
|
207
|
+
*/
|
|
208
|
+
export function creditsDefaultSessionModel(input: {
|
|
209
|
+
settings: Settings;
|
|
210
|
+
selections: readonly WorkspaceModelSelection[];
|
|
211
|
+
workspaceSettings: unknown;
|
|
212
|
+
}): DefaultModelSelection | null {
|
|
213
|
+
if (input.settings.billingMode !== "stripe") return null;
|
|
214
|
+
return selectDefaultSessionModel({
|
|
215
|
+
settings: input.settings,
|
|
216
|
+
selections: input.selections,
|
|
217
|
+
workspaceDefaults: resolveWorkspaceSessionDefaults(input.workspaceSettings),
|
|
218
|
+
creditsAvailable: true,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Resolve the default for an already-loaded workspace selection. The credit
|
|
224
|
+
* ledger is read only when it can change the answer.
|
|
225
|
+
*/
|
|
226
|
+
export async function resolveDefaultSessionModelForSelections(
|
|
227
|
+
db: Database,
|
|
228
|
+
input: {
|
|
229
|
+
settings: Settings;
|
|
230
|
+
accountId: string;
|
|
231
|
+
workspaceSettings: unknown;
|
|
232
|
+
selections: readonly WorkspaceModelSelection[];
|
|
233
|
+
},
|
|
234
|
+
): Promise<DefaultModelSelection> {
|
|
235
|
+
const decision = {
|
|
236
|
+
settings: input.settings,
|
|
237
|
+
selections: input.selections,
|
|
238
|
+
workspaceDefaults: resolveWorkspaceSessionDefaults(input.workspaceSettings),
|
|
239
|
+
};
|
|
240
|
+
const withoutCredits = selectDefaultSessionModel({ ...decision, creditsAvailable: false });
|
|
241
|
+
if (
|
|
242
|
+
withoutCredits.source !== "deployment" ||
|
|
243
|
+
input.settings.billingMode !== "stripe" ||
|
|
244
|
+
creditsCandidate(decision) === null
|
|
245
|
+
) {
|
|
246
|
+
return withoutCredits;
|
|
247
|
+
}
|
|
248
|
+
return (await organizationHoldsCredits(db, input.accountId))
|
|
249
|
+
? selectDefaultSessionModel({ ...decision, creditsAvailable: true })
|
|
250
|
+
: withoutCredits;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export type WorkspaceModelSelectionContext = {
|
|
254
|
+
accountId: string;
|
|
255
|
+
workspaceId: string;
|
|
256
|
+
/**
|
|
257
|
+
* The subject whose connected-subscription authority applies: the
|
|
258
|
+
* authenticated caller for direct creates, or a scheduled task's immutable
|
|
259
|
+
* execution owner. Never another member.
|
|
260
|
+
*/
|
|
261
|
+
subjectId: string;
|
|
262
|
+
/**
|
|
263
|
+
* An already-frozen SuperGrok authority (a scheduled task's snapshot).
|
|
264
|
+
* Omitted means the subject's current acceptance authority, exactly as a
|
|
265
|
+
* direct Send resolves it.
|
|
266
|
+
*/
|
|
267
|
+
xaiAuthoritySnapshot?: XaiProviderAccountAuthoritySnapshotV1 | undefined;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Connection restrictions and SuperGrok readiness for the subject. With a
|
|
272
|
+
* frozen authority (a scheduled task's snapshot), a user pool that no longer
|
|
273
|
+
* resolves (disconnected, reconnected under a new authority generation, or its
|
|
274
|
+
* owner left) means SuperGrok is not ready: no SuperGrok model is selectable,
|
|
275
|
+
* and resolution falls through to credits or the deployment default instead of
|
|
276
|
+
* failing the occurrence. The frozen authority is never swapped for the
|
|
277
|
+
* subject's current one; the SuperGrok restriction is closed outright, while
|
|
278
|
+
* the other providers' restrictions do not depend on SuperGrok authority.
|
|
279
|
+
*/
|
|
280
|
+
async function connectionRestrictionsAndXaiReadiness(
|
|
281
|
+
db: Database,
|
|
282
|
+
settings: Settings,
|
|
283
|
+
context: WorkspaceModelSelectionContext,
|
|
284
|
+
): Promise<{ restrictions: ConnectionModelRestrictions; xaiSubscriptionActive: boolean }> {
|
|
285
|
+
const { workspaceId, subjectId, xaiAuthoritySnapshot } = context;
|
|
286
|
+
if (!xaiAuthoritySnapshot) {
|
|
287
|
+
const [restrictions, xaiSubscriptionActive] = await Promise.all([
|
|
288
|
+
getWorkspaceConnectionModelRestrictions(db, workspaceId, subjectId),
|
|
289
|
+
workspaceXaiSubscriptionActive(db, settings, workspaceId, subjectId),
|
|
290
|
+
]);
|
|
291
|
+
return { restrictions, xaiSubscriptionActive };
|
|
292
|
+
}
|
|
293
|
+
const frozen = await Promise.allSettled([
|
|
294
|
+
getWorkspaceConnectionModelRestrictions(db, workspaceId, subjectId, xaiAuthoritySnapshot),
|
|
295
|
+
workspaceXaiSubscriptionActiveForAuthority(db, settings, {
|
|
296
|
+
workspaceId,
|
|
297
|
+
subjectId,
|
|
298
|
+
authoritySnapshot: xaiAuthoritySnapshot,
|
|
299
|
+
}),
|
|
300
|
+
]);
|
|
301
|
+
const [restrictions, readiness] = frozen;
|
|
302
|
+
if (restrictions.status === "fulfilled" && readiness.status === "fulfilled") {
|
|
303
|
+
return { restrictions: restrictions.value, xaiSubscriptionActive: readiness.value };
|
|
304
|
+
}
|
|
305
|
+
for (const result of frozen) {
|
|
306
|
+
if (result.status === "rejected" && !(result.reason instanceof XaiAuthorityPoolInactiveError)) {
|
|
307
|
+
throw result.reason;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const current = await getWorkspaceConnectionModelRestrictions(db, workspaceId, subjectId);
|
|
311
|
+
return {
|
|
312
|
+
restrictions: { ...current, "supergrok/": [] },
|
|
313
|
+
xaiSubscriptionActive: false,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Load the same inputs the workspace model catalog route evaluates. */
|
|
318
|
+
export async function loadWorkspaceModelSelectionInput(
|
|
319
|
+
db: Database,
|
|
320
|
+
settings: Settings,
|
|
321
|
+
context: WorkspaceModelSelectionContext,
|
|
322
|
+
): Promise<WorkspaceModelSelectionInput> {
|
|
323
|
+
const { accountId, workspaceId } = context;
|
|
324
|
+
const [
|
|
325
|
+
{ restrictions: connectionModelRestrictions, xaiSubscriptionActive },
|
|
326
|
+
policy,
|
|
327
|
+
codexSubscriptionActive,
|
|
328
|
+
workspaceGatewayConnectionActive,
|
|
329
|
+
workspaceGatewayCustomModels,
|
|
330
|
+
openRouterConnectionActive,
|
|
331
|
+
workspaceOpenRouterCustomModels,
|
|
332
|
+
organizationGatewayConnectionActive,
|
|
333
|
+
organizationOpenRouterConnectionActive,
|
|
334
|
+
organizationGatewayCustomModels,
|
|
335
|
+
organizationOpenRouterCustomModels,
|
|
336
|
+
] = await Promise.all([
|
|
337
|
+
connectionRestrictionsAndXaiReadiness(db, settings, context),
|
|
338
|
+
getWorkspaceModelPolicy(db, workspaceId),
|
|
339
|
+
workspaceCodexSubscriptionActive(db, settings, workspaceId),
|
|
340
|
+
workspaceVercelAiGatewayConnectionActive(db, workspaceId),
|
|
341
|
+
listWorkspaceGatewayCustomModels(db, { accountId, workspaceId }),
|
|
342
|
+
workspaceOpenRouterConnectionActive(db, workspaceId),
|
|
343
|
+
listWorkspaceOpenRouterCustomModels(db, { accountId, workspaceId }),
|
|
344
|
+
organizationModelProviderConnectionActiveForWorkspace(db, {
|
|
345
|
+
accountId,
|
|
346
|
+
workspaceId,
|
|
347
|
+
providerKind: "vercel_gateway",
|
|
348
|
+
}),
|
|
349
|
+
organizationModelProviderConnectionActiveForWorkspace(db, {
|
|
350
|
+
accountId,
|
|
351
|
+
workspaceId,
|
|
352
|
+
providerKind: "openrouter",
|
|
353
|
+
}),
|
|
354
|
+
listOrganizationModelProviderCustomModelsForWorkspace(db, {
|
|
355
|
+
accountId,
|
|
356
|
+
workspaceId,
|
|
357
|
+
providerKind: "vercel_gateway",
|
|
358
|
+
}),
|
|
359
|
+
listOrganizationModelProviderCustomModelsForWorkspace(db, {
|
|
360
|
+
accountId,
|
|
361
|
+
workspaceId,
|
|
362
|
+
providerKind: "openrouter",
|
|
363
|
+
}),
|
|
364
|
+
]);
|
|
365
|
+
return {
|
|
366
|
+
connectionModelRestrictions,
|
|
367
|
+
settings,
|
|
368
|
+
policy,
|
|
369
|
+
codexSubscriptionActive,
|
|
370
|
+
xaiSubscriptionActive,
|
|
371
|
+
workspaceGatewayConnectionActive,
|
|
372
|
+
workspaceGatewayCustomModels,
|
|
373
|
+
workspaceOpenRouterConnectionActive: openRouterConnectionActive,
|
|
374
|
+
workspaceOpenRouterCustomModels,
|
|
375
|
+
organizationGatewayConnectionActive,
|
|
376
|
+
organizationOpenRouterConnectionActive,
|
|
377
|
+
organizationGatewayCustomModels,
|
|
378
|
+
organizationOpenRouterCustomModels,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Server-side default for new work that names no model: API and Slack session
|
|
384
|
+
* creates, new-chat drafts that follow the default, and scheduled-task
|
|
385
|
+
* occurrences. The result is resolved once and then frozen by the accepted
|
|
386
|
+
* session or occurrence like any other model choice.
|
|
387
|
+
*/
|
|
388
|
+
export async function resolveDefaultSessionModel(
|
|
389
|
+
db: Database,
|
|
390
|
+
settings: Settings,
|
|
391
|
+
context: WorkspaceModelSelectionContext & { workspaceSettings?: unknown },
|
|
392
|
+
): Promise<DefaultModelSelection> {
|
|
393
|
+
const [selectionInput, workspaceSettings] = await Promise.all([
|
|
394
|
+
loadWorkspaceModelSelectionInput(db, settings, context),
|
|
395
|
+
context.workspaceSettings !== undefined
|
|
396
|
+
? Promise.resolve(context.workspaceSettings)
|
|
397
|
+
: getWorkspace(db, context.workspaceId).then((workspace) => workspace?.settings ?? {}),
|
|
398
|
+
]);
|
|
399
|
+
return await resolveDefaultSessionModelForSelections(db, {
|
|
400
|
+
settings,
|
|
401
|
+
accountId: context.accountId,
|
|
402
|
+
workspaceSettings,
|
|
403
|
+
selections: resolveWorkspaceModelSelection(selectionInput),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The default a scheduled occurrence uses when its task names no model and the
|
|
409
|
+
* occurrence creates its own session: resolved under the task's immutable
|
|
410
|
+
* execution owner (or its creator for a workspace/service task) and the task's
|
|
411
|
+
* frozen SuperGrok authority, never another member's. The worker stamps the
|
|
412
|
+
* result onto the accepted execution, so retries and recovery replay it; the
|
|
413
|
+
* API's manual-trigger limit check uses the same resolution so it gates the
|
|
414
|
+
* model that will run.
|
|
415
|
+
*/
|
|
416
|
+
export async function resolveScheduledTaskDefaultModel(
|
|
417
|
+
db: Database,
|
|
418
|
+
settings: Settings,
|
|
419
|
+
task: Pick<ScheduledTask, "id" | "accountId" | "workspaceId" | "ownerSubjectId" | "createdBy">,
|
|
420
|
+
): Promise<DefaultModelSelection> {
|
|
421
|
+
return await resolveDefaultSessionModel(db, settings, {
|
|
422
|
+
accountId: task.accountId,
|
|
423
|
+
workspaceId: task.workspaceId,
|
|
424
|
+
subjectId: task.ownerSubjectId ?? task.createdBy.subjectId,
|
|
425
|
+
xaiAuthoritySnapshot: await getScheduledTaskXaiProviderAccountAuthoritySnapshot(
|
|
426
|
+
db,
|
|
427
|
+
task.workspaceId,
|
|
428
|
+
task.id,
|
|
429
|
+
),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The model a manual trigger's limit pre-check evaluates, matching what the
|
|
435
|
+
* occurrence will run: the task's explicit model, else the target or reusable
|
|
436
|
+
* session's model when that session exists, else the resolved scheduled
|
|
437
|
+
* default. The worker still resolves authoritatively at dispatch.
|
|
438
|
+
*/
|
|
439
|
+
export async function resolveScheduledTaskPreflightModel(
|
|
440
|
+
db: Database,
|
|
441
|
+
settings: Settings,
|
|
442
|
+
task: Pick<
|
|
443
|
+
ScheduledTask,
|
|
444
|
+
| "id"
|
|
445
|
+
| "accountId"
|
|
446
|
+
| "workspaceId"
|
|
447
|
+
| "ownerSubjectId"
|
|
448
|
+
| "createdBy"
|
|
449
|
+
| "agentConfig"
|
|
450
|
+
| "runMode"
|
|
451
|
+
| "targetSessionId"
|
|
452
|
+
| "reusableSessionId"
|
|
453
|
+
>,
|
|
454
|
+
): Promise<string> {
|
|
455
|
+
if (task.agentConfig.model) return task.agentConfig.model;
|
|
456
|
+
const targetSessionId =
|
|
457
|
+
task.runMode === "existing_session"
|
|
458
|
+
? task.targetSessionId
|
|
459
|
+
: task.runMode === "reusable_session"
|
|
460
|
+
? task.reusableSessionId
|
|
461
|
+
: null;
|
|
462
|
+
if (targetSessionId) {
|
|
463
|
+
const session = await getSession(db, task.workspaceId, targetSessionId);
|
|
464
|
+
if (session) return session.model;
|
|
465
|
+
}
|
|
466
|
+
return (await resolveScheduledTaskDefaultModel(db, settings, task)).model;
|
|
467
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import { withLockedCapabilityInstallation } from "@opengeni/db/capability-reconciliation";
|
|
6
6
|
import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
|
|
7
|
+
import { pinnedFetch } from "@opengeni/network";
|
|
7
8
|
import {
|
|
8
9
|
CapabilityCatalogItem,
|
|
9
10
|
capabilityCatalogItemIsTrustedForExposure,
|
|
@@ -344,7 +345,12 @@ export async function prepareCapabilityEnable(input: EnableCapabilityInput) {
|
|
|
344
345
|
...installationMetadata,
|
|
345
346
|
...(connectionRef && !headers
|
|
346
347
|
? authDeferredMcpConnectivity()
|
|
347
|
-
: await validateMcpCapabilityConnection(
|
|
348
|
+
: await validateMcpCapabilityConnection(
|
|
349
|
+
item,
|
|
350
|
+
input.probeMcpServer,
|
|
351
|
+
headers ?? undefined,
|
|
352
|
+
input.settings,
|
|
353
|
+
)),
|
|
348
354
|
};
|
|
349
355
|
if (connectionRef) {
|
|
350
356
|
installationConfig.connectionRef = connectionRef;
|
|
@@ -669,6 +675,7 @@ export type McpCapabilityProbeInput = {
|
|
|
669
675
|
url: string;
|
|
670
676
|
timeoutMs: number;
|
|
671
677
|
headers?: Record<string, string>;
|
|
678
|
+
settings?: Settings;
|
|
672
679
|
};
|
|
673
680
|
|
|
674
681
|
export type McpCapabilityProbeResult = {
|
|
@@ -683,6 +690,7 @@ export async function validateMcpCapabilityConnection(
|
|
|
683
690
|
item: CapabilityCatalogItem,
|
|
684
691
|
probe: McpCapabilityProbe = probeStreamableHttpMcpServer,
|
|
685
692
|
headers?: Record<string, string>,
|
|
693
|
+
settings?: Settings,
|
|
686
694
|
): Promise<Record<string, unknown>> {
|
|
687
695
|
if (item.kind !== "mcp") {
|
|
688
696
|
return {};
|
|
@@ -699,6 +707,7 @@ export async function validateMcpCapabilityConnection(
|
|
|
699
707
|
url: item.endpointUrl,
|
|
700
708
|
timeoutMs: mcpCapabilityProbeTimeoutMs,
|
|
701
709
|
...(headers ? { headers } : {}),
|
|
710
|
+
...(settings ? { settings } : {}),
|
|
702
711
|
});
|
|
703
712
|
return {
|
|
704
713
|
mcpConnectivity: {
|
|
@@ -717,6 +726,8 @@ export async function validateMcpCapabilityConnection(
|
|
|
717
726
|
async function probeStreamableHttpMcpServer(
|
|
718
727
|
input: McpCapabilityProbeInput,
|
|
719
728
|
): Promise<McpCapabilityProbeResult> {
|
|
729
|
+
const settings = input.settings;
|
|
730
|
+
if (!settings) throw new Error("Network policy is unavailable for MCP validation");
|
|
720
731
|
const controller = new AbortController();
|
|
721
732
|
const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
722
733
|
const client = new Client(
|
|
@@ -729,6 +740,11 @@ async function probeStreamableHttpMcpServer(
|
|
|
729
740
|
signal: controller.signal,
|
|
730
741
|
...(input.headers ? { headers: input.headers } : {}),
|
|
731
742
|
},
|
|
743
|
+
fetch: (url, init) =>
|
|
744
|
+
pinnedFetch(url, init, settings, {
|
|
745
|
+
label: "MCP capability probe",
|
|
746
|
+
requireHttpsOutsideLocalTest: true,
|
|
747
|
+
}),
|
|
732
748
|
});
|
|
733
749
|
await client.connect(transport as unknown as Transport, {
|
|
734
750
|
timeout: input.timeoutMs,
|
|
@@ -1314,6 +1330,7 @@ function fikenCatalogItem(fikenConnections: ConnectionMetadata[]): CapabilityCat
|
|
|
1314
1330
|
metadata: {
|
|
1315
1331
|
connectorMode: "first_party_fiken",
|
|
1316
1332
|
ownership: "workspace",
|
|
1333
|
+
connectionStatus: fikenConnection?.status ?? null,
|
|
1317
1334
|
// Derived from the contracts catalog so a new fiken_* tool cannot be
|
|
1318
1335
|
// registered without also appearing on the capability tile.
|
|
1319
1336
|
firstPartyMcpTools: FIRST_PARTY_MCP_TOOL_NAMES.filter((name) => name.startsWith("fiken_")),
|
|
@@ -1563,6 +1580,11 @@ export function applyCapabilityEnablement(
|
|
|
1563
1580
|
item: CapabilityCatalogItem,
|
|
1564
1581
|
installation: CapabilityInstallation | undefined,
|
|
1565
1582
|
): CapabilityCatalogItem {
|
|
1583
|
+
if (item.surfaceType === "first_party_fiken") {
|
|
1584
|
+
// Fiken is an API item with connection-derived state, not a generic API
|
|
1585
|
+
// installation. Preserve that state before the non-MCP fallback below.
|
|
1586
|
+
return { ...item, connectionRef: null };
|
|
1587
|
+
}
|
|
1566
1588
|
if (item.kind === "skill" || item.kind === "api" || item.kind === "plugin") {
|
|
1567
1589
|
return { ...item, enabled: false, enabledReason: null, connectionRef: null };
|
|
1568
1590
|
}
|
|
@@ -1572,12 +1594,6 @@ export function applyCapabilityEnablement(
|
|
|
1572
1594
|
// publishes a personal connection UUID or collapses many accounts to one.
|
|
1573
1595
|
return { ...item, connectionRef: null };
|
|
1574
1596
|
}
|
|
1575
|
-
if (item.surfaceType === "first_party_fiken") {
|
|
1576
|
-
// Fiken connector state is derived from the authoritative workspace
|
|
1577
|
-
// connection row while the catalog is built; browseable never means an
|
|
1578
|
-
// account is already connected.
|
|
1579
|
-
return { ...item, connectionRef: null };
|
|
1580
|
-
}
|
|
1581
1597
|
if (item.surfaceType === "codex_apps") {
|
|
1582
1598
|
// Unlike ordinary built-ins, Apps availability is derived from the exact
|
|
1583
1599
|
// active workspace designation above. Do not overwrite it with the
|
|
@@ -221,6 +221,31 @@ test("published and pending searches reuse one query embedding", async () => {
|
|
|
221
221
|
expect(calls).toBe(1);
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
test("a keyword fallback in the first view never retries an unfunded provider in the review view", async () => {
|
|
225
|
+
const searches: Array<{ mode: string | undefined; settingsProvided: boolean }> = [];
|
|
226
|
+
await prepareKnowledgeSave(
|
|
227
|
+
db,
|
|
228
|
+
context,
|
|
229
|
+
{ query: "Acme renewal" },
|
|
230
|
+
() => {
|
|
231
|
+
throw new Error("An unfunded provider was called");
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
list: async () => ({ entries: [], nextCursor: null }),
|
|
235
|
+
get: async () => null,
|
|
236
|
+
search: async (_db, _context, request, _embedder, settings) => {
|
|
237
|
+
searches.push({ mode: request.mode, settingsProvided: !!settings });
|
|
238
|
+
return { entries: [], nextCursor: null, searchMode: "keyword" };
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{ documentEmbeddingProvider: "openai", documentEmbeddingBillingMode: "credits" } as never,
|
|
242
|
+
);
|
|
243
|
+
expect(searches).toEqual([
|
|
244
|
+
{ mode: "keyword", settingsProvided: false },
|
|
245
|
+
{ mode: "keyword", settingsProvided: false },
|
|
246
|
+
]);
|
|
247
|
+
});
|
|
248
|
+
|
|
224
249
|
test("catalog rechecks current metadata after a collection changes", async () => {
|
|
225
250
|
const old = group("Old name");
|
|
226
251
|
const current = {
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
type KnowledgeContext,
|
|
10
10
|
} from "@opengeni/db";
|
|
11
11
|
import type { DocumentEmbedder } from "@opengeni/documents";
|
|
12
|
+
import type { Settings } from "@opengeni/config";
|
|
13
|
+
import { paidDocumentEmbedding } from "../billing/limits";
|
|
12
14
|
import { searchKnowledgeEntries } from "./knowledge-search";
|
|
13
15
|
|
|
14
16
|
const defaults = {
|
|
@@ -29,6 +31,7 @@ export async function prepareKnowledgeSave(
|
|
|
29
31
|
input: KnowledgeSavePreparationRequest,
|
|
30
32
|
embedder: () => DocumentEmbedder,
|
|
31
33
|
services = defaults,
|
|
34
|
+
settings?: Settings,
|
|
32
35
|
): Promise<KnowledgeSavePreparationResponse> {
|
|
33
36
|
const request = KnowledgeSavePreparationRequest.parse(input);
|
|
34
37
|
if (context.actor.kind !== "agent" && !(context.actor.kind === "human" && context.actor.review)) {
|
|
@@ -56,20 +59,38 @@ export async function prepareKnowledgeSave(
|
|
|
56
59
|
};
|
|
57
60
|
// Search is independent of collection placement and always includes the
|
|
58
61
|
// separate unapproved view. No authoring policy or write is invoked here.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
// Preparation returns BOTH views plus a catalog. A charge on the first
|
|
63
|
+
// semantic search would precede the second view and catalog; any later
|
|
64
|
+
// failure would leave no deliverable preparation result. Paid mode uses
|
|
65
|
+
// keyword discovery until a single-operation settlement seam exists.
|
|
66
|
+
const paid = settings != null && paidDocumentEmbedding(settings);
|
|
67
|
+
const published = await services.search(
|
|
68
|
+
db,
|
|
69
|
+
context,
|
|
70
|
+
{
|
|
71
|
+
query: request.query,
|
|
72
|
+
limit: request.limit,
|
|
73
|
+
view: "published",
|
|
74
|
+
...(paid ? { mode: "keyword" as const } : {}),
|
|
75
|
+
},
|
|
76
|
+
sharedEmbedder,
|
|
77
|
+
paid ? undefined : settings,
|
|
78
|
+
);
|
|
79
|
+
// Reuse the first request's provider result (and charge). If it fell back
|
|
80
|
+
// to keyword because funding or the provider was unavailable, do not retry
|
|
81
|
+
// an unfunded embedding request in the second review view.
|
|
82
|
+
const needsReview = await services.search(
|
|
83
|
+
db,
|
|
84
|
+
context,
|
|
85
|
+
{
|
|
86
|
+
query: request.query,
|
|
87
|
+
limit: request.limit,
|
|
88
|
+
view: "needs_review",
|
|
89
|
+
...(published.searchMode === "keyword" ? { mode: "keyword" as const } : {}),
|
|
90
|
+
},
|
|
91
|
+
sharedEmbedder,
|
|
92
|
+
);
|
|
93
|
+
const matches = { published, needs_review: needsReview };
|
|
73
94
|
for (const view of ["published", "needs_review"] as const) {
|
|
74
95
|
if (request.collectionCursors && request.collectionCursors[view] === null) continue;
|
|
75
96
|
let cursor = request.collectionCursors?.[view] ?? undefined;
|