@happyvertical/smrt-inference 0.0.0-stage → 0.60.15

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 (107) hide show
  1. package/AGENTS.md +201 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +73 -2
  5. package/agents/fusions.md +92 -0
  6. package/agents/hosted.md +145 -0
  7. package/agents/resolver.md +65 -0
  8. package/agents/settings.md +84 -0
  9. package/dist/__smrt-register__.d.ts +2 -0
  10. package/dist/__smrt-register__.d.ts.map +1 -0
  11. package/dist/chunks/InferenceOverrideCollection-wl7sI-3C.js +398 -0
  12. package/dist/chunks/InferenceOverrideCollection-wl7sI-3C.js.map +1 -0
  13. package/dist/chunks/provider-config-4Bx8N52X.js +1847 -0
  14. package/dist/chunks/provider-config-4Bx8N52X.js.map +1 -0
  15. package/dist/classify.d.ts +32 -0
  16. package/dist/classify.d.ts.map +1 -0
  17. package/dist/config.d.ts +89 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/errors.d.ts +51 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/executor.d.ts +45 -0
  22. package/dist/executor.d.ts.map +1 -0
  23. package/dist/fusion-store.d.ts +16 -0
  24. package/dist/fusion-store.d.ts.map +1 -0
  25. package/dist/fusion.d.ts +155 -0
  26. package/dist/fusion.d.ts.map +1 -0
  27. package/dist/gate.d.ts +143 -0
  28. package/dist/gate.d.ts.map +1 -0
  29. package/dist/gate.js +94 -0
  30. package/dist/gate.js.map +1 -0
  31. package/dist/hosted.d.ts +454 -0
  32. package/dist/hosted.d.ts.map +1 -0
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +8 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/manifest.json +453 -0
  38. package/dist/models/InferenceOverride.d.ts +38 -0
  39. package/dist/models/InferenceOverride.d.ts.map +1 -0
  40. package/dist/models/InferenceOverrideCollection.d.ts +35 -0
  41. package/dist/models/InferenceOverrideCollection.d.ts.map +1 -0
  42. package/dist/models.d.ts +11 -0
  43. package/dist/models.d.ts.map +1 -0
  44. package/dist/models.js +2 -0
  45. package/dist/node.d.ts +110 -0
  46. package/dist/node.d.ts.map +1 -0
  47. package/dist/node.js +1361 -0
  48. package/dist/node.js.map +1 -0
  49. package/dist/provider-config.d.ts +113 -0
  50. package/dist/provider-config.d.ts.map +1 -0
  51. package/dist/register.d.ts +30 -0
  52. package/dist/register.d.ts.map +1 -0
  53. package/dist/resolver.d.ts +166 -0
  54. package/dist/resolver.d.ts.map +1 -0
  55. package/dist/resolver.js +288 -0
  56. package/dist/resolver.js.map +1 -0
  57. package/dist/server.d.ts +24 -0
  58. package/dist/server.d.ts.map +1 -0
  59. package/dist/server.js +985 -0
  60. package/dist/server.js.map +1 -0
  61. package/dist/settings/cache.d.ts +5 -0
  62. package/dist/settings/cache.d.ts.map +1 -0
  63. package/dist/settings/keys.d.ts +70 -0
  64. package/dist/settings/keys.d.ts.map +1 -0
  65. package/dist/settings/layers.d.ts +54 -0
  66. package/dist/settings/layers.d.ts.map +1 -0
  67. package/dist/settings/override-service.d.ts +119 -0
  68. package/dist/settings/override-service.d.ts.map +1 -0
  69. package/dist/settings/preferences.d.ts +137 -0
  70. package/dist/settings/preferences.d.ts.map +1 -0
  71. package/dist/settings/resolver.d.ts +34 -0
  72. package/dist/settings/resolver.d.ts.map +1 -0
  73. package/dist/settings/settings-service.d.ts +282 -0
  74. package/dist/settings/settings-service.d.ts.map +1 -0
  75. package/dist/settings/targets.d.ts +93 -0
  76. package/dist/settings/targets.d.ts.map +1 -0
  77. package/dist/smrt-knowledge.json +335 -0
  78. package/dist/smrt-knowledge.okf.md +80 -0
  79. package/dist/svelte/components/InferenceAliasEditor.svelte +210 -0
  80. package/dist/svelte/components/InferenceAliasEditor.svelte.d.ts +13 -0
  81. package/dist/svelte/components/InferenceAliasEditor.svelte.d.ts.map +1 -0
  82. package/dist/svelte/components/InferenceDefaultView.svelte +330 -0
  83. package/dist/svelte/components/InferenceDefaultView.svelte.d.ts +15 -0
  84. package/dist/svelte/components/InferenceDefaultView.svelte.d.ts.map +1 -0
  85. package/dist/svelte/components/InferenceFusionList.svelte +135 -0
  86. package/dist/svelte/components/InferenceFusionList.svelte.d.ts +16 -0
  87. package/dist/svelte/components/InferenceFusionList.svelte.d.ts.map +1 -0
  88. package/dist/svelte/components/InferenceSettingsPanel.svelte +345 -0
  89. package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts +65 -0
  90. package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts.map +1 -0
  91. package/dist/svelte/components/InferenceTierRow.svelte +199 -0
  92. package/dist/svelte/components/InferenceTierRow.svelte.d.ts +14 -0
  93. package/dist/svelte/components/InferenceTierRow.svelte.d.ts.map +1 -0
  94. package/dist/svelte/components/InferenceUsageTable.svelte +127 -0
  95. package/dist/svelte/components/InferenceUsageTable.svelte.d.ts +15 -0
  96. package/dist/svelte/components/InferenceUsageTable.svelte.d.ts.map +1 -0
  97. package/dist/svelte/index.d.ts +17 -0
  98. package/dist/svelte/index.d.ts.map +1 -0
  99. package/dist/svelte/index.js +14 -0
  100. package/dist/svelte/types.d.ts +197 -0
  101. package/dist/svelte/types.d.ts.map +1 -0
  102. package/dist/svelte/types.js +96 -0
  103. package/dist/types.d.ts +47 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +13 -0
  106. package/dist/types.js.map +1 -0
  107. package/package.json +105 -3
