@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,263 @@
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 { Inject, Injectable, Module } from "@nestjs/common";
11
+ import { ApiError } from "../errors.js";
12
+ import { ExtensionRegistry, ExtensionRegistryModule } from "../extensions/registry.js";
13
+ import { audit } from "../identity/audit.js";
14
+ import { SuperAdminLock, SuperAdminLockModule } from "../identity/super-admin.js";
15
+ import { CacheService } from "../infra/cache/cache.service.js";
16
+ import { Cacheable, CacheEvict } from "../infra/cache/cacheable.decorator.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 { OrgLock, OrgLockModule } from "../orgs/org-lock.js";
21
+ /** The key of a model in a disabled set: `provider\u0000model`. */
22
+ export const modelKey = (provider, model) => `${provider}\u0000${model}`;
23
+ /** How long a disabled list stays cached (s); every write evicts it after its commit. */
24
+ const DISABLED_TTL_SECONDS = 300;
25
+ /** The cache key of a scope's disabled list (`platform` or `org.<id>`). */
26
+ const PLATFORM_SCOPE = "platform";
27
+ const orgScope = (orgId) => `org.${orgId}`;
28
+ /**
29
+ * Models disabled on two layers (spec B §5.3, table `model_disabled`): the platform's list (the
30
+ * super admin; such a model is invisible to every org) and each org's (an org admin). Every model a
31
+ * provider lists is enabled unless a layer disabled it; a row keys on the model id, so it applies to
32
+ * whichever list the org sees, and an id the list does not contain is kept harmlessly. Disabling
33
+ * never creates an org config copy.
34
+ *
35
+ * The lists are read from PG and cached in Redis (`@Cacheable`, evicted after the commit of each
36
+ * write), never in process memory. `ProviderRegistry` filters its lists and `resolve` with them;
37
+ * purposes check their targets with {@link isEnabled}.
38
+ */
39
+ let ModelEnablement = class ModelEnablement {
40
+ kdb;
41
+ txHost;
42
+ cacheService;
43
+ extensions;
44
+ adminLock;
45
+ orgLock;
46
+ constructor(kdb, txHost, cacheService, extensions, adminLock, orgLock) {
47
+ this.kdb = kdb;
48
+ this.txHost = txHost;
49
+ this.cacheService = cacheService;
50
+ this.extensions = extensions;
51
+ this.adminLock = adminLock;
52
+ this.orgLock = orgLock;
53
+ }
54
+ get db() {
55
+ return this.kdb.db;
56
+ }
57
+ /** One layer's disabled models (keys {@link modelKey}): the platform's (`null`) or the org's own. */
58
+ async disabled(orgId) {
59
+ return new Set(await this.disabledKeys(orgId === null ? PLATFORM_SCOPE : orgScope(orgId)));
60
+ }
61
+ async disabledKeys(scope) {
62
+ const orgId = scope === PLATFORM_SCOPE ? null : scope.slice("org.".length);
63
+ const rows = await this.rowsOf(orgId);
64
+ return rows.map((r) => modelKey(r.provider, r.model)).sort();
65
+ }
66
+ rowsOf(orgId, provider) {
67
+ let q = this.db
68
+ .selectFrom("model_disabled")
69
+ .select(["provider", "model"])
70
+ .where((eb) => (orgId === null ? eb("org_id", "is", null) : eb("org_id", "=", orgId)));
71
+ if (provider !== undefined)
72
+ q = q.where("provider", "=", provider);
73
+ return q.execute();
74
+ }
75
+ /**
76
+ * The extensions `orgId` explicitly turned off: a row of the org's own switch
77
+ * (`extension_org_configs.enabled = false`, what the org's extension or plugin switch writes when
78
+ * an org admin turns it off), never a required bootstrap plugin. An extension with no row for the
79
+ * org is not in the set (it keeps working as before: spec A §8 with B's no-breakage anti-goal).
80
+ * Read from PG on every call (one small query; the switch has its own writers, nothing to evict).
81
+ */
82
+ async switchedOff(orgId) {
83
+ const rows = await this.db
84
+ .selectFrom("extension_org_configs as eoc")
85
+ .leftJoin("extension_installs as ri", (j) => j.onRef("ri.name", "=", "eoc.plugin").on("ri.required", "=", true))
86
+ .select("eoc.plugin")
87
+ .where("eoc.org_id", "=", orgId)
88
+ .where("eoc.enabled", "=", false)
89
+ .where("ri.name", "is", null)
90
+ .execute();
91
+ return new Set(rows.map((r) => r.plugin));
92
+ }
93
+ /** Whether neither the platform nor `orgId` disabled the model (the lists only; see the class). */
94
+ async isEnabled(orgId, provider, model) {
95
+ const key = modelKey(provider, model);
96
+ const [platform, org] = await Promise.all([this.disabled(null), this.disabled(orgId)]);
97
+ return !platform.has(key) && !org.has(key);
98
+ }
99
+ /**
100
+ * Changes the platform's disabled ids of `provider` by `change` (super admin, re-checked under
101
+ * the lock): ids in neither list keep their stored state. Returns what now points at a newly
102
+ * disabled model, across orgs.
103
+ */
104
+ async setPlatform(provider, change, actor) {
105
+ await this.adminLock.lockAndAuthorize(actor.accountId);
106
+ return this.apply(null, provider, change, actor);
107
+ }
108
+ /**
109
+ * Changes the org's disabled ids of `provider` by `change` (`org.models.manage`, re-checked under
110
+ * the org lock): ids in neither list keep their stored state, so rows for models the org's page
111
+ * does not show (platform-disabled, or no longer listed) survive. Returns what in the org now
112
+ * points at a newly disabled model (its agents, its overrides and the deployment defaults it uses).
113
+ */
114
+ async setOrg(orgId, provider, change, actor) {
115
+ await this.orgLock.lockAndAuthorize(orgId, actor.accountId, "org.models.manage");
116
+ return this.apply(orgId, provider, change, actor);
117
+ }
118
+ async apply(orgId, provider, change, actor) {
119
+ if (!this.extensions.list("model-provider").some((e) => e.plugin === provider)) {
120
+ throw new ApiError("model.provider_not_found", `Model provider ${provider} is not installed`);
121
+ }
122
+ const disable = new Set(change.disable);
123
+ const both = change.enable.filter((m) => disable.has(m));
124
+ if (both.length > 0) {
125
+ throw new ApiError("validation_failed", `A model cannot be both disabled and enabled: ${[...new Set(both)].join(", ")}`);
126
+ }
127
+ const had = new Set((await this.rowsOf(orgId, provider)).map((r) => r.model));
128
+ const added = [...disable].filter((m) => !had.has(m)).sort();
129
+ const removed = [...new Set(change.enable)].filter((m) => had.has(m)).sort();
130
+ if (removed.length > 0) {
131
+ await this.db
132
+ .deleteFrom("model_disabled")
133
+ .where((eb) => (orgId === null ? eb("org_id", "is", null) : eb("org_id", "=", orgId)))
134
+ .where("provider", "=", provider)
135
+ .where("model", "in", removed)
136
+ .execute();
137
+ }
138
+ if (added.length > 0) {
139
+ await this.db
140
+ .insertInto("model_disabled")
141
+ .values(added.map((model) => ({
142
+ org_id: orgId,
143
+ provider,
144
+ model,
145
+ disabled_by: actor.accountId,
146
+ })))
147
+ .execute();
148
+ }
149
+ if (added.length > 0 || removed.length > 0) {
150
+ await audit(this.db, {
151
+ actorAccountId: actor.accountId,
152
+ ...(actor.tokenId !== undefined && { actorTokenId: actor.tokenId }),
153
+ ...(orgId !== null && { orgId }),
154
+ action: orgId === null ? "model.disabled_set" : "org.model_disabled_set",
155
+ target: provider,
156
+ detail: {
157
+ scope: orgId === null ? "platform" : "org",
158
+ disabled: added,
159
+ enabled: removed,
160
+ },
161
+ });
162
+ }
163
+ return {
164
+ affected: {
165
+ agents: await this.agentsUsing(orgId, provider, added),
166
+ purposes: await this.purposesUsing(orgId, provider, added),
167
+ },
168
+ };
169
+ }
170
+ /** Live agents (of `orgId`, or of every org) whose current version uses one of `models`. */
171
+ async agentsUsing(orgId, provider, models) {
172
+ if (models.length === 0)
173
+ return [];
174
+ const wanted = new Set(models);
175
+ let q = this.db
176
+ .selectFrom("agents as a")
177
+ .innerJoin("agent_versions as v", (j) => j.onRef("v.agent_id", "=", "a.id").onRef("v.version", "=", "a.current_version"))
178
+ .select(["a.id", "a.org_id", "v.definition"])
179
+ .where("a.deleted_at", "is", null)
180
+ .orderBy("a.org_id")
181
+ .orderBy("a.id");
182
+ if (orgId !== null)
183
+ q = q.where("a.org_id", "=", orgId);
184
+ const out = [];
185
+ for (const r of await q.execute()) {
186
+ const def = r.definition;
187
+ const uses = [def?.model, def?.fallback].some((x) => {
188
+ const ref = x;
189
+ return ref?.provider === provider && typeof ref.model === "string" && wanted.has(ref.model);
190
+ });
191
+ if (uses) {
192
+ out.push({
193
+ id: r.id,
194
+ name: typeof def?.name === "string" ? def.name : r.id,
195
+ orgId: r.org_id,
196
+ });
197
+ }
198
+ }
199
+ return out;
200
+ }
201
+ /**
202
+ * Purpose mappings targeting one of `models`: for the platform every default and override, for
203
+ * an org its overrides and the deployment defaults (an org that disabled a default's model has
204
+ * that purpose unconfigured).
205
+ */
206
+ async purposesUsing(orgId, provider, models) {
207
+ if (models.length === 0)
208
+ return [];
209
+ let q = this.db
210
+ .selectFrom("model_purpose_mappings")
211
+ .select(["purpose", "org_id"])
212
+ .where("provider", "=", provider)
213
+ .where("model", "in", models)
214
+ .orderBy("purpose")
215
+ .orderBy("org_id", (ob) => ob.nullsFirst());
216
+ if (orgId === null) {
217
+ return (await q.execute()).map((r) => ({ purpose: r.purpose, orgId: r.org_id }));
218
+ }
219
+ q = q.where((eb) => eb.or([eb("org_id", "is", null), eb("org_id", "=", orgId)]));
220
+ const rows = await q.execute();
221
+ // a default the org overrides is not what the org uses
222
+ const overridden = new Set((await this.db
223
+ .selectFrom("model_purpose_mappings")
224
+ .select("purpose")
225
+ .where("org_id", "=", orgId)
226
+ .execute()).map((r) => r.purpose));
227
+ return rows
228
+ .filter((r) => r.org_id !== null || !overridden.has(r.purpose))
229
+ .map((r) => ({ purpose: r.purpose, orgId: r.org_id }));
230
+ }
231
+ };
232
+ __decorate([
233
+ Cacheable({ key: "capk:model-disabled:#{0}", ttl: DISABLED_TTL_SECONDS })
234
+ ], ModelEnablement.prototype, "disabledKeys", null);
235
+ __decorate([
236
+ Transactional(),
237
+ CacheEvict({ key: `capk:model-disabled:${PLATFORM_SCOPE}` })
238
+ ], ModelEnablement.prototype, "setPlatform", null);
239
+ __decorate([
240
+ Transactional(),
241
+ CacheEvict({ key: "capk:model-disabled:org.#{0}" })
242
+ ], ModelEnablement.prototype, "setOrg", null);
243
+ ModelEnablement = __decorate([
244
+ Injectable(),
245
+ __param(0, Inject(KernelDb)),
246
+ __param(1, Inject(TransactionHost)),
247
+ __param(2, Inject(CacheService)),
248
+ __param(3, Inject(ExtensionRegistry)),
249
+ __param(4, Inject(SuperAdminLock)),
250
+ __param(5, Inject(OrgLock))
251
+ ], ModelEnablement);
252
+ export { ModelEnablement };
253
+ /** The one {@link ModelEnablement}: the provider registry, purposes and the controllers read it. */
254
+ let ModelEnablementModule = class ModelEnablementModule {
255
+ };
256
+ ModelEnablementModule = __decorate([
257
+ Module({
258
+ imports: [ExtensionRegistryModule, SuperAdminLockModule, OrgLockModule],
259
+ providers: [ModelEnablement],
260
+ exports: [ModelEnablement],
261
+ })
262
+ ], ModelEnablementModule);
263
+ export { ModelEnablementModule };
@@ -1,3 +1,7 @@
1
- /** Model prices and model purposes. */
1
+ /**
2
+ * Model prices, model purposes, the model disabled lists and the `ctx.models` embeddings broker.
3
+ * Below the extension host (the host injects {@link ModelEmbeddings} for `ctx.models`): it must not
4
+ * import `ExtensionsModule`.
5
+ */
2
6
  export declare class ModelsModule {
3
7
  }
