@happyvertical/smrt-inference 0.0.0-stage → 0.60.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +206 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +73 -2
- package/agents/fusions.md +105 -0
- package/agents/hosted.md +161 -0
- package/agents/resolver.md +65 -0
- package/agents/settings.md +107 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/chunks/InferenceOverrideCollection-Dat4TrK5.js +423 -0
- package/dist/chunks/InferenceOverrideCollection-Dat4TrK5.js.map +1 -0
- package/dist/chunks/provider-config-BSpIQRVM.js +1873 -0
- package/dist/chunks/provider-config-BSpIQRVM.js.map +1 -0
- package/dist/classify.d.ts +32 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/executor.d.ts +45 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/fusion-store.d.ts +16 -0
- package/dist/fusion-store.d.ts.map +1 -0
- package/dist/fusion.d.ts +155 -0
- package/dist/fusion.d.ts.map +1 -0
- package/dist/gate.d.ts +143 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +94 -0
- package/dist/gate.js.map +1 -0
- package/dist/hosted.d.ts +461 -0
- package/dist/hosted.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +453 -0
- package/dist/models/InferenceOverride.d.ts +38 -0
- package/dist/models/InferenceOverride.d.ts.map +1 -0
- package/dist/models/InferenceOverrideCollection.d.ts +35 -0
- package/dist/models/InferenceOverrideCollection.d.ts.map +1 -0
- package/dist/models.d.ts +11 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +2 -0
- package/dist/node.d.ts +110 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +1414 -0
- package/dist/node.js.map +1 -0
- package/dist/provider-config.d.ts +113 -0
- package/dist/provider-config.d.ts.map +1 -0
- package/dist/register.d.ts +30 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/resolver.d.ts +166 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +291 -0
- package/dist/resolver.js.map +1 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1138 -0
- package/dist/server.js.map +1 -0
- package/dist/settings/cache.d.ts +5 -0
- package/dist/settings/cache.d.ts.map +1 -0
- package/dist/settings/keys.d.ts +70 -0
- package/dist/settings/keys.d.ts.map +1 -0
- package/dist/settings/layers.d.ts +68 -0
- package/dist/settings/layers.d.ts.map +1 -0
- package/dist/settings/override-service.d.ts +119 -0
- package/dist/settings/override-service.d.ts.map +1 -0
- package/dist/settings/preferences.d.ts +137 -0
- package/dist/settings/preferences.d.ts.map +1 -0
- package/dist/settings/resolver.d.ts +41 -0
- package/dist/settings/resolver.d.ts.map +1 -0
- package/dist/settings/settings-service.d.ts +355 -0
- package/dist/settings/settings-service.d.ts.map +1 -0
- package/dist/settings/targets.d.ts +112 -0
- package/dist/settings/targets.d.ts.map +1 -0
- package/dist/settings/transcripts.d.ts +33 -0
- package/dist/settings/transcripts.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +341 -0
- package/dist/smrt-knowledge.okf.md +80 -0
- package/dist/svelte/components/InferenceAliasEditor.svelte +210 -0
- package/dist/svelte/components/InferenceAliasEditor.svelte.d.ts +13 -0
- package/dist/svelte/components/InferenceAliasEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceDefaultView.svelte +330 -0
- package/dist/svelte/components/InferenceDefaultView.svelte.d.ts +15 -0
- package/dist/svelte/components/InferenceDefaultView.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceFusionList.svelte +141 -0
- package/dist/svelte/components/InferenceFusionList.svelte.d.ts +16 -0
- package/dist/svelte/components/InferenceFusionList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceSettingsPanel.svelte +392 -0
- package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts +84 -0
- package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceTierRow.svelte +199 -0
- package/dist/svelte/components/InferenceTierRow.svelte.d.ts +14 -0
- package/dist/svelte/components/InferenceTierRow.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceTranscriptSettings.svelte +165 -0
- package/dist/svelte/components/InferenceTranscriptSettings.svelte.d.ts +18 -0
- package/dist/svelte/components/InferenceTranscriptSettings.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceUsageTable.svelte +177 -0
- package/dist/svelte/components/InferenceUsageTable.svelte.d.ts +21 -0
- package/dist/svelte/components/InferenceUsageTable.svelte.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +17 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +14 -0
- package/dist/svelte/types.d.ts +262 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +124 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +105 -3
package/dist/server.js
ADDED
|
@@ -0,0 +1,1138 @@
|
|
|
1
|
+
import { B as InferenceConfigError, I as findInferenceConfigIssues, N as findFusionDefinitionIssues, P as findFusionDefinitionPromptIssues, b as listFusions, y as getFusion, z as validateInferenceTarget } from "./chunks/provider-config-BSpIQRVM.js";
|
|
2
|
+
import { checkInferenceBindings, inferenceProbeUrl, probeInferenceCandidate } from "./node.js";
|
|
3
|
+
import { C as getInferenceOverrideGeneration, S as tierBinding, _ as normalizeFusionDefinition, a as INFERENCE_BINDING_NAME_PATTERN, b as parseInferenceBinding, c as TRANSCRIPTS_BINDING, d as inferenceTargetRef, f as isAllowedByList, g as normalizeDefaultTier, h as normalizeAllowList, i as DEFAULT_TIER_BINDING, l as describeInferenceTarget, m as normalizeAliasDefinition, n as APP_INFERENCE_SCOPE_ID, o as INFERENCE_MODES, p as matchesAllowPattern, r as InferenceOverride, s as InferenceBindingValueError, t as InferenceOverrideCollection, u as findKeyMember, v as normalizeTierTarget, x as stripKeyMembers, y as normalizeTranscriptSettings } from "./chunks/InferenceOverrideCollection-Dat4TrK5.js";
|
|
4
|
+
import { DEFAULT_RETENTION_POLICY, DEFAULT_TRANSCRIPT_MAX_BYTES, DEFAULT_TRANSCRIPT_SAMPLE, INFERENCE_PRESETS, INFERENCE_PRESET_IDS, INFERENCE_TIERS, config, getInferenceConfig, getInferenceTranscriptsConfig, inferenceTargetKind, isInferencePresetId, isInferenceTier, mergeInferenceConfigs, reportAiUsage, resolveAutoTier, setAiTranscriptAppSettings } 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 "transcripts": return normalizeTranscriptSettings(binding, value);
|
|
164
|
+
case "alias": return normalizeAliasDefinition(binding, value);
|
|
165
|
+
case "fusion": return normalizeFusionDefinition(binding, value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function bindingValueRef(binding, value) {
|
|
169
|
+
if (binding === "default-tier") return `tier:${String(value)}`;
|
|
170
|
+
return inferenceTargetRef(value);
|
|
171
|
+
}
|
|
172
|
+
async function isTenantValueAllowed(binding, value, tenantId, allowList, policy) {
|
|
173
|
+
const ref = bindingValueRef(binding, value);
|
|
174
|
+
if (!isAllowedByList(ref, allowList)) return false;
|
|
175
|
+
if (!policy) return true;
|
|
176
|
+
const parsed = parseInferenceBinding(binding);
|
|
177
|
+
try {
|
|
178
|
+
return await policy({
|
|
179
|
+
binding,
|
|
180
|
+
tier: parsed?.kind === "tier" ? parsed.name : void 0,
|
|
181
|
+
ref,
|
|
182
|
+
value,
|
|
183
|
+
tenantId
|
|
184
|
+
}) === true;
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
var InferenceOverrideService = class {
|
|
190
|
+
constructor(overrides, authorize, options = {}) {
|
|
191
|
+
this.overrides = overrides;
|
|
192
|
+
this.authorize = authorize;
|
|
193
|
+
this.options = options;
|
|
194
|
+
}
|
|
195
|
+
overrides;
|
|
196
|
+
authorize;
|
|
197
|
+
options;
|
|
198
|
+
/**
|
|
199
|
+
* Set (or, with `null`, revert) one binding at one scope. Tenants may write
|
|
200
|
+
* only `tier:*` and `default-tier`, and only an allow-listed target;
|
|
201
|
+
* reverting is always allowed.
|
|
202
|
+
*
|
|
203
|
+
* @throws {InvalidInferenceScopeError} for a scope nothing reads.
|
|
204
|
+
* @throws {InferenceBindingValueError} for an invalid binding or value.
|
|
205
|
+
* @throws {InferenceOverrideAuthorizationError} when the authorizer declines.
|
|
206
|
+
* @throws {InferenceTenantTargetNotAllowedError} when the allow-list refuses.
|
|
207
|
+
*/
|
|
208
|
+
async setValue(binding, scopeType, scopeId, value, options = {}) {
|
|
209
|
+
assertValidInferenceScope(scopeType, scopeId);
|
|
210
|
+
const normalized = normalizeBindingValue(binding, value);
|
|
211
|
+
const parsed = parseInferenceBinding(binding);
|
|
212
|
+
if (scopeType === "tenant" && (parsed?.kind === "alias" || parsed?.kind === "fusion")) throw new InferenceBindingValueError(binding, "aliases and fusions are defined at the app scope only");
|
|
213
|
+
if (scopeType === "tenant" && parsed?.kind === "transcripts") throw new InferenceBindingValueError(binding, "the transcript setting is app scope only");
|
|
214
|
+
await this.require({
|
|
215
|
+
operation: normalized === null ? "remove" : "set",
|
|
216
|
+
binding,
|
|
217
|
+
scopeType,
|
|
218
|
+
scopeId,
|
|
219
|
+
value: normalized
|
|
220
|
+
});
|
|
221
|
+
if (scopeType === "tenant" && normalized !== null) {
|
|
222
|
+
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));
|
|
223
|
+
}
|
|
224
|
+
await options.beforeWrite?.(normalized);
|
|
225
|
+
await this.overrides.setOverride(binding, scopeType, scopeId, { target: normalized });
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Set (or, with `null`, remove) the tenant allow-list of a `tier:*` or
|
|
229
|
+
* `default-tier` binding. The list lives on the app row.
|
|
230
|
+
*/
|
|
231
|
+
async setTenantAllowList(binding, allowList) {
|
|
232
|
+
const parsed = parseInferenceBinding(binding);
|
|
233
|
+
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");
|
|
234
|
+
const normalized = normalizeAllowList(binding, allowList);
|
|
235
|
+
await this.require({
|
|
236
|
+
operation: "allow-list",
|
|
237
|
+
binding,
|
|
238
|
+
scopeType: "app",
|
|
239
|
+
scopeId: APP_INFERENCE_SCOPE_ID,
|
|
240
|
+
tenantAllowList: normalized
|
|
241
|
+
});
|
|
242
|
+
await this.overrides.setOverride(binding, "app", APP_INFERENCE_SCOPE_ID, { tenantAllowList: normalized });
|
|
243
|
+
}
|
|
244
|
+
/** Delete the whole row (value and, on the app row, the allow-list). */
|
|
245
|
+
async removeOverride(binding, scopeType, scopeId) {
|
|
246
|
+
assertValidInferenceScope(scopeType, scopeId);
|
|
247
|
+
if (!parseInferenceBinding(binding)) throw new InferenceBindingValueError(String(binding), "unknown binding id");
|
|
248
|
+
await this.require({
|
|
249
|
+
operation: "remove",
|
|
250
|
+
binding,
|
|
251
|
+
scopeType,
|
|
252
|
+
scopeId
|
|
253
|
+
});
|
|
254
|
+
return this.overrides.removeOverride(binding, scopeType, scopeId);
|
|
255
|
+
}
|
|
256
|
+
async require(request) {
|
|
257
|
+
let allowed = false;
|
|
258
|
+
try {
|
|
259
|
+
allowed = await this.authorize(request) === true;
|
|
260
|
+
} catch {
|
|
261
|
+
allowed = false;
|
|
262
|
+
}
|
|
263
|
+
if (!allowed) throw new InferenceOverrideAuthorizationError(request);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/settings/layers.ts
|
|
268
|
+
function mergeSettingsConfigs(...layers) {
|
|
269
|
+
return mergeInferenceConfigs(...layers);
|
|
270
|
+
}
|
|
271
|
+
function layerFromRows(rows, include) {
|
|
272
|
+
const layer = {};
|
|
273
|
+
for (const [binding, row] of rows) {
|
|
274
|
+
if (!include(binding)) continue;
|
|
275
|
+
const parsed = parseInferenceBinding(binding);
|
|
276
|
+
if (!parsed) continue;
|
|
277
|
+
let value;
|
|
278
|
+
try {
|
|
279
|
+
value = row.getValue();
|
|
280
|
+
} catch {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (value === null) continue;
|
|
284
|
+
switch (parsed.kind) {
|
|
285
|
+
case "tier":
|
|
286
|
+
layer.tiers ??= {};
|
|
287
|
+
layer.tiers[parsed.name] = value;
|
|
288
|
+
break;
|
|
289
|
+
case "default-tier":
|
|
290
|
+
layer.defaultTier = value;
|
|
291
|
+
break;
|
|
292
|
+
case "transcripts":
|
|
293
|
+
layer.transcripts = value;
|
|
294
|
+
break;
|
|
295
|
+
case "alias":
|
|
296
|
+
layer.aliases ??= {};
|
|
297
|
+
layer.aliases[parsed.name] = value;
|
|
298
|
+
break;
|
|
299
|
+
case "fusion":
|
|
300
|
+
layer.fusions ??= {};
|
|
301
|
+
layer.fusions[parsed.name] = value;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return layer;
|
|
306
|
+
}
|
|
307
|
+
function isUnreadableFusionRow(row) {
|
|
308
|
+
if (!row || row.target === null || row.target === void 0) return false;
|
|
309
|
+
try {
|
|
310
|
+
row.getValue();
|
|
311
|
+
return false;
|
|
312
|
+
} catch {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function unreadableFusionIssue(name) {
|
|
317
|
+
return {
|
|
318
|
+
path: `fusion:${name}`,
|
|
319
|
+
code: "fusion-invalid",
|
|
320
|
+
message: `app row fusion:${name} cannot be read (not a JSON object, or a credential-shaped member); fix or remove it`
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async function readInferenceOverrideRows(overrides, tenantId) {
|
|
324
|
+
const tenant = tenantId ?? null;
|
|
325
|
+
const [appRows, tenantRows] = await Promise.all([overrides.getScopeMap("app", APP_INFERENCE_SCOPE_ID), tenant ? overrides.getScopeMap("tenant", tenant) : Promise.resolve(/* @__PURE__ */ new Map())]);
|
|
326
|
+
return {
|
|
327
|
+
tenantId: tenant,
|
|
328
|
+
appRows,
|
|
329
|
+
tenantRows
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async function loadInferenceOverrideLayers(overrides, options = {}) {
|
|
333
|
+
return buildInferenceOverrideLayers(await readInferenceOverrideRows(overrides, options.tenantId), options.tenantTargetPolicy);
|
|
334
|
+
}
|
|
335
|
+
async function buildInferenceOverrideLayers(rows, tenantTargetPolicy) {
|
|
336
|
+
const { tenantId, appRows, tenantRows } = rows;
|
|
337
|
+
const options = { tenantTargetPolicy };
|
|
338
|
+
const app = layerFromRows(appRows, () => true);
|
|
339
|
+
const blocked = [];
|
|
340
|
+
const allowedTenant = /* @__PURE__ */ new Set();
|
|
341
|
+
if (tenantId) for (const [binding, row] of tenantRows) {
|
|
342
|
+
const parsed = parseInferenceBinding(binding);
|
|
343
|
+
if (parsed?.kind !== "tier" && parsed?.kind !== "default-tier") continue;
|
|
344
|
+
let value;
|
|
345
|
+
try {
|
|
346
|
+
value = row.getValue();
|
|
347
|
+
} catch {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (value === null) continue;
|
|
351
|
+
let allowList = null;
|
|
352
|
+
try {
|
|
353
|
+
allowList = appRows.get(binding)?.getTenantAllowList() ?? null;
|
|
354
|
+
} catch {
|
|
355
|
+
allowList = null;
|
|
356
|
+
}
|
|
357
|
+
if (await isTenantValueAllowed(binding, value, tenantId, allowList, options.tenantTargetPolicy)) allowedTenant.add(binding);
|
|
358
|
+
else blocked.push(binding);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
app,
|
|
362
|
+
tenant: layerFromRows(tenantRows, (binding) => allowedTenant.has(binding)),
|
|
363
|
+
blockedTenantBindings: blocked.sort(),
|
|
364
|
+
appRows,
|
|
365
|
+
tenantRows
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/settings/preferences.ts
|
|
370
|
+
var INFERENCE_PREFERENCE_KIND = "inference";
|
|
371
|
+
var INFERENCE_PREFERENCE_SURFACE = "default";
|
|
372
|
+
var CUSTOMIZE_INFERENCE_PERMISSION = "inference.customize";
|
|
373
|
+
var PERSONALIZE_INFERENCE_PERMISSION = "inference.personalize";
|
|
374
|
+
var INFERENCE_PERMISSION_DEFINITIONS = [{
|
|
375
|
+
slug: CUSTOMIZE_INFERENCE_PERMISSION,
|
|
376
|
+
category: "inference",
|
|
377
|
+
name: "Customize Inference",
|
|
378
|
+
description: "Change the default inference mode, provider and model everyone in the tenant uses"
|
|
379
|
+
}, {
|
|
380
|
+
slug: PERSONALIZE_INFERENCE_PERMISSION,
|
|
381
|
+
category: "inference",
|
|
382
|
+
name: "Personalize Inference",
|
|
383
|
+
description: "Keep a personal inference mode, provider and model on top of the tenant default"
|
|
384
|
+
}];
|
|
385
|
+
var PROVIDER_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
386
|
+
var MODEL_PATTERN = /^[!-~\u00a1-\uffff]{1,200}$/;
|
|
387
|
+
function issue(path, message) {
|
|
388
|
+
return {
|
|
389
|
+
path,
|
|
390
|
+
code: "invalid_setting",
|
|
391
|
+
message
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function validBaseUrl(value) {
|
|
395
|
+
try {
|
|
396
|
+
const url = new URL(value);
|
|
397
|
+
return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password && !url.search && !url.hash;
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function validateInferencePreferences(payload) {
|
|
403
|
+
if (payload === null || payload === void 0) return {
|
|
404
|
+
ok: true,
|
|
405
|
+
canonical: null,
|
|
406
|
+
issues: []
|
|
407
|
+
};
|
|
408
|
+
if (typeof payload !== "object" || Array.isArray(payload)) return {
|
|
409
|
+
ok: false,
|
|
410
|
+
canonical: null,
|
|
411
|
+
issues: [issue("", "inference preferences are an object")]
|
|
412
|
+
};
|
|
413
|
+
const keyMember = findKeyMember(payload);
|
|
414
|
+
if (keyMember) return {
|
|
415
|
+
ok: false,
|
|
416
|
+
canonical: null,
|
|
417
|
+
issues: [issue(keyMember, "keys are never stored as preferences; they go to smrt-secrets")]
|
|
418
|
+
};
|
|
419
|
+
const input = payload;
|
|
420
|
+
const issues = [];
|
|
421
|
+
const canonical = {};
|
|
422
|
+
for (const [member, value] of Object.entries(input)) {
|
|
423
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
424
|
+
switch (member) {
|
|
425
|
+
case "mode":
|
|
426
|
+
if (INFERENCE_MODES.includes(value)) canonical.mode = value;
|
|
427
|
+
else issues.push(issue(member, `mode is one of ${INFERENCE_MODES.join(", ")}`));
|
|
428
|
+
break;
|
|
429
|
+
case "provider":
|
|
430
|
+
if (typeof value === "string" && PROVIDER_PATTERN.test(value)) canonical.provider = value;
|
|
431
|
+
else issues.push(issue(member, "invalid provider id"));
|
|
432
|
+
break;
|
|
433
|
+
case "baseUrl":
|
|
434
|
+
if (typeof value === "string" && validBaseUrl(value)) canonical.baseUrl = value;
|
|
435
|
+
else issues.push(issue(member, "the base URL is an http(s) URL without credentials, query or fragment"));
|
|
436
|
+
break;
|
|
437
|
+
case "model":
|
|
438
|
+
if (typeof value === "string" && MODEL_PATTERN.test(value)) canonical.model = value;
|
|
439
|
+
else issues.push(issue(member, "invalid model id"));
|
|
440
|
+
break;
|
|
441
|
+
case "defaultTier":
|
|
442
|
+
if (isInferenceTier(value)) canonical.defaultTier = value;
|
|
443
|
+
else issues.push(issue(member, `the default tier is one of ${INFERENCE_TIERS.join(", ")}`));
|
|
444
|
+
break;
|
|
445
|
+
default: issues.push(issue(member, "unknown inference preference"));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
ok: issues.length === 0,
|
|
450
|
+
canonical: Object.keys(canonical).length > 0 ? canonical : null,
|
|
451
|
+
issues
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
var inferencePreferenceKind = {
|
|
455
|
+
kind: INFERENCE_PREFERENCE_KIND,
|
|
456
|
+
formatVersion: 1,
|
|
457
|
+
permissions: {
|
|
458
|
+
tenant: CUSTOMIZE_INFERENCE_PERMISSION,
|
|
459
|
+
user: PERSONALIZE_INFERENCE_PERMISSION
|
|
460
|
+
},
|
|
461
|
+
validate(payload) {
|
|
462
|
+
return validateInferencePreferences(payload);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
function createInferencePreferences(store, surfaceId = INFERENCE_PREFERENCE_SURFACE) {
|
|
466
|
+
return {
|
|
467
|
+
async read() {
|
|
468
|
+
const state = await store.load(INFERENCE_PREFERENCE_KIND, surfaceId);
|
|
469
|
+
const tenant = state.tenant.payload ?? null;
|
|
470
|
+
const user = state.user?.payload ?? null;
|
|
471
|
+
return {
|
|
472
|
+
choices: {
|
|
473
|
+
...tenant ?? {},
|
|
474
|
+
...user ?? {}
|
|
475
|
+
},
|
|
476
|
+
tenant,
|
|
477
|
+
user,
|
|
478
|
+
revisions: {
|
|
479
|
+
tenant: state.tenant.revision,
|
|
480
|
+
user: state.user?.revision ?? null
|
|
481
|
+
},
|
|
482
|
+
issues: state.issues,
|
|
483
|
+
canCustomize: state.canCustomize
|
|
484
|
+
};
|
|
485
|
+
},
|
|
486
|
+
/** Save choices at a scope (default `user`); `revision` is the one the client loaded. */
|
|
487
|
+
write(choices, options) {
|
|
488
|
+
return store.save(INFERENCE_PREFERENCE_KIND, surfaceId, {
|
|
489
|
+
scope: options.scope ?? "user",
|
|
490
|
+
payload: choices,
|
|
491
|
+
revision: options.revision
|
|
492
|
+
});
|
|
493
|
+
},
|
|
494
|
+
reset(options = {}) {
|
|
495
|
+
return store.reset(INFERENCE_PREFERENCE_KIND, surfaceId, {
|
|
496
|
+
scope: options.scope ?? "user",
|
|
497
|
+
revision: options.revision
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/settings/transcripts.ts
|
|
504
|
+
function storedTranscriptSettings(appRows) {
|
|
505
|
+
const row = appRows.get(TRANSCRIPTS_BINDING);
|
|
506
|
+
if (!row) return null;
|
|
507
|
+
try {
|
|
508
|
+
return row.getValue() ?? null;
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function applyStoredTranscriptSettings(appRows) {
|
|
514
|
+
const value = storedTranscriptSettings(appRows);
|
|
515
|
+
setAiTranscriptAppSettings(value);
|
|
516
|
+
return value;
|
|
517
|
+
}
|
|
518
|
+
async function loadInferenceTranscriptSettings(options = {}) {
|
|
519
|
+
return applyStoredTranscriptSettings(await (options.overrides ?? await InferenceOverrideCollection.create({
|
|
520
|
+
...options.db ?? {},
|
|
521
|
+
defaultListLimit: void 0,
|
|
522
|
+
maxListLimit: void 0
|
|
523
|
+
})).getScopeMap("app", APP_INFERENCE_SCOPE_ID));
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/settings/resolver.ts
|
|
527
|
+
var MAX_CACHED_TENANTS = 500;
|
|
528
|
+
function expandStoredAliases(target, appAliases, configAliases) {
|
|
529
|
+
const expand = (alias, seen) => {
|
|
530
|
+
if (seen.has(alias)) return [];
|
|
531
|
+
const next = new Set(seen).add(alias);
|
|
532
|
+
const definition = appAliases[alias] ?? configAliases[alias];
|
|
533
|
+
return inline(definition?.candidates ?? [], next);
|
|
534
|
+
};
|
|
535
|
+
const inline = (candidates, seen) => candidates.flatMap((candidate) => candidate.alias && Object.hasOwn(appAliases, candidate.alias) ? expand(candidate.alias, seen) : [candidate]);
|
|
536
|
+
const kind = inferenceTargetKind(target);
|
|
537
|
+
if (kind === "candidates") return {
|
|
538
|
+
...target,
|
|
539
|
+
candidates: inline(target.candidates ?? [], /* @__PURE__ */ new Set())
|
|
540
|
+
};
|
|
541
|
+
if (kind !== "alias") return target;
|
|
542
|
+
const name = target.alias;
|
|
543
|
+
if (!Object.hasOwn(appAliases, name)) return target;
|
|
544
|
+
return {
|
|
545
|
+
kind: "candidates",
|
|
546
|
+
candidates: expand(name, /* @__PURE__ */ new Set())
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function expandStoredFusionAliases(definition, appAliases, configAliases) {
|
|
550
|
+
return {
|
|
551
|
+
...definition,
|
|
552
|
+
seats: definition.seats.map((seat) => typeof seat.target === "string" ? { ...seat } : {
|
|
553
|
+
...seat,
|
|
554
|
+
target: expandStoredAliases({ ...seat.target }, appAliases, configAliases)
|
|
555
|
+
})
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function createInferenceOverrideResolver(options = {}) {
|
|
559
|
+
const ttlMs = options.ttlMs ?? 3e4;
|
|
560
|
+
const cache = /* @__PURE__ */ new Map();
|
|
561
|
+
let collection;
|
|
562
|
+
const getCollection = () => {
|
|
563
|
+
if (options.overrides) return Promise.resolve(options.overrides);
|
|
564
|
+
collection ??= InferenceOverrideCollection.create({
|
|
565
|
+
...options.db ?? {},
|
|
566
|
+
defaultListLimit: void 0,
|
|
567
|
+
maxListLimit: void 0
|
|
568
|
+
});
|
|
569
|
+
return collection;
|
|
570
|
+
};
|
|
571
|
+
const rowsFor = async (tenantId) => {
|
|
572
|
+
const key = tenantId ?? "";
|
|
573
|
+
const generation = getInferenceOverrideGeneration();
|
|
574
|
+
const hit = cache.get(key);
|
|
575
|
+
if (hit && hit.generation === generation && Date.now() - hit.at < ttlMs) return hit.rows;
|
|
576
|
+
const rows = await readInferenceOverrideRows(await getCollection(), tenantId);
|
|
577
|
+
cache.delete(key);
|
|
578
|
+
cache.set(key, {
|
|
579
|
+
at: Date.now(),
|
|
580
|
+
generation,
|
|
581
|
+
rows
|
|
582
|
+
});
|
|
583
|
+
if (cache.size > MAX_CACHED_TENANTS) {
|
|
584
|
+
const oldest = cache.keys().next().value;
|
|
585
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
586
|
+
}
|
|
587
|
+
return rows;
|
|
588
|
+
};
|
|
589
|
+
return {
|
|
590
|
+
async resolve(request, context) {
|
|
591
|
+
const tenantId = typeof request.tenantId === "string" && request.tenantId ? request.tenantId : null;
|
|
592
|
+
const rows = await rowsFor(tenantId);
|
|
593
|
+
applyStoredTranscriptSettings(rows.appRows);
|
|
594
|
+
const layers = await buildInferenceOverrideLayers(rows, options.tenantTargetPolicy);
|
|
595
|
+
const merged = mergeSettingsConfigs(context.config, layers.app, layers.tenant);
|
|
596
|
+
const tier = request.tier === "auto" ? resolveAutoTier(merged) : request.tier;
|
|
597
|
+
const stored = layers.tenant.tiers?.[tier] ?? layers.app.tiers?.[tier];
|
|
598
|
+
if (!stored) {
|
|
599
|
+
const next = {
|
|
600
|
+
...request,
|
|
601
|
+
tier
|
|
602
|
+
};
|
|
603
|
+
const deferred = options.base ? await options.base.resolve(next, context) : void 0;
|
|
604
|
+
if (deferred !== void 0) return deferred;
|
|
605
|
+
return request.tier === "auto" ? context.resolveFromConfig(next) : void 0;
|
|
606
|
+
}
|
|
607
|
+
validateInferenceTarget(stored, `inference.tiers.${tier}`, merged);
|
|
608
|
+
return {
|
|
609
|
+
tierResolved: tier,
|
|
610
|
+
target: expandStoredAliases({ ...stored }, layers.app.aliases ?? {}, context.config?.aliases ?? {}),
|
|
611
|
+
via: "resolver"
|
|
612
|
+
};
|
|
613
|
+
},
|
|
614
|
+
async lookupFusion(key, request, context) {
|
|
615
|
+
const rows = await rowsFor(null);
|
|
616
|
+
if (isUnreadableFusionRow(rows.appRows.get(`fusion:${key}`))) throw new InferenceConfigError([unreadableFusionIssue(key)]);
|
|
617
|
+
const app = (await buildInferenceOverrideLayers(rows)).app;
|
|
618
|
+
const stored = Object.hasOwn(app.fusions ?? {}, key) ? app.fusions?.[key] : void 0;
|
|
619
|
+
if (!stored) return options.base?.lookupFusion ? options.base.lookupFusion(key, request, context) : void 0;
|
|
620
|
+
const merged = mergeSettingsConfigs(context.config, app);
|
|
621
|
+
const path = `fusion:${key}`;
|
|
622
|
+
const issues = [...findFusionDefinitionIssues(stored, path, merged), ...findFusionDefinitionPromptIssues(stored, path)];
|
|
623
|
+
if (issues.length > 0) throw new InferenceConfigError(issues);
|
|
624
|
+
return expandStoredFusionAliases(stored, app.aliases ?? {}, context.config?.aliases ?? {});
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/settings/settings-service.ts
|
|
630
|
+
function defaultRegistryFusions() {
|
|
631
|
+
return listFusions().flatMap((key) => {
|
|
632
|
+
const definition = getFusion(key);
|
|
633
|
+
return definition ? [{
|
|
634
|
+
...definition,
|
|
635
|
+
key
|
|
636
|
+
}] : [];
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
var InferenceSettingsConfigError = class extends Error {
|
|
640
|
+
constructor(issues) {
|
|
641
|
+
super(`Inference binding refused: ${issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
642
|
+
this.issues = issues;
|
|
643
|
+
this.name = "InferenceSettingsConfigError";
|
|
644
|
+
}
|
|
645
|
+
issues;
|
|
646
|
+
status = 400;
|
|
647
|
+
};
|
|
648
|
+
var InferenceBindingInUseError = class extends Error {
|
|
649
|
+
constructor(binding, referrers, tenantReferences) {
|
|
650
|
+
const parts = [...referrers];
|
|
651
|
+
if (tenantReferences > 0) parts.push(`${tenantReferences} tenant binding(s)`);
|
|
652
|
+
super(`${binding} is still referenced by ${parts.join(", ")}`);
|
|
653
|
+
this.binding = binding;
|
|
654
|
+
this.referrers = referrers;
|
|
655
|
+
this.tenantReferences = tenantReferences;
|
|
656
|
+
this.name = "InferenceBindingInUseError";
|
|
657
|
+
}
|
|
658
|
+
binding;
|
|
659
|
+
referrers;
|
|
660
|
+
tenantReferences;
|
|
661
|
+
status = 409;
|
|
662
|
+
};
|
|
663
|
+
function withoutListBounds(options) {
|
|
664
|
+
return {
|
|
665
|
+
...options,
|
|
666
|
+
defaultListLimit: void 0,
|
|
667
|
+
maxListLimit: void 0
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function isRecord(value) {
|
|
671
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
672
|
+
}
|
|
673
|
+
function rawValue(layers, scope, binding) {
|
|
674
|
+
const row = (scope === "app" ? layers.appRows : layers.tenantRows).get(binding);
|
|
675
|
+
if (!row) return null;
|
|
676
|
+
try {
|
|
677
|
+
return row.getValue();
|
|
678
|
+
} catch {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function allowListOf(layers, binding) {
|
|
683
|
+
try {
|
|
684
|
+
return layers.appRows.get(binding)?.getTenantAllowList() ?? null;
|
|
685
|
+
} catch {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function nonEmpty(target) {
|
|
690
|
+
return target && Object.keys(target).length > 0 ? stripKeyMembers(target) : null;
|
|
691
|
+
}
|
|
692
|
+
function bindingRefersTo(binding, value, kind, name) {
|
|
693
|
+
if (!isRecord(value)) return false;
|
|
694
|
+
if (binding.startsWith("tier:")) return kind === "alias" ? value.alias === name : value.fusion === name || value.kind === "fusion" && value.fusion === name;
|
|
695
|
+
if (kind === "alias" && binding.startsWith("alias:") && binding !== `alias:${name}`) return Array.isArray(value.candidates) && value.candidates.some((candidate) => isRecord(candidate) && candidate.alias === name);
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
function transcriptRetentionDays(policy) {
|
|
699
|
+
if (policy?.enabled === false || policy?.aiTranscripts === false) return null;
|
|
700
|
+
return policy?.aiTranscripts?.maxAgeDays ?? DEFAULT_RETENTION_POLICY.aiTranscripts.maxAgeDays ?? null;
|
|
701
|
+
}
|
|
702
|
+
var PRESET_LABELS = {
|
|
703
|
+
openrouter: "OpenRouter",
|
|
704
|
+
lemonade: "Lemonade (local)",
|
|
705
|
+
"llama.cpp": "llama.cpp (local)",
|
|
706
|
+
"lm-studio": "LM Studio (local)"
|
|
707
|
+
};
|
|
708
|
+
var InferenceSettingsService = class InferenceSettingsService {
|
|
709
|
+
constructor(overrides, options = {}) {
|
|
710
|
+
this.overrides = overrides;
|
|
711
|
+
this.options = options;
|
|
712
|
+
this.writes = new InferenceOverrideService(overrides, options.authorize ?? (() => false), { tenantTargetPolicy: options.tenantTargetPolicy });
|
|
713
|
+
this.readConfig = options.config ?? getInferenceConfig;
|
|
714
|
+
}
|
|
715
|
+
overrides;
|
|
716
|
+
options;
|
|
717
|
+
writes;
|
|
718
|
+
readConfig;
|
|
719
|
+
/** Build a service over the given database options (list bounds stripped). */
|
|
720
|
+
static async create(options = {}, serviceOptions = {}) {
|
|
721
|
+
const overrides = await InferenceOverrideCollection.create(withoutListBounds(options));
|
|
722
|
+
return new InferenceSettingsService(overrides, serviceOptions);
|
|
723
|
+
}
|
|
724
|
+
loadLayers(tenantId) {
|
|
725
|
+
return loadInferenceOverrideLayers(this.overrides, {
|
|
726
|
+
tenantId,
|
|
727
|
+
tenantTargetPolicy: this.options.tenantTargetPolicy
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* The effective config for a tenant: config < app rows < allowed tenant
|
|
732
|
+
* rows. What the override resolver binds and the doctor checks.
|
|
733
|
+
* Server-side only: config layers may carry literal keys, so never send
|
|
734
|
+
* this to a browser (the row methods strip them).
|
|
735
|
+
*/
|
|
736
|
+
async getEffectiveConfig(options = {}) {
|
|
737
|
+
const layers = await this.loadLayers(options.tenantId);
|
|
738
|
+
return mergeSettingsConfigs(this.readConfig(), layers.app, layers.tenant);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* One row per tier (`cheap`, `good`, `great`, `best`): the effective
|
|
742
|
+
* target, each scope's raw override, and both revert previews.
|
|
743
|
+
*/
|
|
744
|
+
async listInferenceSettings(options = {}) {
|
|
745
|
+
const tenantId = options.tenantId ?? null;
|
|
746
|
+
const layers = await this.loadLayers(tenantId);
|
|
747
|
+
const config = this.readConfig();
|
|
748
|
+
return INFERENCE_TIERS.map((tier) => {
|
|
749
|
+
const binding = tierBinding(tier);
|
|
750
|
+
const configTarget = nonEmpty(config?.tiers?.[tier]);
|
|
751
|
+
const appTarget = rawValue(layers, "app", binding);
|
|
752
|
+
const tenantTarget = tenantId ? rawValue(layers, "tenant", binding) : null;
|
|
753
|
+
const tenantBlocked = layers.blockedTenantBindings.includes(binding);
|
|
754
|
+
const tenantEffective = tenantBlocked ? null : tenantTarget;
|
|
755
|
+
const appRevertTarget = configTarget;
|
|
756
|
+
const inheritedTarget = appTarget ?? configTarget;
|
|
757
|
+
const effectiveTarget = tenantEffective ?? appTarget ?? configTarget;
|
|
758
|
+
const supplyingLevel = tenantEffective ? "tenant" : appTarget ? "app" : configTarget ? "config" : "default";
|
|
759
|
+
return {
|
|
760
|
+
binding,
|
|
761
|
+
tier,
|
|
762
|
+
effectiveTarget,
|
|
763
|
+
effectiveLabel: describeInferenceTarget(effectiveTarget),
|
|
764
|
+
supplyingLevel,
|
|
765
|
+
configTarget,
|
|
766
|
+
appTarget,
|
|
767
|
+
tenantTarget,
|
|
768
|
+
appRevertTarget,
|
|
769
|
+
appRevertLabel: describeInferenceTarget(appRevertTarget),
|
|
770
|
+
inheritedTarget: tenantId ? inheritedTarget : null,
|
|
771
|
+
inheritedLabel: tenantId ? describeInferenceTarget(inheritedTarget) : null,
|
|
772
|
+
tenantAllowList: allowListOf(layers, binding),
|
|
773
|
+
tenantBlocked
|
|
774
|
+
};
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
/** `auto`'s default tier with the same per-scope shape. */
|
|
778
|
+
async getDefaultTierSetting(options = {}) {
|
|
779
|
+
const tenantId = options.tenantId ?? null;
|
|
780
|
+
const layers = await this.loadLayers(tenantId);
|
|
781
|
+
const config = this.readConfig();
|
|
782
|
+
const configTier = config?.defaultTier ?? null;
|
|
783
|
+
const appTier = rawValue(layers, "app", DEFAULT_TIER_BINDING);
|
|
784
|
+
const tenantTier = tenantId ? rawValue(layers, "tenant", DEFAULT_TIER_BINDING) : null;
|
|
785
|
+
const tenantBlocked = layers.blockedTenantBindings.includes(DEFAULT_TIER_BINDING);
|
|
786
|
+
const tenantEffective = tenantBlocked ? null : tenantTier;
|
|
787
|
+
const appRevertTier = resolveAutoTier(config);
|
|
788
|
+
const inheritedTier = appTier ?? appRevertTier;
|
|
789
|
+
return {
|
|
790
|
+
binding: DEFAULT_TIER_BINDING,
|
|
791
|
+
effectiveTier: tenantEffective ?? inheritedTier,
|
|
792
|
+
supplyingLevel: tenantEffective ? "tenant" : appTier ? "app" : configTier ? "config" : "default",
|
|
793
|
+
configTier,
|
|
794
|
+
appTier,
|
|
795
|
+
tenantTier,
|
|
796
|
+
appRevertTier,
|
|
797
|
+
inheritedTier: tenantId ? inheritedTier : null,
|
|
798
|
+
tenantAllowList: allowListOf(layers, DEFAULT_TIER_BINDING),
|
|
799
|
+
tenantBlocked
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
/** Every alias: config entries, replaced whole by app rows. Sorted by name. */
|
|
803
|
+
async listInferenceAliases() {
|
|
804
|
+
const layers = await this.loadLayers(null);
|
|
805
|
+
const configAliases = this.readConfig()?.aliases ?? {};
|
|
806
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(configAliases), ...Object.keys(layers.app.aliases ?? {})])].sort().map((name) => {
|
|
807
|
+
const app = layers.app.aliases?.[name];
|
|
808
|
+
const alias = app ?? configAliases[name];
|
|
809
|
+
const setting = {
|
|
810
|
+
name,
|
|
811
|
+
source: app ? "app" : "config",
|
|
812
|
+
candidates: Array.isArray(alias?.candidates) ? stripKeyMembers(alias.candidates) : [],
|
|
813
|
+
inConfig: Object.hasOwn(configAliases, name)
|
|
814
|
+
};
|
|
815
|
+
if (alias?.description) setting.description = alias.description;
|
|
816
|
+
return setting;
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Fusions from the host's registry (code, read-only), the config's
|
|
821
|
+
* `inference.fusions`, and app rows (editable JSON). A later source
|
|
822
|
+
* shadows an earlier one by name, the executor's precedence (#4800): app
|
|
823
|
+
* row, then config, then registry. Each entry carries the issues that
|
|
824
|
+
* would make the executor refuse it, so the list shows what would run.
|
|
825
|
+
*/
|
|
826
|
+
async listInferenceFusions() {
|
|
827
|
+
const layers = await this.loadLayers(null);
|
|
828
|
+
const config = this.readConfig();
|
|
829
|
+
const merged = mergeSettingsConfigs(config, layers.app);
|
|
830
|
+
const out = /* @__PURE__ */ new Map();
|
|
831
|
+
const put = (setting, raw, path, against) => {
|
|
832
|
+
const previous = out.get(setting.name);
|
|
833
|
+
const shadows = previous ? [...previous.shadows ?? [], previous.source] : [];
|
|
834
|
+
const issues = [...findFusionDefinitionIssues(raw, path, against), ...findFusionDefinitionPromptIssues(raw, path)];
|
|
835
|
+
if (shadows.length > 0) setting.shadows = shadows;
|
|
836
|
+
if (issues.length > 0) setting.issues = issues;
|
|
837
|
+
out.set(setting.name, setting);
|
|
838
|
+
};
|
|
839
|
+
const registry = this.options.registryFusions ?? defaultRegistryFusions;
|
|
840
|
+
for (const fusion of registry()) {
|
|
841
|
+
if (!fusion || typeof fusion.key !== "string") continue;
|
|
842
|
+
const definition = stripKeyMembers(JSON.parse(JSON.stringify(fusion)));
|
|
843
|
+
const { key: _key, ...raw } = fusion;
|
|
844
|
+
put({
|
|
845
|
+
name: fusion.key,
|
|
846
|
+
source: "registry",
|
|
847
|
+
definition,
|
|
848
|
+
description: typeof fusion.description === "string" ? fusion.description : void 0,
|
|
849
|
+
editable: false
|
|
850
|
+
}, raw, `fusions.${fusion.key}`, config);
|
|
851
|
+
}
|
|
852
|
+
if (isRecord(config?.fusions)) for (const [name, definition] of Object.entries(config.fusions)) {
|
|
853
|
+
if (!isRecord(definition)) continue;
|
|
854
|
+
put({
|
|
855
|
+
name,
|
|
856
|
+
source: "config",
|
|
857
|
+
definition: stripKeyMembers(definition),
|
|
858
|
+
description: typeof definition.description === "string" ? definition.description : void 0,
|
|
859
|
+
editable: false
|
|
860
|
+
}, definition, `inference.fusions.${name}`, config);
|
|
861
|
+
}
|
|
862
|
+
for (const [name, definition] of Object.entries(layers.app.fusions ?? {})) put({
|
|
863
|
+
name,
|
|
864
|
+
source: "app",
|
|
865
|
+
definition,
|
|
866
|
+
description: typeof definition.description === "string" ? definition.description : void 0,
|
|
867
|
+
editable: true
|
|
868
|
+
}, definition, `fusion:${name}`, merged);
|
|
869
|
+
for (const [binding, row] of layers.appRows) {
|
|
870
|
+
if (!binding.startsWith("fusion:") || !isUnreadableFusionRow(row)) continue;
|
|
871
|
+
const name = binding.slice(7);
|
|
872
|
+
put({
|
|
873
|
+
name,
|
|
874
|
+
source: "app",
|
|
875
|
+
definition: {},
|
|
876
|
+
editable: true
|
|
877
|
+
}, void 0, binding, merged);
|
|
878
|
+
const entry = out.get(name);
|
|
879
|
+
if (entry) entry.issues = [unreadableFusionIssue(name)];
|
|
880
|
+
}
|
|
881
|
+
return [...out.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Bind (or, with `null`, revert) one tier at one scope. Tenant writes must
|
|
885
|
+
* pass the allow-list; every write goes through the authorizer.
|
|
886
|
+
*/
|
|
887
|
+
async setTierOverride(request) {
|
|
888
|
+
if (!INFERENCE_TIERS.includes(request.tier)) throw new InferenceBindingValueError(`tier:${String(request.tier)}`, "unknown tier");
|
|
889
|
+
const binding = tierBinding(request.tier);
|
|
890
|
+
if (request.target !== null) await this.assertConfigStaysValid(request.scopeType, request.scopeId, { tiers: { [request.tier]: request.target } });
|
|
891
|
+
await this.writes.setValue(binding, request.scopeType, request.scopeId, request.target);
|
|
892
|
+
}
|
|
893
|
+
/** Set (or revert) `auto`'s default tier at one scope. */
|
|
894
|
+
async setDefaultTierOverride(request) {
|
|
895
|
+
await this.writes.setValue(DEFAULT_TIER_BINDING, request.scopeType, request.scopeId, request.tier === null ? null : { tier: request.tier });
|
|
896
|
+
}
|
|
897
|
+
/** Set (or remove) the tenant allow-list of a tier or of the default tier. */
|
|
898
|
+
async setTenantAllowList(request) {
|
|
899
|
+
await this.writes.setTenantAllowList(request.binding, request.allowList);
|
|
900
|
+
}
|
|
901
|
+
/** Define (or, with `null`, remove) an app-level alias. */
|
|
902
|
+
async setAlias(request) {
|
|
903
|
+
const binding = `alias:${request.name}`;
|
|
904
|
+
if (!INFERENCE_BINDING_NAME_PATTERN.test(String(request.name))) throw new InferenceBindingValueError(binding, "invalid alias name");
|
|
905
|
+
if (request.alias !== null) await this.assertConfigStaysValid("app", APP_INFERENCE_SCOPE_ID, { aliases: { [request.name]: request.alias } });
|
|
906
|
+
await this.writes.setValue(binding, "app", APP_INFERENCE_SCOPE_ID, request.alias, { beforeWrite: (normalized) => normalized === null ? this.assertNotReferenced("alias", request.name) : Promise.resolve() });
|
|
907
|
+
}
|
|
908
|
+
/** Store (or, with `null`, remove) an app-level fusion config entry (#4753 shape). */
|
|
909
|
+
async setFusion(request) {
|
|
910
|
+
const binding = `fusion:${request.name}`;
|
|
911
|
+
if (!INFERENCE_BINDING_NAME_PATTERN.test(String(request.name))) throw new InferenceBindingValueError(binding, "invalid fusion name");
|
|
912
|
+
if (request.definition !== null) {
|
|
913
|
+
const normalized = normalizeFusionDefinition(binding, request.definition);
|
|
914
|
+
await this.assertConfigStaysValid("app", APP_INFERENCE_SCOPE_ID, { fusions: { [request.name]: normalized } });
|
|
915
|
+
}
|
|
916
|
+
await this.writes.setValue(binding, "app", APP_INFERENCE_SCOPE_ID, request.definition, { beforeWrite: (normalized) => normalized === null ? this.assertNotReferenced("fusion", request.name) : Promise.resolve() });
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Refuse removing an app alias or fusion that a stored binding (any
|
|
920
|
+
* scope, any tenant) or the config still names, unless config or the
|
|
921
|
+
* registry keeps defining it. Otherwise the removal would leave dangling
|
|
922
|
+
* references that fail at call time.
|
|
923
|
+
*/
|
|
924
|
+
async assertNotReferenced(kind, name) {
|
|
925
|
+
const config = this.readConfig();
|
|
926
|
+
if (kind === "alias" ? Object.hasOwn(config?.aliases ?? {}, name) : Object.hasOwn(config?.fusions ?? {}, name) || [...(this.options.registryFusions ?? defaultRegistryFusions)()].some((fusion) => fusion?.key === name)) return;
|
|
927
|
+
const referrers = [];
|
|
928
|
+
for (const [tier, target] of Object.entries(config?.tiers ?? {})) if (bindingRefersTo(`tier:${tier}`, target, kind, name)) referrers.push(`config tiers.${tier}`);
|
|
929
|
+
for (const [alias, definition] of Object.entries(config?.aliases ?? {})) if (bindingRefersTo(`alias:${alias}`, definition, kind, name)) referrers.push(`config aliases.${alias}`);
|
|
930
|
+
let tenantReferences = 0;
|
|
931
|
+
for (const row of await this.overrides.list({})) {
|
|
932
|
+
let value;
|
|
933
|
+
try {
|
|
934
|
+
value = row.getValue();
|
|
935
|
+
} catch {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (!bindingRefersTo(row.binding, value, kind, name)) continue;
|
|
939
|
+
if (row.tenantId) tenantReferences += 1;
|
|
940
|
+
else referrers.push(row.binding);
|
|
941
|
+
}
|
|
942
|
+
if (referrers.length > 0 || tenantReferences > 0) throw new InferenceBindingInUseError(`${kind}:${name}`, referrers, tenantReferences);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Refuse a write that adds a config issue (unknown alias, cycle, key
|
|
946
|
+
* without endpoint, unknown preset) to the layered config it lands in.
|
|
947
|
+
* Issues that already existed do not block unrelated writes.
|
|
948
|
+
*/
|
|
949
|
+
async assertConfigStaysValid(scopeType, scopeId, change) {
|
|
950
|
+
const layers = await this.loadLayers(scopeType === "tenant" ? scopeId : null);
|
|
951
|
+
const before = mergeSettingsConfigs(this.readConfig(), layers.app, layers.tenant);
|
|
952
|
+
const after = mergeSettingsConfigs(before, change);
|
|
953
|
+
const seen = new Set(findInferenceConfigIssues(before).map((issue) => `${issue.code}:${issue.path}`));
|
|
954
|
+
const added = findInferenceConfigIssues(after).filter((issue) => !seen.has(`${issue.code}:${issue.path}`));
|
|
955
|
+
if (added.length > 0) throw new InferenceSettingsConfigError(added);
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Test connection for the default view: probe the endpoint's cheapest
|
|
959
|
+
* list call (`./node`'s `probeInferenceCandidate`, 3 s). The stored key
|
|
960
|
+
* record (from the host's `SecretService`) is sent only when the probe URL
|
|
961
|
+
* is on the key's bound origin; no environment key is read.
|
|
962
|
+
*/
|
|
963
|
+
async testConnection(input, options = {}) {
|
|
964
|
+
const candidate = {};
|
|
965
|
+
if (input.provider) candidate.provider = input.provider;
|
|
966
|
+
if (input.preset) candidate.preset = input.preset;
|
|
967
|
+
if (input.baseUrl) candidate.baseUrl = input.baseUrl;
|
|
968
|
+
if (input.model) candidate.model = input.model;
|
|
969
|
+
const url = inferenceProbeUrl(candidate);
|
|
970
|
+
const apiKey = url ? inferenceKeyFor(options.key, url) : void 0;
|
|
971
|
+
if (options.key && url && !apiKey) {
|
|
972
|
+
let where = "that endpoint";
|
|
973
|
+
try {
|
|
974
|
+
where = new URL(url).origin;
|
|
975
|
+
} catch {}
|
|
976
|
+
return {
|
|
977
|
+
reachable: false,
|
|
978
|
+
durationMs: 0,
|
|
979
|
+
keySent: false,
|
|
980
|
+
detail: `the stored key is bound to ${options.key.origin} and is never sent to ${where}; save a key for this endpoint`
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
const result = await probeInferenceCandidate(apiKey && url ? {
|
|
984
|
+
...candidate,
|
|
985
|
+
baseUrl: candidate.baseUrl ?? url.replace(/\/(?:models|api\/tags)$/, ""),
|
|
986
|
+
apiKey
|
|
987
|
+
} : candidate, {
|
|
988
|
+
env: {},
|
|
989
|
+
fetch: options.fetch,
|
|
990
|
+
timeoutMs: options.timeoutMs
|
|
991
|
+
});
|
|
992
|
+
return {
|
|
993
|
+
...result,
|
|
994
|
+
detail: redactInferenceSecret(result.detail, apiKey),
|
|
995
|
+
keySent: !!apiKey
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Per-candidate test for the alias editor. A candidate already in the
|
|
1000
|
+
* effective config (config file or an app row) reads its key from
|
|
1001
|
+
* `apiKeyEnv` (or its provider's variable) exactly as the doctor does,
|
|
1002
|
+
* sent only to its own endpoint; any other candidate (an unsaved edit) is
|
|
1003
|
+
* probed with no key at all.
|
|
1004
|
+
*/
|
|
1005
|
+
async testCandidate(candidate, options = {}) {
|
|
1006
|
+
if (findKeyLike(candidate)) throw new InferenceBindingValueError("candidate", "a tested candidate never carries a literal key");
|
|
1007
|
+
const configured = await this.isConfiguredCandidate(candidate);
|
|
1008
|
+
return {
|
|
1009
|
+
...await probeInferenceCandidate(candidate, {
|
|
1010
|
+
...options,
|
|
1011
|
+
env: configured ? options.env ?? process.env : {}
|
|
1012
|
+
}),
|
|
1013
|
+
keySent: false
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
async isConfiguredCandidate(candidate) {
|
|
1017
|
+
const config = await this.getEffectiveConfig();
|
|
1018
|
+
const wanted = canonicalCandidate(candidate);
|
|
1019
|
+
return Object.values(config?.aliases ?? {}).some((alias) => (alias?.candidates ?? []).some((entry) => canonicalCandidate(entry) === wanted));
|
|
1020
|
+
}
|
|
1021
|
+
/** `smrt doctor --ai` over the effective config for a tenant. */
|
|
1022
|
+
async doctor(options = {}) {
|
|
1023
|
+
return checkInferenceBindings({
|
|
1024
|
+
config: await this.getEffectiveConfig({ tenantId: options.tenantId }) ?? {},
|
|
1025
|
+
probe: options.probe,
|
|
1026
|
+
env: options.env,
|
|
1027
|
+
fetch: options.fetch
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* The app's transcript setting (#4781): effective values, the stored app
|
|
1032
|
+
* row, its revert preview, the env override and the retention window.
|
|
1033
|
+
* The same for every tenant (no tenant override).
|
|
1034
|
+
*/
|
|
1035
|
+
async getTranscriptSettings() {
|
|
1036
|
+
const appValue = storedTranscriptSettings(await this.overrides.getScopeMap("app", APP_INFERENCE_SCOPE_ID));
|
|
1037
|
+
const merged = this.options.transcriptsConfig ?? getInferenceTranscriptsConfig;
|
|
1038
|
+
const resolved = (layer) => {
|
|
1039
|
+
const value = merged(layer);
|
|
1040
|
+
return {
|
|
1041
|
+
enabled: value?.enabled === true,
|
|
1042
|
+
sample: value?.sample ?? DEFAULT_TRANSCRIPT_SAMPLE,
|
|
1043
|
+
maxBytes: value?.maxBytes ?? DEFAULT_TRANSCRIPT_MAX_BYTES
|
|
1044
|
+
};
|
|
1045
|
+
};
|
|
1046
|
+
const effective = resolved(appValue);
|
|
1047
|
+
const appRevert = resolved(null);
|
|
1048
|
+
const forcedOn = resolved({ enabled: true }).enabled;
|
|
1049
|
+
const envEnabled = forcedOn === resolved({ enabled: false }).enabled ? forcedOn : null;
|
|
1050
|
+
const config$1 = merged(null);
|
|
1051
|
+
const supplyingLevel = envEnabled !== null ? "env" : appValue?.enabled !== void 0 ? "app" : config$1?.enabled !== void 0 ? "config" : "default";
|
|
1052
|
+
return {
|
|
1053
|
+
binding: TRANSCRIPTS_BINDING,
|
|
1054
|
+
...effective,
|
|
1055
|
+
supplyingLevel,
|
|
1056
|
+
appValue,
|
|
1057
|
+
appRevert,
|
|
1058
|
+
envEnabled,
|
|
1059
|
+
retentionDays: transcriptRetentionDays((this.options.retention ?? (() => config.toJSON().retention))()),
|
|
1060
|
+
promptOverrides: Object.keys(config$1?.prompts ?? {}).sort()
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Store (or, with `null`, remove) the app's transcript setting. Always
|
|
1065
|
+
* the app row: there is no tenant scope. Goes through the authorizer
|
|
1066
|
+
* (request `{ binding: 'transcripts', scopeType: 'app' }`); on success the
|
|
1067
|
+
* value is installed as core's app settings layer in this process.
|
|
1068
|
+
*/
|
|
1069
|
+
async setTranscriptSettings(request) {
|
|
1070
|
+
await this.writes.setValue(TRANSCRIPTS_BINDING, "app", APP_INFERENCE_SCOPE_ID, request.settings);
|
|
1071
|
+
applyStoredTranscriptSettings(await this.overrides.getScopeMap("app", APP_INFERENCE_SCOPE_ID));
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* The Usage tab: `reportAiUsage()` (#4751) grouped by model, tier, prompt
|
|
1075
|
+
* key, fusion and/or template hash. Pass `by: ['model']` where tier and
|
|
1076
|
+
* prompt tags do not exist yet; NULL dimensions are their own bucket,
|
|
1077
|
+
* never dropped. `transcripts: true` adds per-bucket transcript counts
|
|
1078
|
+
* (#4781; ids only, never content).
|
|
1079
|
+
*/
|
|
1080
|
+
usage(options = {}) {
|
|
1081
|
+
return reportAiUsage(this.overrides.db, {
|
|
1082
|
+
...options,
|
|
1083
|
+
by: options.by && options.by.length > 0 ? options.by : ["model"]
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
/** Local discovery for the model field (#4674's loopback probe). */
|
|
1087
|
+
async discoverModels() {
|
|
1088
|
+
const { discoverLocalInferenceModels } = await import("@happyvertical/smrt-core/byo-inference");
|
|
1089
|
+
return (await discoverLocalInferenceModels()).filter((entry) => entry.available).map((entry) => ({
|
|
1090
|
+
service: entry.service,
|
|
1091
|
+
baseUrl: entry.baseUrl,
|
|
1092
|
+
models: entry.available ? entry.models : []
|
|
1093
|
+
}));
|
|
1094
|
+
}
|
|
1095
|
+
/** Provider choices: #4674 presets, then the AI SDK provider list. */
|
|
1096
|
+
listProviderOptions() {
|
|
1097
|
+
const presets = INFERENCE_PRESET_IDS.map((id) => {
|
|
1098
|
+
const preset = INFERENCE_PRESETS[id];
|
|
1099
|
+
return {
|
|
1100
|
+
id,
|
|
1101
|
+
label: PRESET_LABELS[id] ?? id,
|
|
1102
|
+
kind: "preset",
|
|
1103
|
+
baseUrl: preset.baseUrl,
|
|
1104
|
+
requiresApiKey: preset.requiresApiKey,
|
|
1105
|
+
local: !preset.requiresApiKey
|
|
1106
|
+
};
|
|
1107
|
+
});
|
|
1108
|
+
const providers = AI_PROVIDER_TYPES.map((id) => ({
|
|
1109
|
+
id,
|
|
1110
|
+
label: id,
|
|
1111
|
+
kind: "provider",
|
|
1112
|
+
requiresApiKey: !!getDefaultAIKeyEnvName(id),
|
|
1113
|
+
...id === "litellm" || id === "bifrost" ? { needsBaseUrl: true } : {},
|
|
1114
|
+
...id === "ollama" ? { local: true } : {}
|
|
1115
|
+
}));
|
|
1116
|
+
return [...presets, ...providers];
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
function canonicalCandidate(candidate) {
|
|
1120
|
+
return JSON.stringify([
|
|
1121
|
+
"provider",
|
|
1122
|
+
"model",
|
|
1123
|
+
"baseUrl",
|
|
1124
|
+
"apiKeyEnv",
|
|
1125
|
+
"preset",
|
|
1126
|
+
"alias"
|
|
1127
|
+
].map((field) => {
|
|
1128
|
+
const value = candidate[field];
|
|
1129
|
+
return typeof value === "string" ? value.trim() : null;
|
|
1130
|
+
}));
|
|
1131
|
+
}
|
|
1132
|
+
function findKeyLike(candidate) {
|
|
1133
|
+
return typeof candidate.apiKey === "string" && candidate.apiKey.length > 0;
|
|
1134
|
+
}
|
|
1135
|
+
//#endregion
|
|
1136
|
+
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, TRANSCRIPTS_BINDING, applyStoredTranscriptSettings, assertValidInferenceScope, bindInferenceKey, buildInferenceOverrideLayers, createInferenceOverrideResolver, createInferencePreferences, describeInferenceTarget, inferenceKeyFor, inferenceKeyOrigin, inferencePreferenceKind, inferenceProviderConfigFromKey, inferenceTargetRef, isAllowedByList, loadInferenceOverrideLayers, loadInferenceTranscriptSettings, matchesAllowPattern, mergeSettingsConfigs, normalizeTranscriptSettings, parseInferenceBinding, readInferenceOverrideRows, redactInferenceSecret, storedTranscriptSettings, stripKeyMembers, tierBinding, validateInferencePreferences };
|
|
1137
|
+
|
|
1138
|
+
//# sourceMappingURL=server.js.map
|