@capybara-hq/kernel 0.1.0-rc.5 → 0.1.0-rc.6

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.
Files changed (63) hide show
  1. package/dist/admin/admin.controller.d.ts +13 -2
  2. package/dist/admin/admin.controller.js +29 -4
  3. package/dist/agents/agents.service.d.ts +3 -1
  4. package/dist/agents/agents.service.js +8 -6
  5. package/dist/agents/from-template.service.js +3 -3
  6. package/dist/agents/models.controller.d.ts +34 -5
  7. package/dist/agents/models.controller.js +50 -6
  8. package/dist/agents/readiness.controller.d.ts +5 -5
  9. package/dist/agents/readiness.controller.js +6 -6
  10. package/dist/catalog/marketplaces.service.d.ts +1 -1
  11. package/dist/config/schema.d.ts +4 -0
  12. package/dist/config/schema.js +24 -0
  13. package/dist/conversations/conversations.service.d.ts +4 -3
  14. package/dist/conversations/conversations.service.js +9 -8
  15. package/dist/ctx/create-ctx.d.ts +15 -1
  16. package/dist/ctx/create-ctx.js +23 -2
  17. package/dist/db/kernel-schema.d.ts +24 -0
  18. package/dist/errors.d.ts +3 -0
  19. package/dist/errors.js +3 -0
  20. package/dist/evals/run-replay.js +4 -2
  21. package/dist/evals/runs.service.d.ts +2 -1
  22. package/dist/evals/runs.service.js +13 -12
  23. package/dist/evals/step-replay.d.ts +2 -1
  24. package/dist/evals/step-replay.js +3 -2
  25. package/dist/extensions/config-hash.d.ts +10 -0
  26. package/dist/extensions/config-hash.js +38 -0
  27. package/dist/extensions/extensions.module.js +6 -12
  28. package/dist/extensions/host.d.ts +6 -10
  29. package/dist/extensions/host.js +22 -50
  30. package/dist/extensions/state.module.d.ts +9 -0
  31. package/dist/extensions/state.module.js +38 -0
  32. package/dist/http/api.module.js +2 -0
  33. package/dist/http/plugin-dispatch.d.ts +4 -1
  34. package/dist/http/plugin-dispatch.js +14 -2
  35. package/dist/infra/config/kernel-config.service.js +3 -0
  36. package/dist/infra/events/kernel-events.d.ts +8 -0
  37. package/dist/models/embeddings.d.ts +82 -0
  38. package/dist/models/embeddings.js +326 -0
  39. package/dist/models/enablement.controller.d.ts +40 -0
  40. package/dist/models/enablement.controller.js +74 -0
  41. package/dist/models/enablement.d.ts +99 -0
  42. package/dist/models/enablement.js +263 -0
  43. package/dist/models/models.module.d.ts +5 -1
  44. package/dist/models/models.module.js +16 -4
  45. package/dist/models/purposes.d.ts +18 -4
  46. package/dist/models/purposes.js +46 -9
  47. package/dist/platform/exclusions.js +18 -0
  48. package/dist/runtime/hook-calls.d.ts +8 -1
  49. package/dist/runtime/hook-calls.js +40 -9
  50. package/dist/runtime/model-call.d.ts +8 -3
  51. package/dist/runtime/model-call.js +28 -5
  52. package/dist/runtime/providers.d.ts +146 -32
  53. package/dist/runtime/providers.js +321 -67
  54. package/dist/runtime/run-worker.d.ts +1 -1
  55. package/dist/runtime/run-worker.js +4 -4
  56. package/dist/runtime/subagents/definition.js +3 -3
  57. package/dist/runtime/usage-report-jobs.d.ts +88 -0
  58. package/dist/runtime/usage-report-jobs.js +158 -0
  59. package/dist/runtime/usage-report.d.ts +33 -38
  60. package/dist/runtime/usage-report.js +152 -84
  61. package/dist/tools/pipeline.js +6 -0
  62. package/package.json +3 -3
  63. package/src/migrations/0089_model_kinds_usage.sql +38 -0