@@ -9,16 +9,28 @@ import { ExtensionRegistryModule } from "../extensions/registry.js";
9
9
  import { IdentityModule } from "../identity/identity.module.js";
10
10
  import { OrgLockModule } from "../orgs/org-lock.js";
11
11
  import { ProvidersModule } from "../runtime/providers.js";
12
+ import { ModelEmbeddings } from "./embeddings.js";
13
+ import { ModelEnablementModule } from "./enablement.js";
12
14
  import { PricesService } from "./prices.js";
13
15
  import { ModelPurposesService } from "./purposes.js";
14
- /** Model prices and model purposes. */
16
+ /**
17
+ * Model prices, model purposes, the model disabled lists and the `ctx.models` embeddings broker.
18
+ * Below the extension host (the host injects {@link ModelEmbeddings} for `ctx.models`): it must not
19
+ * import `ExtensionsModule`.
20
+ */
15
21
  let ModelsModule = class ModelsModule {
16
22
  };
17
23
  ModelsModule = __decorate([
18
24
  Module({
19
- imports: [ExtensionRegistryModule, IdentityModule, OrgLockModule, ProvidersModule],
20
- providers: [PricesService, ModelPurposesService],
21
- exports: [PricesService, ModelPurposesService],
25
+ imports: [
26
+ ExtensionRegistryModule,
27
+ IdentityModule,
28
+ OrgLockModule,
29
+ ProvidersModule,
30
+ ModelEnablementModule,
31
+ ],
32
+ providers: [PricesService, ModelPurposesService, ModelEmbeddings],
33
+ exports: [PricesService, ModelPurposesService, ModelEnablementModule, ModelEmbeddings],
22
34
  })
23
35
  ], ModelsModule);
