@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3-canary.36199476632001",
|
|
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": {
|
|
@@ -50,17 +50,17 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
53
|
-
"@opengeni/capabilities": "^0.3.4",
|
|
54
|
-
"@opengeni/codex": "^0.2.
|
|
55
|
-
"@opengeni/config": "^2.1.
|
|
56
|
-
"@opengeni/contracts": "^5.1.
|
|
57
|
-
"@opengeni/db": "^6.0.
|
|
58
|
-
"@opengeni/documents": "^0.8.
|
|
59
|
-
"@opengeni/events": "^0.4.
|
|
60
|
-
"@opengeni/network": "^0.3.1",
|
|
61
|
-
"@opengeni/observability": "^0.8.
|
|
62
|
-
"@opengeni/runtime": "^4.0.
|
|
63
|
-
"@opengeni/storage": "^0.2.
|
|
53
|
+
"@opengeni/capabilities": "^0.3.4-canary.36199476632001",
|
|
54
|
+
"@opengeni/codex": "^0.2.26-canary.36199476632001",
|
|
55
|
+
"@opengeni/config": "^2.1.1-canary.36199476632001",
|
|
56
|
+
"@opengeni/contracts": "^5.1.1-canary.36199476632001",
|
|
57
|
+
"@opengeni/db": "^6.0.3-canary.36199476632001",
|
|
58
|
+
"@opengeni/documents": "^0.8.34-canary.36199476632001",
|
|
59
|
+
"@opengeni/events": "^0.4.32-canary.36199476632001",
|
|
60
|
+
"@opengeni/network": "^0.3.1-canary.36199476632001",
|
|
61
|
+
"@opengeni/observability": "^0.8.32-canary.36199476632001",
|
|
62
|
+
"@opengeni/runtime": "^4.0.3-canary.36199476632001",
|
|
63
|
+
"@opengeni/storage": "^0.2.133-canary.36199476632001",
|
|
64
64
|
"hono": "^4.12.18",
|
|
65
65
|
"zod": "^4.2.1"
|
|
66
66
|
},
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getSandbox,
|
|
14
14
|
getVariableSet,
|
|
15
15
|
NewSessionDraftAccessError,
|
|
16
|
+
newSessionDraftModelProvided,
|
|
16
17
|
newSessionDraftSelectedProjectChannelId,
|
|
17
18
|
newSessionDraftToolsProvided,
|
|
18
19
|
newSessionSelectionHistory,
|
|
@@ -38,6 +39,8 @@ import {
|
|
|
38
39
|
type AccessGrantAuthorization,
|
|
39
40
|
} from "../access";
|
|
40
41
|
import { assertConfiguredModel, assertWorkspaceModelPolicyAllows } from "../domain/sessions";
|
|
42
|
+
import { resolveDefaultSessionModel } from "../default-session-model";
|
|
43
|
+
import { canonicalizeConfiguredModelId, type Settings } from "@opengeni/config";
|
|
41
44
|
|
|
42
45
|
type NewSessionDraftDependencies = Pick<AppDependencies, "settings" | "db" | "objectStorage">;
|
|
43
46
|
|
|
@@ -59,6 +62,9 @@ function mapNewSessionDraft(
|
|
|
59
62
|
model: row.model,
|
|
60
63
|
reasoningEffort: row.reasoningEffort,
|
|
61
64
|
latencyMode: row.latencyMode,
|
|
65
|
+
...(newSessionDraftModelProvided(row) !== undefined
|
|
66
|
+
? { modelProvided: newSessionDraftModelProvided(row) }
|
|
67
|
+
: {}),
|
|
62
68
|
...(selectedProjectChannelId !== undefined ? { selectedProjectChannelId } : {}),
|
|
63
69
|
options: publicNewSessionDraftOptions(row),
|
|
64
70
|
selectionHistory: newSessionSelectionHistory(row),
|
|
@@ -66,15 +72,71 @@ function mapNewSessionDraft(
|
|
|
66
72
|
});
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Whether a stored draft's model policy was the person's choice. A row written
|
|
77
|
+
* before the marker existed (or by an older client) counts as a choice unless
|
|
78
|
+
* it is exactly the deployment default policy (model, reasoning, standard
|
|
79
|
+
* speed), which is what an untouched composer used to save.
|
|
80
|
+
*/
|
|
81
|
+
export function draftModelProvided(
|
|
82
|
+
settings: Settings,
|
|
83
|
+
draft: Pick<NewSessionDraftValue, "model" | "reasoningEffort" | "latencyMode" | "modelProvided">,
|
|
84
|
+
): boolean {
|
|
85
|
+
if (draft.modelProvided !== undefined) return draft.modelProvided;
|
|
86
|
+
return !(
|
|
87
|
+
canonicalizeConfiguredModelId(settings, draft.model) ===
|
|
88
|
+
canonicalizeConfiguredModelId(settings, settings.openaiModel) &&
|
|
89
|
+
draft.reasoningEffort === settings.openaiReasoningEffort &&
|
|
90
|
+
draft.latencyMode === "standard"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The resolved new-chat default for this actor and workspace. */
|
|
95
|
+
async function actorDefaultModel(
|
|
96
|
+
deps: Pick<NewSessionDraftDependencies, "db" | "settings">,
|
|
97
|
+
grant: AccessGrant,
|
|
98
|
+
workspaceId: string,
|
|
99
|
+
) {
|
|
100
|
+
return await resolveDefaultSessionModel(deps.db, deps.settings, {
|
|
101
|
+
accountId: grant.accountId,
|
|
102
|
+
workspaceId,
|
|
103
|
+
subjectId: grant.subjectId,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type NewSessionDraftReadOptions = {
|
|
108
|
+
/**
|
|
109
|
+
* Project a draft that follows the default onto today's resolved default.
|
|
110
|
+
* Callers that only reuse a chosen model (Slack defaults) skip the
|
|
111
|
+
* resolution; session creation resolves the default itself.
|
|
112
|
+
*/
|
|
113
|
+
projectDefaultModel?: boolean;
|
|
114
|
+
};
|
|
115
|
+
|
|
69
116
|
async function hydrateNewSessionDraft(
|
|
70
117
|
deps: Pick<NewSessionDraftDependencies, "db" | "settings">,
|
|
71
118
|
grant: AccessGrant,
|
|
72
119
|
workspaceId: string,
|
|
73
120
|
row: Awaited<ReturnType<typeof getNewSessionDraftInTransaction>>,
|
|
121
|
+
readOptions: NewSessionDraftReadOptions = {},
|
|
74
122
|
): Promise<NewSessionDraftValue | null> {
|
|
75
123
|
if (!row) return null;
|
|
76
|
-
const
|
|
77
|
-
if (!
|
|
124
|
+
const stored = mapNewSessionDraft(row);
|
|
125
|
+
if (!stored) return null;
|
|
126
|
+
// A draft that follows the default is projected onto today's default, so a
|
|
127
|
+
// later subscription connect or credit purchase replaces an untouched free
|
|
128
|
+
// model. The stored row is unchanged until the person saves again.
|
|
129
|
+
const modelProvided = draftModelProvided(deps.settings, stored);
|
|
130
|
+
let mapped: NewSessionDraftValue = { ...stored, modelProvided };
|
|
131
|
+
if (!modelProvided && readOptions.projectDefaultModel !== false) {
|
|
132
|
+
const resolved = await actorDefaultModel(deps, grant, workspaceId);
|
|
133
|
+
mapped = {
|
|
134
|
+
...mapped,
|
|
135
|
+
model: resolved.model,
|
|
136
|
+
reasoningEffort: resolved.reasoningEffort,
|
|
137
|
+
latencyMode: resolved.model === stored.model ? stored.latencyMode : "standard",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
78
140
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
79
141
|
deps.db,
|
|
80
142
|
workspaceId,
|
|
@@ -191,6 +253,7 @@ async function getActorNewSessionDraftInFileScope(
|
|
|
191
253
|
deps: Pick<NewSessionDraftDependencies, "settings" | "db">,
|
|
192
254
|
grant: AccessGrant,
|
|
193
255
|
workspaceId: string,
|
|
256
|
+
readOptions: NewSessionDraftReadOptions = {},
|
|
194
257
|
): Promise<NewSessionDraftValue> {
|
|
195
258
|
const row = await withWorkspaceSubjectRls(deps.db, workspaceId, grant.subjectId, (scoped) =>
|
|
196
259
|
getNewSessionDraftInTransaction(scoped, {
|
|
@@ -198,21 +261,26 @@ async function getActorNewSessionDraftInFileScope(
|
|
|
198
261
|
subjectId: grant.subjectId,
|
|
199
262
|
}),
|
|
200
263
|
);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
264
|
+
const hydrated = await hydrateNewSessionDraft(deps, grant, workspaceId, row, readOptions);
|
|
265
|
+
if (hydrated) return hydrated;
|
|
266
|
+
const resolved =
|
|
267
|
+
readOptions.projectDefaultModel === false
|
|
268
|
+
? { model: deps.settings.openaiModel, reasoningEffort: deps.settings.openaiReasoningEffort }
|
|
269
|
+
: await actorDefaultModel(deps, grant, workspaceId);
|
|
270
|
+
return {
|
|
271
|
+
revision: 0,
|
|
272
|
+
text: "",
|
|
273
|
+
resources: [],
|
|
274
|
+
tools: [],
|
|
275
|
+
toolsProvided: false,
|
|
276
|
+
model: resolved.model,
|
|
277
|
+
reasoningEffort: resolved.reasoningEffort,
|
|
278
|
+
latencyMode: "standard",
|
|
279
|
+
modelProvided: false,
|
|
280
|
+
options: {},
|
|
281
|
+
selectionHistory: { projects: [] },
|
|
282
|
+
updatedAt: null,
|
|
283
|
+
};
|
|
216
284
|
}
|
|
217
285
|
|
|
218
286
|
/**
|
|
@@ -284,6 +352,7 @@ async function saveActorNewSessionDraftInFileScope(
|
|
|
284
352
|
model: input.model,
|
|
285
353
|
reasoningEffort: input.reasoningEffort,
|
|
286
354
|
latencyMode: input.latencyMode,
|
|
355
|
+
...(input.modelProvided !== undefined ? { modelProvided: input.modelProvided } : {}),
|
|
287
356
|
...(input.selectedProjectChannelId !== undefined
|
|
288
357
|
? { selectedProjectChannelId: input.selectedProjectChannelId }
|
|
289
358
|
: {}),
|
|
@@ -303,7 +372,10 @@ async function saveActorNewSessionDraftInFileScope(
|
|
|
303
372
|
}),
|
|
304
373
|
),
|
|
305
374
|
);
|
|
306
|
-
|
|
375
|
+
// Report the same model-policy marker a read of this row reports, so the
|
|
376
|
+
// save response and the next GET agree for old and new clients alike.
|
|
377
|
+
const mapped = mapNewSessionDraft(saved)!;
|
|
378
|
+
return { ...mapped, modelProvided: draftModelProvided(deps.settings, mapped) };
|
|
307
379
|
} catch (error) {
|
|
308
380
|
if (error instanceof NewSessionDraftAccessError) {
|
|
309
381
|
throw new HTTPException(403, { message: error.message });
|
|
@@ -317,12 +389,13 @@ export async function getActorNewSessionDraft(
|
|
|
317
389
|
grant: AccessGrant,
|
|
318
390
|
workspaceId: string,
|
|
319
391
|
authorization?: AccessGrantAuthorization,
|
|
392
|
+
readOptions: NewSessionDraftReadOptions = {},
|
|
320
393
|
): Promise<NewSessionDraftValue> {
|
|
321
394
|
const actor = authorization
|
|
322
395
|
? await fileOwnerContextForAccess(deps, authorization, "sessions:read")
|
|
323
396
|
: { subjectId: grant.subjectId, privateFileOwnerSubjectId: null };
|
|
324
397
|
return withSessionRlsActorContext(actor, () =>
|
|
325
|
-
getActorNewSessionDraftInFileScope(deps, grant, workspaceId),
|
|
398
|
+
getActorNewSessionDraftInFileScope(deps, grant, workspaceId, readOptions),
|
|
326
399
|
);
|
|
327
400
|
}
|
|
328
401
|
export async function saveActorNewSessionDraft(
|
|
@@ -341,11 +414,17 @@ export async function getActorNewSessionDefaults(
|
|
|
341
414
|
grant: AccessGrant,
|
|
342
415
|
workspaceId: string,
|
|
343
416
|
) {
|
|
344
|
-
|
|
417
|
+
// The model is reused only when the person chose it, so a draft that follows
|
|
418
|
+
// the default is not projected here; session creation resolves it once.
|
|
419
|
+
const draft = await getActorNewSessionDraft(deps, grant, workspaceId, undefined, {
|
|
420
|
+
projectDefaultModel: false,
|
|
421
|
+
});
|
|
345
422
|
const options = draft.options;
|
|
346
423
|
return {
|
|
347
|
-
//
|
|
348
|
-
|
|
424
|
+
// Only a model the person chose is carried over. A draft that follows the
|
|
425
|
+
// default (including the synthetic revision-zero form) leaves the model
|
|
426
|
+
// out, so session creation resolves the same default itself.
|
|
427
|
+
...(draft.revision > 0 && draft.modelProvided === true
|
|
349
428
|
? {
|
|
350
429
|
model: draft.model,
|
|
351
430
|
reasoningEffort: draft.reasoningEffort,
|
|
@@ -27,6 +27,11 @@ import {
|
|
|
27
27
|
import { requireVariableSetEncryption } from "../domain/environments";
|
|
28
28
|
import { normalizedSessionMcpCredentialHeaders } from "../domain/sessions";
|
|
29
29
|
import { externalContinuationCommitAuthorizer } from "./external-continuation";
|
|
30
|
+
import {
|
|
31
|
+
listOwnConnectionAccountsForGrant,
|
|
32
|
+
personalConnectionDelegationSourceForGrant,
|
|
33
|
+
} from "../domain/personal-connection-delegations";
|
|
34
|
+
import { nativeSessionConnectionReplacement } from "../domain/session-native-connection-replacement";
|
|
30
35
|
|
|
31
36
|
/** HMAC domains separate request identity from key availability. The key tag
|
|
32
37
|
* carries no request material and neither fingerprint is returned publicly. */
|
|
@@ -80,11 +85,20 @@ export async function rotateSessionMcpCredentialsForRequest(
|
|
|
80
85
|
id: update.id,
|
|
81
86
|
expectedCredentialVersion: update.expectedCredentialVersion,
|
|
82
87
|
expectedServerUrl: update.expectedServerUrl,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
...("nativeConnectionId" in update
|
|
89
|
+
? {
|
|
90
|
+
nativeConnectionId: update.nativeConnectionId,
|
|
91
|
+
...(update.replacementServerUrl
|
|
92
|
+
? { replacementServerUrl: update.replacementServerUrl }
|
|
93
|
+
: {}),
|
|
94
|
+
}
|
|
95
|
+
: {
|
|
96
|
+
headers: Object.fromEntries(
|
|
97
|
+
Object.entries(normalizedSessionMcpCredentialHeaders(update.headers))
|
|
98
|
+
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
99
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
100
|
+
),
|
|
101
|
+
}),
|
|
88
102
|
}))
|
|
89
103
|
.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)),
|
|
90
104
|
};
|
|
@@ -102,20 +116,45 @@ export async function rotateSessionMcpCredentialsForRequest(
|
|
|
102
116
|
actorType: grant.principalKind === "service" ? ("service" as const) : ("human" as const),
|
|
103
117
|
};
|
|
104
118
|
const externalAuthorize = externalContinuationCommitAuthorizer(authorization);
|
|
119
|
+
let authorizedGrant = grant;
|
|
105
120
|
try {
|
|
106
121
|
return await rotateSessionMcpCredentialsAtomically(deps.db, {
|
|
107
122
|
...scope,
|
|
108
123
|
operationKey: request.operationKey,
|
|
109
124
|
...sessionMcpRotationFingerprint(encryptionKey, scope, request),
|
|
110
|
-
updates: request.updates.map((
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
updates: request.updates.map((update) =>
|
|
126
|
+
"nativeConnectionId" in update
|
|
127
|
+
? update
|
|
128
|
+
: {
|
|
129
|
+
id: update.id,
|
|
130
|
+
expectedCredentialVersion: update.expectedCredentialVersion,
|
|
131
|
+
expectedServerUrl: update.expectedServerUrl,
|
|
132
|
+
headersEncrypted: Object.fromEntries(
|
|
133
|
+
Object.entries(update.headers).map(([name, value]) => [
|
|
134
|
+
name,
|
|
135
|
+
encryptVariableSetValue(encryptionKey, value),
|
|
136
|
+
]),
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
),
|
|
140
|
+
resolveNativeConnection: async (tx, server, nativeConnectionId, replacementServerUrl) => {
|
|
141
|
+
const connections = await listOwnConnectionAccountsForGrant(tx, authorizedGrant);
|
|
142
|
+
const source = personalConnectionDelegationSourceForGrant(authorizedGrant);
|
|
143
|
+
try {
|
|
144
|
+
return nativeSessionConnectionReplacement({
|
|
145
|
+
accountId: grant.accountId,
|
|
146
|
+
workspaceId: grant.workspaceId,
|
|
147
|
+
subjectId: source.kind === "subject" ? source.subjectId : null,
|
|
148
|
+
serverUrl: server.url,
|
|
149
|
+
...(replacementServerUrl ? { replacementServerUrl } : {}),
|
|
150
|
+
currentRef: server.connectionRef,
|
|
151
|
+
nativeConnectionId,
|
|
152
|
+
connections,
|
|
153
|
+
});
|
|
154
|
+
} catch {
|
|
155
|
+
throw new SessionMcpCredentialRotationError("connection_unavailable");
|
|
156
|
+
}
|
|
157
|
+
},
|
|
119
158
|
authorize: async (tx) => {
|
|
120
159
|
await externalAuthorize?.(tx);
|
|
121
160
|
const fresh = await reauthorize(tx);
|
|
@@ -145,6 +184,7 @@ export async function rotateSessionMcpCredentialsForRequest(
|
|
|
145
184
|
operation: "session.mcp.credentials.rotate",
|
|
146
185
|
surface: "http",
|
|
147
186
|
});
|
|
187
|
+
authorizedGrant = fresh;
|
|
148
188
|
},
|
|
149
189
|
});
|
|
150
190
|
} catch (error) {
|
|
@@ -162,7 +202,9 @@ export async function rotateSessionMcpCredentialsForRequest(
|
|
|
162
202
|
? 403
|
|
163
203
|
: error.code === "not_found"
|
|
164
204
|
? 404
|
|
165
|
-
: error.code === "invalid_request" ||
|
|
205
|
+
: error.code === "invalid_request" ||
|
|
206
|
+
error.code === "brokered_server" ||
|
|
207
|
+
error.code === "connection_unavailable"
|
|
166
208
|
? 422
|
|
167
209
|
: 409;
|
|
168
210
|
throw new HTTPException(status, { message: `credential_rotation_${error.code}` });
|
package/src/billing/limits.ts
CHANGED
|
@@ -25,6 +25,26 @@ import type { ApiRouteDeps } from "../dependencies";
|
|
|
25
25
|
|
|
26
26
|
export type LimitDependencies = Pick<ApiRouteDeps, "db" | "settings">;
|
|
27
27
|
|
|
28
|
+
/** Deterministic/local embeddings never debit credits, even in paid mode. */
|
|
29
|
+
export function paidDocumentEmbedding(settings: Settings): boolean {
|
|
30
|
+
return (
|
|
31
|
+
settings.documentEmbeddingBillingMode === "credits" &&
|
|
32
|
+
settings.documentEmbeddingProvider === "openai"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Round up each provider request so a positive input cannot become a free debit. */
|
|
37
|
+
export function documentEmbeddingCostMicros(settings: Settings, inputBytes: number): number {
|
|
38
|
+
if (!Number.isSafeInteger(inputBytes) || inputBytes < 0)
|
|
39
|
+
throw new Error("invalid embedding bytes");
|
|
40
|
+
const amount =
|
|
41
|
+
(BigInt(inputBytes) * BigInt(settings.documentEmbeddingRateMicrosPerMillionBytes) + 999_999n) /
|
|
42
|
+
1_000_000n;
|
|
43
|
+
if (amount > BigInt(Number.MAX_SAFE_INTEGER))
|
|
44
|
+
throw new Error("embedding cost exceeds safe integer");
|
|
45
|
+
return Number(amount);
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
export type LimitCheckInput = {
|
|
29
49
|
accountId: string;
|
|
30
50
|
workspaceId?: string;
|
|
@@ -296,9 +316,10 @@ function usesCreditLimits(deps: LimitDependencies): boolean {
|
|
|
296
316
|
}
|
|
297
317
|
|
|
298
318
|
function isCostlyAction(action: LimitAction): boolean {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
319
|
+
// Document admission retains the monthly chunk quota above, but parsing and
|
|
320
|
+
// source retention do not consume embedding credits. The worker admits paid
|
|
321
|
+
// embedding separately and settles its actual provider input after use.
|
|
322
|
+
return action === "agent_run:create" || action === "tokens:consume";
|
|
302
323
|
}
|
|
303
324
|
|
|
304
325
|
function blocked(code: string, message: string): LimitDecision {
|