package/dist/server.js ADDED
@@ -0,0 +1,985 @@
1
+ import { F as findInferenceConfigIssues, R as validateInferenceTarget, b as listFusions, y as getFusion } from "./chunks/provider-config-4Bx8N52X.js";
2
+ import { checkInferenceBindings, inferenceProbeUrl, probeInferenceCandidate } from "./node.js";
3
+ import { _ as normalizeTierTarget, a as INFERENCE_BINDING_NAME_PATTERN, b as tierBinding, c as describeInferenceTarget, d as isAllowedByList, f as matchesAllowPattern, g as normalizeFusionDefinition, h as normalizeDefaultTier, i as DEFAULT_TIER_BINDING, l as findKeyMember, m as normalizeAllowList, n as APP_INFERENCE_SCOPE_ID, o as INFERENCE_MODES, p as normalizeAliasDefinition, r as InferenceOverride, s as InferenceBindingValueError, t as InferenceOverrideCollection, u as inferenceTargetRef, v as parseInferenceBinding, x as getInferenceOverrideGeneration, y as stripKeyMembers } from "./chunks/InferenceOverrideCollection-wl7sI-3C.js";
4
+ import { INFERENCE_PRESETS, INFERENCE_PRESET_IDS, INFERENCE_TIERS, getInferenceConfig, inferenceTargetKind, isInferencePresetId, isInferenceTier, mergeInferenceConfigs, reportAiUsage, resolveAutoTier } from "@happyvertical/smrt-core";
5
+ import { AI_PROVIDER_TYPES } from "@happyvertical/ai";
6
+ import { getDefaultAIKeyEnvName } from "@happyvertical/smrt-config";
7
+ //#region src/settings/keys.ts
8
+ var InferenceKeyOriginError = class extends Error {
9
+ status = 400;
10
+ constructor(reason) {
11
+ super(`Cannot bind the key: ${reason}`);
12
+ this.name = "InferenceKeyOriginError";
13
+ }
14
+ };
15
+ var LOOPBACK = /* @__PURE__ */ new Set([
16
+ "localhost",
17
+ "127.0.0.1",
18
+ "[::1]",
19
+ "::1"
20
+ ]);
21
+ var SDK_DEFAULT_BASE = {
22
+ anthropic: "https://api.anthropic.com",
23
+ gemini: "https://generativelanguage.googleapis.com"
24
+ };
25
+ var NEEDS_BASE_URL = /* @__PURE__ */ new Set(["litellm", "bifrost"]);
26
+ function text(value) {
27
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
28
+ }
29
+ function originOf(url) {
30
+ let parsed;
31
+ try {
32
+ parsed = new URL(url);
33
+ } catch {
34
+ throw new InferenceKeyOriginError("the base URL is not a URL");
35
+ }
36
+ if (parsed.username || parsed.password) throw new InferenceKeyOriginError("the base URL carries credentials");
37
+ if (parsed.protocol === "https:") return parsed.origin;
38
+ if (parsed.protocol === "http:" && LOOPBACK.has(parsed.hostname)) return parsed.origin;
39
+ throw new InferenceKeyOriginError("a key travels only over https (plain http is allowed for loopback servers)");
40
+ }
41
+ function inferenceKeyOrigin(input) {
42
+ const baseUrl = text(input.baseUrl);
43
+ if (baseUrl) return originOf(baseUrl);
44
+ const provider = text(input.provider)?.toLowerCase();
45
+ const preset = text(input.preset) ?? (provider && isInferencePresetId(provider) ? provider : void 0);
46
+ if (!preset && (!provider || NEEDS_BASE_URL.has(provider))) return null;
47
+ if (preset && !isInferencePresetId(preset)) return null;
48
+ const url = inferenceProbeUrl(preset ? { preset } : { provider });
49
+ return url ? originOf(url) : null;
50
+ }
51
+ function bindInferenceKey(input) {
52
+ const provider = text(input.provider);
53
+ const apiKey = text(input.key);
54
+ if (!provider) throw new InferenceKeyOriginError("no provider");
55
+ if (!apiKey) throw new InferenceKeyOriginError("the key is empty");
56
+ const origin = inferenceKeyOrigin(input);
57
+ if (!origin) throw new InferenceKeyOriginError(`no endpoint is known for "${provider}"; enter its base URL first`);
58
+ const record = {
59
+ provider,
60
+ origin,
61
+ apiKey
62
+ };
63
+ const preset = text(input.preset);
64
+ const baseUrl = text(input.baseUrl);
65
+ if (preset) record.preset = preset;
66
+ if (baseUrl) record.baseUrl = baseUrl;
67
+ return record;
68
+ }
69
+ function inferenceProviderConfigFromKey(record, choice = {}) {
70
+ const model = text(choice.model);
71
+ const preset = text(record.preset) ?? (isInferencePresetId(record.provider) ? record.provider : void 0);
72
+ if (preset) {
73
+ if (!model) throw new InferenceKeyOriginError("a preset key needs a model");
74
+ const custom = text(record.baseUrl);
75
+ const presetUrl = isInferencePresetId(preset) ? inferenceProbeUrl({ preset }) : void 0;
76
+ const endpoint = custom ?? presetUrl;
77
+ if (!endpoint || originOf(endpoint) !== record.origin) throw new InferenceKeyOriginError(`no endpoint on ${record.origin} is known for preset "${preset}"`);
78
+ if (custom && (!presetUrl || originOf(presetUrl) !== record.origin)) return {
79
+ provider: "openai",
80
+ baseUrl: custom,
81
+ model,
82
+ apiKey: record.apiKey
83
+ };
84
+ return {
85
+ preset,
86
+ model,
87
+ apiKey: record.apiKey
88
+ };
89
+ }
90
+ let baseUrl = text(record.baseUrl);
91
+ if (!baseUrl) {
92
+ const provider = record.provider.toLowerCase();
93
+ const url = inferenceProbeUrl({ provider });
94
+ baseUrl = Object.hasOwn(SDK_DEFAULT_BASE, provider) ? SDK_DEFAULT_BASE[provider] : url?.replace(/\/(?:models|api\/tags)$/, "");
95
+ }
96
+ if (!baseUrl || originOf(baseUrl) !== record.origin) throw new InferenceKeyOriginError(`no endpoint on ${record.origin} is known for "${record.provider}"; enter its base URL`);
97
+ return {
98
+ provider: record.provider,
99
+ baseUrl,
100
+ ...model ? { model } : {},
101
+ apiKey: record.apiKey
102
+ };
103
+ }
104
+ function inferenceKeyFor(record, url) {
105
+ if (!record?.apiKey || !record.origin) return void 0;
106
+ try {
107
+ return new URL(url).origin === record.origin ? record.apiKey : void 0;
108
+ } catch {
109
+ return;
110
+ }
111
+ }
112
+ var REDACTED_KEY = "[redacted]";
113
+ function redactInferenceSecret(text2, secret) {
114
+ let out = text2;
115
+ if (secret && secret.length >= 4) out = out.split(secret).join(REDACTED_KEY);
116
+ return out.replace(/\b(sk|rk|pk)-[A-Za-z0-9_-]{8,}/g, REDACTED_KEY).replace(/\b(Bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi, `$1 ${REDACTED_KEY}`);
117
+ }
118
+ //#endregion
119
+ //#region src/settings/override-service.ts
120
+ var InferenceOverrideAuthorizationError = class extends Error {
121
+ constructor(request) {
122
+ super(`Inference override ${request.operation} denied for ${request.scopeType} scope`);
123
+ this.request = request;
124
+ this.name = "InferenceOverrideAuthorizationError";
125
+ }
126
+ request;
127
+ status = 403;
128
+ };
129
+ var InferenceTenantTargetNotAllowedError = class extends Error {
130
+ constructor(binding, ref) {
131
+ super(`Tenants may not bind ${binding} to ${ref}: not on the tenant allow-list`);
132
+ this.binding = binding;
133
+ this.ref = ref;
134
+ this.name = "InferenceTenantTargetNotAllowedError";
135
+ }
136
+ binding;
137
+ ref;
138
+ status = 403;
139
+ };
140
+ var InvalidInferenceScopeError = class extends Error {
141
+ constructor(scopeType, scopeId, reason) {
142
+ super(`Invalid inference override scope (${scopeType}): ${reason}`);
143
+ this.scopeType = scopeType;
144
+ this.scopeId = scopeId;
145
+ this.name = "InvalidInferenceScopeError";
146
+ }
147
+ scopeType;
148
+ scopeId;
149
+ status = 400;
150
+ };
151
+ function assertValidInferenceScope(scopeType, scopeId) {
152
+ if (scopeType !== "app" && scopeType !== "tenant") throw new InvalidInferenceScopeError(String(scopeType), String(scopeId), "scope type must be \"app\" or \"tenant\"");
153
+ if (typeof scopeId !== "string") throw new InvalidInferenceScopeError(scopeType, String(scopeId), "scope id must be a string");
154
+ if (scopeType === "app" && scopeId !== "__app__") throw new InvalidInferenceScopeError(scopeType, scopeId, `the app scope is only ever "${APP_INFERENCE_SCOPE_ID}"`);
155
+ if (scopeType === "tenant" && (!scopeId.trim() || scopeId !== scopeId.trim() || scopeId === "__app__")) throw new InvalidInferenceScopeError(scopeType, scopeId, "a tenant scope id must be non-blank, already trimmed, and not the app scope id");
156
+ }
157
+ function normalizeBindingValue(binding, value) {
158
+ const parsed = parseInferenceBinding(binding);
159
+ if (!parsed) throw new InferenceBindingValueError(String(binding), "unknown binding id");
160
+ switch (parsed.kind) {
161
+ case "tier": return normalizeTierTarget(binding, value);
162
+ case "default-tier": return normalizeDefaultTier(binding, value);
163
+ case "alias": return normalizeAliasDefinition(binding, value);
164
+ case "fusion": return normalizeFusionDefinition(binding, value);
165
+ }
166
+ }
167
+ function bindingValueRef(binding, value) {
168
+ if (binding === "default-tier") return `tier:${String(value)}`;
169
+ return inferenceTargetRef(value);
170
+ }
171
+ async function isTenantValueAllowed(binding, value, tenantId, allowList, policy) {
172
+ const ref = bindingValueRef(binding, value);
173
+ if (!isAllowedByList(ref, allowList)) return false;
174
+ if (!policy) return true;
175
+ const parsed = parseInferenceBinding(binding);
176
+ try {
177
+ return await policy({
178
+ binding,
179
+ tier: parsed?.kind === "tier" ? parsed.name : void 0,
180
+ ref,
181
+ value,
182
+ tenantId
183
+ }) === true;
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+ var InferenceOverrideService = class {
189
+ constructor(overrides, authorize, options = {}) {
190
+ this.overrides = overrides;
191
+ this.authorize = authorize;
192
+ this.options = options;
193
+ }
194
+ overrides;
195
+ authorize;
196
+ options;
197
+ /**
198
+ * Set (or, with `null`, revert) one binding at one scope. Tenants may write
199
+ * only `tier:*` and `default-tier`, and only an allow-listed target;
200
+ * reverting is always allowed.
201
+ *
202
+ * @throws {InvalidInferenceScopeError} for a scope nothing reads.
203
+ * @throws {InferenceBindingValueError} for an invalid binding or value.
204
+ * @throws {InferenceOverrideAuthorizationError} when the authorizer declines.
205
+ * @throws {InferenceTenantTargetNotAllowedError} when the allow-list refuses.
206
+ */
207
+ async setValue(binding, scopeType, scopeId, value, options = {}) {
208
+ assertValidInferenceScope(scopeType, scopeId);
209
+ const normalized = normalizeBindingValue(binding, value);
210
+ const parsed = parseInferenceBinding(binding);
211
+ if (scopeType === "tenant" && (parsed?.kind === "alias" || parsed?.kind === "fusion")) throw new InferenceBindingValueError(binding, "aliases and fusions are defined at the app scope only");
212
+ await this.require({
213
+ operation: normalized === null ? "remove" : "set",
214
+ binding,
215
+ scopeType,
216
+ scopeId,
217
+ value: normalized
218
+ });
219
+ if (scopeType === "tenant" && normalized !== null) {
220
+ if (!await isTenantValueAllowed(binding, normalized, scopeId, (await this.overrides.findByScope(binding, "app", "__app__"))?.getTenantAllowList() ?? null, this.options.tenantTargetPolicy)) throw new InferenceTenantTargetNotAllowedError(binding, bindingValueRef(binding, normalized));
221
+ }
222
+ await options.beforeWrite?.(normalized);
223
+ await this.overrides.setOverride(binding, scopeType, scopeId, { target: normalized });
224
+ }
225
+ /**
226
+ * Set (or, with `null`, remove) the tenant allow-list of a `tier:*` or
227
+ * `default-tier` binding. The list lives on the app row.
228
+ */
229
+ async setTenantAllowList(binding, allowList) {
230
+ const parsed = parseInferenceBinding(binding);
231
+ if (!parsed || parsed.kind !== "tier" && parsed.kind !== "default-tier") throw new InferenceBindingValueError(String(binding), "only tier and default-tier bindings carry a tenant allow-list");
232
+ const normalized = normalizeAllowList(binding, allowList);
233
+ await this.require({
234
+ operation: "allow-list",
235
+ binding,
236
+ scopeType: "app",
237
+ scopeId: APP_INFERENCE_SCOPE_ID,
238
+ tenantAllowList: normalized
239
+ });
240
+ await this.overrides.setOverride(binding, "app", APP_INFERENCE_SCOPE_ID, { tenantAllowList: normalized });
241
+ }
242
+ /** Delete the whole row (value and, on the app row, the allow-list). */
243
+ async removeOverride(binding, scopeType, scopeId) {
244
+ assertValidInferenceScope(scopeType, scopeId);
245
+ if (!parseInferenceBinding(binding)) throw new InferenceBindingValueError(String(binding), "unknown binding id");
246
+ await this.require({
247
+ operation: "remove",
248
+ binding,
249
+ scopeType,
250
+ scopeId
251
+ });
252
+ return this.overrides.removeOverride(binding, scopeType, scopeId);
253
+ }
254
+ async require(request) {
255
+ let allowed = false;
256
+ try {
257
+ allowed = await this.authorize(request) === true;
258
+ } catch {
259
+ allowed = false;
260
+ }
261
+ if (!allowed) throw new InferenceOverrideAuthorizationError(request);
262
+ }
263
+ };
264
+ //#endregion
265
+ //#region src/settings/layers.ts
266
+ function mergeSettingsConfigs(...layers) {
267
+ return mergeInferenceConfigs(...layers);
268
+ }
269
+ function layerFromRows(rows, include) {
270
+ const layer = {};
271
+ for (const [binding, row] of rows) {
272
+ if (!include(binding)) continue;
273
+ const parsed = parseInferenceBinding(binding);
274
+ if (!parsed) continue;
275
+ let value;
276
+ try {
277
+ value = row.getValue();
278
+ } catch {
279
+ continue;
280
+ }
281
+ if (value === null) continue;
282
+ switch (parsed.kind) {
283
+ case "tier":
284
+ layer.tiers ??= {};
285
+ layer.tiers[parsed.name] = value;
286
+ break;
287
+ case "default-tier":
288
+ layer.defaultTier = value;
289
+ break;
290
+ case "alias":
291
+ layer.aliases ??= {};
292
+ layer.aliases[parsed.name] = value;
293
+ break;
294
+ case "fusion":
295
+ layer.fusions ??= {};
296
+ layer.fusions[parsed.name] = value;
297
+ break;
298
+ }
299
+ }
300
+ return layer;
301
+ }
302
+ async function readInferenceOverrideRows(overrides, tenantId) {
303
+ const tenant = tenantId ?? null;
304
+ const [appRows, tenantRows] = await Promise.all([overrides.getScopeMap("app", APP_INFERENCE_SCOPE_ID), tenant ? overrides.getScopeMap("tenant", tenant) : Promise.resolve(/* @__PURE__ */ new Map())]);
305
+ return {
306
+ tenantId: tenant,
307
+ appRows,
308
+ tenantRows
309
+ };
310
+ }
311
+ async function loadInferenceOverrideLayers(overrides, options = {}) {
312
+ return buildInferenceOverrideLayers(await readInferenceOverrideRows(overrides, options.tenantId), options.tenantTargetPolicy);
313
+ }
314
+ async function buildInferenceOverrideLayers(rows, tenantTargetPolicy) {
315
+ const { tenantId, appRows, tenantRows } = rows;
316
+ const options = { tenantTargetPolicy };
317
+ const app = layerFromRows(appRows, () => true);
318
+ const blocked = [];
319
+ const allowedTenant = /* @__PURE__ */ new Set();
320
+ if (tenantId) for (const [binding, row] of tenantRows) {
321
+ const parsed = parseInferenceBinding(binding);
322
+ if (parsed?.kind !== "tier" && parsed?.kind !== "default-tier") continue;
323
+ let value;
324
+ try {
325
+ value = row.getValue();
326
+ } catch {
327
+ continue;
328
+ }
329
+ if (value === null) continue;
330
+ let allowList = null;
331
+ try {
332
+ allowList = appRows.get(binding)?.getTenantAllowList() ?? null;
333
+ } catch {
334
+ allowList = null;
335
+ }
336
+ if (await isTenantValueAllowed(binding, value, tenantId, allowList, options.tenantTargetPolicy)) allowedTenant.add(binding);
337
+ else blocked.push(binding);
338
+ }
339
+ return {
340
+ app,
341
+ tenant: layerFromRows(tenantRows, (binding) => allowedTenant.has(binding)),
342
+ blockedTenantBindings: blocked.sort(),
343
+ appRows,
344
+ tenantRows
345
+ };
346
+ }
347
+ //#endregion
348
+ //#region src/settings/preferences.ts
349
+ var INFERENCE_PREFERENCE_KIND = "inference";
350
+ var INFERENCE_PREFERENCE_SURFACE = "default";
351
+ var CUSTOMIZE_INFERENCE_PERMISSION = "inference.customize";
352
+ var PERSONALIZE_INFERENCE_PERMISSION = "inference.personalize";
353
+ var INFERENCE_PERMISSION_DEFINITIONS = [{
354
+ slug: CUSTOMIZE_INFERENCE_PERMISSION,
355
+ category: "inference",
356
+ name: "Customize Inference",
357
+ description: "Change the default inference mode, provider and model everyone in the tenant uses"
358
+ }, {
359
+ slug: PERSONALIZE_INFERENCE_PERMISSION,
360
+ category: "inference",
361
+ name: "Personalize Inference",
362
+ description: "Keep a personal inference mode, provider and model on top of the tenant default"
363
+ }];
364
+ var PROVIDER_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
365
+ var MODEL_PATTERN = /^[!-~\u00a1-\uffff]{1,200}$/;
366
+ function issue(path, message) {
367
+ return {
368
+ path,
369
+ code: "invalid_setting",
370
+ message
371
+ };
372
+ }
373
+ function validBaseUrl(value) {
374
+ try {
375
+ const url = new URL(value);
376
+ return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password && !url.search && !url.hash;
377
+ } catch {
378
+ return false;
379
+ }
380
+ }
381
+ function validateInferencePreferences(payload) {
382
+ if (payload === null || payload === void 0) return {
383
+ ok: true,
384
+ canonical: null,
385
+ issues: []
386
+ };
387
+ if (typeof payload !== "object" || Array.isArray(payload)) return {
388
+ ok: false,
389
+ canonical: null,
390
+ issues: [issue("", "inference preferences are an object")]
391
+ };
392
+ const keyMember = findKeyMember(payload);
393
+ if (keyMember) return {
394
+ ok: false,
395
+ canonical: null,
396
+ issues: [issue(keyMember, "keys are never stored as preferences; they go to smrt-secrets")]
397
+ };
398
+ const input = payload;
399
+ const issues = [];
400
+ const canonical = {};
401
+ for (const [member, value] of Object.entries(input)) {
402
+ if (value === void 0 || value === null || value === "") continue;
403
+ switch (member) {
404
+ case "mode":
405
+ if (INFERENCE_MODES.includes(value)) canonical.mode = value;
406
+ else issues.push(issue(member, `mode is one of ${INFERENCE_MODES.join(", ")}`));
407
+ break;
408
+ case "provider":
409
+ if (typeof value === "string" && PROVIDER_PATTERN.test(value)) canonical.provider = value;
410
+ else issues.push(issue(member, "invalid provider id"));
411
+ break;
412
+ case "baseUrl":
413
+ if (typeof value === "string" && validBaseUrl(value)) canonical.baseUrl = value;
414
+ else issues.push(issue(member, "the base URL is an http(s) URL without credentials, query or fragment"));
415
+ break;
416
+ case "model":
417
+ if (typeof value === "string" && MODEL_PATTERN.test(value)) canonical.model = value;
418
+ else issues.push(issue(member, "invalid model id"));
419
+ break;
420
+ case "defaultTier":
421
+ if (isInferenceTier(value)) canonical.defaultTier = value;
422
+ else issues.push(issue(member, `the default tier is one of ${INFERENCE_TIERS.join(", ")}`));
423
+ break;
424
+ default: issues.push(issue(member, "unknown inference preference"));
425
+ }
426
+ }
427
+ return {
428
+ ok: issues.length === 0,
429
+ canonical: Object.keys(canonical).length > 0 ? canonical : null,
430
+ issues
431
+ };
432
+ }
433
+ var inferencePreferenceKind = {
434
+ kind: INFERENCE_PREFERENCE_KIND,
435
+ formatVersion: 1,
436
+ permissions: {
437
+ tenant: CUSTOMIZE_INFERENCE_PERMISSION,
438
+ user: PERSONALIZE_INFERENCE_PERMISSION
439
+ },
440
+ validate(payload) {
441
+ return validateInferencePreferences(payload);
442
+ }
443
+ };
444
+ function createInferencePreferences(store, surfaceId = INFERENCE_PREFERENCE_SURFACE) {
445
+ return {
446
+ async read() {
447
+ const state = await store.load(INFERENCE_PREFERENCE_KIND, surfaceId);
448
+ const tenant = state.tenant.payload ?? null;
449
+ const user = state.user?.payload ?? null;
450
+ return {
451
+ choices: {
452
+ ...tenant ?? {},
453
+ ...user ?? {}
454
+ },
455
+ tenant,
456
+ user,
457
+ revisions: {
458
+ tenant: state.tenant.revision,
459
+ user: state.user?.revision ?? null
460
+ },
461
+ issues: state.issues,
462
+ canCustomize: state.canCustomize
463
+ };
464
+ },
465
+ /** Save choices at a scope (default `user`); `revision` is the one the client loaded. */
466
+ write(choices, options) {
467
+ return store.save(INFERENCE_PREFERENCE_KIND, surfaceId, {
468
+ scope: options.scope ?? "user",
469
+ payload: choices,
470
+ revision: options.revision
471
+ });
472
+ },
473
+ reset(options = {}) {
474
+ return store.reset(INFERENCE_PREFERENCE_KIND, surfaceId, {
475
+ scope: options.scope ?? "user",
476
+ revision: options.revision
477
+ });
478
+ }
479
+ };
480
+ }
481
+ //#endregion
482
+ //#region src/settings/resolver.ts
483
+ var MAX_CACHED_TENANTS = 500;
484
+ function expandStoredAliases(target, appAliases, configAliases) {
485
+ if (inferenceTargetKind(target) !== "alias") return target;
486
+ const name = target.alias;
487
+ if (!Object.hasOwn(appAliases, name)) return target;
488
+ const expand = (alias, seen) => {
489
+ if (seen.has(alias)) return [];
490
+ const next = new Set(seen).add(alias);
491
+ return ((appAliases[alias] ?? configAliases[alias])?.candidates ?? []).flatMap((candidate) => candidate.alias && Object.hasOwn(appAliases, candidate.alias) ? expand(candidate.alias, next) : [candidate]);
492
+ };
493
+ return {
494
+ kind: "candidates",
495
+ candidates: expand(name, /* @__PURE__ */ new Set())
496
+ };
497
+ }
498
+ function createInferenceOverrideResolver(options = {}) {
499
+ const ttlMs = options.ttlMs ?? 3e4;
500
+ const cache = /* @__PURE__ */ new Map();
501
+ let collection;
502
+ const getCollection = () => {
503
+ if (options.overrides) return Promise.resolve(options.overrides);
504
+ collection ??= InferenceOverrideCollection.create({
505
+ ...options.db ?? {},
506
+ defaultListLimit: void 0,
507
+ maxListLimit: void 0
508
+ });
509
+ return collection;
510
+ };
511
+ const rowsFor = async (tenantId) => {
512
+ const key = tenantId ?? "";
513
+ const generation = getInferenceOverrideGeneration();
514
+ const hit = cache.get(key);
515
+ if (hit && hit.generation === generation && Date.now() - hit.at < ttlMs) return hit.rows;
516
+ const rows = await readInferenceOverrideRows(await getCollection(), tenantId);
517
+ cache.delete(key);
518
+ cache.set(key, {
519
+ at: Date.now(),
520
+ generation,
521
+ rows
522
+ });
523
+ if (cache.size > MAX_CACHED_TENANTS) {
524
+ const oldest = cache.keys().next().value;
525
+ if (oldest !== void 0) cache.delete(oldest);
526
+ }
527
+ return rows;
528
+ };
529
+ return { async resolve(request, context) {
530
+ const tenantId = typeof request.tenantId === "string" && request.tenantId ? request.tenantId : null;
531
+ const layers = await buildInferenceOverrideLayers(await rowsFor(tenantId), options.tenantTargetPolicy);
532
+ const merged = mergeSettingsConfigs(context.config, layers.app, layers.tenant);
533
+ const tier = request.tier === "auto" ? resolveAutoTier(merged) : request.tier;
534
+ const stored = layers.tenant.tiers?.[tier] ?? layers.app.tiers?.[tier];
535
+ if (!stored) {
536
+ const next = {
537
+ ...request,
538
+ tier
539
+ };
540
+ const deferred = options.base ? await options.base.resolve(next, context) : void 0;
541
+ if (deferred !== void 0) return deferred;
542
+ return request.tier === "auto" ? context.resolveFromConfig(next) : void 0;
543
+ }
544
+ validateInferenceTarget(stored, `inference.tiers.${tier}`, merged);
545
+ return {
546
+ tierResolved: tier,
547
+ target: expandStoredAliases({ ...stored }, layers.app.aliases ?? {}, context.config?.aliases ?? {}),
548
+ via: "resolver"
549
+ };
550
+ } };
551
+ }
552
+ //#endregion
553
+ //#region src/settings/settings-service.ts
554
+ function defaultRegistryFusions() {
555
+ return listFusions().flatMap((key) => {
556
+ const definition = getFusion(key);
557
+ return definition ? [{
558
+ ...definition,
559
+ key
560
+ }] : [];
561
+ });
562
+ }
563
+ var InferenceSettingsConfigError = class extends Error {
564
+ constructor(issues) {
565
+ super(`Inference binding refused: ${issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
566
+ this.issues = issues;
567
+ this.name = "InferenceSettingsConfigError";
568
+ }
569
+ issues;
570
+ status = 400;
571
+ };
572
+ var InferenceBindingInUseError = class extends Error {
573
+ constructor(binding, referrers, tenantReferences) {
574
+ const parts = [...referrers];
575
+ if (tenantReferences > 0) parts.push(`${tenantReferences} tenant binding(s)`);
576
+ super(`${binding} is still referenced by ${parts.join(", ")}`);
577
+ this.binding = binding;
578
+ this.referrers = referrers;
579
+ this.tenantReferences = tenantReferences;
580
+ this.name = "InferenceBindingInUseError";
581
+ }
582
+ binding;
583
+ referrers;
584
+ tenantReferences;
585
+ status = 409;
586
+ };
587
+ function withoutListBounds(options) {
588
+ return {
589
+ ...options,
590
+ defaultListLimit: void 0,
591
+ maxListLimit: void 0
592
+ };
593
+ }
594
+ function isRecord(value) {
595
+ return !!value && typeof value === "object" && !Array.isArray(value);
596
+ }
597
+ function rawValue(layers, scope, binding) {
598
+ const row = (scope === "app" ? layers.appRows : layers.tenantRows).get(binding);
599
+ if (!row) return null;
600
+ try {
601
+ return row.getValue();
602
+ } catch {
603
+ return null;
604
+ }
605
+ }
606
+ function allowListOf(layers, binding) {
607
+ try {
608
+ return layers.appRows.get(binding)?.getTenantAllowList() ?? null;
609
+ } catch {
610
+ return null;
611
+ }
612
+ }
613
+ function nonEmpty(target) {
614
+ return target && Object.keys(target).length > 0 ? stripKeyMembers(target) : null;
615
+ }
616
+ function bindingRefersTo(binding, value, kind, name) {
617
+ if (!isRecord(value)) return false;
618
+ if (binding.startsWith("tier:")) return kind === "alias" ? value.alias === name : value.fusion === name || value.kind === "fusion" && value.fusion === name;
619
+ if (kind === "alias" && binding.startsWith("alias:") && binding !== `alias:${name}`) return Array.isArray(value.candidates) && value.candidates.some((candidate) => isRecord(candidate) && candidate.alias === name);
620
+ return false;
621
+ }
622
+ var PRESET_LABELS = {
623
+ openrouter: "OpenRouter",
624
+ lemonade: "Lemonade (local)",
625
+ "llama.cpp": "llama.cpp (local)",
626
+ "lm-studio": "LM Studio (local)"
627
+ };
628
+ var InferenceSettingsService = class InferenceSettingsService {
629
+ constructor(overrides, options = {}) {
630
+ this.overrides = overrides;
631
+ this.options = options;
632
+ this.writes = new InferenceOverrideService(overrides, options.authorize ?? (() => false), { tenantTargetPolicy: options.tenantTargetPolicy });
633
+ this.readConfig = options.config ?? getInferenceConfig;
634
+ }
635
+ overrides;
636
+ options;
637
+ writes;
638
+ readConfig;
639
+ /** Build a service over the given database options (list bounds stripped). */
640
+ static async create(options = {}, serviceOptions = {}) {
641
+ const overrides = await InferenceOverrideCollection.create(withoutListBounds(options));
642
+ return new InferenceSettingsService(overrides, serviceOptions);
643
+ }
644
+ loadLayers(tenantId) {
645
+ return loadInferenceOverrideLayers(this.overrides, {
646
+ tenantId,
647
+ tenantTargetPolicy: this.options.tenantTargetPolicy
648
+ });
649
+ }
650
+ /**
651
+ * The effective config for a tenant: config < app rows < allowed tenant
652
+ * rows. What the override resolver binds and the doctor checks.
653
+ * Server-side only: config layers may carry literal keys, so never send
654
+ * this to a browser (the row methods strip them).
655
+ */
656
+ async getEffectiveConfig(options = {}) {
657
+ const layers = await this.loadLayers(options.tenantId);
658
+ return mergeSettingsConfigs(this.readConfig(), layers.app, layers.tenant);
659
+ }
660
+ /**
661
+ * One row per tier (`cheap`, `good`, `great`, `best`): the effective
662
+ * target, each scope's raw override, and both revert previews.
663
+ */
664
+ async listInferenceSettings(options = {}) {
665
+ const tenantId = options.tenantId ?? null;
666
+ const layers = await this.loadLayers(tenantId);
667
+ const config = this.readConfig();
668
+ return INFERENCE_TIERS.map((tier) => {
669
+ const binding = tierBinding(tier);
670
+ const configTarget = nonEmpty(config?.tiers?.[tier]);
671
+ const appTarget = rawValue(layers, "app", binding);
672
+ const tenantTarget = tenantId ? rawValue(layers, "tenant", binding) : null;
673
+ const tenantBlocked = layers.blockedTenantBindings.includes(binding);
674
+ const tenantEffective = tenantBlocked ? null : tenantTarget;
675
+ const appRevertTarget = configTarget;
676
+ const inheritedTarget = appTarget ?? configTarget;
677
+ const effectiveTarget = tenantEffective ?? appTarget ?? configTarget;
678
+ const supplyingLevel = tenantEffective ? "tenant" : appTarget ? "app" : configTarget ? "config" : "default";
679
+ return {
680
+ binding,
681
+ tier,
682
+ effectiveTarget,
683
+ effectiveLabel: describeInferenceTarget(effectiveTarget),
684
+ supplyingLevel,
685
+ configTarget,
686
+ appTarget,
687
+ tenantTarget,
688
+ appRevertTarget,
689
+ appRevertLabel: describeInferenceTarget(appRevertTarget),
690
+ inheritedTarget: tenantId ? inheritedTarget : null,
691
+ inheritedLabel: tenantId ? describeInferenceTarget(inheritedTarget) : null,
692
+ tenantAllowList: allowListOf(layers, binding),
693
+ tenantBlocked
694
+ };
695
+ });
696
+ }
697
+ /** `auto`'s default tier with the same per-scope shape. */
698
+ async getDefaultTierSetting(options = {}) {
699
+ const tenantId = options.tenantId ?? null;
700
+ const layers = await this.loadLayers(tenantId);
701
+ const config = this.readConfig();
702
+ const configTier = config?.defaultTier ?? null;
703
+ const appTier = rawValue(layers, "app", DEFAULT_TIER_BINDING);
704
+ const tenantTier = tenantId ? rawValue(layers, "tenant", DEFAULT_TIER_BINDING) : null;
705
+ const tenantBlocked = layers.blockedTenantBindings.includes(DEFAULT_TIER_BINDING);
706
+ const tenantEffective = tenantBlocked ? null : tenantTier;
707
+ const appRevertTier = resolveAutoTier(config);
708
+ const inheritedTier = appTier ?? appRevertTier;
709
+ return {
710
+ binding: DEFAULT_TIER_BINDING,
711
+ effectiveTier: tenantEffective ?? inheritedTier,
712
+ supplyingLevel: tenantEffective ? "tenant" : appTier ? "app" : configTier ? "config" : "default",
713
+ configTier,
714
+ appTier,
715
+ tenantTier,
716
+ appRevertTier,
717
+ inheritedTier: tenantId ? inheritedTier : null,
718
+ tenantAllowList: allowListOf(layers, DEFAULT_TIER_BINDING),
719
+ tenantBlocked
720
+ };
721
+ }
722
+ /** Every alias: config entries, replaced whole by app rows. Sorted by name. */
723
+ async listInferenceAliases() {
724
+ const layers = await this.loadLayers(null);
725
+ const configAliases = this.readConfig()?.aliases ?? {};
726
+ return [.../* @__PURE__ */ new Set([...Object.keys(configAliases), ...Object.keys(layers.app.aliases ?? {})])].sort().map((name) => {
727
+ const app = layers.app.aliases?.[name];
728
+ const alias = app ?? configAliases[name];
729
+ const setting = {
730
+ name,
731
+ source: app ? "app" : "config",
732
+ candidates: Array.isArray(alias?.candidates) ? stripKeyMembers(alias.candidates) : [],
733
+ inConfig: Object.hasOwn(configAliases, name)
734
+ };
735
+ if (alias?.description) setting.description = alias.description;
736
+ return setting;
737
+ });
738
+ }
739
+ /**
740
+ * Fusions from the host's registry (code, read-only), the config's
741
+ * `inference.fusions`, and app rows (editable JSON). A later source
742
+ * shadows an earlier one by name.
743
+ */
744
+ async listInferenceFusions() {
745
+ const layers = await this.loadLayers(null);
746
+ const out = /* @__PURE__ */ new Map();
747
+ const registry = this.options.registryFusions ?? defaultRegistryFusions;
748
+ for (const fusion of registry()) {
749
+ if (!fusion || typeof fusion.key !== "string") continue;
750
+ const definition = stripKeyMembers(JSON.parse(JSON.stringify(fusion)));
751
+ out.set(fusion.key, {
752
+ name: fusion.key,
753
+ source: "registry",
754
+ definition,
755
+ description: typeof fusion.description === "string" ? fusion.description : void 0,
756
+ editable: false
757
+ });
758
+ }
759
+ const configFusions = this.readConfig()?.fusions;
760
+ if (isRecord(configFusions)) for (const [name, definition] of Object.entries(configFusions)) {
761
+ if (!isRecord(definition)) continue;
762
+ out.set(name, {
763
+ name,
764
+ source: "config",
765
+ definition: stripKeyMembers(definition),
766
+ description: typeof definition.description === "string" ? definition.description : void 0,
767
+ editable: false
768
+ });
769
+ }
770
+ for (const [name, definition] of Object.entries(layers.app.fusions ?? {})) out.set(name, {
771
+ name,
772
+ source: "app",
773
+ definition,
774
+ description: typeof definition.description === "string" ? definition.description : void 0,
775
+ editable: true
776
+ });
777
+ return [...out.values()].sort((a, b) => a.name.localeCompare(b.name));
778
+ }
779
+ /**
780
+ * Bind (or, with `null`, revert) one tier at one scope. Tenant writes must
781
+ * pass the allow-list; every write goes through the authorizer.
782
+ */
783
+ async setTierOverride(request) {
784
+ if (!INFERENCE_TIERS.includes(request.tier)) throw new InferenceBindingValueError(`tier:${String(request.tier)}`, "unknown tier");
785
+ const binding = tierBinding(request.tier);
786
+ if (request.target !== null) await this.assertConfigStaysValid(request.scopeType, request.scopeId, { tiers: { [request.tier]: request.target } });
787
+ await this.writes.setValue(binding, request.scopeType, request.scopeId, request.target);
788
+ }
789
+ /** Set (or revert) `auto`'s default tier at one scope. */
790
+ async setDefaultTierOverride(request) {
791
+ await this.writes.setValue(DEFAULT_TIER_BINDING, request.scopeType, request.scopeId, request.tier === null ? null : { tier: request.tier });
792
+ }
793
+ /** Set (or remove) the tenant allow-list of a tier or of the default tier. */
794
+ async setTenantAllowList(request) {
795
+ await this.writes.setTenantAllowList(request.binding, request.allowList);
796
+ }
797
+ /** Define (or, with `null`, remove) an app-level alias. */
798
+ async setAlias(request) {
799
+ const binding = `alias:${request.name}`;
800
+ if (!INFERENCE_BINDING_NAME_PATTERN.test(String(request.name))) throw new InferenceBindingValueError(binding, "invalid alias name");
801
+ if (request.alias !== null) await this.assertConfigStaysValid("app", APP_INFERENCE_SCOPE_ID, { aliases: { [request.name]: request.alias } });
802
+ await this.writes.setValue(binding, "app", APP_INFERENCE_SCOPE_ID, request.alias, { beforeWrite: (normalized) => normalized === null ? this.assertNotReferenced("alias", request.name) : Promise.resolve() });
803
+ }
804
+ /** Store (or, with `null`, remove) an app-level fusion config entry (#4753 shape). */
805
+ async setFusion(request) {
806
+ const binding = `fusion:${request.name}`;
807
+ if (!INFERENCE_BINDING_NAME_PATTERN.test(String(request.name))) throw new InferenceBindingValueError(binding, "invalid fusion name");
808
+ await this.writes.setValue(binding, "app", APP_INFERENCE_SCOPE_ID, request.definition, { beforeWrite: (normalized) => normalized === null ? this.assertNotReferenced("fusion", request.name) : Promise.resolve() });
809
+ }
810
+ /**
811
+ * Refuse removing an app alias or fusion that a stored binding (any
812
+ * scope, any tenant) or the config still names, unless config or the
813
+ * registry keeps defining it. Otherwise the removal would leave dangling
814
+ * references that fail at call time.
815
+ */
816
+ async assertNotReferenced(kind, name) {
817
+ const config = this.readConfig();
818
+ if (kind === "alias" ? Object.hasOwn(config?.aliases ?? {}, name) : Object.hasOwn(config?.fusions ?? {}, name) || [...this.options.registryFusions?.() ?? []].some((fusion) => fusion?.key === name)) return;
819
+ const referrers = [];
820
+ for (const [tier, target] of Object.entries(config?.tiers ?? {})) if (bindingRefersTo(`tier:${tier}`, target, kind, name)) referrers.push(`config tiers.${tier}`);
821
+ for (const [alias, definition] of Object.entries(config?.aliases ?? {})) if (bindingRefersTo(`alias:${alias}`, definition, kind, name)) referrers.push(`config aliases.${alias}`);
822
+ let tenantReferences = 0;
823
+ for (const row of await this.overrides.list({})) {
824
+ let value;
825
+ try {
826
+ value = row.getValue();
827
+ } catch {
828
+ continue;
829
+ }
830
+ if (!bindingRefersTo(row.binding, value, kind, name)) continue;
831
+ if (row.tenantId) tenantReferences += 1;
832
+ else referrers.push(row.binding);
833
+ }
834
+ if (referrers.length > 0 || tenantReferences > 0) throw new InferenceBindingInUseError(`${kind}:${name}`, referrers, tenantReferences);
835
+ }
836
+ /**
837
+ * Refuse a write that adds a config issue (unknown alias, cycle, key
838
+ * without endpoint, unknown preset) to the layered config it lands in.
839
+ * Issues that already existed do not block unrelated writes.
840
+ */
841
+ async assertConfigStaysValid(scopeType, scopeId, change) {
842
+ const layers = await this.loadLayers(scopeType === "tenant" ? scopeId : null);
843
+ const before = mergeSettingsConfigs(this.readConfig(), layers.app, layers.tenant);
844
+ const after = mergeSettingsConfigs(before, change);
845
+ const seen = new Set(findInferenceConfigIssues(before).map((issue) => `${issue.code}:${issue.path}`));
846
+ const added = findInferenceConfigIssues(after).filter((issue) => !seen.has(`${issue.code}:${issue.path}`));
847
+ if (added.length > 0) throw new InferenceSettingsConfigError(added);
848
+ }
849
+ /**
850
+ * Test connection for the default view: probe the endpoint's cheapest
851
+ * list call (`./node`'s `probeInferenceCandidate`, 3 s). The stored key
852
+ * record (from the host's `SecretService`) is sent only when the probe URL
853
+ * is on the key's bound origin; no environment key is read.
854
+ */
855
+ async testConnection(input, options = {}) {
856
+ const candidate = {};
857
+ if (input.provider) candidate.provider = input.provider;
858
+ if (input.preset) candidate.preset = input.preset;
859
+ if (input.baseUrl) candidate.baseUrl = input.baseUrl;
860
+ if (input.model) candidate.model = input.model;
861
+ const url = inferenceProbeUrl(candidate);
862
+ const apiKey = url ? inferenceKeyFor(options.key, url) : void 0;
863
+ if (options.key && url && !apiKey) {
864
+ let where = "that endpoint";
865
+ try {
866
+ where = new URL(url).origin;
867
+ } catch {}
868
+ return {
869
+ reachable: false,
870
+ durationMs: 0,
871
+ keySent: false,
872
+ detail: `the stored key is bound to ${options.key.origin} and is never sent to ${where}; save a key for this endpoint`
873
+ };
874
+ }
875
+ const result = await probeInferenceCandidate(apiKey && url ? {
876
+ ...candidate,
877
+ baseUrl: candidate.baseUrl ?? url.replace(/\/(?:models|api\/tags)$/, ""),
878
+ apiKey
879
+ } : candidate, {
880
+ env: {},
881
+ fetch: options.fetch,
882
+ timeoutMs: options.timeoutMs
883
+ });
884
+ return {
885
+ ...result,
886
+ detail: redactInferenceSecret(result.detail, apiKey),
887
+ keySent: !!apiKey
888
+ };
889
+ }
890
+ /**
891
+ * Per-candidate test for the alias editor. A candidate already in the
892
+ * effective config (config file or an app row) reads its key from
893
+ * `apiKeyEnv` (or its provider's variable) exactly as the doctor does,
894
+ * sent only to its own endpoint; any other candidate (an unsaved edit) is
895
+ * probed with no key at all.
896
+ */
897
+ async testCandidate(candidate, options = {}) {
898
+ if (findKeyLike(candidate)) throw new InferenceBindingValueError("candidate", "a tested candidate never carries a literal key");
899
+ const configured = await this.isConfiguredCandidate(candidate);
900
+ return {
901
+ ...await probeInferenceCandidate(candidate, {
902
+ ...options,
903
+ env: configured ? options.env ?? process.env : {}
904
+ }),
905
+ keySent: false
906
+ };
907
+ }
908
+ async isConfiguredCandidate(candidate) {
909
+ const config = await this.getEffectiveConfig();
910
+ const wanted = canonicalCandidate(candidate);
911
+ return Object.values(config?.aliases ?? {}).some((alias) => (alias?.candidates ?? []).some((entry) => canonicalCandidate(entry) === wanted));
912
+ }
913
+ /** `smrt doctor --ai` over the effective config for a tenant. */
914
+ async doctor(options = {}) {
915
+ return checkInferenceBindings({
916
+ config: await this.getEffectiveConfig({ tenantId: options.tenantId }) ?? {},
917
+ probe: options.probe,
918
+ env: options.env,
919
+ fetch: options.fetch
920
+ });
921
+ }
922
+ /**
923
+ * The Usage tab: `reportAiUsage()` (#4751) grouped by model, tier and/or
924
+ * prompt key. Pass `by: ['model']` where tier and prompt tags do not exist
925
+ * yet; NULL dimensions are their own bucket, never dropped.
926
+ */
927
+ usage(options = {}) {
928
+ return reportAiUsage(this.overrides.db, {
929
+ ...options,
930
+ by: options.by && options.by.length > 0 ? options.by : ["model"]
931
+ });
932
+ }
933
+ /** Local discovery for the model field (#4674's loopback probe). */
934
+ async discoverModels() {
935
+ const { discoverLocalInferenceModels } = await import("@happyvertical/smrt-core/byo-inference");
936
+ return (await discoverLocalInferenceModels()).filter((entry) => entry.available).map((entry) => ({
937
+ service: entry.service,
938
+ baseUrl: entry.baseUrl,
939
+ models: entry.available ? entry.models : []
940
+ }));
941
+ }
942
+ /** Provider choices: #4674 presets, then the AI SDK provider list. */
943
+ listProviderOptions() {
944
+ const presets = INFERENCE_PRESET_IDS.map((id) => {
945
+ const preset = INFERENCE_PRESETS[id];
946
+ return {
947
+ id,
948
+ label: PRESET_LABELS[id] ?? id,
949
+ kind: "preset",
950
+ baseUrl: preset.baseUrl,
951
+ requiresApiKey: preset.requiresApiKey,
952
+ local: !preset.requiresApiKey
953
+ };
954
+ });
955
+ const providers = AI_PROVIDER_TYPES.map((id) => ({
956
+ id,
957
+ label: id,
958
+ kind: "provider",
959
+ requiresApiKey: !!getDefaultAIKeyEnvName(id),
960
+ ...id === "litellm" || id === "bifrost" ? { needsBaseUrl: true } : {},
961
+ ...id === "ollama" ? { local: true } : {}
962
+ }));
963
+ return [...presets, ...providers];
964
+ }
965
+ };
966
+ function canonicalCandidate(candidate) {
967
+ return JSON.stringify([
968
+ "provider",
969
+ "model",
970
+ "baseUrl",
971
+ "apiKeyEnv",
972
+ "preset",
973
+ "alias"
974
+ ].map((field) => {
975
+ const value = candidate[field];
976
+ return typeof value === "string" ? value.trim() : null;
977
+ }));
978
+ }
979
+ function findKeyLike(candidate) {
980
+ return typeof candidate.apiKey === "string" && candidate.apiKey.length > 0;
981
+ }
982
+ //#endregion
983
+ export { APP_INFERENCE_SCOPE_ID, CUSTOMIZE_INFERENCE_PERMISSION, DEFAULT_TIER_BINDING, INFERENCE_MODES, INFERENCE_PERMISSION_DEFINITIONS, INFERENCE_PREFERENCE_KIND, INFERENCE_PREFERENCE_SURFACE, InferenceBindingInUseError, InferenceBindingValueError, InferenceKeyOriginError, InferenceOverride, InferenceOverrideAuthorizationError, InferenceOverrideCollection, InferenceOverrideService, InferenceSettingsConfigError, InferenceSettingsService, InferenceTenantTargetNotAllowedError, InvalidInferenceScopeError, PERSONALIZE_INFERENCE_PERMISSION, REDACTED_KEY, assertValidInferenceScope, bindInferenceKey, buildInferenceOverrideLayers, createInferenceOverrideResolver, createInferencePreferences, describeInferenceTarget, inferenceKeyFor, inferenceKeyOrigin, inferencePreferenceKind, inferenceProviderConfigFromKey, inferenceTargetRef, isAllowedByList, loadInferenceOverrideLayers, matchesAllowPattern, mergeSettingsConfigs, parseInferenceBinding, readInferenceOverrideRows, redactInferenceSecret, stripKeyMembers, tierBinding, validateInferencePreferences };
984
+
985
+ //# sourceMappingURL=server.js.map