24
36
  export { ModelsModule };
@@ -13,7 +13,9 @@ import { ProviderRegistry } from "../runtime/providers.js";
13
13
  * - 用途 = 内核用途 {@link KERNEL_PURPOSES}(目前只有评委 `eval`),或已加载插件 `permissions.models` 声明的;
14
14
  * - 映射 = 部署默认(`org_id` 为 null,超级管理员设)+ 组织覆盖(组织管理员设,`org.models.manage`);
15
15
  * 生效的映射先取组织覆盖、再取部署默认,都没有 → `model.unconfigured`;
16
- * - 目标模型按部署范围解析(与运行时一致),参数按目标模型自己的声明严格校验;不做模型白名单;
16
+ * - a target's kind must equal the purpose's kind; an org's override must be enabled in the org on
17
+ * save (a deployment default: not disabled on the platform), and an effective target disabled in
18
+ * the org (by either layer) counts as unconfigured there (spec B §5.3);参数按目标模型自己的声明严格校验;
17
19
  * - 每次都查 PG,不在进程内缓存(多副本下改映射立即生效)。`ctx.models` 仍预留,插件暂不能调用。
18
20
  */
19
21
  /**
@@ -139,7 +141,7 @@ export declare class ModelPurposesService {
139
141
  readonly txHost: TransactionHost;
140
142
  private readonly adminLock;
141
143
  private readonly orgLock;
142
- constructor(db: KernelDb, extensions: ExtensionRegistry, config: KernelConfigService, providers: Pick<ProviderRegistry, "resolve">, txHost: TransactionHost, adminLock: SuperAdminLock, orgLock: OrgLock);
144
+ constructor(db: KernelDb, extensions: ExtensionRegistry, config: KernelConfigService, providers: Pick<ProviderRegistry, "resolve" | "platformInfo">, txHost: TransactionHost, adminLock: SuperAdminLock, orgLock: OrgLock);
143
145
  /** 全部已声明的用途:内核的在前,其后按插件加载顺序。 */