@@ -0,0 +1,82 @@
1
+ import { type ExtensionEmbedRequest, type ExtensionEmbedResult, type ExtensionModelTarget, type ExtensionPermissions, type Logger } from "@capybara-hq/plugin-sdk";
2
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
3
+ import { KernelDb } from "../infra/database/kernel-db.js";
4
+ import { TransactionHost } from "../infra/database/transaction-host.js";
5
+ import { KernelEvents } from "../infra/events/kernel-events.js";
6
+ import { type HookRunner } from "../runtime/hook-calls.js";
7
+ import { ProviderRegistry } from "../runtime/providers.js";
8
+ import { ModelPurposesService } from "./purposes.js";
9
+ /** An extension's `permissions.models` (what it declared, as approved in its manifest). */
10
+ export type DeclaredModelPurposes = readonly NonNullable<ExtensionPermissions["models"]>[number][];
11
+ /** Most texts one `ctx.models.embed` call may carry (the kernel splits them by `maxBatch`). */
12
+ export declare const EMBED_MAX_INPUTS = 2048;
13
+ /**
14
+ * The `ctx.models` broker for embeddings (model platform spec B §5.4). Every call:
15
+ * - accepts only a purpose the calling extension declared in `permissions.models` with
16
+ * `kind: "embedding"` (`model.purpose_unknown` otherwise: the manifest is what was approved), for
17
+ * an org that enabled the extension (`extension.not_enabled`);
18
+ * - resolves the org's effective target ({@link ModelPurposesService.resolve}: unmapped, or mapped
19
+ * to a model the org cannot use, is `model.unconfigured`) and the model in the org's view, enabled
20
+ * and of kind `embedding` ({@link ProviderRegistry.resolve}, bound to the org's effective config);
21
+ * - fires `model.request` once (`purpose: "embedding"`, every text of the call, no conversation
22
+ * fields) through the host's hook registry before anything is sent; a policy denial (a deny, a
23
+ * hook error or timeout, a policy extension not loaded here or without a valid config in the org)
24
+ * writes one `hook.denied` audit row and rejects with `ApiError("model.hook_denied")` (403) whose
25
+ * message is the one a denied run step fails with: no provider call, no usage row. A call with no input sends nothing
26
+ * and fires nothing;
27
+ * - splits the input into batches of the model's `maxBatch`, calls the provider's `embed` once per
28
+ * batch in order with the extension's signal (plus the request's own `signal`), each batch
29
+ * limited by `models.embedTimeoutMs` (`ModelError("timeout")`), checks each result ({@link validateEmbedResult}: a
30
+ * wrong count, dimension or a non-finite number is a `ModelError` of kind `server`), and
31
+ * concatenates the vectors; no retries (an extension retries itself if it wants to);
32
+ * - writes one `model_usage` row (the sum of the batches' input tokens) and announces it after the
33
+ * commit (`model_usage.recorded`: the usage reporter's `ModelUsageJobs` enqueues its
34
+ * `usage.report` delivery; the usage sweep catches up a lost one). A call that fails after the
35
+ * provider answered some batches (an invalid result, a later batch failing) still records their
36
+ * input tokens before the error reaches the extension; one that fails before any answer records
37
+ * nothing.
38
+ * Purpose params are not sent: an `embed` request carries none (`EmbedRequest`).
39
+ */
40
+ export declare class ModelEmbeddings {
41
+ private readonly kdb;
42
+ readonly txHost: TransactionHost;
43
+ private readonly purposes;
44
+ private readonly providers;
45
+ /** `model_usage.recorded`: the usage reporter enqueues the row's delivery. */
46
+ private readonly events;
47
+ private readonly log;
48
+ /** `models.embedTimeoutMs` (hot). */
49
+ private readonly config;
50
+ constructor(kdb: KernelDb, txHost: TransactionHost, purposes: Pick<ModelPurposesService, "resolve">, providers: Pick<ProviderRegistry, "resolve">,
51
+ /** `model_usage.recorded`: the usage reporter enqueues the row's delivery. */
52
+ events: Pick<KernelEvents, "emit">, log: Pick<Logger, "warn">,
53
+ /** `models.embedTimeoutMs` (hot). */
54
+ config: Pick<KernelConfigService, "get">);
55
+ /** Refuses a purpose `plugin` did not declare with kind `embedding`, or an org it is off in. */
56
+ private admit;
57
+ /** The org's target for the purpose and the model bound to the org's config. */
58
+ private target;
59
+ /**
60
+ * One batch to the provider, limited by `models.embedTimeoutMs` (hot, read per call): at the limit
61
+ * the call's signal aborts and the batch fails with `ModelError("timeout")`, even when the
62
+ * provider ignores the signal (its late answer is dropped).
63
+ */
64
+ private callWithTimeout;
65
+ /**
66
+ * `ctx.models.embed` for `plugin` (its declared purposes), aborted by `signal`; `hooks` is the
67
+ * host's hook registry (`model.request`), absent only for a ctx built without a host (tests).
68
+ */
69
+ embed(plugin: string, declared: DeclaredModelPurposes, request: ExtensionEmbedRequest, signal: AbortSignal, hooks: HookRunner | undefined): Promise<ExtensionEmbedResult>;
70
+ /**
71
+ * `model.request` for an embed call (policy hooks may deny it). Rejects with the caller's abort
72
+ * reason when `signal` aborts while the hooks run (an abort is never a deny).
73
+ */
74
+ private checkHooks;
75
+ /** `ctx.models.describe` for `plugin`: null when the purpose maps to no model the org can use. */
76
+ describe(plugin: string, declared: DeclaredModelPurposes, req: {
77
+ orgId: string;
78
+ purpose: string;
79
+ }): Promise<ExtensionModelTarget | null>;
80
+ /** One `model_usage` row; its `usage.report` delivery is enqueued after the commit. */
81
+ private record;
82
+ }
@@ -0,0 +1,326 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { ModelError, validateEmbedResult, } from "@capybara-hq/plugin-sdk";
11
+ import { Inject, Injectable } from "@nestjs/common";
12
+ import { ApiError } from "../errors.js";
13
+ import { isExtensionEnabled } from "../extensions/enabled.js";
14
+ import { audit } from "../identity/audit.js";
15
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
16
+ import { afterCommit } from "../infra/database/after-commit.js";
17
+ import { KernelDb } from "../infra/database/kernel-db.js";
18
+ import { TransactionHost } from "../infra/database/transaction-host.js";
19
+ import { Transactional } from "../infra/database/transactional.decorator.js";
20
+ import { KernelEvents } from "../infra/events/kernel-events.js";
21
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
22
+ import { embeddingDeniedAudit } from "../runtime/hook-calls.js";
23
+ import { modelDeniedText } from "../runtime/model-call.js";
24
+ import { ProviderRegistry } from "../runtime/providers.js";
25
+ import { ModelPurposesService } from "./purposes.js";
26
+ /** Most texts one `ctx.models.embed` call may carry (the kernel splits them by `maxBatch`). */
27
+ export const EMBED_MAX_INPUTS = 2048;
28
+ /**
29
+ * The `ctx.models` broker for embeddings (model platform spec B §5.4). Every call:
30
+ * - accepts only a purpose the calling extension declared in `permissions.models` with
31
+ * `kind: "embedding"` (`model.purpose_unknown` otherwise: the manifest is what was approved), for
32
+ * an org that enabled the extension (`extension.not_enabled`);
33
+ * - resolves the org's effective target ({@link ModelPurposesService.resolve}: unmapped, or mapped
34
+ * to a model the org cannot use, is `model.unconfigured`) and the model in the org's view, enabled
35
+ * and of kind `embedding` ({@link ProviderRegistry.resolve}, bound to the org's effective config);
36
+ * - fires `model.request` once (`purpose: "embedding"`, every text of the call, no conversation
37
+ * fields) through the host's hook registry before anything is sent; a policy denial (a deny, a
38
+ * hook error or timeout, a policy extension not loaded here or without a valid config in the org)
39
+ * writes one `hook.denied` audit row and rejects with `ApiError("model.hook_denied")` (403) whose
40
+ * message is the one a denied run step fails with: no provider call, no usage row. A call with no input sends nothing
41
+ * and fires nothing;
42
+ * - splits the input into batches of the model's `maxBatch`, calls the provider's `embed` once per
43
+ * batch in order with the extension's signal (plus the request's own `signal`), each batch
44
+ * limited by `models.embedTimeoutMs` (`ModelError("timeout")`), checks each result ({@link validateEmbedResult}: a
45
+ * wrong count, dimension or a non-finite number is a `ModelError` of kind `server`), and
46
+ * concatenates the vectors; no retries (an extension retries itself if it wants to);
47
+ * - writes one `model_usage` row (the sum of the batches' input tokens) and announces it after the
48
+ * commit (`model_usage.recorded`: the usage reporter's `ModelUsageJobs` enqueues its
49
+ * `usage.report` delivery; the usage sweep catches up a lost one). A call that fails after the
50
+ * provider answered some batches (an invalid result, a later batch failing) still records their
51
+ * input tokens before the error reaches the extension; one that fails before any answer records
52
+ * nothing.
53
+ * Purpose params are not sent: an `embed` request carries none (`EmbedRequest`).
54
+ */
55
+ let ModelEmbeddings = class ModelEmbeddings {
56
+ kdb;
57
+ txHost;
58
+ purposes;
59
+ providers;
60
+ events;
61
+ log;
62
+ config;
63
+ constructor(kdb, txHost, purposes, providers,
64
+ /** `model_usage.recorded`: the usage reporter enqueues the row's delivery. */
65
+ events, log,
66
+ /** `models.embedTimeoutMs` (hot). */
67
+ config) {
68
+ this.kdb = kdb;
69
+ this.txHost = txHost;
70
+ this.purposes = purposes;
71
+ this.providers = providers;
72
+ this.events = events;
73
+ this.log = log;
74
+ this.config = config;
75
+ }
76
+ /** Refuses a purpose `plugin` did not declare with kind `embedding`, or an org it is off in. */
77
+ async admit(plugin, declared, orgId, purpose) {
78
+ if (!declared.some((d) => d.purpose === purpose && d.kind === "embedding")) {
79
+ throw new ApiError("model.purpose_unknown", `Extension ${plugin} declared no embedding purpose "${purpose}" in permissions.models`);
80
+ }
81
+ if (!(await isExtensionEnabled(this.kdb.db, orgId, plugin))) {
82
+ throw new ApiError("extension.not_enabled", `Extension ${plugin} is not enabled in organization ${orgId}`);
83
+ }
84
+ }
85
+ /** The org's target for the purpose and the model bound to the org's config. */
86
+ async target(orgId, purpose) {
87
+ const target = await this.purposes.resolve(orgId, purpose);
88
+ const { provider, info } = await this.providers.resolve(orgId, target.model, {
89
+ kind: "embedding",
90
+ });
91
+ if (info.embedding === undefined || provider.embed === undefined) {
92
+ // the registry drops such models from the list; kept as a guard
93
+ throw new ApiError("model.unconfigured", `The model configured for the "${purpose}" purpose cannot embed`);
94
+ }
95
+ return {
96
+ ref: target.model,
97
+ info: info.embedding,
98
+ embed: provider.embed.bind(provider),
99
+ };
100
+ }
101
+ /**
102
+ * One batch to the provider, limited by `models.embedTimeoutMs` (hot, read per call): at the limit
103
+ * the call's signal aborts and the batch fails with `ModelError("timeout")`, even when the
104
+ * provider ignores the signal (its late answer is dropped).
105
+ */
106
+ async callWithTimeout(embed, batch, signal, model) {
107
+ const ms = this.config.get().models.embedTimeoutMs;
108
+ const timer = new AbortController();
109
+ const both = AbortSignal.any([signal, timer.signal]);
110
+ let handle;
111
+ let onAbort;
112
+ const call = Promise.resolve().then(() => embed(batch, { signal: both }));
113
+ call.catch(() => { });
114
+ try {
115
+ return await Promise.race([
116
+ call,
117
+ new Promise((_, reject) => {
118
+ handle = setTimeout(() => {
119
+ const err = new ModelError("timeout", `Model ${model.provider}/${model.id} did not answer an embed call within ${ms} ms`);
120
+ timer.abort(err);
121
+ reject(err);
122
+ }, ms);
123
+ onAbort = () => reject(signal.reason);
124
+ signal.addEventListener("abort", onAbort, { once: true });
125
+ }),
126
+ ]);
127
+ }
128
+ finally {
129
+ clearTimeout(handle);
130
+ // ctx.signal lives as long as the instance: never leave a listener per batch on it
131
+ if (onAbort)
132
+ signal.removeEventListener("abort", onAbort);
133
+ }
134
+ }
135
+ /**
136
+ * `ctx.models.embed` for `plugin` (its declared purposes), aborted by `signal`; `hooks` is the
137
+ * host's hook registry (`model.request`), absent only for a ctx built without a host (tests).
138
+ */
139
+ async embed(plugin, declared, request, signal, hooks) {
140
+ checkRequest(request);
141
+ // a copy, read once: the texts the hooks see are the texts sent, whatever the extension does
142
+ // with its own request object meanwhile
143
+ const req = { ...request, input: request.input.slice() };
144
+ await this.admit(plugin, declared, req.orgId, req.purpose);
145
+ const t = await this.target(req.orgId, req.purpose);
146
+ const dimensions = req.dimensions ?? t.info.defaultDimensions;
147
+ if (!t.info.dimensions.includes(dimensions)) {
148
+ throw new ApiError("validation_failed", `Model ${t.ref.provider}/${t.ref.model} cannot produce ${dimensions} dimensions (it offers ${t.info.dimensions.join(", ")})`);
149
+ }
150
+ const model = { provider: t.ref.provider, id: t.ref.model };
151
+ if (req.input.length === 0)
152
+ return { vectors: [], model, dimensions };
153
+ const vectors = [];
154
+ /** Input tokens of the batches the provider answered (billed even if the call fails later). */
155
+ let inputTokens = 0;
156
+ let answered = 0;
157
+ const size = Math.max(1, t.info.maxBatch);
158
+ // the instance's signal (revocation) and the caller's own (e.g. its tool call aborted)
159
+ const callSignal = req.signal ? AbortSignal.any([signal, req.signal]) : signal;
160
+ await this.checkHooks(hooks, plugin, req, model, callSignal);
161
+ try {
162
+ for (let i = 0; i < req.input.length; i += size) {
163
+ callSignal.throwIfAborted();
164
+ const batch = {
165
+ model: t.ref.model,
166
+ input: req.input.slice(i, i + size),
167
+ ...(req.dimensions !== undefined && { dimensions: req.dimensions }),
168
+ ...(req.inputType !== undefined && { inputType: req.inputType }),
169
+ };
170
+ const res = await this.callWithTimeout(t.embed, batch, callSignal, model);
171
+ answered++;
172
+ inputTokens += tokensOf(res);
173
+ const problem = checkResult(batch, t.info, res);
174
+ if (problem !== null) {
175
+ throw new ModelError("server", `Model ${model.provider}/${model.id} returned an invalid embedding: ${problem}`);
176
+ }
177
+ for (const v of res.vectors)
178
+ vectors.push(v);
179
+ }
180
+ }
181
+ catch (err) {
182
+ // the batches already answered were billed by the provider: recorded, then the error
183
+ if (answered > 0) {
184
+ await this.record(plugin, req.orgId, req.purpose, model, inputTokens).catch((e) => this.log.warn("cannot record the usage of a failed embed call", {
185
+ plugin,
186
+ orgId: req.orgId,
187
+ err: e,
188
+ }));
189
+ }
190
+ throw err;
191
+ }
192
+ await this.record(plugin, req.orgId, req.purpose, model, inputTokens);
193
+ return { vectors, model, dimensions };
194
+ }
195
+ /**
196
+ * `model.request` for an embed call (policy hooks may deny it). Rejects with the caller's abort
197
+ * reason when `signal` aborts while the hooks run (an abort is never a deny).
198
+ */
199
+ async checkHooks(hooks, plugin, req, model, signal) {
200
+ if (!hooks?.has("model.request"))
201
+ return;
202
+ const call = {
203
+ purpose: "embedding",
204
+ orgId: req.orgId,
205
+ extension: plugin,
206
+ embeddingPurpose: req.purpose,
207
+ provider: model.provider,
208
+ model: model.id,
209
+ input: req.input,
210
+ ...(req.dimensions !== undefined && { dimensions: req.dimensions }),
211
+ ...(req.inputType !== undefined && { inputType: req.inputType }),
212
+ };
213
+ const verdict = await hooks.run("model.request", call, signal);
214
+ if (verdict.allowed)
215
+ return;
216
+ await audit(this.kdb.db, embeddingDeniedAudit(call, verdict));
217
+ this.log.warn("embed call blocked by a policy hook", {
218
+ plugin,
219
+ orgId: req.orgId,
220
+ hookPlugin: verdict.plugin,
221
+ cause: verdict.cause,
222
+ });
223
+ throw new ApiError("model.hook_denied", modelDeniedText(verdict.plugin, verdict.reason));
224
+ }
225
+ /** `ctx.models.describe` for `plugin`: null when the purpose maps to no model the org can use. */
226
+ async describe(plugin, declared, req) {
227
+ if (typeof req?.orgId !== "string" || typeof req.purpose !== "string") {
228
+ throw new ApiError("validation_failed", "describe needs orgId and purpose strings");
229
+ }
230
+ await this.admit(plugin, declared, req.orgId, req.purpose);
231
+ try {
232
+ const t = await this.target(req.orgId, req.purpose);
233
+ return {
234
+ provider: t.ref.provider,
235
+ id: t.ref.model,
236
+ kind: "embedding",
237
+ embedding: structuredClone(t.info),
238
+ };
239
+ }
240
+ catch (e) {
241
+ if (e instanceof ApiError && e.code === "model.unconfigured")
242
+ return null;
243
+ throw e;
244
+ }
245
+ }
246
+ /** One `model_usage` row; its `usage.report` delivery is enqueued after the commit. */
247
+ async record(plugin, orgId, purpose, model, inputTokens) {
248
+ const row = await this.kdb.db
249
+ .insertInto("model_usage")
250
+ .values({
251
+ org_id: orgId,
252
+ extension: plugin,
253
+ purpose,
254
+ provider: model.provider,
255
+ model: model.id,
256
+ input_tokens: Math.round(inputTokens),
257
+ })
258
+ .returning("id")
259
+ .executeTakeFirstOrThrow();
260
+ // fire and forget: the extension's call never waits on the enqueue (PG, Redis); a lost one is
261
+ // caught up by the usage sweep
262
+ afterCommit(() => {
263
+ void this.events
264
+ .emit("model_usage.recorded", { id: row.id })
265
+ .catch((err) => this.log.warn("model usage event failed", { usageId: row.id, err }));
266
+ });
267
+ }
268
+ };
269
+ __decorate([
270
+ Transactional()
271
+ ], ModelEmbeddings.prototype, "record", null);
272
+ ModelEmbeddings = __decorate([
273
+ Injectable(),
274
+ __param(0, Inject(KernelDb)),
275
+ __param(1, Inject(TransactionHost)),
276
+ __param(2, Inject(ModelPurposesService)),
277
+ __param(3, Inject(ProviderRegistry)),
278
+ __param(4, Inject(KernelEvents)),
279
+ __param(5, Inject(KERNEL_LOGGER)),
280
+ __param(6, Inject(KernelConfigService))
281
+ ], ModelEmbeddings);
282
+ export { ModelEmbeddings };
283
+ /** A result's input tokens when it reports a usable number, else 0 (an invalid result). */
284
+ function tokensOf(res) {
285
+ const n = res?.usage?.inputTokens;
286
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : 0;
287
+ }
288
+ /** The request's shape (an extension is untyped JavaScript at run time). */
289
+ function checkRequest(req) {
290
+ const bad = (why) => {
291
+ throw new ApiError("validation_failed", `Invalid embed request: ${why}`);
292
+ };
293
+ if (typeof req !== "object" || req === null)
294
+ bad("expected an object");
295
+ if (typeof req.orgId !== "string" || req.orgId === "")
296
+ bad("orgId must be a string");
297
+ if (typeof req.purpose !== "string" || req.purpose === "")
298
+ bad("purpose must be a string");
299
+ if (!Array.isArray(req.input) || req.input.some((t) => typeof t !== "string")) {
300
+ bad("input must be an array of strings");
301
+ }
302
+ if (req.input.length > EMBED_MAX_INPUTS)
303
+ bad(`at most ${EMBED_MAX_INPUTS} inputs per call`);
304
+ if (req.dimensions !== undefined && !Number.isInteger(req.dimensions)) {
305
+ bad("dimensions must be an integer");
306
+ }
307
+ if (req.signal !== undefined && !(req.signal instanceof AbortSignal)) {
308
+ bad("signal must be an AbortSignal");
309
+ }
310
+ if (req.inputType !== undefined && req.inputType !== "query" && req.inputType !== "document") {
311
+ bad('inputType must be "query" or "document"');
312
+ }
313
+ }
314
+ /** {@link validateEmbedResult}, plus the result's own shape (a provider is untyped at run time). */
315
+ function checkResult(req, info, res) {
316
+ const r = res;
317
+ if (typeof r !== "object" || r === null || !Array.isArray(r.vectors))
318
+ return "no vectors";
319
+ if (!r.vectors.every((v) => Array.isArray(v)))
320
+ return "a vector is not an array";
321
+ const tokens = r.usage?.inputTokens;
322
+ if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens < 0) {
323
+ return "usage.inputTokens is not a non-negative number";
324
+ }
325
+ return validateEmbedResult(req, info, r);
326
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { type Principal } from "../identity/principal.js";
3
+ import { type OrgContext } from "../orgs/org.guard.js";
4
+ import { type ModelDisableResult, ModelEnablement } from "./enablement.js";
5
+ /** `:provider` of a disabled-list route. */
6
+ export declare const ModelProviderParams: z.ZodObject<{
7
+ provider: z.ZodString;
8
+ }, z.core.$strip>;
9
+ declare const ModelProviderParamsDto_base: import("../infra/validation/create-zod-dto.js").ZodDtoClass<z.ZodObject<{
10
+ provider: z.ZodString;
11
+ }, z.core.$strip>>;
12
+ export declare class ModelProviderParamsDto extends ModelProviderParamsDto_base {
13
+ }
14
+ /**
15
+ * The body of a disabled-list PATCH: the model ids of the provider to disable and to enable. An id
16
+ * in neither list keeps its stored state (a page never drops ids it does not show); an id in both
17
+ * is a 400 `validation_failed`.
18
+ */
19
+ export declare const ModelDisabledPatch: z.ZodObject<{
20
+ disable: z.ZodDefault<z.ZodArray<z.ZodString>>;
21
+ enable: z.ZodDefault<z.ZodArray<z.ZodString>>;
22
+ }, z.core.$strict>;
23
+ declare const ModelDisabledPatchDto_base: import("../infra/validation/create-zod-dto.js").ZodDtoClass<z.ZodObject<{
24
+ disable: z.ZodDefault<z.ZodArray<z.ZodString>>;
25
+ enable: z.ZodDefault<z.ZodArray<z.ZodString>>;
26
+ }, z.core.$strict>>;
27
+ export declare class ModelDisabledPatchDto extends ModelDisabledPatchDto_base {
28
+ }
29
+ /**
30
+ * The org's disabled models of one provider (`PATCH /api/v1/orgs/:orgId/models/:provider/disabled`,
31
+ * spec B §5.3): `org.models.manage`, people only. Disables and enables the ids the body names; the
32
+ * response lists the org's agents and purposes that now point at a newly disabled model (the
33
+ * disable is not refused for them).
34
+ */
35
+ export declare class ModelEnablementController {
36
+ private readonly enablement;
37
+ constructor(enablement: ModelEnablement);
38
+ patch(principal: Principal, org: OrgContext, params: z.infer<typeof ModelProviderParams>, body: z.infer<typeof ModelDisabledPatch>): Promise<ModelDisableResult>;
39
+ }
40
+ export {};
@@ -0,0 +1,74 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { Controller, Inject, Patch } from "@nestjs/common";
11
+ import { z } from "zod";
12
+ import { CurrentPrincipal, patTokenId, requireUser, } from "../identity/principal.js";
13
+ import { createZodDto } from "../infra/validation/create-zod-dto.js";
14
+ import { ZBody, ZParam } from "../infra/validation/zod-params.js";
15
+ import { CurrentOrg, OrgAction } from "../orgs/org.guard.js";
16
+ import { ModelEnablement } from "./enablement.js";
17
+ /** `:provider` of a disabled-list route. */
18
+ export const ModelProviderParams = z.object({ provider: z.string().min(1).max(200) });
19
+ export class ModelProviderParamsDto extends createZodDto(ModelProviderParams, "ModelProviderParams") {
20
+ }
21
+ const modelIds = z.array(z.string().min(1).max(200)).max(5000).default([]);
22
+ /**
23
+ * The body of a disabled-list PATCH: the model ids of the provider to disable and to enable. An id
24
+ * in neither list keeps its stored state (a page never drops ids it does not show); an id in both
25
+ * is a 400 `validation_failed`.
26
+ */
27
+ export const ModelDisabledPatch = z
28
+ .object({ disable: modelIds, enable: modelIds })
29
+ .strict()
30
+ .superRefine((v, ctx) => {
31
+ const disable = new Set(v.disable);
32
+ const both = [...new Set(v.enable.filter((m) => disable.has(m)))];
33
+ if (both.length > 0) {
34
+ ctx.addIssue({
35
+ code: "custom",
36
+ path: ["enable"],
37
+ message: `A model cannot be both disabled and enabled: ${both.join(", ")}`,
38
+ });
39
+ }
40
+ });
41
+ export class ModelDisabledPatchDto extends createZodDto(ModelDisabledPatch, "ModelDisabledPatch") {
42
+ }
43
+ /**
44
+ * The org's disabled models of one provider (`PATCH /api/v1/orgs/:orgId/models/:provider/disabled`,
45
+ * spec B §5.3): `org.models.manage`, people only. Disables and enables the ids the body names; the
46
+ * response lists the org's agents and purposes that now point at a newly disabled model (the
47
+ * disable is not refused for them).
48
+ */
49
+ let ModelEnablementController = class ModelEnablementController {
50
+ enablement;
51
+ constructor(enablement) {
52
+ this.enablement = enablement;
53
+ }
54
+ async patch(principal, org, params, body) {
55
+ const p = requireUser(principal);
56
+ return this.enablement.setOrg(org.orgId, params.provider, body, {
57
+ accountId: p.account.id,
58
+ tokenId: patTokenId(p),
59
+ });
60
+ }
61
+ };
62
+ __decorate([
63
+ Patch(":provider/disabled"),
64
+ OrgAction("org.models.manage"),
65
+ __param(0, CurrentPrincipal()),
66
+ __param(1, CurrentOrg()),
67
+ __param(2, ZParam(ModelProviderParamsDto)),
68
+ __param(3, ZBody(ModelDisabledPatchDto))
69
+ ], ModelEnablementController.prototype, "patch", null);
70
+ ModelEnablementController = __decorate([
71
+ Controller("api/v1/orgs/:orgId/models"),
72
+ __param(0, Inject(ModelEnablement))
73
+ ], ModelEnablementController);
74
+ export { ModelEnablementController };
@@ -0,0 +1,99 @@
1
+ import { ExtensionRegistry } from "../extensions/registry.js";
2
+ import { SuperAdminLock } from "../identity/super-admin.js";
3
+ import { CacheService } from "../infra/cache/cache.service.js";
4
+ import { KernelDb } from "../infra/database/kernel-db.js";
5
+ import { TransactionHost } from "../infra/database/transaction-host.js";
6
+ import { OrgLock } from "../orgs/org-lock.js";
7
+ /** The key of a model in a disabled set: `provider\u0000model`. */
8
+ export declare const modelKey: (provider: string, model: string) => string;
9
+ /** An agent whose current version uses a newly disabled model as its main model or fallback. */
10
+ export interface AffectedModelAgent {
11
+ id: string;
12
+ name: string;
13
+ orgId: string;
14
+ }
15
+ /** A purpose mapping targeting a newly disabled model: a deployment default (`orgId` null) or an org override. */
16
+ export interface AffectedModelPurpose {
17
+ purpose: string;
18
+ orgId: string | null;
19
+ }
20
+ /** The result of a disabled-list write: what now points at a model it disabled (spec B §5.3). */
21
+ export interface ModelDisableResult {
22
+ affected: {
23
+ agents: AffectedModelAgent[];
24
+ purposes: AffectedModelPurpose[];
25
+ };
26
+ }
27
+ /**
28
+ * A change to one layer's disabled list of a provider: ids to disable and ids to enable. An id in
29
+ * neither list keeps its stored state; an id in both is refused.
30
+ */
31
+ export interface ModelDisableChange {
32
+ disable: string[];
33
+ enable: string[];
34
+ }
35
+ /** The acting person of a write. */
36
+ export interface ModelDisableActor {
37
+ accountId: string;
38
+ tokenId?: string | undefined;
39
+ }
40
+ /**
41
+ * Models disabled on two layers (spec B §5.3, table `model_disabled`): the platform's list (the
42
+ * super admin; such a model is invisible to every org) and each org's (an org admin). Every model a
43
+ * provider lists is enabled unless a layer disabled it; a row keys on the model id, so it applies to
44
+ * whichever list the org sees, and an id the list does not contain is kept harmlessly. Disabling
45
+ * never creates an org config copy.
46
+ *
47
+ * The lists are read from PG and cached in Redis (`@Cacheable`, evicted after the commit of each
48
+ * write), never in process memory. `ProviderRegistry` filters its lists and `resolve` with them;
49
+ * purposes check their targets with {@link isEnabled}.
50
+ */
51
+ export declare class ModelEnablement {
52
+ private readonly kdb;
53
+ readonly txHost: TransactionHost;
54
+ readonly cacheService: CacheService;
55
+ private readonly extensions;
56
+ private readonly adminLock;
57
+ private readonly orgLock;
58
+ constructor(kdb: KernelDb, txHost: TransactionHost, cacheService: CacheService, extensions: Pick<ExtensionRegistry, "list">, adminLock: Pick<SuperAdminLock, "lockAndAuthorize">, orgLock: Pick<OrgLock, "lockAndAuthorize">);
59
+ private get db();
60
+ /** One layer's disabled models (keys {@link modelKey}): the platform's (`null`) or the org's own. */
61
+ disabled(orgId: string | null): Promise<Set<string>>;
62
+ private disabledKeys;
63
+ private rowsOf;
64
+ /**
65
+ * The extensions `orgId` explicitly turned off: a row of the org's own switch
66
+ * (`extension_org_configs.enabled = false`, what the org's extension or plugin switch writes when
67
+ * an org admin turns it off), never a required bootstrap plugin. An extension with no row for the
68
+ * org is not in the set (it keeps working as before: spec A §8 with B's no-breakage anti-goal).
69
+ * Read from PG on every call (one small query; the switch has its own writers, nothing to evict).
70
+ */
71
+ switchedOff(orgId: string): Promise<Set<string>>;
72
+ /** Whether neither the platform nor `orgId` disabled the model (the lists only; see the class). */
73
+ isEnabled(orgId: string, provider: string, model: string): Promise<boolean>;
74
+ /**
75
+ * Changes the platform's disabled ids of `provider` by `change` (super admin, re-checked under
76
+ * the lock): ids in neither list keep their stored state. Returns what now points at a newly
77
+ * disabled model, across orgs.
78
+ */
79
+ setPlatform(provider: string, change: ModelDisableChange, actor: ModelDisableActor): Promise<ModelDisableResult>;
80
+ /**
81
+ * Changes the org's disabled ids of `provider` by `change` (`org.models.manage`, re-checked under
82
+ * the org lock): ids in neither list keep their stored state, so rows for models the org's page
83
+ * does not show (platform-disabled, or no longer listed) survive. Returns what in the org now
84
+ * points at a newly disabled model (its agents, its overrides and the deployment defaults it uses).
85
+ */
86
+ setOrg(orgId: string, provider: string, change: ModelDisableChange, actor: ModelDisableActor): Promise<ModelDisableResult>;
87
+ private apply;
88
+ /** Live agents (of `orgId`, or of every org) whose current version uses one of `models`. */
89
+ private agentsUsing;
90
+ /**
91
+ * Purpose mappings targeting one of `models`: for the platform every default and override, for
92
+ * an org its overrides and the deployment defaults (an org that disabled a default's model has
93
+ * that purpose unconfigured).
94
+ */
95
+ private purposesUsing;
96
+ }
97
+ /** The one {@link ModelEnablement}: the provider registry, purposes and the controllers read it. */
98
+ export declare class ModelEnablementModule {
99
+ }