144
146
  private declared;
145
147
  private find;
@@ -147,7 +149,10 @@ export declare class ModelPurposesService {
147
149
  private views;
148
150
  /** 全部用途与映射;不给 `orgId` = 部署级视图(`override` 一律为 null)。 */
149
151
  list(orgId?: string): Promise<PurposeView[]>;
150
- /** 校验目标:模型可解析(部署范围)、参数按它的声明严格通过。 */
152
+ /**
153
+ * 校验目标:模型可解析且启用(an org's override for the org, a deployment default in the
154
+ * platform view)、its kind is the purpose's `kind`、参数按它的声明严格通过。
155
+ */
151
156
  private check;
152
157
  private set;
153
158
  /** The mapping write of {@link set}, under the super admin or org lock. */
@@ -156,7 +161,16 @@ export declare class ModelPurposesService {
156
161
  setDefault(purpose: string, target: PurposeTarget | null, actor: string, actorTokenId?: string): Promise<PurposeView>;
157
162
  /** 设置(或 `null` 清除)组织覆盖;事务内锁组织并复核 `org.models.manage`。 */
158
163
  setOrg(orgId: string, purpose: string, target: PurposeTarget | null, actor: string, actorTokenId?: string): Promise<PurposeView>;
159
- /** 生效的映射:组织覆盖,否则部署默认;都没有 → `model.unconfigured`;用途未声明 → `model.purpose_unknown`。 */
164
+ /**
165
+ * 生效的映射:组织覆盖,否则部署默认;都没有 → `model.unconfigured`;用途未声明 → `model.purpose_unknown`。
166
+ * An effective target the org cannot use counts as unconfigured there (spec B §5.3): checked in
167
+ * the registry's org view, so a model the platform or the org disabled, a provider off or needing
168
+ * configuration in the org, a model gone from the org's list or of another kind than the purpose
169
+ * all give `model.unconfigured`. A provider whose list fails (`ProviderListError`) keeps the
170
+ * mapping: the call that follows resolves the model again and fails there, retryably, so paths
171
+ * that only read the mapping (sub-agent tiers, eval start, `ctx.models.describe`) do not fail on
172
+ * a transient list error.
173
+ */
160
174
  resolve(orgId: string, purpose: string): Promise<PurposeTarget>;
161
175
  /** The target a {@link chat} call on `purpose` uses: the mapping, else `fallback` when unmapped. */
162
176
  private chatTarget;
@@ -29,7 +29,9 @@ import { ProviderListError, ProviderRegistry } from "../runtime/providers.js";
29
29
  * - 用途 = 内核用途 {@link KERNEL_PURPOSES}(目前只有评委 `eval`),或已加载插件 `permissions.models` 声明的;
30
30
  * - 映射 = 部署默认(`org_id` 为 null,超级管理员设)+ 组织覆盖(组织管理员设,`org.models.manage`);
31
31
  * 生效的映射先取组织覆盖、再取部署默认,都没有 → `model.unconfigured`;
32
- * - 目标模型按部署范围解析(与运行时一致),参数按目标模型自己的声明严格校验;不做模型白名单;
32
+ * - a target's kind must equal the purpose's kind; an org's override must be enabled in the org on
33
+ * save (a deployment default: not disabled on the platform), and an effective target disabled in
34
+ * the org (by either layer) counts as unconfigured there (spec B §5.3);参数按目标模型自己的声明严格校验;
33
35
  * - 每次都查 PG,不在进程内缓存(多副本下改映射立即生效)。`ctx.models` 仍预留,插件暂不能调用。
34
36
  */
35
37
  /**
@@ -189,11 +191,17 @@ let ModelPurposesService = class ModelPurposesService {
189
191
  async list(orgId) {
190
192
  return this.views(this.declared(), orgId);
191
193
  }
192
- /** 校验目标:模型可解析(部署范围)、参数按它的声明严格通过。 */
193
- async check(target) {
194
+ /**
195
+ * 校验目标:模型可解析且启用(an org's override for the org, a deployment default in the
196
+ * platform view)、its kind is the purpose's `kind`、参数按它的声明严格通过。
197
+ */
198
+ async check(orgId, target, kind) {
194
199
  let info;
195
200
  try {
196
- ({ info } = await this.providers.resolve(target.model));
201
+ info =
202
+ orgId === null
203
+ ? await this.providers.platformInfo(target.model, { kind: "callable" })
204
+ : (await this.providers.resolve(orgId, target.model, { kind: "callable" })).info;
197
205
  }
198
206
  catch (e) {
199
207
  if (e instanceof ProviderListError)
@@ -203,6 +211,14 @@ let ModelPurposesService = class ModelPurposesService {
203
211
  }
204
212
  throw e;
205
213
  }
214
+ if (info.kind !== kind) {
215
+ throw invalid([
216
+ {
217
+ path: "target.model",
218
+ message: `this purpose needs ${kind === "embedding" ? "an" : "a"} ${kind} model, not ${info.kind}`,
219
+ },
220
+ ]);
221
+ }
206
222
  const issues = paramIssues("target.params", info.parameters, target.params);
207
223
  if (issues.length > 0)
208
224
  throw invalid(issues);
@@ -210,7 +226,7 @@ let ModelPurposesService = class ModelPurposesService {
210
226
  async set(orgId, purpose, target, actor, actorTokenId) {
211
227
  const decl = this.find(purpose);
212
228
  if (target)
213
- await this.check(target);
229
+ await this.check(orgId, target, decl.kind);
214
230
  await this.write(orgId, purpose, target, actor, actorTokenId);
215
231
  const [view] = await this.views([decl], orgId ?? undefined);
216
232
  return view;
@@ -260,12 +276,33 @@ let ModelPurposesService = class ModelPurposesService {
260
276
  setOrg(orgId, purpose, target, actor, actorTokenId) {
261
277
  return this.set(orgId, purpose, target, actor, actorTokenId);
262
278
  }
263
- /** 生效的映射:组织覆盖,否则部署默认;都没有 → `model.unconfigured`;用途未声明 → `model.purpose_unknown`。 */
279
+ /**
280
+ * 生效的映射:组织覆盖,否则部署默认;都没有 → `model.unconfigured`;用途未声明 → `model.purpose_unknown`。
281
+ * An effective target the org cannot use counts as unconfigured there (spec B §5.3): checked in
282
+ * the registry's org view, so a model the platform or the org disabled, a provider off or needing
283
+ * configuration in the org, a model gone from the org's list or of another kind than the purpose
284
+ * all give `model.unconfigured`. A provider whose list fails (`ProviderListError`) keeps the
285
+ * mapping: the call that follows resolves the model again and fails there, retryably, so paths
286
+ * that only read the mapping (sub-agent tiers, eval start, `ctx.models.describe`) do not fail on
287
+ * a transient list error.
288
+ */
264
289
  async resolve(orgId, purpose) {
265
- const [view] = await this.views([this.find(purpose)], orgId);
290
+ const decl = this.find(purpose);
291
+ const [view] = await this.views([decl], orgId);
266
292
  if (!view?.effective) {
267
293
  throw new ApiError("model.unconfigured", `No model is configured for the "${purpose}" purpose`);
268
294
  }
295
+ try {
296
+ await this.providers.resolve(orgId, view.effective.model, { kind: decl.kind });
297
+ }
298
+ catch (e) {
299
+ if (e instanceof ProviderListError)
300
+ return view.effective;
301
+ if (e instanceof ApiError && e.code === "agent.model_unavailable") {
302
+ throw new ApiError("model.unconfigured", `The model configured for the "${purpose}" purpose is not available in this organization`);
303
+ }
304
+ throw e;
305
+ }
269
306
  return view.effective;
270
307
  }
271
308
  /** The target a {@link chat} call on `purpose` uses: the mapping, else `fallback` when unmapped. */
@@ -286,7 +323,7 @@ let ModelPurposesService = class ModelPurposesService {
286
323
  */
287
324
  async chatLimits(orgId, purpose, opts = {}) {
288
325
  const target = await this.chatTarget(orgId, purpose, opts.fallback);
289
- const { info } = await this.providers.resolve(target.model);
326
+ const { info } = await this.providers.resolve(orgId, target.model);
290
327
  return { contextWindow: info.contextWindow, maxOutputTokens: info.maxOutputTokens };
291
328
  }
292
329
  /**
@@ -301,7 +338,7 @@ let ModelPurposesService = class ModelPurposesService {
301
338
  */
302
339
  async chat(orgId, purpose, req, signal, opts = {}) {
303
340
  const target = await this.chatTarget(orgId, purpose, opts.fallback);
304
- const { provider, info } = await this.providers.resolve(target.model);
341
+ const { provider, info } = await this.providers.resolve(orgId, target.model);
305
342
  const request = {
306
343
  ...req,
307
344
  model: info.id,
@@ -50,6 +50,12 @@ export const PLATFORM_EXCLUSIONS = [
50
50
  reason: "Platform model catalog read that an agent could mirror later",
51
51
  issue: "https://github.com/capybara-hq/core/issues/91",
52
52
  },
53
+ {
54
+ route: "PATCH /api/v1/admin/models/:provider/disabled",
55
+ category: "later",
56
+ reason: "Platform model disabled list write that an agent could mirror later behind confirmation",
57
+ issue: "https://github.com/capybara-hq/core/issues/91",
58
+ },
53
59
  {
54
60
  route: "GET /api/v1/admin/model-prices",
55
61
  category: "later",
@@ -193,6 +199,12 @@ export const PLATFORM_EXCLUSIONS = [
193
199
  reason: "Model list available to the org, readable by an agent later",
194
200
  issue: "https://github.com/capybara-hq/core/issues/91",
195
201
  },
202
+ {
203
+ route: "GET /api/v1/orgs/:orgId/models/catalog",
204
+ category: "later",
205
+ reason: "Org model catalog with enablement, readable by an agent later",
206
+ issue: "https://github.com/capybara-hq/core/issues/91",
207
+ },
196
208
  {
197
209
  route: "POST /api/v1/orgs/:orgId/agents/:agentId/pending-connectors/:key/bind",
198
210
  category: "later",
@@ -1048,6 +1060,12 @@ export const PLATFORM_EXCLUSIONS = [
1048
1060
  reason: "Org model per purpose, settable by an agent later",
1049
1061
  issue: "https://github.com/capybara-hq/core/issues/91",
1050
1062
  },
1063
+ {
1064
+ route: "PATCH /api/v1/orgs/:orgId/models/:provider/disabled",
1065
+ category: "later",
1066
+ reason: "Org model disabled list write that an agent could mirror later behind confirmation",
1067
+ issue: "https://github.com/capybara-hq/core/issues/91",
1068
+ },
1051
1069
  // orgs
1052
1070
  {
1053
1071
  route: "POST /api/v1/orgs/:orgId/extensions/:extension/directory/sync",
@@ -1,4 +1,4 @@
1
- import type { AvailableHook, HookModelRequest, HookParentScope, HookPayloads, HookScope, Logger, ModelRequest } from "@capybara-hq/plugin-sdk";
1
+ import type { AvailableHook, EmbeddingRequestHookPayload, HookModelRequest, HookParentScope, HookPayloads, HookScope, Logger, ModelRequest } from "@capybara-hq/plugin-sdk";
2
2
  import type { AppendEvent, EventLog } from "../conversations/event-log.js";
3
3
  import type { EventPayload } from "../conversations/events.js";
4
4
  import type { HookOutcome, HookRegistry } from "../extensions/hooks.js";
@@ -67,6 +67,13 @@ export declare function hookDeniedEvent(at: DenialSite, mount: Extract<Available
67
67
  * and that the named plugin is only the first policy plugin on the mount.
68
68
  */
69
69
  export declare function hookDeniedAudit(at: DenialSite, payload: EventPayload<"hook_denied">, denial: HookDenial): AuditEntry;
70
+ /**
71
+ * The `hook.denied` audit entry of a denied embedding call (`ctx.models.embed`, spec B §5.4): no
72
+ * actor and no conversation; the target is the calling extension. The detail has the same fields
73
+ * as a conversation denial's (mount, plugin, sanitized reason, cause, raw error, markers) plus the
74
+ * call's purpose, extension, embedding purpose and model.
75
+ */
76
+ export declare function embeddingDeniedAudit(call: Pick<EmbeddingRequestHookPayload, "orgId" | "extension" | "embeddingPurpose" | "provider" | "model">, denial: HookDenial): AuditEntry;
70
77
  /** Audit entry shape (the argument of `audit()`). */
71
78
  export type AuditEntry = Parameters<typeof audit>[1];
72
79
  /**
@@ -105,15 +105,46 @@ export function hookDeniedAudit(at, payload, denial) {
105
105
  conversationId: at.conversationId,
106
106
  runId: at.runId,
107
107
  ...(at.agentId !== undefined && { agentId: at.agentId }),
108
- // the raw hook error: audit only, never in the conversation event (Ruling R12)
109
- ...(denial.detail !== undefined && { error: denial.detail }),
110
- ...(denial.preflight
111
- ? { preflight: true, firstPolicyPlugin: denial.plugin, causedByPlugin: false }
112
- : {}),
113
- // M-1: the named plugin is enabled but not loaded on the replica that handled the call
114
- ...(denial.notLoaded ? { notLoaded: true } : {}),
115
- // layering spec A §6: the named plugin's effective config is not valid in the organization
116
- ...(denial.needsConfig ? { needsConfig: true } : {}),
108
+ ...denialAuditDetail(denial),
109
+ },
110
+ };
111
+ }
112
+ /** The denial's audit-only fields: the raw hook error and the R10 / M-1 / spec A §6 markers. */
113
+ function denialAuditDetail(denial) {
114
+ return {
115
+ // the raw hook error: audit only, never in the conversation event (Ruling R12)
116
+ ...(denial.detail !== undefined && { error: denial.detail }),
117
+ ...(denial.preflight
118
+ ? { preflight: true, firstPolicyPlugin: denial.plugin, causedByPlugin: false }
119
+ : {}),
120
+ // M-1: the named plugin is enabled but not loaded on the replica that handled the call
121
+ ...(denial.notLoaded ? { notLoaded: true } : {}),
122
+ // layering spec A §6: the named plugin's effective config is not valid in the organization
123
+ ...(denial.needsConfig ? { needsConfig: true } : {}),
124
+ };
125
+ }
126
+ /**
127
+ * The `hook.denied` audit entry of a denied embedding call (`ctx.models.embed`, spec B §5.4): no
128
+ * actor and no conversation; the target is the calling extension. The detail has the same fields
129
+ * as a conversation denial's (mount, plugin, sanitized reason, cause, raw error, markers) plus the
130
+ * call's purpose, extension, embedding purpose and model.
131
+ */
132
+ export function embeddingDeniedAudit(call, denial) {
133
+ return {
134
+ orgId: call.orgId,
135
+ action: "hook.denied",
136
+ target: call.extension,
137
+ detail: {
138
+ mount: "model.request",
139
+ plugin: denial.plugin,
140
+ reason: denial.reason,
141
+ cause: denial.cause,
142
+ purpose: "embedding",
143
+ extension: call.extension,
144
+ embeddingPurpose: call.embeddingPurpose,
145
+ provider: call.provider,
146
+ model: call.model,
147
+ ...denialAuditDetail(denial),
117
148
  },
118
149
  };
119
150
  }
@@ -1,4 +1,4 @@
1
- import { type ContentBlock, type Logger, type ModelProvider, type ModelRequest, type ModelStreamEvent, type ModelUsage, type StopReason } from "@capybara-hq/plugin-sdk";
1
+ import { type ContentBlock, type Logger, type ModelRequest, type ModelStreamEvent, type ModelUsage, type StopReason } from "@capybara-hq/plugin-sdk";
2
2
  import type { AgentDefinition } from "../agents/definition.js";
3
3
  import type { AppendEvent, EventLog } from "../conversations/event-log.js";
4
4
  import { type ConversationEvent } from "../conversations/events.js";
@@ -9,7 +9,7 @@ import { type BuildModelRequestInput, type MediaBytes, type MediaInfo, type Mode
9
9
  import { type ConversationKindForHooks, type HookRunner } from "./hook-calls.js";
10
10
  import type { SubagentParent } from "./loop.js";
11
11
  import { MediaCache } from "./media-cache.js";
12
- import { type ProviderRegistry } from "./providers.js";
12
+ import { type BoundProvider, type ProviderRegistry } from "./providers.js";
13
13
  import type { StepLog } from "./step-log.js";
14
14
  import type { UsageReporter } from "./usage-report.js";
15
15
  /** 单轮收尾的尾部提醒(模型可见,英文;spec 6 轮次额度收尾)。 */
@@ -28,6 +28,11 @@ export declare const TOOLS_UNAVAILABLE_PREFIX = "The following tools are unavail
28
28
  export declare const TOOLS_AVAILABLE_AGAIN = "All previously unavailable tools are available again.";
29
29
  /** 不可用工具提醒:`The following tools are unavailable in this run: <ref> (<reason>), ...`。 */
30
30
  export declare function toolsUnavailableReminder(unavailable: readonly UnavailableTool[]): string;
31
+ /**
32
+ * `run_failed` message of a model request denied by a policy hook (user-facing, never model-visible);
33
+ * also the message of the `model.hook_denied` error a denied `ctx.models.embed` call rejects with.
34
+ */
35
+ export declare const modelDeniedText: (plugin: string, reason: string) => string;
31
36
  /**
32
37
  * 流式增量的出口(推给前端等订阅方);`step_reset` 表示丢弃本步已推出的增量、重新开始。
33
38
  * `stream_end` (the step's model stream completed) and `step_end` (the step's `assistant_message`
@@ -212,6 +217,6 @@ export declare function assembleModelRequest(input: AssembleModelRequestInput):
212
217
  * 外部 `signal` 触发 → `StepAborted`。每次 `next()` 都与停止信号竞速,供应商不响应 signal 也不会卡住。
213
218
  * 运行时与评测单步回放(`evals/step-replay.ts`)、评委共用。
214
219
  */
215
- export declare function consumeModelStream(provider: ModelProvider, request: ModelRequest, idleMs: number, signal: AbortSignal, onDelta: (ev: Exclude<ModelStreamEvent, {
220
+ export declare function consumeModelStream(provider: BoundProvider, request: ModelRequest, idleMs: number, signal: AbortSignal, onDelta: (ev: Exclude<ModelStreamEvent, {
216
221
  type: "message_complete";
217
222
  }>) => void): Promise<StreamOutcome>;